Why a typed graph for your codebase, and why you still write the docs
A codebase already has a graph: imports, calls, inheritance. It does not have the graph that matters, the one that says why the code is the way it is. That one is typed, it is small, and a person has to write it.
The graph you already have
Every language server can tell you who calls verifyToken. Every indexer can draw the import tree. Tools that turn a repository into a knowledge graph by parsing it are easy to build and they work: files, classes, functions, edges for calls, imports and extends.
That graph answers what questions. What calls this? What breaks if I rename it? What does this module depend on? A coding agent that greps well, or has an LSP, can already answer those without any help.
It cannot answer the questions you actually get stuck on:
- Why are tokens checked here and not in the gateway?
- Is this retry loop a requirement or an accident?
- Which function is supposed to enforce the expiry rule?
- We decided sessions are stateless. What in the code still contradicts that?
None of these is in the source. The source says what the program does, not what it was meant to do. For that you need a second set of nodes, written by people: the design decisions, constraints and specs. And you need to connect the two.
Why the links have to be typed
The obvious way to connect docs and code is a link. A function mentions a section, a section mentions a function. lat.md does exactly this well: [[src/auth.ts#verifyToken]] in a doc, // @lat: [[auth#Token expiry]] in code, and a check that fails when a link points nowhere.
A plain link says two things are related. It does not say how. These are very different facts:
// this function implements the rule
// @tg: implements:: [[auth#Token expiry]] {since: 2}
// this function is a leftover that breaks the rule
// @tg: -contradicts:: [[design#Stateless sessions]]
// this test proves the rule
// @tg: verifies:: [[auth#Token expiry]]
With plain links all three are "references", and a reader has to open each one to find out which. With types you can ask the graph. The edge starts at the declaration that follows the comment, so it names the function, not the file. A - in front of the type makes the edge negative, which gives you conflicts as a first-class thing instead of a note in a comment. Properties such as {since: 2} hold facts about the relationship itself.
Once links have types, whole classes of question become one query instead of an afternoon of grep:
-- design sections that no code implements MATCH (s:Section) OPTIONAL MATCH (c:CodeSymbol)-[r:implements]->(s) WITH s, count(r) AS n WHERE n = 0 RETURN s.section -- everything that contradicts a design decision MATCH (c:CodeSymbol)-[r:contradicts]->(s:Section) RETURN c.path, c.symbol, s.title
These run with tg cypher --code annotated, over the same openCypher engine the Obsidian plugin uses. The point is not the query language. The point is that "what is documented but not built" and "what is built against the design" stop being things you find out in code review.
What tg does differently from lat.md
tg reads the same lat.md/ folder and agrees with lat.md's check on the projects it is tested against, so this is an extension, not a fork of the idea. The full comparison has the table and says where lat.md is stronger. The short version of the differences that follow from the typed graph:
- Typed, signed edges from code.
@tg:carries a type, a sign and properties.@lat:still works and means a plain reference. - A query over docs and code together. Sections and code symbols are nodes in one property graph. lat.md lets you locate by name, follow references and search; it has no query that joins them by relationship type.
- Search that needs no setup. A lexical index with no model and no key, so it works on a fresh clone and inside a prompt hook. Embeddings are optional, and a provider outage falls back to lexical results.
- Agents get graph tools. The MCP server has eight tools, the six lat.md has plus
tg_cypherandtg_edges, so an agent can ask "what implements this?" instead of grepping for it. - The same graph in Obsidian. If your notes live in a vault, the plugin shows code nodes next to them, with the same styling for edge types.
Where lat.md wins
Honest accounting matters here, because "typed" is not free. lat.md parses source with real grammars; tg uses regex finders, so unusual syntax, macros and generated code can slip past it (tg says "cannot tell" rather than guess). lat.md ships more agent templates and has far more real-world use. If you only need links that are checked, and not queried, the plain format is enough and you gain nothing from typing them. Typing pays off when you want to ask the graph things, or when you keep contradiction and verification as separate facts.
Why not generate the graph from the code
This is the obvious objection. We already parse the code. Let a model read it, summarize each module into a doc, infer the edges, and keep the whole thing regenerated on every commit. No writing, no drift. It is a good idea for the structural half of the graph, and tg does not fight it: code nodes are derived in memory from the source and never written to disk. But it cannot replace the docs, for four reasons.
1. Code is the only source of the "what", so it cannot check itself
The value of check is that two independent things must agree: what you wrote down and what the code does. If you generate the docs from the code, they agree by construction. Drift becomes impossible to detect, because there is nothing left for the code to drift from. A bug becomes the spec. A generated doc that says "tokens are accepted for up to 90 days" is faithful and wrong, and it will pass every check you run on it.
2. Intent is not in the artifact
A model reading the source can see a retry loop with three attempts. It cannot know that three is the number your payment provider's SLA allows, that it was five until an outage, or that nobody may raise it. It will produce a plausible reason, and plausible reasons are the dangerous kind. The reasons, constraints and rejected alternatives exist only in the heads and discussions of the people who made the decision. Writing them down is the act of recording them; it cannot be recovered afterwards from the result.
3. The graph worth having is a small curated one
A graph inferred from code has an edge for every call, and it is as large as the code. Nobody reads it, and an agent pulls it into context as noise. The typed graph in a lat.md/ folder has a node per decision and an edge per claim someone chose to make. It is small on purpose, a few hundred sections for a real project, and each one is something a person decided mattered. That curation is the information. A long list of things you could say about the code is not the same as the ten things that must stay true.
4. Writing is where the design gets checked
Writing "this function implements the expiry rule" forces you to find the rule, and to notice there are two functions, or none. Deriving the edge automatically skips that moment, and that moment is half of the value. The leading-paragraph rule in lat.md is the same idea in miniature: if a section cannot be summarized in a sentence, the section is not clear yet.
What this looks like in practice
| Layer | Who produces it | Examples |
|---|---|---|
| Structure | Derived from the source | Symbols, files, calls, imports |
| Intent | Written by people | Design sections, constraints, decisions, test specs |
| Typed links between them | Written by people, next to the code | implements, verifies, -contradicts |
| Consistency | Checked by a machine | Broken links, uncovered specs, sections nothing implements |
Agents can help with the written layers. An agent that has just changed a function is well placed to draft the doc update, and the stop hook that tg init installs blocks it from finishing when it changed a lot of code and touched nothing in lat.md/. But a person reads the sentence before it lands, because the sentence is a claim, and a claim nobody has read is the same as no claim.
What it costs
You write more than you would with a generator, and the typed syntax is one more thing to learn. We think the cost is right for the same reason tests are: it is the only part of the system that is not a function of the code. If you want to see whether it earns its keep, start small: write the five sections you most often explain to new people, link two or three functions to each, and run tg check and one query. If the graph tells you something you did not already know, keep going.
Try it
npm install -g @typedgraph/cli cd your-project tg init # dry run: prints a diff, writes nothing tg check tg cypher --code annotated "MATCH (c:CodeSymbol)-[r]->(s:Section) RETURN c.symbol, type(r), s.title"
The CLI article covers the commands, and the docs have the full reference.