Ontology Language Analysis Runtime
Traceability
- ADR ID:
ADR-0009 - Status: Proposed
- Decision date: 2026-09-09
- Spec: Studio Ontology Language Runtime
Context
Studio's memories module needs an Ontology view of a target project: typed entities (functions, classes, interfaces, modules, …) and, eventually, the relationships between them (calls, imports, inheritance). Memories alone cannot supply this — they are the model's summarized recollection of a session, not a structural index of the source tree. Building the entity layer requires parsing source directly.
We looked at how Zed (/Users/phodal/test/zed, a mature Rust editor built by the creators of tree-sitter) is built, since it does exactly this kind of per-file structural analysis across a large language set. Its shape:
- A native
tree-sittercore plus onetree-sitter-<language>crate per first-party language (Rust, Python, Go, TypeScript/TSX, Bash, C, C++, CSS, HTML, JSON, YAML, Markdown, …), statically linked. - Each language ships four to ten small
.scmtree-sitter query files (highlights,outline,brackets,indents,injections,overrides,runnables,textobjects,debugger), compiled into a generic capture-index-drivenGrammarstruct (language_core::grammar) rather than bespoke per-language Rust code. - Languages outside that first-party set are community extensions that ship a grammar as a
.wasmmodule, loaded at runtime through tree-sitter's ownwasmfeature (tree_sitter::WasmStore, backed bywasmtime-c-api) — Zed's core binary never statically links most of the languages it supports. - Critically, tree-sitter in Zed never resolves cross-file relationships.
call_hierarchyandproject_symbols— anything that answers "who calls this" or "where is this defined elsewhere" — is answered entirely by the Language Server Protocol. Tree-sitter's job stops at one file's syntax tree and the structural queries run over it.
Separately, this repository already runs three Rust sidecars from packages/better-harness-desktop/rust — harness-oxc-service, harness-acp-host, harness-evidence-host — each a newline-delimited-JSON process Node spawns over stdio, with an additional NSXPC transport on macOS (harness-<name>-xpc / harness-<name>-client, a byte-for-byte JSONL forwarder). harness-oxc-service already builds a real semantic layer (scopes, bindings, references) for JS/TS via oxc_semantic — that is a stronger foundation for JS/TS ontology data than a tree-sitter outline query would be, and this decision does not replace it.
The open question this ADR resolves: given we need Java, Rust, TypeScript, JavaScript, Go, Python, C#, Swift, Kotlin, and SQL coverage, should the host be Rust with grammars loaded as WASM modules (Zed's own split), and should any of this instead just be a Node/web-tree-sitter module given Studio is already an Electron/Node codebase?
Decision
A fourth Rust sidecar,
harness-ontology-service, joinsoxc-service/acp-host/evidence-hostunderpackages/better-harness-desktop/rust, with the same NDJSON-over-stdio wire shape (version/id/method/params, 4 MiB request / 16 MiB response ceilings, fail-closed on malformed or oversized frames) and the same NSXPC bridge structure on macOS. Node's role stays a thin client, as it already is for the other three hosts.We are not doing this in Node with
web-tree-sitter. Bulk ontology extraction is a CPU-bound, whole-tree parse over potentially the entire target project — exactly the kind of work the existing three sidecars already exist to keep off the Node event loop. Adding a second parsing runtime (WASM-in-Node) beside the Rust one already used for JS/TS (oxc_semantic) would split this capability across two toolchains for no offsetting benefit;web-tree-sitter's WASM-hosted engine is also measurably slower than tree-sitter's native core for large-file/bulk parsing, which is the dominant cost here, not per-keystroke latency.The parsing engine is native
tree-sitter; grammars load as WASM modules loaded at runtime, not statically linked per language, using tree-sitter's ownwasmfeature (tree_sitter::WasmStore,wasmtimeunder the hood) — the same split Zed itself uses between its first-party languages and its extension marketplace. AGrammarSourceenum keeps both paths live:Native(a linkedtree-sitter-*crate) andWasmPending(registered and routable, grammar bytes supplied later). This means adding or updating a language does not require recompiling or redistributing the host binary once WASM loading is wired up.This slice ships six languages as
Native— Rust, Python, Go, TypeScript, TSX, and JavaScript (the last two reuse the TSX grammar, as Zed's own language configs do) — because their grammar crates are published, stable, and match Zed's own pinned versions where Zed vendors them directly:Language Crate Version Matches Zed Rust tree-sitter-rust0.24.2 Yes — identical pin Python tree-sitter-python0.25.0 Yes — identical pin Go tree-sitter-go0.25.0 Yes — identical pin TypeScript / TSX / JavaScript tree-sitter-typescript0.23.2 Close — Zed pins a zed-industriesfork of the same grammar for an upstream PR; we track the plain upstream releaseJava, C#, Swift, Kotlin, and SQL are registered as
WasmPending— visible inhost.describeand routable, butontology.extractreportsgrammar-unavailableuntil a.wasmgrammar buffer is wired in (Spec task, not this ADR). Zed's own extensions for these languages live in its separatezed-industries/extensionsmarketplace repository, which we could not fetch to confirm exact pinned revisions; the candidate grammars, picked by adoption on crates.io, are[NEEDS CLARIFICATION: confirm against Zed's extension registry before pinning]:Language Candidate crate Source repo crates.io downloads Java tree-sitter-javatree-sitter/tree-sitter-java10.9M C# tree-sitter-c-sharptree-sitter/tree-sitter-c-sharp5.8M Swift tree-sitter-swiftalex-pinkus/tree-sitter-swift5.1M Kotlin tree-sitter-kotlin-ngtree-sitter-grammars/tree-sitter-kotlin2.3M (vs. 0.4M for the older fwcd/tree-sitter-kotlin)SQL tree-sitter-sequelderekstride/tree-sitter-sql1.0M Per-language entity queries are original, not ported from Zed. Zed's
.scmquery files are GPL-licensed editor outline queries with UI-specific concerns (test-runnable detection, breadcrumb context, nested variable capture) this use case does not need. We wrote a smaller, original query set per language — same@item/@nametree-sitter capture-naming convention (that convention is a mechanism, not Zed's expression), scoped to named declarations relevant to an ontology (classes, functions, methods, interfaces, structs, traits, enums, modules).Cross-file relationships are explicitly out of this decision. This service answers "what entities does this file define," not "who calls this" or "what does this import resolve to" — matching the boundary tree-sitter itself has in Zed. A later relationship-resolution stage can reuse
oxc_semantic's existing scope/binding data for JS/TS and will need its own design for the other languages (name/import resolution, or astack-graphs-style approach) — not decided here.
Consequences
Studio gains a per-file structural entity extractor for six languages today, with the remaining five routable but inert until their grammars are loaded. The wire, transport, and packaging shape is proven (three sibling hosts already ship this way), so the new risk surface is narrow: the grammar registry and the entity queries. Any future language addition is one languages::<name> module (native) or one WASM buffer (pending) — it does not touch the wire protocol. The explicit non-goal on cross-file relationships means Studio's Ontology view cannot show call graphs or inheritance edges from this service alone until a follow-up ADR picks a resolution strategy; presenting only per-file entities in the interim must not be framed as a full ontology graph.