Build a working ontology from defence logistics data
Build an ontology from an empty Python file, inspect actual graph records and their sources, and extend a maintenance example to multiple required parts. Six chapters combine plain-language explanations with a downloadable, locally tested implementation.
The question: How do we actually create, validate and use an ontology from operational source records?
You’ll learn: Construct an ontology step by step, trace RDF facts to CSV records, validate and query them, and migrate the model to support several required parts per order.
For: Domain experts, analysts and engineers; concepts are explained before the runnable commands.
Executed teaching prototype · AI-assisted source check . This is not a human expert endorsement.
1. Start with a decision and a model you can build#
A maintenance coordinator opens WO-204 for generator GEN-17. The job needs two PF-9 filters. North depot reports six on hand. Two are quarantined and three others are reserved, leaving one available. The job is one filter short.
This guide uses invented records from routine generator maintenance in a defence support organization. You will create the ontology, populate it from those records, validate the graph, and query it. The companion implementation runs on your computer using Python, RDFLib, and pySHACL. Its outputs are inspectable files, and its tests include inputs that must be rejected.
Step 1: Specify the question and its accounting rules#
Write the question before choosing software: Can North supply the two filters requested by WO-204, and which records explain the answer? The maintenance coordinator prepares the evidence; an authorized supervisor would own any later operational decision.
For this fixture, “on hand” includes every physically recorded filter, including reserved and quarantined units. The reservations belong to other work; the baseline does not represent their owning orders. All quantities use the same part, depot, observation, and unit. Reserved and quarantined units must not overlap.
Available = On hand − Quarantined − Reserved
Available = 6 − 2 − 3 = 1 filter
Shortage = max(0, Required − Available)
Shortage = max(0, 2 − 1) = 1 filterAn inventory system that already subtracts reservations from “on hand” needs a different mapping. Reusing this formula unchanged would subtract them twice. Agreeing on each input's meaning is a modelling task, even when the final arithmetic is simple.
Record three expected outcomes: the supplied fixture yields one available and one short; stale evidence withholds the calculation; an unresolved source identity rejects the build. These become acceptance checks throughout the guide.
Step 2: Decide whether an ontology is useful here#
The shortage calculation can also be implemented with SQL. If one application owns consistent tables and only needs this report, documented keys, joins, and constraints may be sufficient. RDF is not required to subtract quantities.
The ontology becomes useful when several applications must share the meaning of an asset, a requirement, a stock observation, and its evidence. It makes those definitions and relationships explicit, so a new question can reuse them. It does not automatically reconcile source systems or make their records correct.
Chapter 6 demonstrates that reuse with an executable change: WO-204 requires both filters and a seal. Each requirement receives its own identity, quantity, and part relationship. A query then follows work order → requirement → part → stock → allocations to explain each line. A relational model can support the same design; choose the representation according to interoperability, query, and operating requirements.
| Situation | Reasonable starting point |
|---|---|
| One stable source and one shortage report | Relational tables, reviewed joins, and explicit accounting rules |
| Several producers use different names for the same equipment | Canonical identity mappings and a shared vocabulary |
| New questions repeatedly follow relationships across those producers | Evaluate an ontology and graph alongside the relational alternative |
| A reader needs passages from service instructions | Retrieval with document versions and citations; combine it with current structured facts when needed |
Step 3: Choose the smallest useful set of things#
The baseline uses six classes. A class is a kind of thing; GEN-17 is one instance of the Asset class. Each choice should preserve a distinction that matters to the question.
| Class | Why it is separate | What would justify a richer model? |
|---|---|---|
| Asset | One generator has its own identity and maintenance history | Components with independent serial numbers or lifecycles |
| WorkOrder | A request is different from the equipment it concerns | Multiple requested parts, introduced in Chapter 6 |
| PartType | PF-9 is a catalogue kind, rather than one physical filter | A PhysicalPart class when individual service histories matter |
| StockSnapshot | Six units is an observation at a depot and time | Multiple observations with a defined current-state policy |
| Allocation | A reservation or quarantine has its own quantity and unit set | Owning job, expiry, reason, and lifecycle relationships |
| InspectionEvidence | An observed status needs a date and source version | Separate checklist and document revision objects |
An Allocation is not just a number on a stock record: keeping it separate lets the lab check the units behind quarantine and reservations. A PartType is not a physical filter: the baseline keeps physical unit IDs as values for disjointness checks. A Depot remains a controlled text value here; give it a class when location, ownership, capacity, or depot relationships become query requirements.
Original diagram of the reference lab. A class describes a kind of record; GEN-17 and WO-204 are individual records. Select a box to see what is represented.
Select a box to inspect its role. On a small screen, scroll the diagram sideways.A request to maintain a particular asset. Its required part and quantity are explicit facts; a request does not by itself authorize a stock allocation or maintenance release.
Read the complete diagram as text
- WorkOrder · WO-204: A request to maintain a particular asset. Its required part and quantity are explicit facts; a request does not by itself authorize a stock allocation or maintenance release.
- Asset · GEN-17: The support generator being maintained. Approved source aliases map to one canonical identity. Similar names are not sufficient evidence of identity.
- InspectionEvidence: A dated inspection record connected to the asset, with provenance. A missing or stale inspection remains a visible reason to stop a proposal.
- PartType · PF-9: The type of filter required. Quantities are comparable only when part identity and unit agree.
- StockSnapshot: An observation of the physical count at a depot and time. The teaching input has six PF-9 filters; this observation must not be mistaken for a continuously live stock service.
- Allocation: A record marking units as reserved or quarantined. The lab checks that these allocations belong to the same stock snapshot and do not overlap.
- WorkOrder · WO-204 → for → Asset · GEN-17
- InspectionEvidence → checks → Asset · GEN-17
- WorkOrder · WO-204 → needs → PartType · PF-9
- StockSnapshot → counts → PartType · PF-9
- Allocation → reduces → StockSnapshot
Read an arrow as a relationship you can inspect. WO-204 concerns GEN-17 and requires PF-9. The stock snapshot concerns PF-9. Allocations refer to that snapshot. Inspection evidence concerns GEN-17. Every relationship has a direction and identified endpoints.
Step 4: Learn the vocabulary through the files#
| Term | Plain meaning | Where you will see it |
|---|---|---|
| Ontology | Shared definitions of kinds of things and their relationships | schema/schema.ttl |
| Knowledge graph | Particular records connected using those definitions | results/instances.ttl |
| RDF triple | One subject–relationship–value statement | WO-204 — asset — GEN-17101 |
| IRI | A full identifier that gives a term or record an unambiguous name | https://example.org/maintenance/id/ORG-TRAIN/asset/GEN-17 |
| OWL | A language for formal ontology descriptions | Class and property declarations102 |
| SHACL | Conditions used to validate a graph | schema/shapes.ttl |
| SPARQL | A query language for matching graph patterns | queries/order_inventory.rq |
| Provenance | Recorded derivation from source records | results/provenance.ttl |
A retrieval assistant can find an approved instruction; it still needs relevant, current stock records to establish availability. The enterprise RAG guide covers retrieval. Here, identities and quantities become explicit graph statements before an explanation is produced.
Step 5: Understand the evidence behind this guide#
Fasina, Sawyerr, and Murainah's 2026 paper combines neural text extraction with symbolic processing in a WebNLG text-to-RDF experiment.103 It motivates checking extracted candidates. Our runnable path starts with structured CSV records, so the complete construction process can be inspected without a model API or an unverified extraction stage.
Operational model documentation also describes connecting objects, properties, relationships, and permitted actions.104 Chapter 4 turns that idea into a concrete source-to-model contract. The implementation and maintenance scenario here are our own; no commercial platform configuration or operational deployment is claimed.
Checkpoint: what you have specified#
You have a question, source meanings, six classes, relationship directions, and expected success and failure outcomes. You should be able to explain why a stock observation is different from a catalogue part, and why two allocations need separate identities. Next, create a small ontology progressively before running the complete importer.
W3C, RDF 1.1 Concepts and Abstract Syntax — triples and IRIs; checked 14 September 2026.
W3C, OWL 2 Web Ontology Language Primer — formal descriptions and semantics; checked 14 September 2026.
E. P. Fasina, B. A. Sawyerr, and A. Murainah, On the Construction of Knowledge Graphs from Unstructured Text using Neuro-Symbolic Networks, UIJSLICTR 17(1), 121–145 (2026) — full text checked 14 September 2026. Journal publication: 7 September 2026; PDF issue imprint: June 2026.
Operational ontology overview — object, relationship, and action concepts; checked 14 September 2026. This reference does not establish deployment of the example.
2. Create the ontology yourself, one step at a time#
Start with an empty Python file. By the end of this chapter, that file will create classes, actual equipment records, relationships, validation rules, and a queryable graph. You will see exactly where each statement comes from.
Our first question is deliberately small: Which asset and part type does work order WO-204 refer to, and how many items does it require? We will build three classes for this question. Stock, allocations, inspection evidence, and source provenance enter the fuller model in chapter 3.
Step 1: Set up the tools and declare three classes#
Download and extract the ontology lab ZIP. Open a terminal inside the extracted folder, where you can see ontology_lab.py, requirements.txt, and lessons. The verified environment is Python 3.12.10 with the exact dependency versions in requirements.txt.
On Windows, run:
python --version
python -m venv .venv
.venv/Scripts/python.exe -m pip install -r requirements.txtOn macOS or Linux, use your installed Python 3.12 interpreter to create the environment and replace .venv/Scripts/python.exe with .venv/bin/python in subsequent commands. Those alternative paths are provided for portability; the included execution record was produced on Windows. Package setup downloads dependencies. The lesson itself reads and writes local files.
Create an empty file called lessons/my_ontology.py in your editor. Add the code below. The imports provide the RDF graph and SHACL validator; M is an identifier namespace. These example.org identifiers are names for things, not addresses the script must fetch.
import csv
import json
from pathlib import Path
from pyshacl import validate
from rdflib import Graph, Literal, Namespace
from rdflib.namespace import OWL, RDF, RDFS, XSD
M = Namespace("https://example.org/maintenance/")
HERE = Path(__file__).resolve().parent
def save(graph, output, name):
output.mkdir(parents=True, exist_ok=True)
graph.serialize(destination=output / name, format="turtle")
print(f"{name}: {len(graph)} triples")
def create_schema():
graph = Graph()
graph.bind("m", M)
graph.bind("owl", OWL)
for kind in (M.Asset, M.PartType, M.WorkOrder):
graph.add((kind, RDF.type, OWL.Class))
return graph
output = HERE / "my-results"
graph = create_schema()
save(graph, output, "01_classes.ttl")Run the file:
.venv/Scripts/python.exe lessons/my_ontology.pyYou should see 01_classes.ttl: 3 triples. Open lessons/my-results/01_classes.ttl. It declares Asset, PartType, and WorkOrder as OWL classes. It does not contain an actual generator yet.
A class describes a kind of thing. An instance identifies one thing of that kind. RDF stores statements as triples: subject, relationship, and value or linked object.201 For example, “GEN-17 is an Asset” is one statement. The complete collection of these statements is the graph.
Step 2: Add properties and two actual records#
Append this block to the same file, below the previous code. Do not replace the earlier steps.
def add_properties_and_entities(graph):
for predicate, target_class in ((M.asset, M.Asset), (M.part, M.PartType)):
graph.add((predicate, RDF.type, OWL.ObjectProperty))
graph.add((predicate, RDFS.label, Literal(str(predicate).rsplit("/", 1)[-1])))
graph.add((predicate, RDFS.range, target_class))
for predicate, datatype in (
(M.recordId, XSD.string),
(M.requiredQuantity, XSD.integer),
(M.unit, XSD.string),
):
graph.add((predicate, RDF.type, OWL.DatatypeProperty))
graph.add((predicate, RDFS.range, datatype))
asset = M["id/ORG-TRAIN/asset/GEN-17"]
part = M["id/ORG-TRAIN/part/PF-9"]
for entity, kind, record_id in (
(asset, M.Asset, "GEN-17"), (part, M.PartType, "PF-9")
):
graph.add((entity, RDF.type, kind))
graph.add((entity, M.recordId, Literal(record_id)))
add_properties_and_entities(graph)
save(graph, output, "02_schema-and-entities.ttl")Run the same command again. The new file 02_schema-and-entities.ttl has 19 triples. Open it and find GEN-17 and PF-9.
We used two kinds of properties. m:asset and m:part point to other graph objects. m:recordId, m:requiredQuantity, and m:unit carry literal values such as a string or integer. A property label gives it a readable name.
Why is PF-9 a PartType instead of an Asset? PF-9 identifies a catalogue type of filter. GEN-17 identifies one physical generator. If individual filters need serial numbers, ownership, or maintenance history, introduce a PartInstance class with its own identity. A catalogue type and one physical item answer different questions.
The range statements describe the kinds of targets those relationships have. They are not an error-handling mechanism. OWL supports logical descriptions and inference; missing information is not automatically false.202 We will explicitly require valid links and values with SHACL in step 4. This lesson does not run an OWL reasoner.
Step 3: Turn one source row into a work order#
Open lessons/orders.csv. It contains exactly this fictional row:
order_id,org,asset_ref,part_ref,required_quantity,unit
WO-204,ORG-TRAIN,GENERATOR_017,FILTER_PF9,2,eachThe source system says GENERATOR_017; our canonical asset ID is GEN-17. We cannot invent that match from spelling similarity. The small reviewed alias dictionary below maps the source identifiers to records we have already created. Its keys include object kind and organization so two different contexts cannot silently share a mapping.
Original mapping diagram. The reference implementation reads a reviewed alias table; it does not ask an LLM to merge similar-looking equipment names.
Select a box to inspect its role. On a small screen, scroll the diagram sideways.A structured source record has an identifier in the source system. Preserve that identifier and the file hash so the mapping can be inspected later.
Read the complete diagram as text
- Asset-register row: A structured source record has an identifier in the source system. Preserve that identifier and the file hash so the mapping can be inspected later.
- Work-order reference: A work order may use a different source identifier for the same generator. A relationship is published only when the approved mapping resolves that identifier.
- Unrecognised alias: An unknown identifier is a failure to resolve identity. It is not automatically merged with the closest name.
- Reviewed alias table: The mapping explicitly associates a source identifier with its canonical asset. Mapping changes should receive review because a wrong merge can attach every later observation to the wrong asset.
- Canonical Asset: The resulting RDF identifier represents the canonical asset. It connects the work order and inspection records without discarding their original source identities.
- Stop for correction: Resolve the source identity and update the reviewed mapping before rebuilding. A failed record does not become an apparently verified relationship.
- Asset-register row → lookup → Reviewed alias table
- Work-order reference → lookup → Reviewed alias table
- Reviewed alias table → maps to → Canonical Asset
- Unrecognised alias → reject → Stop for correction
Append the following code. The checks happen before the graph receives the work-order statements.
def map_order(graph, source):
with source.open(newline="", encoding="utf-8") as handle:
reader = csv.DictReader(handle)
expected = ["order_id", "org", "asset_ref", "part_ref", "required_quantity", "unit"]
if reader.fieldnames != expected:
raise ValueError("Unexpected CSV columns")
rows = list(reader)
if len(rows) != 1:
raise ValueError("This lesson accepts exactly one source row")
row = rows[0]
if row["order_id"] != "WO-204" or row["org"] != "ORG-TRAIN":
raise ValueError("This lesson only models WO-204 in ORG-TRAIN")
aliases = {
("asset", "ORG-TRAIN", "GENERATOR_017"): M["id/ORG-TRAIN/asset/GEN-17"],
("part", "ORG-TRAIN", "FILTER_PF9"): M["id/ORG-TRAIN/part/PF-9"],
}
asset = aliases.get(("asset", row["org"], row["asset_ref"]))
part = aliases.get(("part", row["org"], row["part_ref"]))
if asset is None or part is None:
raise ValueError("Unknown alias: review the source mapping")
if (asset, RDF.type, M.Asset) not in graph or (part, RDF.type, M.PartType) not in graph:
raise ValueError("Mapped identity is absent from the canonical register")
quantity_text = row["required_quantity"]
if not quantity_text.isascii() or not quantity_text.isdecimal() or not 1 <= int(quantity_text) <= 999:
raise ValueError("Quantity must be a whole number from 1 to 999")
if row["unit"] != "each":
raise ValueError("This lesson accepts each, not boxes or other units")
order = M["id/ORG-TRAIN/order/WO-204"]
for predicate, value in (
(RDF.type, M.WorkOrder), (M.recordId, Literal(row["order_id"])),
(M.asset, asset), (M.part, part),
(M.requiredQuantity, Literal(int(quantity_text), datatype=XSD.integer)),
(M.unit, Literal(row["unit"])),
):
graph.add((order, predicate, value))
return order
order = map_order(graph, HERE / "orders.csv")
save(graph, output, "03_mapped-order.ttl")Run the file again. 03_mapped-order.ttl now has 25 triples. The new order contributed six statements: its class, record ID, asset link, part link, required quantity, and unit.
Find the order's m:asset property in the Turtle file. Its value is GEN-17's full identifier, not a copied generator name. Follow that identifier to the asset's own statements. That link is what allows a later query to join the two records without guessing what the text means.
| Source field | Graph result | Decision made during mapping |
|---|---|---|
order_id | The order identifier and m:recordId | This lesson accepts the single specified order |
asset_ref | m:asset → canonical GEN-17 | A reviewed alias must resolve to an existing Asset |
part_ref | m:part → canonical PF-9 | A reviewed alias must resolve to an existing PartType |
required_quantity | m:requiredQuantity → typed integer 2 | Reject fractions, zero, negative values, and values above this lesson's 999-item bound |
unit | m:unit → each | Reject unsupported units instead of inventing a conversion |
This is a small, intentional importer. The full lab reads canonical records and aliases from separate CSV files and supports more than one organization. An organization code in a local file is a scope value; it does not authenticate the person opening that file.
Checkpoint: Change GENERATOR_017 to GENERATOR_018 in this lesson CSV and rerun. It must stop with “Unknown alias: review the source mapping.” Restore the original value before continuing. Existing generated files may remain from the earlier successful run, so only a successful current run establishes that the outputs match your source.
Step 4: Make a rule pass, then deliberately break it#
A SHACL shape is an executable acceptance rule for the supplied graph.203 This shape requires one asset link, one part link, one positive integer requirement within the chosen bound, and the unit each.
Append this block. First it validates the real graph. Then it makes a separate copy and changes the quantity to the string "two". The original graph stays valid.
SHAPES = """
@prefix m: <https://example.org/maintenance/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
m:WorkOrderShape a sh:NodeShape ;
sh:targetClass m:WorkOrder ;
sh:property [ sh:path m:asset ; sh:minCount 1 ; sh:maxCount 1 ; sh:class m:Asset ] ;
sh:property [ sh:path m:part ; sh:minCount 1 ; sh:maxCount 1 ; sh:class m:PartType ] ;
sh:property [ sh:path m:requiredQuantity ; sh:minCount 1 ; sh:maxCount 1 ;
sh:datatype xsd:integer ; sh:minInclusive 1 ; sh:maxInclusive 999 ] ;
sh:property [ sh:path m:unit ; sh:minCount 1 ; sh:maxCount 1 ; sh:in ( "each" ) ] .
"""
def check(graph):
return validate(
graph, shacl_graph=Graph().parse(data=SHAPES, format="turtle"),
inference="none", advanced=False, js=False, do_owl_imports=False,
)
def demonstrate_validation(graph, order, output):
(output / "04_shapes.ttl").write_text(SHAPES, encoding="utf-8")
conforms, report_graph, report_text = check(graph)
(output / "04_valid-report.txt").write_text(report_text, encoding="utf-8")
if not conforms:
raise ValueError("The valid teaching graph failed SHACL")
broken = Graph()
for triple in graph:
broken.add(triple)
broken.set((order, M.requiredQuantity, Literal("two")))
broken.serialize(destination=output / "05_broken-graph.ttl", format="turtle")
bad_conforms, bad_report, bad_text = check(broken)
(output / "05_invalid-report.txt").write_text(bad_text, encoding="utf-8")
bad_report.serialize(destination=output / "05_invalid-report.ttl", format="turtle")
if bad_conforms:
raise ValueError("The deliberately broken graph unexpectedly passed")
print(f"valid graph conforms: {conforms}; broken graph conforms: {bad_conforms}")
demonstrate_validation(graph, order, output)Run the file. In addition to the three graph counts, it should print:
valid graph conforms: True; broken graph conforms: FalseOpen 04_valid-report.txt and 05_invalid-report.txt. The second report identifies the affected work order, m:requiredQuantity, and a datatype violation. 05_broken-graph.ttl contains the actual broken graph, so you can inspect the input that failed. No failure message has been hard-coded as a substitute for validation.
Why validate after checking the CSV? The importer catches source-format and identity problems early. SHACL checks the resulting graph's contract. It still catches a wrong value if a different importer or a later graph edit bypasses those source checks. Neither check proves that the warehouse or maintenance record is physically correct.
Step 5: Query the relationships you created#
Append the final block. SPARQL is a query language for RDF graphs. Read the pattern as a connected question: find this order, follow its asset and part links, then return their record IDs and the order's quantity and unit.
QUERY = """
PREFIX m: <https://example.org/maintenance/>
SELECT ?assetId ?partId ?quantity ?unit WHERE {
?order a m:WorkOrder ; m:asset ?asset ; m:part ?part ;
m:requiredQuantity ?quantity ; m:unit ?unit .
?asset m:recordId ?assetId .
?part m:recordId ?partId .
}
"""
def query_order(graph, order, output):
conforms, _, _ = check(graph)
if not conforms:
raise ValueError("Query refused: graph does not conform to the shapes")
rows = [
{"assetId": str(row.assetId), "partId": str(row.partId),
"quantity": int(row.quantity), "unit": str(row.unit)}
for row in graph.query(QUERY, initBindings={"order": order})
]
(output / "06_order.rq").write_text(QUERY, encoding="utf-8")
(output / "06_query-result.json").write_text(json.dumps(rows, indent=2) + "\n", encoding="utf-8")
print(json.dumps(rows))
return rows
query_order(graph, order, output)Run the file again. The final line should be:
[{"assetId": "GEN-17", "partId": "PF-9", "quantity": 2, "unit": "each"}]Open 06_order.rq to inspect the query and 06_query-result.json to inspect its result. The query checks graph conformity before returning an answer and binds the requested order as an RDF value. It does not accept arbitrary query text or perform a business action.
You have now created an ontology schema and populated it from a source row. The query result came from the explicit relationships and values in your graph. It was not generated by a language model.
Check your work and know what to add next#
The ZIP includes lessons/build_step_by_step.py, which contains the same construction functions and a runner. Use it to compare against your own file; it writes to lessons/results/, keeping your my-results/ separate. Run the reference and its tests with:
.venv/Scripts/python.exe lessons/build_step_by_step.py
.venv/Scripts/python.exe -m unittest discover -s lessons -p test_progressive.py -vSeven tests were executed successfully. They check the staged graph counts and actual query result, a real SHACL failure, unknown identity, wrong organization, unsupported units, fractional quantity, and refusal to query a graph missing its required asset link. Triple serialization order and blank-node names can differ; those are not changes to the graph's meaning.
| What you created | What you should verify |
|---|---|
| Three classes plus properties | A class is a category, not one specific asset |
| Two canonical records and a mapped order | The order links to existing identifiers |
| An explicit source-to-graph mapping | Unknown references fail instead of becoming guessed links |
| Valid and invalid SHACL reports | The string "two" genuinely fails the integer rule |
| A fixed SPARQL query and JSON output | GEN-17 needs 2 items of part type PF-9 for this order |
Why not add all possible classes now? Introduce a concept when a question needs its identity, properties, relationships, or history. The full baseline adds StockSnapshot because inventory changes over time, Allocation because reserved and quarantined units need separate evidence, and InspectionEvidence because a status has a source and observation time. A Depot can remain a controlled value until you need depot-specific properties or relationships. More classes alone do not produce a more useful ontology.
This lesson deliberately does not calculate availability. Chapter 3 runs the complete baseline, which adds those three classes, dated records, stock rules, and file-and-row provenance. Its populated data graph has 121 triples; do not compare that count with this lesson's 25-triple graph, which includes its small schema. Keep their output folders separate.
W3C, RDF 1.1 Concepts and Abstract Syntax: https://www.w3.org/TR/rdf11-concepts/ — identifiers and graph statements checked 14 September 2026.
W3C, OWL 2 Web Ontology Language Primer, sections 2 and 4.6: https://www.w3.org/TR/owl2-primer/ — description, inference, and open-world distinctions checked 14 September 2026.
W3C, Shapes Constraint Language (SHACL): https://www.w3.org/TR/shacl/ — validation semantics checked 14 September 2026. The lesson's shapes and data are original examples.
3. Build the graph, validate it, and follow the evidence#
Chapter 2 created a small ontology progressively, ending with 25 triples and a relationship query. Now use the complete importer to add stock snapshots, allocations, inspection evidence, and source lineage. The baseline produces 121 instance triples. The smaller lesson and this complete build use separate output directories, so you can compare them.
Keep the original fixture unchanged for this first run. An expected result is useful only if the inputs are known. The reference clock is fixed at 14 September 2026, 08:00 UTC, so the same dated records can produce reproducible freshness checks.
Step 1: Build from explicit source records#
From the lab directory, run the build with the environment created in chapter 2:
.\.venv\Scripts\python.exe ontology_lab.py buildThe command reads the CSV fixtures and constructs schema/schema.ttl, schema/shapes.ttl, results/instances.ttl, and results/dataset.trig, alongside provenance and build records. Turtle represents the populated statements; TriG carries the dataset's separate named graphs. These files are produced from the functions in ontology_lab.py; they are not the persistent place to edit the model.
Start by following the work-order row. Its fields have specific destinations:
| Source value | Graph role | Meaning preserved |
|---|---|---|
order_id=WO-204 | m:recordId on a WorkOrder | The job's canonical record identifier |
org=ORG-TRAIN | m:org | Its synthetic organization scope |
asset_ref=GENERATOR_017 | m:asset to an Asset | Canonical generator GEN-17 |
part_ref=FILTER_PF9 | m:part to a PartType | Canonical catalogue item PF-9 |
required_quantity=2 | m:requiredQuantity | A typed integer requirement |
unit=each | m:unit | The quantity counts individual filters |
The mapper resolves those aliases through the explicit table before writing the relationships. It must not create a new generator simply because a source uses a different nickname. An unresolved reference is an ingestion problem to fix, not an invitation to invent an entity.
Step 2: Inspect the instance the program created#
These selected triples show the resulting work order and its typed targets. They are a readable excerpt of the instance graph, with prefixes added for clarity:
@prefix m: <https://example.org/maintenance/> .
@prefix order: <https://example.org/maintenance/id/ORG-TRAIN/order/> .
@prefix asset: <https://example.org/maintenance/id/ORG-TRAIN/asset/> .
@prefix part: <https://example.org/maintenance/id/ORG-TRAIN/part/> .
order:WO-204 a m:WorkOrder ;
m:recordId "WO-204" ; m:org "ORG-TRAIN" ;
m:asset asset:GEN-17 ; m:part part:PF-9 ;
m:requiredQuantity 2 ; m:unit "each" .
asset:GEN-17 a m:Asset .
part:PF-9 a m:PartType .Save this complete script as inspect_order.py in the lab directory. It reads the generated RDF and prints the work order's linked records. Using the class and identifier fields makes the example independent of Turtle's formatting or statement order.
from rdflib import Graph, Namespace, RDF
m = Namespace("https://example.org/maintenance/")
graph = Graph().parse("results/instances.ttl", format="turtle")
orders = [
node for node in graph.subjects(RDF.type, m.WorkOrder)
if str(graph.value(node, m.recordId)) == "WO-204"
and str(graph.value(node, m.org)) == "ORG-TRAIN"
]
if len(orders) != 1:
raise RuntimeError("Expected exactly one scoped work order")
order = orders[0]
asset = graph.value(order, m.asset)
part = graph.value(order, m.part)
print("order:", graph.value(order, m.recordId))
print("asset:", graph.value(asset, m.recordId))
print("part:", graph.value(part, m.recordId))
print("required:", graph.value(order, m.requiredQuantity))
print("unit:", graph.value(order, m.unit))Run it using the same environment:
.\.venv\Scripts\python.exe inspect_order.pyThe supplied fixture produced these fields when the script was checked:
order: WO-204
asset: GEN-17
part: PF-9
required: 2
unit: eachYou have followed a schema-to-instance relationship using the actual graph. The script inspects just the work order; the full query also checks stock, allocations, and evidence before calculating availability.
Step 3: Run the acceptance checks#
Execute the validator explicitly:
.\.venv\Scripts\python.exe ontology_lab.py validateThe validator applies the supplied shapes and writes results/validation-report.txt and .ttl. For the baseline, its console result is:
{
"conforms": true,
"inference": "rdfs",
"truthOrAuthorizationCertified": false
}A passing report establishes that the data satisfies those selected conditions. It does not certify physical stock, legal authority, or overall maintenance readiness. SHACL evaluates the conditions provided to it; the coverage of those conditions remains an engineering decision.301
Read a failure at the level of the record and field. A missing asset relationship needs an identity or mapping correction. An invalid quantity needs the unit and source value checked. Rebuilding from corrected source records preserves the explanation of what changed; editing the generated output merely to remove an error can hide the underlying defect.
The program also checks business invariants beyond simple datatypes. North's six recorded filter units include two quarantined units and three separately reserved units. Those allocation sets must be disjoint and refer to the intended snapshot. Correct integers attached to the wrong snapshot would still produce an unreliable answer.
Step 4: Ask the maintenance question#
Run the scoped query:
.\.venv\Scripts\python.exe ontology_lab.py query --org ORG-TRAIN --order WO-204The supplied fixture produces one available filter and a shortage of one. The result retains the inputs: required two, on hand six, quarantined two, reserved three, and unit each. A bare answer of “no” would prevent the coordinator from seeing whether the problem was stock, stale evidence, or an incorrect mapping.
The command writes results/explanation.json and results/human-review-request.json. The request is marked UNSUBMITTED_LOCAL_DRAFT and changes no inventory. The organization argument selects a local test scope; Chapter 5 collects the implemented and production-required boundaries in one table.
Chapter 4 opens the query itself and examines the calculation. Before moving on, follow results/provenance.ttl back to the source row and file. The build uses PROV entities and activities to record derivation.302 Locators such as orders.csv#record=2 identify CSV records, counting the header as record 1. Hashes in results/manifest.json identify input bytes; they do not prove the supplier was correct. A later source revision requires a new, checked build.
Step 5: Place neural extraction behind a candidate boundary#
Unstructured maintenance notes would need an additional intake path. A future extractor could read “the two North filters are on hold” and propose an allocation candidate. That phrase alone does not establish which snapshot or individual units it concerns, or whether “on hold” means reservation or quarantine. Preserve the passage and ask for the missing context.
Original pipeline diagram. The top two rows are the executable CSV path. Bottom row: optional neural extension, NOT IMPLEMENTED. A text-extraction model could propose candidate records for review; no neural model is run by this lab.
Select a box to inspect its role. On a small screen, scroll the diagram sideways.Read source records with explicit identifiers, units, organization scope and timestamps. Keep immutable input hashes with the output generation.
Read the complete diagram as text
- CSV source files: Read source records with explicit identifiers, units, organization scope and timestamps. Keep immutable input hashes with the output generation.
- Resolve & normalize: Apply the reviewed identity mapping and typed conversions. Unknown identities or inconsistent records are rejected instead of guessed.
- Build RDF records: Create instances of the ontology classes and their relationships using RDFLib. Attach source provenance so a query result can be traced back.
- Run SHACL checks: Check required fields, data types, relationships and relevant integrity rules. OWL domain and range statements are not a substitute for this validation gate.
- Inspect the result: Read the validation report and correct failed inputs or mappings. A successful syntax parse alone is not sufficient to publish trusted data.
- Query the graph: Use the validated graph to answer the sample maintenance question. Reviewable evidence and an explicit shortage are the output; no stock is allocated by this tutorial.
- Optional: source text: NOT IMPLEMENTED. A separately approved text or document source could feed an extraction stage. The article paper studies text-to-graph extraction; the downloadable lab starts with CSV records.
- Optional: extract candidates: NOT IMPLEMENTED. A model could propose entities and relationships with quoted evidence and uncertainty. A proposal would still need identity resolution, validation and review.
- Optional: review candidates: NOT IMPLEMENTED. A reviewer would compare the candidates against the source and publish reviewed structured records at the intake boundary. No neural output enters this lab.
- CSV source files → read → Resolve & normalize
- Resolve & normalize → map → Build RDF records
- Build RDF records → check → Run SHACL checks
- Run SHACL checks → report → Inspect the result
- Inspect the result → use → Query the graph
- Optional: source text → propose → Optional: extract candidates
- Optional: extract candidates → review → Optional: review candidates
The proposed extension is: source passage → candidate entities and relationships → canonical identity resolution → explicit checks → human review → published facts. It is not implemented in this lab. The current runnable path ingests the structured CSV fixture deterministically. The PDF extraction guide explains the separate document-reading problem.
Fasina, Sawyerr, and Murainah report precision 0.822, recall 0.824, and F1 0.823 for their WebNLG text-to-RDF experiment.303 Those are the authors' measurements, not results from this maintenance lab. Their approach motivates checking extracted candidates; a high extraction score still does not establish physical stock or operational authority.
An extractor's confidence is a review signal, not a truth label. OWL's open-world semantics also mean missing knowledge is not automatically false; OWL is not a validation firewall.304 Keep uncertainty, contradictions, and unresolved references outside accepted operational facts until the relevant checks and reviewers resolve them.
For a future extraction test, label the expected facts first. If a synthetic trial has 9 correct predictions, 3 incorrect predictions, and 1 missed fact, its illustrative scores are:
Precision = TP ÷ (TP + FP) = 9 ÷ 12 = 0.75
Recall = TP ÷ (TP + FN) = 9 ÷ 10 = 0.90
F1 = 2TP ÷ (2TP + FP + FN) = 18 ÷ 22 ≈ 0.818That arithmetic is a teaching example, not an executed neural evaluation. Record which kinds of mistakes matter: confusing quarantine with reservation may be more consequential than missing a descriptive nickname.
Checkpoint: what you created and what to inspect#
You have the complete schema, shapes, populated RDF, source lineage, validation report, and one-filter shortage result. Confirm that the inspection script prints GEN-17, PF-9, two, and each. Find orders.csv#record=2 in the provenance and compare its source values with the graph. Chapter 4 provides an interactive viewer of these actual exported records and explains the query and integration contract.
W3C, Shapes Constraint Language — data-graph validation and report semantics; checked 14 September 2026.
W3C, PROV-O: The PROV Ontology — entity, activity, and derivation terms; checked 14 September 2026.
E. P. Fasina, B. A. Sawyerr, and A. Murainah, On the Construction of Knowledge Graphs from Unstructured Text using Neuro-Symbolic Networks, UIJSLICTR 17(1), 121–145 (2026), table 6, printed page 138; discussion, pages 139–140 — full text checked 14 September 2026. This was a text-to-RDF research experiment.
W3C, OWL 2 Web Ontology Language Primer — sections 2 and 4.2, open-world distinction; checked 14 September 2026.
4. Inspect the real graph and define the integration contract#
You now have a populated graph and a checked result. This chapter connects the two: follow WO-204 to its asset and part, inspect the stock and allocations, and open the source rows behind those facts. Then define the contract another application would need to use the same model.
Step 1: Explore the records that were actually built#
The explorer below contains an export of the verified, fictional ORG-TRAIN baseline graph. It is generated from the lab artifacts. It is not a drawing of records that might exist. Selecting a record changes only this page; the viewer makes no model or business-system calls.
Follow WO-204 from graph to source
These 7 records and 6 relationships come from the lab’s generated RDF. Select a record, follow a relationship, then inspect the original source row.
Fictional scope: ORG-TRAIN · Saved comparison time: . This is a fixed teaching snapshot.
On a narrow screen, scroll the graph sideways. Every record is also available in the selector below.
WO-204 WorkOrder
- org
- ORG-TRAIN
- recordId
- WO-204
- requiredQuantity
- 2integer
- unit
- each
Follow a relationship
- WO-204 → asset →
- WO-204 → part →
Inspect the exported build identity
Schema version: 1.0.0
Input fingerprint: 51b127fe0d01615e8c3137bf0c1b8dc45983b01c1ce0ea5d6b015258520d629f
Importer SHA-256: 0f956fe682b1a33f108cfd5420a34a29ddb479e96c12c2b75cdc2650aea080ff
Exporter SHA-256: de404043247cc314794a17c1cecd59f34b7c6f6ddaefcdd291dac2fc4bb476bf
Start at WO-204. Follow its asset relationship to GEN-17 and its part relationship to PF-9. Inspect the property requiredQuantity, then open its orders.csv source record. Follow a stock snapshot and its allocations to see which values supplied the shortage calculation.
Notice the difference between a relationship and a literal property: m:part points to another identified record; m:requiredQuantity contains an integer. The source row uses FILTER_PF9, while the graph uses the canonical PF-9 identity. The alias record explains that transformation. File hashes identify the bytes used for this export; their presence does not independently certify the source.
Step 2: Ask the graph directly#
For a small executable inspection, save the following as inspect_graph.py in the lab directory after Chapter 3's build:
from rdflib import Graph
graph = Graph().parse("results/instances.ttl", format="turtle")
query = """
PREFIX m: <https://example.org/maintenance/>
SELECT ?asset ?part ?needed ?unit
WHERE {
?order a m:WorkOrder ;
m:org "ORG-TRAIN" ;
m:recordId "WO-204" ;
m:asset ?asset ;
m:part ?part ;
m:requiredQuantity ?needed ;
m:unit ?unit .
}
"""
for row in graph.query(query):
print(tuple(str(term) for term in row))Run .\.venv\Scripts\python.exe inspect_graph.py. The supplied fixture returns the GEN-17 asset IRI, PF-9 part IRI, 2, and each. Each ?name is a variable; the patterns find records having all of the stated relationships and values. SELECT chooses the returned columns.401 An empty result means this pattern found no match, so first inspect the identifiers and mapping.
The inspection query only reads relationships. Run the full business query to apply the availability and evidence checks:
.\.venv\Scripts\python.exe ontology_lab.py query --org ORG-TRAIN --order WO-204Open queries/order_inventory.rq. It binds the organization and order ID through RDFLib's initBindings, joins the matching asset, part, and stock, then sums allocations by kind. The code requires exactly one matching snapshot. It does not combine depots or identify which work order owns each reservation. The --org argument selects a local demonstration scope; it is not a login.
Establish quantities at their own grain: one stock snapshot and its allocations. Joining every inspection record into that sum could repeat the stock row and inflate it. DISTINCT cannot repair every bad join because two legitimate allocations can have the same quantity. Attach descriptive evidence after calculating the correctly scoped quantities.
Step 3: Specify each field and relationship#
A reusable integration needs more than class names. For the baseline, these are actual mappings and cardinalities; “one” means exactly one value on each accepted record. SHACL checks selected cardinalities, while the importer and query supply additional business checks.
| Source fields | Model destination | Type and multiplicity |
|---|---|---|
orders.csv: order_id, org | WorkOrder identity and scope | One canonical ID and one organization |
asset_ref | WorkOrder m:asset → Asset | One resolved asset |
part_ref, required_quantity, unit | WorkOrder m:part, m:requiredQuantity, m:unit | One part; positive integer; each |
inventory.csv: snapshot_id, part_ref, depot | StockSnapshot identity, m:part, m:depot | One part and one depot value |
on_hand, unit_ids, observed_at, revision | Snapshot quantity, physical unit values, time, revision | Nonnegative integer; distinct unit IDs; timezone-aware timestamp; positive revision |
allocations.csv: snapshot_id, kind, quantity | Allocation m:snapshot, m:kind, m:quantity | One snapshot; reserved or quarantined; nonnegative integer |
inspections.csv: asset_ref, status, source_version | Evidence m:asset, m:status, m:sourceVersion | One asset, enumerated status, and source-version string |
The record key is the full IRI built from organization, entity kind, and canonical ID. It is not a display name. A WorkOrder has one asset; an Asset may have several orders. Each Allocation has one snapshot; a snapshot may have several allocations. The baseline's one-part WorkOrder constraint is deliberate and is replaced with requirement-line objects in Chapter 6.
Document who may establish or revise each fact. The following ownership and update rules are a proposed integration contract for a real pilot, rather than services supplied by the local lab:
| Producer and owner | Authoritative contribution | Update rule to implement |
|---|---|---|
| Equipment register / asset steward | Asset identity and approved aliases | Review identity changes; preserve existing references |
| Maintenance planning / job owner | Work-order request | Version changed requirements; do not infer approval from creation |
| Stock control / depot owner | Counts, unit identities, allocation state | Publish a coherent snapshot; reject conflicting revisions |
| Inspection register / evidence owner | Observation, status, source version | Preserve the observed time and source revision |
Object, property, and relationship documentation provides useful implementation concepts, but it does not choose these domain contracts for you.403404405 The keys, source meanings, cardinalities, and ownership decisions above must be reviewed against the actual organization.
Step 4: Choose components for concrete responsibilities#
Original architecture image. Solid connections show the local executable path. The bottom row identifies services a deployed application would still need; those services are not silently claimed as part of the lab.
Select a box to inspect its role. On a small screen, scroll the diagram sideways.The bounded sample data and reviewed identity map are the lab inputs. Real source connectors would need ownership, change detection and access policy.
Read the complete diagram as text
- Versioned input files: The bounded sample data and reviewed identity map are the lab inputs. Real source connectors would need ownership, change detection and access policy.
- Python + RDFLib: The build command creates the RDF/OWL model and maps records into a graph. There is no graph-database service or LLM API hidden behind this step.
- Ontology + instances: Schema, instance data and provenance are concrete files that can be inspected independently of the website. A database service becomes a separate deployment choice.
- Query & review output: The sample query computes availability and shortage and emits its explanation. This output supports a review; it does not issue a command to a maintenance or inventory system.
- RDFLib SPARQL: The query path uses explicit organization scope and typed relationships. The CLI scope selector is a test boundary, not enterprise authentication.
- pySHACL validation: Run the supplied shape and integrity checks before using the graph. Keep the report and failed-record evidence.
- Deployment: identity: Replace the demonstration scope selector with verified principals and server-enforced policy. Protect data, derived outputs, logs and exports.
- Deployment: workflow: A durable scheduler and controlled command handler would manage publication, retries, approval expiry and downstream changes. They require their own tests.
- Deployment: operations: Backups, monitoring, schema migration, secret handling and approved network boundaries are additional operational responsibilities.
- Versioned input files → ingest → Python + RDFLib
- Python + RDFLib → write → Ontology + instances
- Ontology + instances → validate → pySHACL validation
- pySHACL validation → gate → RDFLib SPARQL
- RDFLib SPARQL → answer → Query & review output
| Component in the lab | Responsibility | Add a service when… |
|---|---|---|
| CSV and JSON fixtures | Repeatable source records | Approved systems must publish updates |
| Python importer | Identity resolution and typed mappings | Updates need scheduling, retries, and quarantine queues |
| RDFLib and RDF files | Store inspectable statements and execute SPARQL402 | Concurrent access, query volume, or availability requires a managed store |
| pySHACL plus business checks | Reject specified invalid records | Several producers need a shared ingestion release gate |
| Query and review JSON | Explain a bounded request | Authenticated users need controlled views or approved actions |
This architecture keeps source interpretation separate from query logic. If you later replace file storage with a graph service, use the same fixture and expected results to check the migration before measuring throughput.
Step 5: Reuse the model for a second question#
Chapter 6 asks which requirement lines of WO-204 are short, and which stock and allocation records explain each line? Its new PartRequirement object owns a part and quantity together. Adding a seal to the filter requirement then produces two independently inspectable paths.
That distinction matters: two m:part values and two quantities placed directly on one WorkOrder would not say which quantity belongs to which part. Giving each line an identity removes that ambiguity. The extension keeps the existing asset, part, stock, and evidence relationships and changes the portion of the model that the new question requires.
Checkpoint: what you can inspect and hand over#
You can trace the actual graph to a CSV record, run a relationship query, explain the aggregation grain, and provide a source-to-model contract. Check that the explorer and results/explanation.json agree on six on hand, two quarantined, three reserved, one available, and one short. Next, preserve that result through an explicit orchestration sequence.
W3C, SPARQL 1.1 Query Language — graph patterns and result columns; checked 14 September 2026. The query is original to this lab.
RDFLib, Getting started — graph parsing and queries; checked 14 September 2026.
Object types and instances — operational model concepts; checked 14 September 2026.
Declared property types — property modelling; checked 14 September 2026.
Relationships between object types — link modelling; checked 14 September 2026. The maintenance mapping and proposed ownership rules are this guide's design.
5. Orchestrate the calculation and preserve a reviewable result#
The query is one stage of the application. A reproducible run records which inputs were transformed, whether validation succeeded, what was requested, and where processing stopped. Here, orchestration is a fixed sequence of commands and artifacts. The sequence has no uncertain planning step that needs an autonomous agent.
Step 1: Run dependent stages with failure handling#
Run these commands from the extracted lab directory using the environment created in Chapter 2. PowerShell must inspect each external program's exit code so a failed stage prevents the next one from running:
.\.venv\Scripts\python.exe ontology_lab.py build
if ($LASTEXITCODE -ne 0) { throw "Build failed; stop here." }
.\.venv\Scripts\python.exe ontology_lab.py validate
if ($LASTEXITCODE -ne 0) { throw "Validation failed; inspect the report." }
.\.venv\Scripts\python.exe ontology_lab.py query --org ORG-TRAIN --order WO-204
if ($LASTEXITCODE -ne 0) { throw "Query failed; inspect the reported condition." }A successful process exit and sufficient stock are different outcomes. The command can successfully explain a shortage. An invalid graph follows the failure path. A nonempty JSON file may contain a diagnostic from a rejected stage, so inspect its status.
build performs the initial SHACL check. validate reopens and checks the generated artifacts. Before a query, load_verified() compares source, importer, query, and generated-artifact hashes with results/manifest.json. Changed inputs require an explicit rebuild.
Original process diagram for the reference implementation. Running a command again should reproduce the graph from its inputs; an external approval-and-write service is a separate extension.
Select a box to inspect its role. On a small screen, scroll the diagram sideways.Identify the exact files, schema and mapping used for this run. Mixing snapshots can produce an answer that never corresponded to any real stock state.
Read the complete diagram as text
- Read the source set: Identify the exact files, schema and mapping used for this run. Mixing snapshots can produce an answer that never corresponded to any real stock state.
- Build the graph: Create canonical objects, links and provenance. Stop on source or identity errors rather than guessing a successful result.
- Validate the graph: Inspect SHACL and integrity results. A failed validation must prevent the normal proposal path from using the failed build.
- Run the scoped query: Select the requested organization and work order, then check part, stock, allocation and inspection evidence. This is deterministic data processing.
- Expose the blockers: Six on hand minus two quarantined and three reserved leaves one. Requiring two therefore produces a shortage of one.
- Emit a review request: The artifact records the facts and blockers for a reviewer. It grants no permission and performs no live inventory change.
- Read the source set → load → Build the graph
- Build the graph → test → Validate the graph
- Validate the graph → use → Run the scoped query
- Run the scoped query → compute → Expose the blockers
- Expose the blockers → record → Emit a review request
The dependencies are concrete: declared source rows produce the graph; validation applies to that graph; a scoped query produces the explanation and local review draft. Keep these artifacts together so another engineer can reproduce the result.
Step 2: Read the implemented boundary in one place#
| Capability | Implemented and exercised here | Required for a real service |
|---|---|---|
| Data ingestion | Fixed CSV contracts, explicit aliases, typed RDF mappings | Approved connectors, feed completeness, failure ownership, and refresh policy |
| Validation | SHACL and explicit identity, unit, allocation, and freshness checks | Domain-specific acceptance coverage and trusted source verification |
| Scope | A CLI selector used by fixed, scoped queries | Verified identity, authorization, and protected data access |
| Integrity and lineage | Local hashes, source locators, and PROV records | Protected audit records and appropriately trusted ingestion |
| Review | An unsubmitted JSON draft bound to its input revision | Authenticated reviewer, approval expiry, and current-state rechecks |
| External actions | No stock reservation, purchase, or equipment release | Authorized command handling, concurrency control, receipts, and reconciliation |
| AI extraction | No model call or neural extraction stage | Candidate extraction evaluation and a review process before facts are accepted |
The local --org flag is not an access-control boundary: anyone with the downloaded files can inspect all their contents. A deployed service must derive scope from verified identity and policy. ABAC is one policy model using requester, resource, operation, and environment attributes.502 A hidden record or property must not silently become a negative business fact.
Step 3: Explain the quantity and the proposal#
The baseline's physical unit sets are disjoint: two quarantined units and three other reserved units within six on hand. The calculation therefore yields:
Available = 6 − 2 − 3 = 1
Shortage = max(0, 2 − 1) = 1The max means extra usable stock produces zero shortage. It does not justify hiding impossible negative availability. If allocations exceed stock, or the same unit appears in both excluded sets, the inputs require correction.
The query writes results/explanation.json and results/human-review-request.json. These selected review fields come from the baseline run:
{
"type": "human-review-request",
"orderId": "WO-204",
"org": "ORG-TRAIN",
"status": "UNSUBMITTED_LOCAL_DRAFT",
"shortage": 1,
"basisSchemaVersion": "1.0.0",
"basisSnapshotRevision": 1,
"requiresFreshnessAndAuthorizationRecheck": true,
"approvalImplemented": false,
"businessWritesExecuted": false
}The complete draft also includes basisInputFingerprint. It has not been delivered to a reviewer. A coordinator can inspect what was requested, which evidence revision supported it, and why another filter is needed. That is the completed output of the baseline workflow.
Step 4: Test freshness separately from structure#
A well-typed integer may still be too old to use. fixtures/scenario.json sets a fixed demonstration clock of 14 September 2026, 08:00 UTC, and a 7,200-second freshness allowance. The baseline's stock, two allocations, and inspection were observed at 07:30, giving four fresh records out of four required.
To test the boundary without editing the fixtures, run:
.\.venv\Scripts\python.exe ontology_lab.py query --org ORG-TRAIN --order WO-204 --as-of 2026-09-15T08:00:00ZInspect results/explanation.json: status becomes insufficient_evidence, while available and shortage become null. Rerun the original query to restore the baseline output. The command accepts an explicit clock for repeatable tests; it does not fetch current depot records.
A connector must distinguish observation time from ingestion time. Downloading an old spreadsheet today does not create a new stock observation. SHACL checks graph conditions; the freshness policy and physical meaning of an observation are separate responsibilities.501
Step 5: Explore changed quantities locally#
Inspect the stock calculation and proposal gates
Change the fictional PF-9 quantities and inspect why the proposal passes or stops. These controls calculate an example in your browser; they do not change an inventory system.
Available = 6 − 2 − 3 = 1. Shortage = 1.
The request is short by 1. Reviewer approval cannot remove a stock shortage.
- Quarantined: 2
- Reserved: 3
- Available: 1
The calculation assumes quarantined and reserved quantities are disjoint subsets of the on-hand count, and every quantity uses the same part, depot, unit and source snapshot. The downloadable lab also validates those source relationships.
With seven units on hand and the same two quarantined and three reserved, two remain available. That removes the quantity shortage. The exercise lets you change quantity, freshness, scope, and a simulated approval state to see which condition controls the outcome.
The controls are teaching inputs. They do not authenticate a supervisor or change inventory. The equation can be satisfied while a real maintenance process still requires other evidence or authorization.
Step 6: Define an action contract before connecting a system#
If a later pilot reserves stock, name a separate operation, for example RequestStockReservation. Agree on the following proposed contract before writing its adapter. This contract is an implementation specification, not an additional command in the download.
| Contract element | Concrete requirement |
|---|---|
| Request identity | Unique operation ID and authenticated requester; derive permitted organization server-side |
| Intended change | Exact work order, requirement line, part, unit, quantity, depot, and selected snapshot |
| Decision basis | Input fingerprint, model version, source revision, approval identity, and expiry |
| Submission checks | Current authorization, fresh evidence, approved requirement, and sufficient stock |
| Concurrent change | Compare the expected revision with current state; reject and re-evaluate on conflict |
| Result and retry | Durable operation receipt; reuse the operation ID; reconcile ambiguous downstream outcomes |
Submission conditions need both business criteria and permissions.503 Passing a quantity check alone is insufficient. If another job reserves the last unit after review, the stale proposal must not proceed with its earlier arithmetic.
An external system can succeed while a later local update fails; documented writeback behavior illustrates why these effects cannot simply be assumed to form one transaction.504 After a timeout, inspect the operation's recorded outcome before retrying. The adapter and downstream service must agree on operation identity and recovery behavior.
Checkpoint: what your workflow guarantees#
You have an ordered build, validation, and query sequence; a result tied to source revisions; and a local review draft. You can demonstrate that stale evidence withholds the calculation. You also have a concrete specification for a future action service. Chapter 6 now changes the ontology itself and proves the new requirement-line model with data, queries, and failure tests.
W3C, Shapes Constraint Language — validation against graph conditions; checked 14 September 2026. Freshness and inventory policies are explicit lab choices.
NIST, SP 800-162: Attribute Based Access Control — attributes and policy decision model; checked 14 September 2026.
Action submission criteria — configured submission conditions; checked 14 September 2026. The proposed reservation contract is this guide's design.
External writeback and webhook behavior — external success with later ontology failure remains possible; checked 14 September 2026. No webhook was configured or invoked.
6. Extend the ontology: one work order, several required parts#
The maintenance team changes its question: “WO-204 needs two filters and one replacement seal. Which requirement is blocked, and by which evidence?” The original model cannot express those two part-and-quantity pairs safely. This chapter implements the change, migrates the existing records, and tests the resulting graph.
You will create a PartRequirement class, preserve the original order identity, add three source files, and run a query that keeps each part's evidence and shortage separate. The complete downloadable lab includes the finished extension under extensions/multipart/; its implementation is multipart_lab.py. All steps below run locally with the Python environment prepared in Chapter 2.
Step 1 — identify the modelling failure before adding fields#
The first model has one part and one requiredQuantity directly on a WorkOrder. Adding a second value to both properties would say “this order has PF-9 and SEAL-2, and quantities 2 and 1.” It would not say which quantity belongs to which part. A query could produce the wrong pairings. A comma-separated string such as PF-9:2,SEAL-2:1 would hide that structure inside text.
Give each requirement its own identity. WO-204-R1 owns the filter/count pair; WO-204-R2 owns the seal/count pair. Both belong to the same order. This is why a line item becomes a class: the relationship itself has facts that need to stay together. Later, a requirement could carry its own due date or approved substitute without overloading the WorkOrder.
Before
WO-204 → part → PF-9
WO-204 → requiredQuantity → 2
After
WO-204 → requirement → WO-204-R1 → part → PF-9
└→ requiredQuantity → 2
WO-204 → requirement → WO-204-R2 → part → SEAL-2
└→ requiredQuantity → 1A relational database can model the same structure with an order-requirement table. The useful ontology lesson is explicit meaning, stable identity, and relationships that several systems can share. Subtracting stock does not, by itself, justify adopting a graph database.
Step 2 — create the new terms and replace the old constraints#
Open extensions/multipart/multipart_lab.py and find model(). It starts from the six-class baseline schema, changes its version to 2.0.0-multipart, and adds these terms. This Turtle fragment describes the same additions; the script generates the complete results/schema.ttl file.
@prefix m: <https://example.org/maintenance/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
m:PartRequirement a owl:Class .
m:requirement a owl:ObjectProperty ; rdfs:range m:PartRequirement .
m:order a owl:ObjectProperty ; rdfs:range m:WorkOrder .The WorkOrder now needs one or more requirement links. Every PartRequirement needs exactly one order, one part, one positive integer requiredQuantity, and one unit equal to each, plus its organization and record ID. The ordinary part, requiredQuantity, and unit properties remain valid on requirement records.
The migration also forbids those three old properties directly on a WorkOrder. Without that check, old and new consumers could read two conflicting versions of the requirement. These are the actual added work-order constraints from model(); the existing asset, organization and ID constraints are retained. minCount and maxCount constrain the number of values in the data being validated.601
@prefix m: <https://example.org/maintenance/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
m:WorkOrderShape
sh:property [ sh:path m:requirement ; sh:minCount 1 ;
sh:class m:PartRequirement ; sh:nodeKind sh:IRI ] ;
sh:property [ sh:path m:part ; sh:maxCount 0 ] ;
sh:property [ sh:path m:requiredQuantity ; sh:maxCount 0 ] ;
sh:property [ sh:path m:unit ; sh:maxCount 0 ] .Step 3 — migrate the existing facts before adding new ones#
The extension loads the already verified baseline graph into memory. It does not modify the baseline's CSVs or generated files. For each existing order, build() first creates a requirement whose stable ID is the order ID followed by -R1. It copies the original part, quantity and unit into that record, links the record to its order, and copies the original source-row lineage. Only then does it remove the three old properties from the in-memory WorkOrder. For order IDs longer than 77 characters, migrated_line_id() uses a deterministic shortened ID with a hash suffix to stay within the 80-character identifier limit; the original order identity is unchanged.
The line() helper performs the guarded creation. It rejects unknown or cross-organization references, duplicate requirement IDs, unsupported units, and a repeated part on the same order. The last rule prevents two lines from independently counting the same available stock. Supporting split lines would require an explicit allocation policy, not simply removing that check.
For the supplied dataset, results/migration.json records two migrations: ORG-TRAIN's WO-204 becomes WO-204-R1, and ORG-OTHER's WO-888 becomes WO-888-R1. Their original order, asset and part identifiers are preserved. The additional requirement comes next; it is not invented while migrating old records.
Provenance stays attached to the source records that support each migrated line, and an additional activity identifies the migration. This uses PROV's derivation and activity relationships to describe lineage.602 It does not turn the migration into an authenticated audit service. Compare the baseline file hashes before and after if you want to verify that its files were preserved; the test suite makes that comparison automatically.
Step 4 — supply the second part, its stock and its requirement#
The extension's three small CSV files are already included. Their paths are relative to the lab directory. Keep the exact headers when creating a fresh copy. First, extensions/multipart/fixtures/parts.csv defines the new part type:
part_id,org,name,unit
SEAL-2,ORG-TRAIN,Training replacement seal,eachNext, extensions/multipart/fixtures/inventory.csv declares three fictional physical seal identities. The observation uses the same time as the baseline's stock records so the existing freshness policy applies consistently.
snapshot_id,org,depot,part_ref,on_hand,unit,unit_ids,observed_at,revision
SNAP-SEALS,ORG-TRAIN,North,SEAL-2,3,each,S1|S2|S3,2026-09-14T07:30:00Z,1Finally, extensions/multipart/fixtures/requirements.csv says that the work order needs one of that part type:
requirement_id,org,order_id,part_id,required_quantity,unit
WO-204-R2,ORG-TRAIN,WO-204,SEAL-2,1,eachThree seals are an explicit exercise input, not a measured operational count. This closed fixture declares zero reservations and zero quarantined seals; existing filter allocations remain unchanged. A real importer would need a completeness contract for its allocation feed before interpreting an absent row as zero. The extension also checks that the three seal unit IDs are unique and match on_hand.
Step 5 — build and validate the extended ontology#
From the directory containing ontology_lab.py, run these commands. If you have just extracted the archive, the first two rebuild and verify the baseline before migrating it. Each failure stops the sequence so an invalid build is not mistaken for an expected result.
.\.venv\Scripts\python.exe ontology_lab.py build
if ($LASTEXITCODE -ne 0) { throw "Baseline build failed." }
.\.venv\Scripts\python.exe ontology_lab.py validate
if ($LASTEXITCODE -ne 0) { throw "Baseline validation failed." }
.\.venv\Scripts\python.exe extensions/multipart/multipart_lab.py build
if ($LASTEXITCODE -ne 0) { throw "Extension build failed." }
.\.venv\Scripts\python.exe extensions/multipart/multipart_lab.py validate
if ($LASTEXITCODE -ne 0) { throw "Extension validation failed." }On Linux or macOS, use .venv/bin/python in place of the Windows interpreter path. The recorded execution used Python 3.12.10 on Windows. The extension uses the baseline's pinned dependencies; it installs nothing and calls no remote service during the exercise.
Inspect extensions/multipart/results/schema.ttl, shapes.ttl, and instances.ttl. The supplied inputs produced 168 instance triples, 100 provenance triples, and three PartRequirement records across both organizations. The validation command returned conforms: true. Find WO-204 and confirm it has two requirement links and no direct part or quantity property. This checks the model change, rather than only the program's exit code.
Step 6 — query each requirement without losing missing evidence#
The fixed query is extensions/multipart/requirements_inventory.rq. It follows WorkOrder → PartRequirement → PartType → StockSnapshot, binds the selected organization on every business join, and aggregates existing allocations per stock snapshot. Its key change is that the requirement is mandatory but its stock match is OPTIONAL. SPARQL's optional pattern permits a result to retain a requirement when matching stock information is absent.603
That matters operationally. If a seal snapshot is missing, the requirement must remain visible as unresolved; dropping the entire row could make the order look complete. The Python query handler requires exactly one in-scope stock snapshot per requirement. Missing or multiple snapshots produce a null availability and an explicit issue. It applies the baseline's two-hour freshness rule to stock, existing allocations and inspection evidence.
.\.venv\Scripts\python.exe extensions/multipart/multipart_lab.py query --org ORG-TRAIN --order WO-204
if ($LASTEXITCODE -ne 0) { throw "Extension query failed." }The actual result in extensions/multipart/results/explanation.json contains these values:
| Requirement | Part | Needed | Available | Shortage |
|---|---|---|---|---|
| WO-204-R1 | PF-9 | 2 | 1 | 1 |
| WO-204-R2 | SEAL-2 | 1 | 3 | 0 |
For PF-9, available = 6 on hand − 3 reserved − 2 quarantined = 1. For SEAL-2, available = 3 on hand − 0 reserved − 0 quarantined = 3. All quantities use each.
The whole order's status is shortage_needs_human_review. A surplus of seals cannot cancel a shortage of filters, so there is no summed cross-part shortage field. Each line carries its own source locators, including the original filter rows or the added seal CSV rows. The inspection remains pending, and maintenanceReleaseAllowed remains false.
Step 7 — change a real requirement and predict the result#
Use a disposable copy of the lab. In the extension's requirements.csv, change only required_quantity from 1 to 4. The changed data row should read:
WO-204-R2,ORG-TRAIN,WO-204,SEAL-2,4,eachRun the query before rebuilding: it must refuse the changed source with an explicit rebuild-required error. Then rerun the extension's build, validate and query commands. PF-9 remains needed 2, available 1, shortage 1. SEAL-2 becomes needed 4, available 3, shortage 1. Restore the original quantity and rebuild before comparing with the shipped output.
This exact exercise is implemented in test_increased_second_requirement_has_its_own_shortage. It demonstrates a useful property of the new model: changing one line's quantity leaves the other line's meaning and arithmetic intact. No code change is needed for this data change because the ontology already represents one-to-many requirements.
Step 8 — run the regression and failure checks#
.\.venv\Scripts\python.exe extensions/multipart/multipart_lab.py test
if ($LASTEXITCODE -ne 0) { throw "Extension tests failed." }The extension suite runs 20 tests against temporary fixture copies. Its reports are extensions/multipart/results/test-summary.json and test-results.txt; the baseline has its separate 24-test suite. These are executable acceptance checks for the local implementation, not claims of load testing or operational accreditation.
| Failure introduced by the test | Expected result |
|---|---|
| Requirement points to the other organization's order | Entire extension build rejected |
| Same part appears twice on one order | Rejected until a deliberate consolidation or allocation policy exists |
Quantity is 1.5, unit is box, or stock IDs disagree with the count | Input rejected |
| Seal stock is stale or missing | Seal result unresolved; whole order has insufficient evidence |
| Two seal depots are present without a selection policy | Availability is unresolved rather than silently summed |
| Old direct quantity remains on a migrated WorkOrder | SHACL rejects the mixed representation |
| Input or generated graph changes after a successful build | Query requires a rebuild |
| Invalid rebuild leaves earlier result files on disk | Invalid manifest prevents querying those stale files |
| Baseline changes and is rebuilt | Extension also requires rebuilding against that new baseline |
Original engineering sequence. The practical result of an ontology change is a versioned schema, mapping, validation rule and query that still agree—not merely another box on a diagram.
Select a box to inspect its role. On a small screen, scroll the diagram sideways.State what a user must be able to answer and which source owns the required fact. This limits the classes and properties that need to be added.
Read the complete diagram as text
- Write the new question: State what a user must be able to answer and which source owns the required fact. This limits the classes and properties that need to be added.
- Change the schema: Add the necessary class or property, its meaning and its expected relationships. Keep stable identifiers and plan what happens to old records.
- Add the source mapping: Map real source fields into the new representation with explicit identifiers, units, dates and provenance. Include unresolved cases in the test inputs.
- Add shapes & tests: Add positive and negative cases for the new constraint. Re-run the existing tests to catch changes that invalidate earlier questions.
- Run the new query: Check its result against known source records and make the explanation traceable. Include a question whose correct result is missing evidence.
- Record the version: Keep the schema, input and tool versions with the results. A deployment needs a separately tested migration, publication and rollback process.
- Write the new question → define → Change the schema
- Change the schema → map → Add the source mapping
- Add the source mapping → check → Add shapes & tests
- Add shapes & tests → verify → Run the new query
- Run the new query → record → Record the version
What you created: a new ontology class, explicit one-to-many relationships, a versioned migration, additional source mappings, revised constraints, a per-requirement query, and tests that check both preserved behaviour and new failure cases. What to check: both requirement identities exist, source locators resolve, the old WorkOrder fields are absent, and the observed query values match the table.
The remaining deployment work is concrete: authenticated identity, source-feed completeness, authorized reads, live revision/freshness checks, depot selection, allocation across competing orders, and durable approval/action handling. The local --org flag implements none of those services. Keep the reproducible graph and tests as the acceptance baseline when adding them.
W3C, SHACL cardinality constraints — minimum and maximum value counts; checked 14 September 2026. The migration's shape design is our implementation.
W3C, PROV-O derivation and activities — lineage vocabulary; checked 14 September 2026. The local migration and integrity limitations are our implementation.
W3C, SPARQL optional graph patterns — retaining solutions when an optional match is absent; checked 14 September 2026. Requirement policy and result interpretation are our implementation.
Sources & further reading
- https://www.w3.org/TR/rdf11-concepts/ ↗
- OWL 2 Web Ontology Language Primer ↗
- On the Construction of Knowledge Graphs from Unstructured Text using Neuro-Symbolic Networks ↗
- Operational ontology overview ↗
- Shapes Constraint Language ↗
- PROV-O derivation and activities ↗
- SPARQL 1.1 Query Language ↗
- Getting started ↗
- Object types and instances ↗
- Declared property types ↗
- Relationships between object types ↗
- Shapes Constraint Language ↗
- SP 800-162: Attribute Based Access Control ↗
- Action submission criteria ↗
- External writeback and webhook behavior ↗
- SHACL cardinality constraints ↗
- SPARQL optional graph patterns ↗