Scaffolds a complete graphlens language adapter package from scratch. Use when asked to create an adapter, add language support, scaffold an adapter, or implement a new language adapter for graphlens. Produces all 5 source modules, pyproject.toml, and test stubs.
Installation
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Scaffolds a complete graphlens language adapter package from scratch. Use when asked to create an adapter, add language support, scaffold an adapter, or implement a new language adapter for graphlens. Produces all 5 source modules, pyproject.toml, and test stubs.
compatibility
Requires Python 3.13+, tree-sitter>=0.24, a tree-sitter-<lang> grammar package, uv workspace
allowed-tools
Bash Read Write Edit WebSearch
graphlens Adapter Generator
Generates a production-ready graphlens-<lang> adapter package following the exact architecture of graphlens-python.
Quick Start
WebSearch the target language (Step 0) โ extensions, package managers, stdlib names
Tree-sitter only โ every adapter must use tree-sitter as its parser (no stdlib ast, no regex)
Pure data producers โ adapters return a GraphLens; they never write to files, databases, or any backend
Entry points โ adapters register via importlib.metadata entry points; callers use adapter_registry.load()
Deterministic node IDs โ always use make_node_id(project_name, qualified_name, kind.value) (SHA-256[:16])
1-based spans โ tree-sitter positions are 0-based; always add +1 to row and col when constructing Span
ImportClassifier pre-pass โ build ImportClassifier(stdlib, third_party, internal) before visiting any file; every IMPORT node must have metadata["origin"] set
Three stacks โ visitor maintains _scope_stack, _container_stack, _kind_stack for scope tracking
dep_parsers constructor param โ adapters accept a custom parser list so callers can inject non-standard package managers
name_span on structural nodes โ visitor records metadata["name_span"] (Span of the name token) on every CLASS, FUNCTION, METHOD, VARIABLE, ATTRIBUTE, TYPE_ALIAS, PARAMETER node so the SpanIndex can map definition positions back to node IDs
OccurrenceRef collection โ visitor collects OccurrenceRef objects (role, 1-based position of the name token, enclosing node ID) for every use-site but does not emit CALLS, REFERENCES, HAS_TYPE, or INHERITS_FROM edges directly โ those are produced by the post-visit resolution pass
SymbolResolver โ each adapter ships a SymbolResolver subclass (_resolver.py) that wraps a type-aware engine; it must never raise โ all errors return None/[]
Post-visit resolution pass, ONCE for the whole analyze() call, never per sub-root โ after every sub-root's structure is built: build SpanIndex from the now-complete graph, call resolver.prepare(project_root, all_files)once, rooted at the top-level project_root with the union of every sub-root's files, then for each OccurrenceRef call resolver.definition_at() and emit the correct edge or fall back to EXTERNAL_SYMBOL. Calling resolver.prepare() inside the per-sub-root loop is a bug: it scopes the resolver's workspace to just that one sub-root (so cross-sub-root references can never resolve) and pays a full subprocess-spawn + re-index cost per sub-root instead of once.
Step-by-Step Generation Process
Step 0 โ Research the language (WebSearch)
Before collecting anything from the user, perform web searches to build accurate language knowledge:
File extensions โ search "{language} source file extensions" and "tree-sitter-{lang} grammar". Identify all commonly used extensions (e.g. .ts, .tsx, .d.ts for TypeScript). Include declaration/header files if they contain importable symbols.
All mainstream package managers (e.g. npm, yarn, pnpm for Node; cargo for Rust; go mod for Go)
The manifest file name(s) each one uses
Where declared dependencies live inside each manifest (key paths)
Whether dev/test groups are separate keys
Module system โ search "{language} import system" and "{language} module resolution". Understand:
How file paths map to importable names
Relative vs absolute import syntax
How the language's equivalent of __init__.py / index.ts works
Standard library / built-ins โ search "{language} standard library modules list". Collect the top-level names callers import from the stdlib.
Document findings before proceeding to Step 1. This research drives file_extensions(), {LANG}_MARKERS, DependencyFileParser implementations, and get_stdlib_names().
Step 1 โ Collect inputs from user
Required:
Language name (e.g. typescript, rust, go) โ used for {lang} placeholder
tree-sitter grammar package (e.g. tree-sitter-typescript) โ PyPI package name
{Lang}ASTVisitor with visit() dispatch via getattr(self, f"_visit_{node.type}", None)
_visit_children() for default traversal
Three stacks initialized in __init__: _scope_stack, _container_stack, _kind_stack
occurrences: list[OccurrenceRef] field on the visitor โ filled during traversal
Handlers for class/struct, function/method, import, variable, attribute nodes
Every structural node (CLASS, FUNCTION, METHOD, VARIABLE, ATTRIBUTE, TYPE_ALIAS, PARAMETER) records metadata["name_span"] = Span of the name token
Call sites, read/write uses, type annotations, base classes โ append OccurrenceRef to self.occurrences; do not emit CALLS/REFERENCES/HAS_TYPE/INHERITS_FROM edges
Collect all OccurrenceRef objects from each visitor into a flat list
Link PROJECT โ top-level modules via CONTAINS
Return (project_id, project_name, occurrences, ...) โ do not resolve anything here
Phase 2 โ in analyze(), once for the whole call, after every sub-root's
Phase 1 has run:
11. Build SpanIndex(graph) from the now-complete graph (spans every sub-root)
12. Call resolver.prepare(project_root, all_files)once, rooted at the
top-level project_root, with the union of every sub-root's files
13. For each OccurrenceRef (across every sub-root) call
resolver.definition_at(file, line, col) โ use SpanIndex.at() to find
the target node โ emit the correct edge (CALLS/REFERENCES/HAS_TYPE/
INHERITS_FROM) or fall back to _get_or_create_external_symbol()
Monorepo rule: one resolver session for the whole analyze() call
A monorepo can yield dozens of sub-roots (e.g. a split-package repo with a
manifest per component โ laravel/framework's illuminate/* sub-packages
is the canonical example). Calling resolver.prepare() once per sub-root,
scoped to that sub-root's own directory, is wrong twice over: (1) the
resolver's workspace never contains sibling sub-roots' files, so
cross-sub-root references can never resolve no matter how good the resolver
is, and (2) each sub-root pays its own subprocess spawn + full re-index from
zero, which dominates wall-clock on a repo with many sub-roots. This applies
unconditionally โ don't try to detect whether sub-roots are "really the same
codebase" first; merging unrelated sub-roots into one resolver session costs
a bit of extra indexing but is never incorrect. See
packages/graphlens-rust/src/graphlens_rust/_adapter.py's analyze() for
the reference implementation.
Step 10 โ Generate __init__.py
"""graphlens_{lang} โ {Language} language adapter for graphlens."""from graphlens_{lang}._adapter import {Lang}Adapter
from graphlens_{lang}._resolver import {Lang}Resolver
__all__ = ["{Lang}Adapter", "{Lang}Resolver"]
Create packages/graphlens-{lang}/ruff.toml. The key rule: [lint.per-file-ignores] must relax annotation, docstring, and security rules for tests/** so pytest code doesn't require full production-grade typing.
test_{lang}_deps.py โ each parser's can_parse and parse, get_stdlib_names
test_{lang}_visitor.py โ visitor unit tests per node type; assert occurrences collected, name_span recorded; assert no CALLS/REFERENCES/HAS_TYPE/INHERITS_FROM edges emitted by visitor
test_{lang}_resolver.py โ {Lang}Resolver.definition_at() and infer_type_at(); assert never raises
test_{lang}_adapter.py โ end-to-end: real source snippets โ correct graph structure; assert resolved CALLS/REFERENCES/HAS_TYPE/INHERITS_FROM edges point to real declaration nodes