« Phase 07 · Warmup · Track Overview
Lab 01 — A Financial Knowledge Graph From Scratch
The problem
Compliance asks: "which of our counterparties are ultimately controlled by an entity on the sanctions list?"
No embedding answers that. It is not a question about text that resembles the query; it is a question about a path — Acme is owned by Northgate, which is owned by Meridian, which is controlled by a sanctioned entity. Three hops, through documents that share no vocabulary with the question.
You build the four pieces that answer it, and the reason there are four is the phase's argument: a triple store because the data is a graph, a reasoner because "ultimately controlled by" is entailed rather than stored, a SHACL validator because OWL is open-world and cannot tell you a record is incomplete, and SPARQL because a property path expresses in one line what would otherwise be a recursive query nobody can read.
What you build
| # | Component | What it does |
|---|---|---|
| 1 | IRI, Literal, Triple, PrefixMap | the RDF data model, with CURIE expansion that does not mistake https://… for a prefix |
| 2 | Graph | a set of triples with three indexes (SPO / POS / OSP) and pattern matching |
| 3 | materialize | forward-chaining over RDFS + an OWL subset, to a fixed point |
| 4 | NodeShape, PropertyShape, validate | SHACL: cardinality, datatype, pattern, node-kind, in, closed shapes, and a real validation report |
| 5 | Query, Pattern, PathStep, execute | SPARQL basic graph patterns, OPTIONAL, FILTER, inverse paths and +/* property paths |
| 6 | expand_neighbourhood | graph-grounded retrieval — structural context a vector index cannot produce |
| 7 | build_ontology, build_facts | a FIBO-shaped ontology of legal entities, control and obligations |
Key concepts
| Concept | Where | Why it matters |
|---|---|---|
| A graph is a set | Graph.add returns bool | idempotence is what makes entailment safe to run repeatedly, and the boolean is the fixed-point signal |
| Rule composition | test_transitivity_composes_with_subproperty | majorityOwns ⇒ controls (rdfs7) then transitivity (owl) derives a fact nobody asserted |
| Entailment is monotone | test_entailment_is_monotone | adding facts never retracts a conclusion — which is what makes materialization a valid cache |
| Open-world assumption | SHACL section | OWL cannot say "this record is missing an LEI"; that is unknown, not false |
| SHACL closes the world | validate | and answers the question a bank actually asks |
| Shapes target entailed types | test_shapes_target_entailed_types | a node typed only by inference is still validated — which is why materialization runs first |
| Property paths | PathStep("bank:controls", "+") | "ultimately controlled by" in one token |
| Cycles are real | test_a_path_traversal_terminates_on_a_cycle | circular shareholdings exist; an unguarded walk does not return |
OPTIONAL is a left join | test_optional_is_a_left_join | a missing name must not delete the owner from the result |
| Variables are the join | execute | there is no join clause because shared variable names are the condition |
| Structural ≠ semantic retrieval | expand_neighbourhood | the right operation when the answer is three hops away |
Files
| File | Role |
|---|---|
| lab.py | your implementation |
| solution.py | reference; python solution.py runs a six-part worked example |
| test_lab.py | 61 tests |
| requirements.txt | pytest |
Run
pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v
python solution.py
Success criteria
-
All 61 tests green against your
lab.py. -
PrefixMap.expand("https://example.org/x")returns that IRI — it is not a CURIE with prefixhttps. -
shortenprefers the longest matching namespace. -
Adding the same triple twice returns
Falsethe second time. -
Opaque controls Acmeis entailed across four hops, through a sub-property. -
A second
materializeadds zero triples in one round — the fixed point is real. -
A literal is never given a type by
rdfs:range. - Materialization terminates on a two-node cycle.
-
A
WARNING-severity shape violation still conforms. - A node typed only by entailment is validated.
-
OPTIONALkeepsent:Opaquein the result with an empty name; the required form drops it. -
bank:controls+reaches the sanctioned entity three hops up.
How this maps to the real stack
| This lab | The real thing | What we simplified |
|---|---|---|
Graph | Apache Jena (TDB2), RDF4J, GraphDB, Amazon Neptune, Stardog; Neo4j for the LPG model | no persistence, no transactions, no named graphs, no quads |
materialize | Jena's rule reasoners, GraphDB rulesets, Stardog reasoning | ours is RDFS + 4 OWL rules; OWL 2 RL has ~80, and OWL 2 DL needs a tableau reasoner |
validate | Apache Jena SHACL, pySHACL, TopBraid, Stardog ICV | no SPARQL-based constraints, no shape inheritance, no sh:or/sh:not |
execute | a real SPARQL 1.1 engine with a parser, optimiser and cost model | no parser, no UNION/MINUS/subqueries/aggregation; joins are nested loops with no reordering |
PathStep | SPARQL 1.1 property paths | we support +, *, ^; the spec also has ?, /, ` |
| FIBO-shaped ontology | the real FIBO, which is thousands of classes across a dozen modules | ours is ten classes with the right shape |
expand_neighbourhood | GraphRAG, LlamaIndex knowledge-graph indexes, Neo4j vector+graph hybrids | no scoring, no community summaries, no LLM extraction |
Honest limits. No persistence and no transactions, so nothing here says anything about consistency under concurrent writes. Nested-loop joins with no reordering, which is fine at lab scale and quadratic at real scale — a production engine's optimiser is most of its value. No blank nodes, which real RDF uses heavily and which complicate identity. And the reasoner is forward-chaining only: a query-time (backward-chaining) reasoner has different, better properties under deletion.
Extensions
- Add
sh:orand shape inheritance. Then discover why SHACL's spec is longer than you expected: constraint composition interacts with severity and with closed shapes. - Backward-chaining. Reason at query time instead of materializing. Compare: no stale inferences after a delete, but every query pays. Measure both on the ownership chain.
- Deletion and re-materialization. Delete one
majorityOwnstriple and show that the materialized closure is now wrong. Then implement truth maintenance, or re-materialize, and compare the cost. - Blank nodes. Add them, and work out what identity means for a node with no IRI — this is where RDF gets genuinely subtle.
- A real SPARQL parser. Write one for the subset here. The grammar is small; the lesson is how much the query planner matters, which you will feel immediately.
- Hybrid retrieval. Wire this into Phase 06: use the graph to select which documents to retrieve, and measure recall against text-only retrieval on ownership questions.
- Real FIBO. Load a FIBO module and try to answer the same question. Budget a day, and expect to spend most of it on namespaces.
Interview / resume bullets
- "Built the platform's knowledge-graph layer: an RDF store with an RDFS/OWL forward-chaining reasoner, so ultimate-beneficial-ownership questions are answered by transitive closure over a sub-property rather than by application code walking a table."
- "Used SHACL for data quality and OWL for inference, and could explain why both are needed — the open-world assumption means an ontology can never tell you a record is incomplete, which is precisely the KYC question."
- "Expressed 'ultimately controlled by an entity on the sanctions list' as a single SPARQL query with a transitive property path, replacing a recursive stored procedure nobody could review."
- "Added graph-grounded retrieval so an agent's context includes entities structurally related to the question, not only text similar to it — which is what answers a three-hop ownership question whose documents share no vocabulary with the query."