Graph Theory

A coding agent that receives a ticket reads prose, picks the parts that look like instructions, searches the code by name, and edits what it finds. Nothing tells it which requirement a method serves, which callers break when that method changes, or what the team decided last quarter about the same field. It fills those gaps with plausible text. A wrong answer that looks right is the expensive kind, because it passes review.

Graph engineering replaces the guesses with lookups. Requirements become atoms with stable ids, code becomes a graph of symbols and calls, and the links between them carry evidence and a status. The setup, commands and modes are on Graph Engineering. The ideas below are what each part rests on, with the sources that shaped them.

Requirements as atoms

ISO/IEC/IEEE 29148:2018 lists the characteristics of a well-formed requirement, and one of them is singular: a requirement states one capability, one characteristic, or one constraint. A requirement that bundles two behaviors can be half implemented and still look done, and one test cannot prove both halves. The singular requirement is the atom everything else links to.

EARS, the Easy Approach to Requirements Syntax (Mavin et al., RE 2009), gives the atom a grammar. It came out of Rolls-Royce engine control work, and it fixes a small set of patterns, each opened by a keyword: When for an event, While for a state, If ... then for unwanted behavior, Where for an optional feature, and a bare shall for behavior that always holds. One trigger, one response, one requirement.

REQ-001: When a shipment is created with a weight above the carrier limit,
         the API shall reject it with HTTP 422 and the code WEIGHT_OVER_LIMIT.

The id matters more than the wording. Text gets edited during review; the id does not change. Once the ticket becomes REQ-001 to REQ-00n, every later step keys off the id and the original ticket text is never read again, so a reworded criterion keeps its links, tests and commits. In harness-kit the PRP writes each success criterion as - [ ] REQ-001: ..., and trace.py init reads those lines, or keeps the ids that the atomize skill already assigned.

Traceability and why links rot

Gotel and Finkelstein named the problem in An Analysis of the Requirements Traceability Problem (RE 1994). They split it in two: post-requirements traceability follows a requirement forward into design, code and tests, and pre-requirements traceability follows it back to who asked for it and why. Their survey found most of the unsolved trouble on the pre side, where the origin of a requirement lives in meetings and documents that no tool links to.

Gotel et al. returned to it in The Grand Challenge of Traceability (2012) with a target they called ubiquitous traceability: links created as a side effect of normal engineering work, trusted, and cheap enough that nobody has to decide whether to keep them. The obstacle they describe is decay. A link written by hand on day one stays the same while the code under it changes every week, and nobody owns updating it.

A stale link does more harm than a missing one, because it looks like provenance. The article Graph Engineering #1 by Pierry Borges records an ownership map where an agent wrote 16 commit hashes, none of which matched any git object. Nothing checked them at write time, and they sat in the file for months, typographically identical to real ones, until an audit caught them.

The fix is to make a link carry how it was made and how far to trust it: status, confidence, the methods behind it, evidence, commit and date. PROPOSED means an analysis thinks the requirement touches the symbol. VALIDATED means the change merged and a test proves it. STALE means the symbol moved or vanished. Two checks keep the status honest: at write time the commit is resolved against git and a hash git cannot find is refused, and at settle time each symbol is looked up again in the current tree. In the article's words, documentation ages silently, evidence expires loudly.

Code property graphs

A compiler builds several views of the same code, and each answers a different question. The abstract syntax tree (AST) is the parsed structure of the source: this method, this call, these arguments. It says what the code is and nothing about the order in which it runs.

The control flow graph (CFG) has one node per statement and an edge to every statement that can run next, so branches and loops become forks and cycles. It answers what can execute after what. The program dependence graph (PDG) adds two kinds of edges: data dependence, where statement B reads a value that statement A wrote, and control dependence, where B runs only if A's condition holds. It answers which statements influence which.

Yamaguchi et al. merged the three in Modeling and Discovering Vulnerabilities with Code Property Graphs (IEEE S&P 2014). The code property graph keeps the AST nodes and lays the CFG and PDG edges over them, all in one property graph, so a single traversal can ask a question that spans syntax, order and data flow, such as an argument that comes from user input and reaches a copy call with no bounds check on the path between. They used it to find 18 previously unknown vulnerabilities in the Linux kernel.

Joern is the open source implementation, with the schema published at cpg.joern.io. It ships frontends for C and C++, Java, JavaScript, Python, Kotlin and other languages, adds a call graph and type information on top of the three base layers, and is queried in a Scala-based language.

For an agent the useful slice is the call graph. A change to method M can only break code that reaches M, which means its callers, their callers, and so on up to the entrypoints. That transitive set is the upper bound on what to review and retest, and it bounds the blast radius before anyone edits. Grep finds a name; the call graph resolves each call to one declaration, so two methods called validate in different classes stay separate.

