Handbooks
Concepts

Analysis fidelity

Two analysis tiers produce identical-looking output. That is a trap, so every adapter declares what it can deliver and the handbook discloses it.

The problem

Handbooks analyzes 18 languages with two engines:

  • Full tier — a hand-written adapter per language, with type-driven call resolution, inherited members, per-attribute state tracking and statement spans.
  • Generic tier — one config-driven engine with a declarative spec per language. The file and function inventory is exact; call relations are best-effort.

Both produce the same intermediate representation. Nothing downstream can tell them apart by looking at nodes and edges.

That is exactly the trap. A reader — and especially an agent — would take a generic-tier call edge for a Python-grade fact, and reason on it accordingly.

The fix: declare, record, disclose

1. Every adapter declares what it can deliver

interface AdapterCapabilities {
  tier: 'full' | 'generic';
  callTypes: readonly CallType[]; // which edge kinds it can actually produce
  selfAttrs: boolean; // can it track self/this attribute reads and writes?
  statementSpans: boolean; // can it report statement spans (resync precision)?
}

This field is required, not optional. Adapters can be registered by anyone, and a hand-rolled one that declares nothing simply gets left out of the graph metadata rather than having a fidelity claim invented for it.

2. Phase 1 records it per language

phase1/graph.json (excerpt)
{
  "metadata": {
    "language": "multi",
    "languages": {
      "typescript": { "tier": "full", "selfAttrs": true, "statementSpans": true, "callTypes": ["..."] },
      "kotlin": { "tier": "generic", "selfAttrs": false, "statementSpans": false, "callTypes": ["..."] }
    }
  }
}

Per language, not per graph — a multi-language run mixes tiers, and a single language: "multi" label would hide that completely.

3. The handbook says so, where trust is formed

When any generic-tier language is present, the overview gains one line, immediately under the system prose:

Analysis fidelity — call relations for Kotlin, Scala come from the generic (config-driven) analyzer: they are best-effort and may be incomplete. The file inventory and the structure of these languages are exact.

And nowhere at all when every language is full-tier, so the common case stays noise-free. The same disclosure appears in the HTML site, the agent index and llms-full.txt.

Which tier is my language?

Full tier: Python, TypeScript (and JavaScript), Go, Rust, Java, C#, C/C++, Ruby, PHP, Swift, Dart, Solidity, Shell.

Generic tier: Kotlin, Scala, Zig, Objective-C, OCaml.

See Language support for extensions and caveats.

What "best-effort" actually costs you

Full tierGeneric tier
File inventoryexactexact
Function and method inventoryexactexact
Line ranges and signaturesexactexact
Direct calls by nameexactmostly
self.method() / this.method()resolvedoften
self.attr.method() via a known typeresolvedno
param.method() via a type annotationresolvedno
Inherited membersresolvedno
Per-attribute state trackingyesno
Statement spans (resync precision)yesno

The things you route on — where a file is, what functions it contains, what lines they occupy — are exact in both tiers. What degrades is the relationship graph, which affects grouping quality and co-change hints rather than addresses.

Two honest caveats

Swift. The bundled grammar aborts the process on V8 ≥ 13 — measured fatal 5 times out of 5 on Node 24, fine on Node 21, and unique to that one grammar among nineteen. The adapter refuses at discovery on such a runtime and names the remedy (node --liftoff-only) rather than taking your whole run down with it.

Shell. A script containing a case statement is skipped, because that grammar throws — its external scanner imports a symbol the pinned WASM linker does not provide. Because case is ubiquitous this is most non-trivial scripts (measured on nvm: all 6 files, all 122 functions), so treat shell coverage as partial. The scan log names the cause, and every skipped script is listed individually in phase1/scan-coverage.json with reason: "unparsable" — a gap you can enumerate, not one you have to guess at.

Both are reported through the logger during the scan. Run with -v to see them.

Moving a language up a tier

A generic-tier language is a declarative spec, not a parser — see Adding a language. Promoting one to full tier means implementing LanguageAdapter directly and declaring honest capabilities. The adapter contract is small on purpose.

On this page