The load & analysis harness
The dependency graphs
The Open Brain builds a single merged, cross-repo, cross-stack dependency graph of every MOTMaster codebase and layers enrichment, layout, serialisation, an interactive WebGL viewer, and refactoring-safety tools on top. This is the structural half of the index — the part that answers "what calls this?" and "what breaks if I change it?" instead of "what looks similar?"
Nodes, edges, and where they come from
A node is a code symbol (class, method, SQL proc); an edge is a directed
dependency source → target. Per-repo graphs are produced by the external Graphify
CLI, merged into one global graph, and enriched in place. The wrapper GraphifyRunner.cs shells to
graphify <repo> update (LLM-free) or extract when an LLM backend is configured
(GraphifyRunner.cs:104-127), and never throws on a missing binary or non-zero exit — it falls back
to the pre-shipped graph.json (:20-24,259-266). The defensive parser
GraphFeatureIndex.ParseRepoGraph (GraphFeatureIndex.cs:1126) tolerates Graphify's
unpinned schema by BFS-walking JSON for nodes/entities/vertices + edges/relationships/links
and reading node fields by trying multiple key names (:1171-1187). MergedNode
(:1501) carries identity, computed InDegree/OutDegree, and enrichment slots
(Layer, Pattern, IsPublicEntry, IsOrphan, IsDeadEnd, X, Y); MergedEdge (:1542)
carries SourceId, TargetId, Kind, Relation, Confidence, FromRepo, ToRepo, ResolvedBy, IsLayerSkip.
"What depends on X?" is answered in one query, not ten greps, and because the parser tolerates schema changes, a Graphify upgrade degrades gracefully instead of breaking the build.
Edge kinds — which fuse well versus poorly
Each edge has a coarse Kind (intra/cross_repo) plus a semantic
Relation, with the vocabulary in EdgeKinds (Models.cs:349-388).
| Relation | Source | How | Confidence today |
|---|---|---|---|
| call / reference | Graphify | native edges parsed as intra (:1193-1219) | forward-only |
| name_match (cross_repo) | LoadAndMerge | distinctive label in ≥2 repos (:945-999) | heuristic, low precision |
| endpoint / sproc_call (cross_repo) | CrossStackResolver | route/sproc join → AddCrossStackEdges (:152-202) | HIGH (ResolvedBy="cross_stack") |
| covered_by (intra) | derived | reverse test→code to code→test (AddCoverageEdges :325-366) | good |
| implements | DiExtractor | AddScoped<IFoo,Foo>() → interface→impl (AddDiEdges :377-446) | extracted |
| reference (usage backfill) | UsageEdgeExtractor | consumer→used-symbol caller edges (AddUsageEdges :457-527) | extracted; fix for ~0% inbound |
| emitted_from | runtime fusion | runtime-log→emitter (AddEmittedFromEdges :233-314) | fuses 0 today |
emitted_from is fully implemented but currently dormant: it is gated on the
motlogs_log_facts corpus, which is empty wherever the journalctl tail never ran, so it materialises
"0 runtime-fused files" (:242-247). motivates (git/ADR) is declared but not yet
materialised. name_match is deliberately low-trust behind a distinctiveness filter
(:1260-1292); the ResolvedBy field lets callers filter cross_stack (trust)
from name_match (noise).The deterministic extractors (endpoint, sproc_call, implements, covered_by) fuse well and can be trusted for refactors, while the honest labelling of low-precision and dormant edges means a caller always knows how much weight an edge deserves.
Cross-stack / cross-language edges
The headline differentiator over single-repo tools: a walk from a frontend button, through the API and
controller, to the stored procedure. CrossStackResolver.RunAsync
(CrossStack/CrossStackResolver.cs:82) scrolls every chunk once and does three things.
- Endpoint extraction —
EndpointExtractorparses C# route definers via Roslyn:[HttpGet("...")]+ class[Route]composition (EndpointExtractor.cs:104-185) and minimal-APIMapGet/MapPost(:187-217). Callers are found by regex — C#HttpClient.GetAsyncon.cs/.razor/.cshtml(:257-298, important because the MOTMaster UI is Blazor so callers are C#, not fetch) and TS/JSfetch()/axios(:322-398). Both are normalised to a canonical path soapi/foomatches/api/foo(:38-56). - Sproc extraction —
SprocExtractorfindsCREATE PROC pr_xxxin.sql(:115-143) andpr_\w+references in C# string literals (:65-109). - Join — matched by normalised route/sproc name into directed caller→definer
edges (
:275-329), written both as Qdrant payload neighbour lists (endpoint_neighbours,sproc_neighbours) and first-class graph edges viaAddCrossStackEdges(:402-403). Each edge anchors on the file's highest-degree ("representative") node (GraphFeatureIndex.cs:156-169).
It is triggered by POST /api/admin/cross-stack/run (Program.cs:2021).
"Where does this UI button hit the database?" becomes one walk across four repos and three languages — the question single-repo indexers cannot answer at all.
Cross-repo resolution by label matching
Because Graphify graphs are per-repo, LoadAndMerge synthesises cross-repo edges. It builds a
global symbol index over all repos (:923-943) and emits a cross_repo/name_match
edge for a distinctive label with a foreign match (:955-997). Confidence scales inversely with
fan-out: 1 repo = EXTRACTED, ≤3 = INFERRED, else AMBIGUOUS. IsDistinctiveLabel
(:1274-1292) rejects common names (Configure, Run, ToString,
length <6) and keeps schema-qualified / snake_case / multi-capital names — so
pr_save_application_log links but Init does not.
The distinctiveness filter is what keeps cross-repo linking useful: it connects the genuinely shared symbols
while refusing to draw noise edges between every ToString in the estate.
Graph coverage — 9/9 repos, and why some used to go blind
Graph generation (Graphify Pass 1, inside IngestionEngine.BuildGraphIndexAsync) only ever ran
for a repo as a side-effect of that repo being ingested, and every real trigger was a single-repo
post-deploy webhook. A repo nobody had redeployed in a while — motlogs/scout/
n8n went July/August-stale — never got re-graphified even though its source was still scanned
fine, dropping coverage to 5 of 9 repos (/api/stats.lastSync.graphReposSkipped is where the
symptom surfaced). GraphBackfillHostedService fixes this with a background sweep, default every
Graph:BackfillIntervalMinutes (360, i.e. 6h) that finds repos whose durable
graph.json — resolved via Graphify:PerRepoCacheDir (default
graphify-cache/<repo>/graph.json, GraphifyRunner.cs:107-118) — is missing or
older than Graph:MaxAgeHours (default 168, i.e. a week), and runs graphify for just that repo,
one at a time, through the same run lock a full ingest takes so the two never race. It is
Graph:BackfillEnabled (default true) and never calls Groq or embeddings — graph only. Landed and
live in the 2026-09-12 execution run: graph coverage went 5/9 → 9/9 repos
(docs/LEDGER_SCALE_PLAN.md §12).
A repo's graph no longer silently rots between full rebuilds just because nobody happened to redeploy it — every repo's dependency graph now self-heals on a bounded schedule instead of depending on deploy cadence.
Reachability — orphan and dead-end flagging
ReachabilityService.FlagOrphansAndDeadEnds (ReachabilityService.cs:70) runs in
Phase 2, after the Layer/Pattern join and before layout. It normalises edges to scoped repo:local
ids and builds forward adjacency (:86-114). Roots come from three unioned sources: structural
(path contains /Controllers/, /Endpoints/, /Hubs/ or a Name ending
Controller/Hub/Job/BackgroundService, :119-150); SQL procs with zero incoming edges
(:152-165); and analytical roots by joined Layer/Pattern (:167-177). A forward BFS
(:179-192) marks unvisited non-external nodes IsOrphan, and visited out-degree-0 nodes
(excluding fields/properties and side-effecting patterns) IsDeadEnd. External references are never
flagged.
It is an architectural-smell detector: it powers the Orphan and Dead-End lenses in /graph2 and
feeds the dead-code agent, surfacing code that nothing reaches without a human reading every file.
Layout — ForceAtlas2 with semantic seeding
ForceAtlas2LayoutService.ComputeLayout (ForceAtlas2LayoutService.cs:52) uses a
two-level scheme: an outer pass collapses each Leiden Cluster into a super-node (mass ∝ log member count) and
runs FA2 on the small graph (:85-172); an inner pass lays out members per cluster in parallel,
normalises to a unit disc, and offsets by the super-node (:174-254). The FA2 core (:265-339)
rebuilds a quadtree each iteration (QuadNode :363-448) for O(n log n) Barnes-Hut repulsion, linear
spring attraction, gravity and adaptive cooling, deterministic via a Layout:Seed golden-angle spiral
(SeedCircle :457-474). The optional semantic seed
(SemanticLayoutService.SeedAsync, SemanticLayoutService.cs:46) reads each file's mean
code vector from Qdrant and projects it onto two random orthonormal directions (Johnson-Lindenstrauss via
Gram-Schmidt, :131-148) for the seed X/Y, which FA2 then refines with
useExistingAsSeed=true. It is off by default (Layout:UseSemanticSeed).
It makes a 60k-node graph legible, and in semantic mode places code that talks about similar things in the same basin — so proximity on screen means relatedness in meaning.
GraphBinaryWriter — the columnar wire format
GraphBinaryWriter.Write (GraphBinaryWriter.cs:95) emits under
<OutputDir>/v2/: nodes.bin (a fixed header plus columnar arrays — FNV1a-32
id-hash, interleaved xy floats, u8 categorical indices for repo/kind/layer/pattern interned to dicts, u16
cluster, in/out degrees, a flags byte, :317-366); edges.bin (src/tgt indices, relation
index, confidence, flags, :374-386); labels.bin (four length-prefixed UTF-8 arrays,
:388-400); meta.json (dicts + bbox + tile manifest); and a tile pyramid per zoom level
(grid 2^z × 2^z, tile/{z}_{x}_{y}.bin, :213-284). Raw little-endian is used instead of
MessagePack so the browser can map Float32Array/Int32Array/Uint8Array views directly — saving about a 50KB
decoder (:21-27) — and every file is Brotli-pre-compressed at write time
(WriteBrotliFile :549-558).
The WebGL viewer opens tens of thousands of nodes instantly and streams only the tiles in view, so a graph with 60k nodes stays interactive on an ordinary laptop.
Caller recall — the forward-only problem and its fix
Graphify is forward-only (callee edges, no inbound usage), so node_neighbours(up) and
blast_radius saw about 0% caller recall (UsageEdgeExtractor.cs:13-17). The fix,
UsageEdgeExtractor.Extract (:35), scans real source on disk for identifier usages
naming a distinctive graph symbol and emits reference caller edges via AddUsageEdges.
C# is handled via Roslyn (IdentifierNameSyntax, excluding usings, :98-110); Razor/cshtml/JS
via a comment/string-aware lexer (:118-147). Edges deliberately exclude comments and strings so the
metric is not circular (:20-23). This lifts inbound recall from about 0% to about 40%.
.ts/.tsx files have no Graphify nodes, so TS callers are not yet
sourceable (:25-26). The reconciliation tools below cover the residual deterministically.A missed caller is a broken build. The backfill recovers about 40% of callers straight from the graph, and the reconciliation tools cover the rest — so a rename can be checked instead of guessed.
blast_radius and blast_radius_diff
BlastRadius (GraphFeatureIndex.cs:760) seeds from changed files or anchors, walks
upstream (incoming = dependents) via BfsNeighbours, keeps the shallowest hop, and
sorts by hop then descending InDegree (:803-838). The MCP wrapper CallBlastRadius
(McpService.cs:3986) surfaces an empty-graph hint pointing at sync_ledger. The shared
BfsNeighbours (:659) walks up/down/both with an edge-kind allow-list against Kind +
Relation (:682-687), capped, with cached adjacency invalidated on any edge change.
blast_radius_diff (CallBlastRadiusDiffAsync :3753) delegates to
ReferenceReconciliationService.DiffAsync (ReferenceReconciliationService.cs:156), which
diffs the graph caller set against a deterministic word-boundary textual sweep into
in_both/graph_only/lexical_only and computes recall =
|graph ∩ textual| / |textual|. The lexical_only set is the graph's recall gap — the callers to
verify before a rename.
It turns "what will this PR break?" into a query and tells you how much to trust the answer — the graph gives the fast list, the textual diff gives the honest completeness number.
reference_set and caller_recall_audit
Both live in ReferenceReconciliationService.cs. ReferenceSetAsync (:147)
returns every whole-identifier occurrence with line numbers, reading bytes off disk (not the truncated payload)
so counts are trustworthy (:251-274), behind a 120s corpus cache. AuditAsync
(:172) picks distinctive singly-defined symbols, diffs graph versus textual callers, and reports
mean recall, the perfect-recall count, the zero-graph-caller count, per-repo means, and the 15 worst offenders
(:194-218). It is refreshed off-thread by CallerRecallHostedService for the dashboard
tile.
WP3.2 added two refinements. Anchor resolution is now repo-scoped
(ResolveAnchorScoped, todo 3a2ce5c3): the underlying
GraphFeatureIndex.ResolveAnchor searches every repo's nodes with no way to scope the search, so a
symbol name existing in two repos used to silently pick whichever node had the higher degree, even when the
caller asked about one specific repo. Now, when repo is supplied, candidates are filtered to it
first; when it is omitted and the name is ambiguous across repos, no winner is guessed — the caller gets an
ambiguity result (one candidate per repo) instead. When PreciseReferenceService is
available (Precise:Enabled — live in production since 2026-09-13, see
the three-lane routing section on Vectors & search for the full detail)
and the caller supplied a concrete repo, reference_set and
blast_radius_diff can answer with compiler truth (truth: "compiler") for C# symbols
instead of only the graph/text sweep.
Together they close the refactoring loop: reference_set is the exhaustive checklist,
blast_radius_diff is the per-symbol scope, and caller_recall_audit is the trend over
time — so you always know whether the graph can be trusted for a given change — and repo-scoped anchoring
plus an optional compiler-truth answer both raise how much that trust is worth.
reference_set,
blast_radius, blast_radius_diff, refactor_plan,
cross_stack_path, prior_decisions, find_similar and
caller_recall_audit were previously hidden from the default MCP tool list. They are now part of
CoreTools (McpService.cs:264-278) — the trimmed, usage-based default surface every
caller sees unless Mcp:AdvertiseAllTools=true — so an agent no longer needs to already know these
tools exist to be offered them.The interactive /graph2 viewer
GraphViewer2Assets.cs serves inline HTML/JS (Sigma.js v3 + Graphology from CDN, with the binary
decoder running in a Web Worker so the main thread never stalls, :1-12) at /graph2 +
/graph2/app.js (Program.cs:976-986), and binary tiles + JSON through
/graph/data/{*name} (:932) which returns the pre-Brotli'd .bin with
Content-Encoding: br when advertised, else decompresses on the fly (a workaround for Chrome refusing
Brotli over the insecure tailnet, :945-967). Lenses include Orphan, Dead-End, Hot-Path,
Stack-Integrity/Layer-Skip, Cross-Repo, Cross-Stack, Public-Entry and Twin-Finder (similarity), grouped into
Discovery / Architectural-smells / Connectivity / Similarity, with a tile-mode fallback for >20k nodes. The
Ask box posts to /api/ask (:992). Every graph route is gated by
IsGraphAuthorized (:3111): loopback is always allowed; a remote request requires a
?token= or Bearer matching Mcp:BearerToken, compared in constant time,
fail-closed.
The whole graph is explorable by a human — with lenses for each kind of smell — and secured so a leaked URL cannot be brute-forced.
The drill-through explorer
drill-through.html is the cross-stack tracer built on the same graph: start from a screen,
controller, stored proc or table (or paste a file path) and walk Screen → API → code → SQL →
Table either as lane-grouped columns or as a force-directed network that grows one hop per tap. It
renders the same accumulated graph as the viewer, but is built for following a single request across repos.
- Depth is a radius around the focused (last-tapped) node — a BFS hop-count over the
current edges — and defaults to 5. Tapping recentres the neighbourhood; anything further
than
depthhops auto-hides. The force layout is tuned (stronger damping, faster cooling, a single settle-fit) to settle quickly and hold still rather than drift. - Source on tap. Double-tapping a node opens its detail panel, which fetches the real
body via
/api/graph/node-detail— for UI, API and code nodes and, as of #73, for SQL procedure and table nodes too (the schema.sqlbody), with the node's trace relations listed beneath. - Ask, scoped. The Ask box (
/api/ask) answers about the focused node's real structural neighbourhood, or the whole system when nothing is focused. Tapping a route reveals its child routes as a navigable tree, and "App routes" opens each UI on its home page.
Pass orchestration order
The passes run in a fixed order in IngestionEngine (:1614-1670): LoadAndMerge →
ChunkAnalysisJoin (Layer/Pattern) → Reachability → LayerSkipTagger → UsageEdgeExtractor (caller backfill) →
optional SemanticLayout.SeedAsync → ForceAtlas2.ComputeLayout → GraphBinaryWriter.Write. Every pass is
try/catch and non-fatal. The cross-stack, covered_by, implements and emitted_from edges are materialised
separately by CrossStackResolver in the whole-corpus post-passes.
emitted_from is fully implemented but dormant (0 edges where
log_facts is empty); motivates is declared but not materialised; inbound recall is
about 0% from Graphify alone, lifted to about 40% by UsageEdgeExtractor; TS/TSX callers remain unreachable, and
the reconciliation tools cover the residual deterministically.Order is load-bearing — each pass consumes what the previous produced (reachability needs the layer join, layout needs everything else) — and non-fatal passes mean one failing enrichment never takes down the whole graph build.
Open Brain — the MOTMaster Architectural Ledger · generated from source 2026-07-07