harness-kit stores only that slice. graph.py index-code runs a Joern frontend, exports methods and call sites through .claude/graph/export_callgraph.sc, and loads them as Fn nodes joined by CALLS edges. graph.py symbols then prints up to 10 callers for each candidate symbol. The article adds a field note: on a Java project with Lombok, Joern without --fetch-dependencies --delombok-mode no-delombok skipped 4,351 of 4,890 files and produced a plausible 188 KB graph, against 16 MB with the flags. Count what the graph holds instead of trusting that it exists.

Temporal knowledge graphs and Graphiti

Knowledge changes in a way code does not. A carrier raises its weight limit, a decision gets reversed, a policy expires. A graph that keeps both facts gives two contradictory answers, and a graph that overwrites the old one loses the record of what was true when the earlier code was written.

Rasmussen et al. describe the answer in Zep: A Temporal Knowledge Graph Architecture for Agent Memory (arXiv:2501.13956, 2025), the paper behind Graphiti. Graphiti keeps three subgraphs. Episodes hold the raw input, a document or a message, with its timestamp, so every fact can point back to where it came from. Entities and the facts between them are extracted from episodes. Communities group related entities with a summary.

Every fact edge is bi-temporal. Valid time records when the fact held in the world, and transaction time records when the system learned it and when it marked it expired. When a new episode contradicts an old fact, Graphiti closes the old edge's validity instead of deleting it, so you can ask what was true on 1 March and also what the system believed on 1 March. The paper reports 94.8% on the Deep Memory Retrieval benchmark against 93.4% for MemGPT, and on LongMemEval accuracy gains of up to 18.5% with response latency cut by about 90%.

The article holds the warning that goes with this. Typed requirements that atomize had already produced were sent through the model anyway: 25 to 30 seconds per requirement, invented facts, one carrier stored under two node names, and 35 of 42 edges with no validity date, which made revocation impossible. A parser reading the same file loaded 26 requirements in 0.86 seconds as 107 nodes and 248 edges, nothing invented, every edge dated. The model earns its place where there is judgment, never where a parser already knows the answer.

In harness-kit Graphiti only runs on graph.py ingest, for unstructured documents such as decisions and meeting notes, on NVIDIA build models instead of a local Ollama. graph.py ingest passes the moment of ingestion as each episode's reference time. Manifests and the CPG load through parsers.

The ontology as a contract

An ontology here is the list of node types that may exist and the edges that are legal between them, with the fields each edge requires. It is the contract between every process that writes the graph and every process that reads it.

The article opens with what happens without one. A knowledge graph held 19,262 nodes, including 1,100 typed requirements, and answered every question with nothing. The writer used one set of node labels, the reader queried another, and the client searched a partition called "main" that no node belonged to. It stayed that way for weeks, because a graph that returns nothing looks the same as a graph that has nothing. Over one month the authors catalogued eight defects across two projects, among them the fabricated hashes, a hardcoded date stamped into generated files, and a retrieval policy written as LLM instructions that drifted from the scoring code that never read it. Every one was a writer and a reader with no verified contract.

The ontology stays a versioned file so that a change arrives as a pull request someone can argue with. It grows only when a real document or real code asks for a type. The article's first ontology survived two documents; the third needed an edge for a policy constraining a feature, and it went in marked as local rather than canonical. An edge that counted skipped abstraction levels, instead of inventing the missing ones, turned "where does our documentation skip levels" into a query that answered 23 places.

harness-kit ships .claude/graph/ontology.yml at version 1 with four node types (Requirement, Decision, Method, Test) and three edges (AFFECTS, IMPLEMENTS, VERIFIED_BY). trace.py reads it on every write and trace.py validate rejects an edge type it does not list, a missing required field, an unknown status, an undeclared requirement or a commit git cannot find.

One writer per layer

The graph holds three layers separated by node labels. Knowledge is requirements, decisions and facts. Code is files, methods and calls. Traceability is the links between the two. Each layer has exactly one writer and the database only stores.

