« 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

#ComponentWhat it does
1IRI, Literal, Triple, PrefixMapthe RDF data model, with CURIE expansion that does not mistake https://… for a prefix
2Grapha set of triples with three indexes (SPO / POS / OSP) and pattern matching
3materializeforward-chaining over RDFS + an OWL subset, to a fixed point
4NodeShape, PropertyShape, validateSHACL: cardinality, datatype, pattern, node-kind, in, closed shapes, and a real validation report
5Query, Pattern, PathStep, executeSPARQL basic graph patterns, OPTIONAL, FILTER, inverse paths and +/* property paths
6expand_neighbourhoodgraph-grounded retrieval — structural context a vector index cannot produce
7build_ontology, build_factsa FIBO-shaped ontology of legal entities, control and obligations

Key concepts

ConceptWhereWhy it matters
A graph is a setGraph.add returns boolidempotence is what makes entailment safe to run repeatedly, and the boolean is the fixed-point signal
Rule compositiontest_transitivity_composes_with_subpropertymajorityOwns ⇒ controls (rdfs7) then transitivity (owl) derives a fact nobody asserted
Entailment is monotonetest_entailment_is_monotoneadding facts never retracts a conclusion — which is what makes materialization a valid cache
Open-world assumptionSHACL sectionOWL cannot say "this record is missing an LEI"; that is unknown, not false
SHACL closes the worldvalidateand answers the question a bank actually asks
Shapes target entailed typestest_shapes_target_entailed_typesa node typed only by inference is still validated — which is why materialization runs first
Property pathsPathStep("bank:controls", "+")"ultimately controlled by" in one token
Cycles are realtest_a_path_traversal_terminates_on_a_cyclecircular shareholdings exist; an unguarded walk does not return
OPTIONAL is a left jointest_optional_is_a_left_joina missing name must not delete the owner from the result
Variables are the joinexecutethere is no join clause because shared variable names are the condition
Structural ≠ semantic retrievalexpand_neighbourhoodthe right operation when the answer is three hops away

Files

FileRole
lab.pyyour implementation
solution.pyreference; python solution.py runs a six-part worked example
test_lab.py61 tests
requirements.txtpytest

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 prefix https.
  • shorten prefers the longest matching namespace.
  • Adding the same triple twice returns False the second time.
  • Opaque controls Acme is entailed across four hops, through a sub-property.
  • A second materialize adds 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.
  • OPTIONAL keeps ent:Opaque in 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 labThe real thingWhat we simplified
GraphApache Jena (TDB2), RDF4J, GraphDB, Amazon Neptune, Stardog; Neo4j for the LPG modelno persistence, no transactions, no named graphs, no quads
materializeJena's rule reasoners, GraphDB rulesets, Stardog reasoningours is RDFS + 4 OWL rules; OWL 2 RL has ~80, and OWL 2 DL needs a tableau reasoner
validateApache Jena SHACL, pySHACL, TopBraid, Stardog ICVno SPARQL-based constraints, no shape inheritance, no sh:or/sh:not
executea real SPARQL 1.1 engine with a parser, optimiser and cost modelno parser, no UNION/MINUS/subqueries/aggregation; joins are nested loops with no reordering
PathStepSPARQL 1.1 property pathswe support +, *, ^; the spec also has ?, /, `
FIBO-shaped ontologythe real FIBO, which is thousands of classes across a dozen modulesours is ten classes with the right shape
expand_neighbourhoodGraphRAG, LlamaIndex knowledge-graph indexes, Neo4j vector+graph hybridsno 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

  1. Add sh:or and shape inheritance. Then discover why SHACL's spec is longer than you expected: constraint composition interacts with severity and with closed shapes.
  2. 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.
  3. Deletion and re-materialization. Delete one majorityOwns triple and show that the materialized closure is now wrong. Then implement truth maintenance, or re-materialize, and compare the cost.
  4. Blank nodes. Add them, and work out what identity means for a node with no IRI — this is where RDF gets genuinely subtle.
  5. 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.
  6. 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.
  7. 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."