Code Intelligence — type/scope-resolved call graph (CONCEPT:EG-KG.compute.type-scope-resolved-call)¶
The Knowledge Graph models code as :Code symbols linked by :calls, :inherits,
:realizes, :dependsOn, :covers. The accuracy of those links is set by how
calls are resolved. Before KG-2.100 resolution was name-only: a method call
obj.run() bound to any symbol named run, and a callee name shared by more than
ten symbols was dropped entirely. KG-2.100 makes resolution type- and
scope-aware, computed in the Rust engine and shipped already-resolved.
What changed¶
- Resolution is in Rust, in one round-trip. The
epistemic-graphengine'sIndexRepositoryop parses every file (rayon) and resolves cross-file calls over the whole batch, returning one merged graph. The Python pipeline calls it once instead of doing a separate parse pass plus a per-symbol Python resolution loop. - A resolution ladder, most-specific first (
crates/eg-compute/src/parser/resolve.rs): same_file— a definition in the caller's own file.scoped— aself/this/super(or implicit-this) call, or an explicit receiver naming a class, binds to that class's method or an inherited one.arity— same-name overloads disambiguated by argument count.unique— a single definition anywhere in the batch.- otherwise unresolved — an ambiguous callee is never guessed.
Every resolved
callsedge carries astrategyand aconfidence. - Structural class edges. Class base/interface lists produce
inherits(subclass to base) andrealizes(class to interface) edges, resolved the same conservative way. - The OWL layer reasons over them.
:inheritsis transitive, so the reasoner extrapolates inheritance chains;:callsreachability stays available to the graph algorithms (PageRank / community detection) over the now-accurate edges.
The two ingest paths, one resolver¶
Both code-ingest paths consume the same Rust resolver:
- Local
EnrichmentPipeline(enrichment/pipeline.py) — when the engine advertisesIndexRepository, oneindex_repositorycall yields the symbols and the resolvedCALLS/INHERITS/REALIZESedges. Name-only resolution remains only as the engine-unreachable fallback. - GitLab / source-sync (
core/gitlab_indexer.py) — already shipped each project toindex_repository; it now also passes theinherits/realizesedges through, namespaced per instance.
Surfaces¶
The resolved graph is queryable on both surfaces (same _execute_tool core):
- MCP:
graph_analyze(action="call_graph", node_id=<symbol>, target=<callees|callers|inherits>). - REST:
GET /graph/analyze/call-graph?id=<symbol>&direction=<callees|callers|inherits>(the action-routedPOST /graph/analyzealso accepts it).
Flow¶
Code intelligence pipeline
Source files feed the index_repository RPC, which tree-sitter parses
(scope, arity, call sites, bases) and resolves through the resolve_site
ladder (same_file → scoped → arity → unique), producing calls edges
(with strategy + confidence), inherits, and realizes into the
Knowledge Graph's :Code symbols. OWL reasoning over those symbols
(transitive inherits, call reachability) and the KG itself both feed the
call_graph query (MCP action and REST twin).
Key files¶
| Layer | File |
|---|---|
| Rust extraction | epistemic-graph/crates/eg-compute/src/parser/tree_sitter.rs |
| Rust resolution | epistemic-graph/crates/eg-compute/src/parser/resolve.rs |
| Pipeline consumer | agent_utilities/knowledge_graph/enrichment/pipeline.py, enrichment/extractors/code_test.py |
| GitLab consumer | agent_utilities/knowledge_graph/core/gitlab_indexer.py |
| Ontology | agent_utilities/knowledge_graph/ontology_software.ttl (:inherits, :realizes) |
| Reasoning | agent_utilities/knowledge_graph/core/owl_bridge.py |
| Surfaces | agent_utilities/mcp/tools/analysis_tools.py, agent_utilities/mcp/kg_server.py |
Model-free similarity (CONCEPT:EG-KG.compute.model-free-similar-code)¶
Code search and clone detection must keep working when the embedder is offline (the recurring embedding-endpoint 502s). So similarity is model-free, computed in the same Rust round-trip:
- Each symbol gets a MinHash signature over its normalized AST-leaf trigrams — identifiers/strings/numbers/types are abstracted to class tokens (so a renamed-variable clone still matches) while keywords/operators/punctuation are kept verbatim (so structure is preserved).
- The resolver LSH-bands the signatures: symbols colliding in any band are
candidate pairs, linked with a symmetric scored
similar_toedge when their estimated Jaccard ≥ 0.5 (capped per node; mega-buckets skipped). The signature is a compute-only input and is stripped from the graph nodes. :similarTois a symmetric OWL property; the reasoner closes it both ways.- Query it embedder-free:
graph_analyze(action="similar_code", node_id=…)/GET /graph/analyze/similar-code?id=….
This is the near-clone signal B4 reuses for CodeClone.
Code ↔ service linking (CONCEPT:AU-KG.compute.http-route-graph)¶
Routes are the seam between a service's code and the live ecosystem. From the route
decorators the parser captured, the routes pass emits Route nodes (method+path)
and serves edges (handler Code → Route); a best-effort name match links each
Route to a deployed ecosystem Service (servedBy). The OWL surpass: reasoning
chains Code –serves→ Route –servedBy→ Service –deployedOn→ Node — a fact a
siloed per-repo code tool can't produce because it never sees the topology. Query
it: graph_analyze(action="routes") / GET /graph/analyze/routes. (gRPC/GraphQL
detection and event channels are later increments.)
Infra, coupling, clones, decisions (CONCEPT:AU-KG.enrichment.read-them-here-so–2.105)¶
The graph spans past the code itself:
- IaC → Resource (AU-KG.enrichment.read-them-here-so). Dockerfiles, K8s/Kustomize manifests, and
Terraform are parsed into
Resourcenodes (image/kind/name) and linked to the deployedServicetheyprovision— so code → infra → topology is one graph. - Git change-coupling → FILE_CHANGES_WITH (AU-KG.ingest.mine-git-history-files). Files that keep changing
together get a symmetric weighted edge — the hidden blast radius the AST can't
see.
graph_analyze(action="change_coupling", target=<repo>). - Near-clones are the
similar_toedges from EG-KG.compute.model-free-similar-code (MinHash) — no separate pass needed. - ADRs (AU-KG.compute.adr-crud).
graph_analyze(action="adr")creates/listsArchitectureDecisionRecordnodes so design decisions live in the same KG.
Change-scoped clone gates¶
The repository also has a local, deterministic clone gate for code review. It
uses two complementary scanners, with their exact versions and thresholds in
pyproject.toml under [tool.agent_utilities.clone_scanners]:
scripts/check_dupehound.pyruns on the normal pre-commit stage. It asks dupehound to compare only staged functions (or the working tree when there is no staged delta) againstHEAD;--base-refenables merge-base/PR semantics. It blocks a newly changed supported-language function that duplicates an existing function and returns exit 2 when the binary, version, or JSON result cannot be trusted.scripts/check_duplication.py enforce --base-ref <ref>is the bounded jscpd differential pass for code, template, configuration, and documentation blocks. Code remains in scope because jscpd can find a copied block inside two different functions, which a whole-function detector cannot. Markdown's plain-text fragments are reported by jscpd as its virtualtextformat, so that report format is explicitly included alongside the extension-backedtxtformat. It recomputes the base and merged clone sets in throwaway worktrees and fails only on new pairs. Thecensusmode is all-format, full-tree, and advisory; it is manual-only so a whole-repository report never adds automatic push time.
Both wrappers read the same exclusion list for generated/vendor/build output,
lockfiles, fixtures, snapshots, and examples. The dupehound wrapper mirrors the
pinned v0.1.2 check classifier: test paths are outside its whole-function
scope, and it passes --exclude-tests explicitly because --include-tests
does not make that command inspect them. Ordinary test code remains in the
jscpd differential scope unless it matches a deliberate fixture/snapshot
exclusion; its block-level coverage is complementary. No baseline file is
written: pre-existing clone debt stays visible in a census and is not silently
converted into a permanent exception. The walkers preserve eligible hidden
directories (including .github for mapped formats) and prune only configured
junk plus Git metadata. Malformed or missing reports, non-regular report files,
clone locations outside the scan roots, inconsistent counts, and incomplete
throwaway-worktree cleanup all return exit 2 rather than a false green. Live
binary/workdir overrides are read through the repository config abstraction,
not directly from os.environ. Dupehound runs during pre-commit; the targeted
jscpd differential remains available manually so the automatic push gate stays
bounded.
The differential list is an explicit, reviewable allowlist; formats not yet
mapped there remain visible to the manual all-format census rather than
silently expanding the blocking scope.
Clone-detection gate flow
A staged change or PR range goes through path selection + exclusions, then
two scanners: dupehound (changed functions) and jscpd (code + non-code
blocks). Either scanner finding something new (a new duplicate, or a new
clone pair) blocks with exit 1; a pre-existing clone pair instead feeds the
real clone-count report, alongside the separate advisory manual census
(all formats).
The native binaries are intentionally not installed by pre-commit. Install the
versions recorded in pyproject.toml ahead of time:
Missing or drifted binaries fail closed rather than passing as an empty scan.
Grammar coverage (CONCEPT:AU-KG.compute.built-ast-extended)¶
The core ast tier parses 9 languages (Python/JS/TS/Go/Rust/Java/C/C++/C#). The
feature-gated ast-extended tier (folded into the engine's full build) adds
Ruby, PHP, Bash, Scala, and Lua — so a slim build stays lean while the deployed
engine spans the common ecosystem languages. New grammars wire in by adding the
crate to the gate, an extension→grammar arm, and any new node-kind mappings; the
resolver/similarity/route passes are language-agnostic and benefit for free.