flowchart LR
  D[documents] -->|graph.py ingest| GI[Graphiti]
  R[repo] -->|graph.py index-code| J[Joern CPG]
  T[trace/*.yml] -->|graph.py sync| S[parser]
  GI --> K[(knowledge)]
  J --> C[(code)]
  S --> TR[(traceability)]
  K & C & TR --> Q[graph.py knowledge, symbols, tests, history]

The rule exists because of the label mismatch. When two processes write the same labels, each holds its own idea of the schema and nothing fails until a reader comes back empty. With one writer per layer, one place encodes each part of the schema, and the reader can be tested against that place. The agent sees none of this: it asks four questions through graph.py and the answers come from whichever backends the mode turns on.

File is truth, graph is projection

The article reports a test nobody plans to run: the team deleted their Neo4j database, with no backup to restore. Every requirement file survived in git, the graph was rebuilt from those files in an afternoon, and the rebuilt graph was better than the lost one because the ontology had improved in between. Had the graph been the only home of that data, a month of work would have gone with one command.

The lesson the article draws is to simplify the infrastructure and never the data. Infrastructure can be swapped later; data that was never recorded is gone. harness-kit keeps trace/{feature_id}.yml in git and ships it with the PR. The graph is FalkorDB embedded through falkordblite, stored in .claude/runtime/graph/kg.db and git-ignored, with no Docker and no server. graph.py sync and graph.py index-code rebuild it from files. The knowledge layer rebuilds by ingesting the source documents again, which costs model calls, so those documents belong in the repo too.

Retrieval before generation

Lewis et al. introduced retrieval-augmented generation in Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (arXiv:2005.11401, NeurIPS 2020). A retriever pulls passages from an index and the generator conditions on them, so facts come from text you can inspect and update by changing the index instead of retraining the model. Edge et al. extended it in From Local to Global: A Graph RAG Approach to Query-Focused Summarization (arXiv:2404.16130, 2024): an LLM builds an entity graph from the corpus, the graph is split into communities, and each community is summarized ahead of time, which answers corpus-wide questions that retrieving the few most similar chunks misses.

Graph engineering takes the order from the first paper and the structure from the second. Retrieval runs first and follows edges, from requirement to symbol to caller to test, instead of ranking text by similarity alone. All lookups fire in parallel: symbols, git history, documents, tests, memory and the CPG. On the article's monolith of 4,900 Java files the CPG takes 15 minutes to build and finds 1,062 entrypoints, 990 of them HTTP, grouped into 77 features with 2,124 contract clauses. It is built once and reindexed on new commits, never per ticket; a smaller service indexed in under a minute.

Narrowing the context

Liu et al. measured why more context is not better in Lost in the Middle: How Language Models Use Long Contexts (arXiv:2307.03172, TACL 2024). Accuracy on multi-document question answering is highest when the relevant passage sits at the start or the end of the input and drops when it sits in the middle. In some settings the model did worse with the answer buried mid-context than with no documents at all.

So the graph narrows the context and never fills it. harness-kit returns at most 5 Graphiti facts and 5 entity summaries per knowledge query and 10 callers per symbol, and the plan's scope list is the only set of files dev may touch, enforced by trace.py gate. The same rule applies to the eval judge, which receives one section per check; see Jev and System One.

How harness-kit maps each idea

Idea Where it lives
Singular requirement, stable id PRP - [ ] REQ-001: lines, read by trace.py init
Link with evidence and status trace/{feature_id}.yml, written only by .claude/scripts/trace.py
Commit checked at write time resolve_commit in trace.py
Links settle after merge trace.py settle --all, run at the start of every plan
Ontology as contract .claude/graph/ontology.yml, enforced by trace.py validate
Call graph bounds blast radius graph.py index-code, graph.py symbols
Temporal knowledge graph.py ingest into Graphiti on NVIDIA build models
One writer per layer Graphiti, Joern, trace.py; FalkorDB only stores
File is truth graph.py sync and index-code rebuild kg.db
Scope as a list trace.py scope, trace.py gate

References

Pierry Borges, Graph Engineering #1: Introduction, unpublished article, source of the field notes and numbers from production use.

ISO/IEC/IEEE 29148:2018, Systems and software engineering, Life cycle processes, Requirements engineering, for the singular requirement. Mavin et al., Easy Approach to Requirements Syntax (EARS), RE 2009.

Gotel and Finkelstein, An Analysis of the Requirements Traceability Problem, RE 1994. Gotel et al., The Grand Challenge of Traceability (v1.0), in Software and Systems Traceability, Springer, 2012.

Yamaguchi et al., Modeling and Discovering Vulnerabilities with Code Property Graphs, IEEE S&P 2014. Joern and the CPG specification.

Rasmussen et al., Zep: A Temporal Knowledge Graph Architecture for Agent Memory, 2025. Graphiti. FalkorDB.

Lewis et al., Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks, NeurIPS 2020. Edge et al., From Local to Global: A Graph RAG Approach to Query-Focused Summarization, 2024. Liu et al., Lost in the Middle: How Language Models Use Long Contexts, TACL 2024.

See also

Graph Engineering, Jev and System One, Evals, References.

This page mirrors Graph Theory in the wiki. Edit it there.