Turn any source's metadata into a semantic knowledge graph that AI agents query through MCP — instead of re-discovering schemas with live introspection queries (or re-grepping a codebase) on every question.
Problem: an agent asked "average users that logged in 3 months ago" today burns many round-trips discovering tables, columns, and joins before writing a single query. Asked "where is auth handled" in a repo, it burns the same round-trips in Grep → Glob → Read chains.
Solution: extract metadata once (schema, enums, foreign keys — or functions, call edges, doc comments) into a graph, index it semantically, and give the agent MCP tools that answer "which tables?", "which columns?", "how do I join them?", "who calls this function?" instantly.
Three source shapes, one graph engine:
| Source | Extracted | Ask it |
|---|---|---|
| Database (SQLite, PostgreSQL) | tables, columns, enums, FKs, indexes, comments | "which tables hold login events, and how do I join them?" |
| API spec (OpenAPI 3.x, Swagger 2.0) | endpoints, schemas, $ref relationships, auth |
"what's the full request to create an order?" |
| Codebase (Go, TS/TSX, JS/JSX, Python, Rust, Java, C#, Kotlin, Swift) | functions, types, call graph, HTTP routes, ORM models, docs, git co-change | "what breaks if I change this signature?" / "which routes touch this model?" / "which docs went stale?" |
source (sqlite | postgres | openapi | codebase)
│ extract (once; incremental after that)
▼
gatt-out/graph.db ← source of truth: typed nodes + edges (SQLite + FTS5,
│ default) — or graph.json (portable, versionable, via --out)
│ index
▼
gatt-out/vectors.json ← semantic layer: node embeddings (Ollama bge-m3, multilingual),
│ cosine search in-process; --qdrant swaps in a Qdrant server
▼
MCP stdio server ← agents call tools; zero live introspection
- Graph structure (nodes, edges, join paths) lives in
graph.dbby default (SQLite with an FTS5 full-text index, incremental delta writes) orgraph.json(portable, diff-able) if you pass--out gatt-out/graph.jsonon the first extract. All commands auto-detect whichever exists;--graph PATHoverrides. - Search is hybrid: FTS5 bm25 (when the graph is SQLite) + semantic embeddings, so results are good even before indexing. If the vector index or Ollama is unavailable,
find_entitiesfalls back to keyword matching. - Vector index is in-process by default. Metadata graphs are small (hundreds to a few thousand nodes), so brute-force cosine takes microseconds — no vector database required. Pass
--qdrant URLtoindex/search/mcpto opt into a Qdrant server (useful for very large or shared indexes).
Three source shapes share one graph so the traversal, search, and join machinery is reused across all of them.
Database — Nodes: database, table, column, view, index — attrs carry data types, enum values, defaults, row counts, DDL.
Edges: HAS_TABLE, HAS_COLUMN, HAS_INDEX, INDEXES, FOREIGN_KEY (column→column), REFERENCES (table→table, with from_column/to_column for join building).
API spec (OpenAPI 3.x / Swagger 2.0) — from FastAPI's /openapi.json, a swaggo-generated swagger.json, or any spec file. Nodes: api, schema (a component model, like a table), property (like a column), endpoint (an HTTP operation).
Edges: HAS_SCHEMA, HAS_PROPERTY, HAS_ENDPOINT, REFERS_TO (property→schema, a $ref — the FK analogue), REFERENCES (schema→schema, derived from $refs, with from_property/cardinality), ACCEPTS/RESPONDS_WITH (endpoint→schema request/response bodies). Named enum components are inlined as a property's allowed values rather than modeled as a relationship, and join_path returns the $ref chain (User → Order (buyer)) instead of a SQL JOIN.
Each endpoint carries what a request actually needs: the full URL (server base — 3.x servers with variables substituted, or 2.0 host+basePath — joined to the path) and the auth scheme (Bearer, Basic, apiKey header X-API-Key, resolved from the operation's security or the global default, OAuth2/public overrides included). So an agent gets a copy-pasteable curl skeleton — method, URL, auth header, and the typed request body with its enums — from one sql_context call instead of loading a multi-megabyte spec.
Codebase — tree-sitter parses Go, TypeScript/TSX, JavaScript/JSX, Python, Rust, Java, C#, Kotlin, and Swift (plus Markdown docs). Nodes: project, file, function (with signature, file:line range, doc comment, body for short functions), definition (types and their methods — plus, for JS/TS/JSX, module-scope const/exported bindings like config objects and lookup tables, so queueDefinitions-style entities are queryable and doc-drift-checkable too), component, feature, doc, comment (a substantive floating comment not attached to any declaration), and route (an Express-style registration in JS/TS/JSX, or an annotation-declared Spring @GetMapping/ASP.NET [HttpGet] handler in Java/C# — class-level @RequestMapping/[Route("api/[controller]")] prefixes joined in) plus — JS/TS/JSX only — state (a tracked property on a cross-file singleton, e.g. config.session), and package — one per resolved local directory, shared by every language whose imports are directory-scoped (Go: keyed by its package X clause; Python: a directory with (or without, PEP 420) __init__.py; Java/Kotlin: resolved against every src/main/java, src/main/kotlin, src/test/java, src/test/kotlin found in the tree; C#: using directives resolved best-effort by stripping each .csproj's <RootNamespace> prefix, since C# namespaces aren't required to mirror folders the way the others are). Functions/definitions in Go/Python/Java/Kotlin/C# carry an exported attr (capitalized name for Go; no leading _ for Python; public modifier for Java/C#; not private/internal for Kotlin) so "what's this package's public API" doesn't need a source read — Rust gets the same exported attr (bare pub, not pub(crate)/pub(super)) but resolves use paths straight to the owning .rs file instead of a package node, since Rust modules are 1:1 with files/mod.rs, never a bare directory. Swift is parsed (functions/types/calls) but its import resolves external modules only — same-module Swift files need no import statement to reference each other, so there's nothing local to resolve. model nodes capture ORM models across languages — Sequelize Model.init/sequelize.define (JS/TS), TypeORM @Entity/@Column decorators, Go structs with gorm:/db:/bun:/xorm: tags, Django/SQLAlchemy classes — with the DB table, the field→column renames a SQL grep can't see, and associations: explicit (A.hasMany(B), resolved even when declared in a central setupAssociations file) plus *_id/…Id/…ID foreign-key-name inference in any language (inferred=true on the edge). Unknown ORMs: declare base classes in .gatt/models.json ({"base_classes": [...]}) or tag a type via annotate model_table=<table>; gatt models lists them all. Two cross-layer edges close the full-stack chain: USES_MODEL (route → every model its handler chain touches, via resolved CALLS + file imports — graph-level, so language-agnostic) and CALLS_ENDPOINT (client call site → route, in any parsed language: axios-style x.get('/path'), fetch(...), method-as-string wrappers (apiRequest("post", "/x"), Go http.NewRequest("POST", ...), Python requests.request("GET", ...)), verb-prefixed members (getForObject, GetAsync), options objects (axios({method, url})), format-string paths (fmt.Sprintf("/x/%d", id), f-strings, $"..."/$var/\(x) interpolation), Retrofit verb annotations (@GET("users/{id}") on Java/Kotlin interface methods), absolute URLs (https://api.x.com/users/1 loses scheme+host — desktop/mobile apps have no relative origin) and base-var templates (`${API_BASE}/users`) — all normalized to :param and matched by method + path tail with ambiguous matches skipped (relative BaseAddress-style paths need a ≥2-segment match); a candidate needs explicit HTTP-verb evidence, so strings.HasPrefix(s, "/x") never wires. In-house wrappers the heuristics can't see: declare them in .gatt/clients.json — {"wrappers": [{"name": "apiRequest", "method_arg": 0, "path_arg": 1}, {"name": "getJSON", "method": "GET", "path_arg": 0}]}). Template files (.vue, .html, .cshtml, .svelte, ... — detected by content, not by an extension list) join the same chain: inline <script> blocks are masked in place and parsed with the JS/TS grammar (line numbers survive; lang="ts" respected; src=/non-JS blocks skipped), so functions inside a Vue SFC or a Razor page become regular graph nodes whose fetch/axios calls wire to routes, and markup-level surface — htmx hx-get/post/put/patch/delete attributes and <form action method> — is scanned textually with the file node as the edge source. gatt routes shows both per route (models: / called from:); blast on a model lists the API surface it backs; search/describe on a frontend function shows Calls backend routes: ….
Edges: CALLS (resolved local calls — the call graph — plus, JS/TS/JSX only, string-keyed dispatch: queueJob("createPdf") against a {createPdf: createPdfHandler} lookup table, or io.emit("connected")/a custom io.receivedMsj("connected") against a io.on("connected", onConnected) registration elsewhere — CALLS can't see either normally, since the call site never names the handler. Both shapes require a function identifier as evidence, not just a matching string, to keep it from over-firing; registration verbs beyond the built-in on/addEventListener/addListener/once/subscribe set — e.g. an in-house receivedMsj-style wrapper's registration side — go in .gatt/dispatch.json ({"registration_verbs": ["receivedMsj"]}); the trigger side needs no configuration, any call whose first argument matches an established registry key wires, regardless of the callee's own name. These edges carry inferred=true + via=dispatch, same convention as the FK-name-inference edges below), HAS_METHOD, BELONGS_TO (also file → package, Go only — Python/Java/Kotlin/C#/Rust packages are inferred from the import graph, not declared per file, so there's no authoritative per-file name to wire), IMPORTS (relative specifiers and tsconfig path aliases resolve to local file nodes for JS/TS/JSX; every other supported language resolves its own import syntax — Go go.mod paths, Python absolute/relative dotted imports, Java/Kotlin dotted imports against detected source roots, C# using namespaces against .csproj root namespaces, Rust crate/self/super/cross-crate use paths against Cargo.toml crate roots — to the target's local package node (or, JS/TS/Python/Rust, directly to a file node when the import names a single module) instead of an opaque external one — blast pkg:<dir> (or blast <file> for Rust) shows every internal importer, the package-level import graph. Unresolvable specifiers (stdlib, third-party, an unconventional layout the heuristic can't find) still fall back to the external node as before), GENERATES (declared generation pipelines), CO_CHANGED (git history), MENTIONS (doc → code it references), REFERENCES (model → model associations), USES_MODEL (route → model), CALLS_ENDPOINT (frontend call site → route), USES_STYLE (template/JSX/stylesheet file → repo stylesheet whose selectors it uses: .class, #id and [data-*] mined from selector position in css/scss/less plus --var custom-property definitions; usage side scans class=/className=/classList, id=/getElementById/'#x' query strings, data-*=/dataset.camelCase/setAttribute('data-*'), and var(--x) — design tokens make stylesheet→stylesheet edges; matched tokens on the edge's selectors attr. Only tokens defined in the repo's own stylesheets link, so Tailwind/Bootstrap utility soup produces zero edges; tag/global/pseudo selectors are excluded by design — every file uses div, and .btn:hover already links through .btn. blast on a stylesheet lists the UI files its selectors reach), HANDLED_BY/USES_MIDDLEWARE (route → function), and — JS/TS/JSX only — READS/WRITES (function → state). Generated code (.pb.go, _gen.go, .min.js, …) is excluded from context packs but still findable via search. Graphs record an absolute repo root, so every command works from any cwd via --graph; a legacy relative-root graph refuses to refresh from the wrong cwd instead of silently re-pointing at the wrong tree.
Requires: Go 1.24+ and Ollama on :11434 with bge-m3 pulled (optional — keyword/FTS fallback works without it).
Measured on a real 113-table CRM: answering one data question costs ~2.3k tokens in 1 tool call with gatt vs ~7.8k tokens across 6 introspection queries (list tables + describe candidates) — and the join chain comes out correct on the first try. On a codebase, one code-query (~3 KB) replaces a 20–50k-token Grep/Read exploration.
go build -o ~/.local/bin/gatt ./cmd/gatt
gatt extract sqlite path/to/db.sqlite # → gatt-out/graph.db
gatt extract postgres "postgres://user:pass@host:5432/db?sslmode=disable"
gatt extract openapi http://localhost:8000/openapi.json # live FastAPI spec (or a .json/.yaml file; OpenAPI 3.x or Swagger 2.0)
gatt extract codebase . # parse a repo → gatt-out/graph.db (pass --out ...json for a portable/diffable graph)
gatt index # embed nodes → gatt-out/vectors.json
gatt install # register MCP server in Claude Code
gatt install --scope agy # register MCP server in Antigravity CLIgatt install uses claude mcp add when the CLI is available (--scope project|user), otherwise merges into ./.mcp.json directly. Use --scope agy to register in ~/.gemini/config/mcp_config.json (Antigravity's global MCP config, per the agy CLI's own embedded docs). --scope auto-detects when omitted: it prefers claude if that's on PATH, and falls back to agy if only that one is — useful on a fresh Windows setup where claude may not be on PATH yet. gatt install also copies its own binary to ~/.local/bin and adds that to PATH if gatt isn't already resolvable (shell rc file on macOS/Linux, HKCU\Environment on Windows); pass --path=false to skip that.
Query from the terminal (same operations the MCP tools expose):
gatt query "how many messages did each client send this month"
# context pack: tables, columns, enums, joins
gatt code-query "how does incremental refresh work"
# context pack: functions (signature, file:line,
# callers/callees, doc), types, docs
gatt impact saveSQLite --depth 3 # transitive callers: what breaks on a signature
# change; test callers tagged [test]
gatt blast shared/schemas/product.json # blast radius of a file: callers + importers +
# generated copies + diverged duplicates
gatt search "user login timestamps" # hybrid search over all nodes
gatt grep "TODO(#42)" --regex # exhaustive literal/regex scan of every file —
# a zero-result answer proves absence; search
# above is semantic/top-N and is NOT exhaustive
gatt tree internal/engine --depth 2 # directory tree annotated with each file's doc
gatt routes # HTTP routes found in code: method, path, handler,
# middleware, models touched, frontend call sites
gatt models # ORM models found in code: DB table, field→column
# renames, associations (explicit + FK-name inferred)
gatt doc-drift # docs whose code references broke (deleted/renamed
# symbols, moved files) or went stale (code changed
# after the doc's last commit)
gatt diff HEAD~5 # structural diff vs a git ref: added/removed/
# changed/renamed/moved functions & types
gatt path clients conversation_messages # FK join path with exact columns
gatt explain messages # one node: attrs + relationships
gatt overview # all tables (or files/components), counts, referencesgatt extract codebase <dir> parses the repo with tree-sitter and builds the call graph. When <dir> is a git checkout, extraction (and gatt grep) additionally respects the repo's own .gitignore/.git/info/exclude via git ls-files — not just a fixed list of common build-output names (dist, build, node_modules, …) — so a project-specific output directory is excluded too, instead of a compiled/minified copy of every function competing with its real source definition as an ambiguous same-named node. Falls back to the fixed skip list alone when <dir> isn't a git checkout.
The agent-workflow commands:
gatt code-query "<question>"— the code analogue ofquery: the most relevant functions (signature, exactfile:line, resolved callers/callees, doc comment, body when short), types with their methods, and matching docs, in one compact pack. An agent reads only the line ranges the pack points at instead of whole files.gatt impact <function>— walksCALLSedges backwards, transitively (--depth, default 3): every caller that breaks if the signature or behavior changes. Run it before refactors;[test]tags show which tests cover the blast radius.gatt blast <file-or-function>— blast radius of modifying any node, including JSON/YAML/SQL/CSS data files (indexed with a content hash): transitive callers plus file importers (relative imports and tsconfig path aliases resolve to local file nodes), regenerated outputs viaGENERATESedges (declared as"generates": [{"from": …, "to": …}]in.gatt/relations.json), a warning when the target is itself generated, same-basename copies flagged[identical]/[diverged]by hash, and git co-change companions — files with no static edge that historically ship in the same commits (a component's stylesheet, the doc page of a service, the e2e test of a controller, i18n bundles). For a function target, bothimpactandblastalso list ashares mutable state:section — the other functions reading/writing the same tracked singleton property (e.g.config.session), a data-flow signalCALLSalone can't see (heuristic, JS/TS/JSX only, one hop). Run it before editing shared schema/config files.gatt grep <pattern>— exhaustive literal (or--regex) scan of every file under the root, using the same skip rules as extraction. Independent of the indexed extension set and independent of ranking: a zero-result answer is a reliable proof of absence, unlikesearch/find_entities, which is semantic/top-N.gatt tree [path]— a directory tree synthesized from file nodes (the graph has no directory nodes), each file annotated with its doc summary: a leading file/package comment, a markdown doc's title, or its earliest function's doc.gatt routes— every HTTP route detected in code: method, path, resolved handler (identifier, inline arrow, orcontroller.methodreference), middleware chain, the ORM models the handler chain touches (models:), and the client call sites that hit it (called from:). Detects Express-stylerouter.get/post/…registrations in JS/TS/JSX; Spring@GetMapping/@RequestMapping(method=…)(Java) and ASP.NET[HttpGet]/[Route](C#) annotations with class prefixes; Go registrations — gin/echor.GET, chir.Get,mux.HandleFunc("GET /x", h)(1.22 patterns; no verb =ANYwildcard), gorilla.Methods("POST")chains — statement-context only, soresp, err := http.Get(…)stays a client call; and Python decorators — Flask/FastAPI@app.get("/x"),@app.route("/x", methods=["POST"]),<int:id>→:param. Anything else: tag handlers viaannotate route_method=… route_path=….gatt models— every ORM model detected in code, grouped by file: DB table, field count, the field→column renames a SQL grep can't see, and associations in both directions. Detection is layered and language-agnostic: SequelizeModel.init/sequelize.define+ explicit associations, TypeORM decorators, Go DB struct tags, Django/SQLAlchemy classes,*_id-style FK-name inference,.gatt/models.jsonoverlay,annotate model_table=…fallback.gatt doc-drift— which documentation lies: markdown docs whose inline-code references no longer resolve (a symbol deleted/renamed, a cited file moved) or point at code whose last git commit postdates the doc's. Run it after refactors to get the list of docs needing an update, and before trusting a doc.gatt diff [ref]— structural diff of the working tree against a git ref (defaultHEAD): added/removed/changed/renamed/moved functions and types, detected by matching signatures across two extractions (not a textual diff), plus the current callers of anything that changed. Reuses git's own rename detection (git diff -M) for whole-file renames; function-level renames/moves are a same-file (then cross-file) signature-match heuristic.
Never stale: every query command (and the MCP server) checks file mtimes before answering (~60ms), re-parses only the files that changed since the last extract, evicts deleted entities, re-wires cross-file edges (calls, imports, mentions, associations, route models), and re-embeds just the changed nodes. Wrong line numbers are worse than no graph, so you never re-run extract by hand mid-session. (Exceptions that refresh only on full re-extract: CO_CHANGED edges — git history has no incremental delta worth mining — and CALLS_ENDPOINT edges from files that didn't themselves change.)
Structure comes from parsing; meaning comes from a curated overlay. gatt init scaffolds a .gatt/ workspace:
.gatt/
gatt.spec.json project name, namespaces, manifest paths
definitions.json high-level business/architectural domains + their critical rules
relations.json features linked to their physical entry points and dependencies
contracts.json API/database contracts
prompt.md a directive you hand to an AI agent to populate the above
Point your agent at .gatt/prompt.md once: it explores the codebase and writes the domain definitions. Extraction then merges the overlay into the graph as component/feature nodes wired to real files — so code-query answers carry architecture context, not just symbols. The overlay lives in git and survives every re-extract.
For a swaggo/Go service, the spec and the code describe the same thing — gatt can wire them together:
gatt extract openapi swagger.json --code . # endpoints/schemas link to their Go source:
# describe_entity shows source: file:line
gatt enrich . # re-link an existing graph after code edits,
# without re-extracting the specThe schema knows structure, not business meaning: whether enabled=false rows count as real contacts, what a "contact" canonically is. Encode that once and every sql_context/describe_entity response carries it, so the agent writes the right query instead of asking:
gatt annotate contacts \
default_filter="enabled = true" \
entity_note="CRM contacts; enabled=false is a draft; source='google' is imported"
gatt annotate contacts --clear # removeAnnotations live in gatt-out/annotations.json (a sidecar keyed by node id), merged over the graph on every load — so they survive re-running extract. Recognized keys: default_filter (a canonical WHERE clause, rendered like the auto-detected soft-delete filter) and entity_note (free-text definition). Any other key is stored and shown too. sql_context also emits the SQL dialect: up front so generated SQL is dialect-correct without inference.
The graph is a snapshot, so agents need to know how old it is and you need a cheap way to tell when the source has drifted:
gatt extract postgres "$DSN" --check # re-read the source, print schema drift, DO NOT write
gatt extract postgres "$DSN" # re-extract; prints the same drift, then writes
gatt index # re-embed only the nodes whose text changed (--full to force all)- Every extraction stamps
extracted_at.sql_contextandgraph_overviewlead with a# source: postgres:app, extracted 3h ago (2026-07-13)line, so the agent can judge staleness and re-verify against the live DB when it matters — instead of trusting a snapshot blindly. --checkis the drift probe. It hits the source and diffs against the current graph (added/removed tables & columns, changed types, FK count deltas) without touching the graph. Run it on a schedule or in CI; a non-empty drift report means it's time to re-extract and re-index.- Codebase graphs refresh themselves.
code-query,impact, and the MCP server detect changed files by mtime and re-parse only those before answering — no manual re-extract. - Re-extraction is non-destructive to curated knowledge. Annotations live in their sidecar and are re-applied on load; if a re-extract removes a node an annotation targeted, you get a warning naming the orphaned annotation. The
.gatt/overlay is merged fresh on every extract. - Re-indexing is incremental.
indexreuses cached vectors for nodes whose embedding text is unchanged (content-hashed) and only embeds what actually moved — so re-indexing a 113-table CRM after a one-column migration embeds two nodes, not two thousand.
Read — answer schema/code questions from the pre-built graph:
| Tool | Purpose |
|---|---|
graph_overview |
Source, node counts, all tables (or API schemas, or files/components) with member counts and references. Orientation call. |
find_entities |
Hybrid search: "user login timestamps" → sessions.logged_in_at. Filter by node type. |
describe_entity |
One node in full: a table (types, enums, row counts, DDL), an API schema/endpoint (properties, $refs, bodies), or a function/type (signature, doc, call edges) + all relationships. |
join_path |
Cheapest FK path between two tables with exact join columns — a ready JOIN ... ON ... hint (or the $ref chain between two API schemas). Routes around hub tables (tenant_id-style FKs every table carries), which a naive shortest path would cut through, producing semantically wrong joins. |
sql_context |
One-shot context pack for a data question: most relevant tables/schemas fully described. Feed straight into SQL generation. |
code_context |
One-shot context pack for a code question: relevant functions, types, docs with file:line and call graph. The code-query CLI, as a tool. |
impact |
Transitive callers of a function to depth N — what breaks if it changes. Run before refactors; [test] tags included. Also lists shared mutable state (JS/TS/JSX). |
blast |
Blast radius of any node — file (incl. data files), function, or type: callers + importers + regenerated outputs + diverged copies. Run before editing shared config/schema files. |
grep |
Exhaustive literal/regex search across every file — a zero-result answer is proof of absence, unlike the semantic/top-N find_entities. |
tree |
Directory tree synthesized from file nodes, each annotated with its doc summary. |
routes |
Every HTTP route detected in code: method, path, handler (incl. controller.method refs), middleware chain, models touched, frontend call sites. The full-stack intersection in one call. |
models |
Every ORM model detected in code: DB table, field→column renames, associations (explicit + FK-name inferred). Language-agnostic layered detection; .gatt/models.json + annotate model_table for unknown ORMs. |
doc_drift |
Docs whose code references broke (deleted/renamed symbols, moved files) or went stale (referenced code committed after the doc). Run after refactors, and before trusting a doc. |
code_diff |
Structural diff vs a git ref (default HEAD): added/removed/changed/renamed/moved functions & types, plus current callers of anything changed. |
Maintain — the agent curates and refreshes the graph itself:
| Tool | Purpose |
|---|---|
annotate_entity |
Persist business knowledge learned mid-session (entity_note, default_filter) onto a node, so every later sql_context/describe_entity carries it. Written to the annotations sidecar; survives re-extraction. |
reload_graph |
Reload graph + vectors from disk to pick up an extraction/annotation made outside the server. Cheap; no source access. |
check_drift |
Re-read the live source and report how the snapshot has drifted — without writing. Needs a source configured. |
refresh_graph |
Re-extract from the live source, re-embed changed nodes, reload. Writes. Needs a source configured. |
check_drift and refresh_graph only appear when the server was started with the source wired in:
gatt install --source-kind postgres --source "$DSN" # or: gatt mcp --source-kind postgres --source "$DSN"The source (DSN/URL) is then stored in the MCP config so the server can re-extract on request — keep that in mind for DB credentials.
Example — join_path users → products:
JOIN orders ON users.id = orders.user_id
JOIN order_items ON orders.id = order_items.order_id
JOIN products ON order_items.product_id = products.id
Implement connector.Connector (internal/connector/connector.go):
type Connector interface {
Name() string
Extract(ctx context.Context) (*graph.Graph, error)
}Emit nodes/edges with the model in internal/graph/model.go, wire into cmd/gatt/main.go. The graph, index, search, and MCP layers need no changes.
- PostgreSQL connector (
pg_catalog: native enums, column comments, multi-column FKs, view dependencies, multi-schema) - OpenAPI connector (endpoints, schemas,
$refrelationships) — OpenAPI 3.x + Swagger 2.0, from a.json/.yamlfile or a livehttp(s)://.../openapi.json(FastAPI, swaggo) - Codebase connector (tree-sitter: Go, TS/TSX, JS/JSX, Python, Rust, Java, C#) with call graph,
.gatt/semantic overlay, and mtime-based incremental refresh - SQLite graph storage (
graph.db): FTS5 full-text index, delta writes - Incremental re-extraction with change detection (
extract --checkreports drift;indexre-embeds only changed nodes) - Spec↔code linking (
extract openapi --code,gatt enrich): endpoints/schemas point at their Go source - HTTP route entities in code (Express-style JS/TS/JSX):
gatt routes - Shared mutable-state data flow (JS/TS/JSX named-import singletons):
impact/blastshared-state section - Exhaustive literal/regex search (
gatt grep) as a proof-of-absence complement to semanticsearch - Annotated directory tree (
gatt tree) and floating "why" comments as queryablecommentnodes - Structural diff against a git ref (
gatt diff): rename/move-aware function/type changes + current callers - Blast radius for any node (
gatt blast): callers + importers (tsconfig aliases resolved) + generated copies + same-basename duplicates by content hash - Git co-change mining (
CO_CHANGED): stylesheets/docs/e2e-tests/i18n that ship together with no static edge - Doc drift (
gatt doc-drift): MENTIONS edges doc→code, broken/stale reference report - Language-agnostic ORM models (
gatt models): Sequelize/TypeORM/gorm/Django/SQLAlchemy + FK-name inference +.gatt/models.jsonoverlay - Full-stack intersection:
USES_MODEL(route → models its handler touches) +CALLS_ENDPOINT(frontend call site → route) - Absolute graph roots + wrong-cwd refresh guard (legacy relative-source graphs refuse instead of corrupting)
- Language-agnostic client-call detection: verb-evidence heuristic over every parsed language (incl. Java/C#), method-as-string wrappers, options objects, format-string paths,
.gatt/clients.jsonoverlay - Template files by content sniff (no extension list):
.vue/.html/.cshtml/.svelte/... — inline<script>masked & parsed as JS/TS, htmx +<form>attributes scanned - Kotlin + Swift parsing; absolute-URL/base-var/relative-path client calls (desktop & mobile apps); Retrofit
@GETclient annotations - Annotation-declared routes: Spring
@GetMapping/@RequestMapping(Java), ASP.NET[HttpGet]/[Route("api/[controller]")](C#) — full-stack chain included - Java + C# parsing (functions, types, call graph, imports, docs)
- Language-scoped local import/package resolution beyond Go: Python (absolute + relative, PEP 420), Java/Kotlin (
src/main/{java,kotlin}source roots), C# (.csproj<RootNamespace>stripping), Rust (Cargo.tomlcrate roots,crate/self/super) — plusexportedtagging per language's own visibility rule - String-keyed dispatch (
CALLS,inferred=true):queueJob("createPdf")against a lookup table,io.emit("connected")/custom verbs (.gatt/dispatch.json) against anio.on("connected", handler)registration — JS/TS/JSX - Sample values for low-cardinality string columns (
status,type, ...) that carry no declared enum — seeTODO(#2)ininternal/connector/postgres/postgres.go - Query-log mining: add edges from JOINs observed in real queries (relationships not declared as FKs)
- Route detection for Go (gin/chi/echo/gorilla/net-http incl. 1.22
"GET /x"patterns and.Methods()chains) and Python (Flask/FastAPI decorators,methods=[...],<int:id>params) - Builder-chain & enum clients: OkHttp
.url().post()(verb text-scanned in the chain; no verb = any-method wildcard), Alamofiremethod: .post,HttpMethod.POST-style enum args - Selector-level CSS linkage (
USES_STYLE):.class/#id/[data-*]/--var→ the templates/JSX/stylesheets using them; repo-defined tokens only (utility frameworks excluded by construction)
cmd/gatt/ CLI: init | extract | enrich | index | query | code-query | impact |
blast | doc-drift | search | grep | tree | routes | models | diff |
path | explain | annotate | overview | mcp | install
internal/engine/ query operations shared by CLI and MCP
internal/graph/ graph model, traversal, persistence (JSON + SQLite/FTS5)
internal/connector/ Connector interface + sqlite/, postgres/, openapi/, codebase/
internal/embed/ Ollama embedding client
internal/store/ VectorStore interface
internal/store/local/ default in-process cosine index (vectors.json)
internal/store/qdrant/ opt-in Qdrant REST backend
internal/mcpserver/ MCP stdio server (official go-sdk)
tools/mkdemo/ generates a demo SQLite DB for testing