dualgraph
v0.2.1
Published
Deterministic dual-edge code-context graph for AI coding sessions: code edges (imports) + capability edges (string-keyed registries imports can't see). Zero LLM tokens at scan and query.
Maintainers
Readme
dualgraph
Deterministic dual-edge code-context graph for AI coding sessions. Zero LLM tokens at scan and query.
Which files a task needs should be a graph lookup, not a model exploring your tree. dualgraph builds that graph mechanically and answers task queries in milliseconds — so your AI assistant spends tokens reasoning, not searching.
npx dualgraph scan src ./project.graph.json
npx dualgraph query ./project.graph.json "ebl transfer surrender holder" 8 36.0 routes/action-catalog.ts (capability:TRANSFER_EBL+EBL_SURRENDER, 80 loc)
36.0 schema/types/ebl.ts (name/symbol, 209 loc)
28.0 routes/forja.ts (capability:TRANSFER_EBL+EBL_SURRENDER, 1006 loc)
...
always-context hubs: schema/builder.ts(211←) lib/db.ts(133←)The dual-edge idea
Import graphs are commodity — and structurally blind. Modern backends wire their most important layers by string keys: action catalogs, GraphQL field registries (mutationField('surrenderEbl', …) exports nothing), route registries, event names. Those layers have zero static import edges. A dependency graph alone will never find them.
dualgraph has two edge classes:
- code edges — derived from imports/exports. Handles tsconfig path aliases (including Nx
tsconfig.base.jsonand Vitetsconfig.app.jsonsplits, per-file scope in monorepos, deepapp/api/x/[id]/nesting), CJSrequire, dynamicimport(). String-keyed declarations are extracted as symbols too: pothos/GraphQL field names (mutationField('surrenderEbl')), Fastify/Express route paths (app.post('/webhooks/whatsapp-reply')), and namespaced event names (emit('order.shipped')) — each is a registry imports can't see, and each gets its own derivation rule (0.2.0). - capability edges — derived from a string-keyed action catalog you point it at (
ID: { route: '...' }entries). An edge exists only if a file literally cites the action ID or its quoted route string — and the edge records which citation. No inference, ever.
Every result says which edge class produced it.
Design rules
- Derive, never author. Every edge is mechanically extracted and can cite its source. No human- or LLM-authored edges.
- Zero LLM tokens. Scan and query are pure computation. The graph feeds a model; it is not one.
- Stale graphs are refused, not served. Query re-fingerprints the source; if it changed since the scan, you get exit 2 and a fix hint — never silently wrong context (
--allow-staleto override knowingly). - Hubs are always-context, not findings. Files imported by everything (
db.ts, schema builders) are reported separately, never compete with task results. - No wrapper, no daemon. Plain CLI. Composes with any agent that can run a shell command. Nothing intercepts your sessions; nothing phones home (usage logs stay in
~/.dualgraph/, yours).
Real numbers (our own use, 2026-06-05)
We run this across our own fleet — these are measured, not projected:
- 663-file backend + 355-file frontend scanned in ~1s; 111-service fleet (≈10k files, 8,814 edges) full scan 1.0s, fingerprint-incremental re-scan 0.1s
- Retrieval on three real tasks: 2 of 3 returned the full expected working set (8/8); the third went from 4/8 (imports only) to ~7/8 once capability edges joined — the missing files were exactly the string-keyed act layer
- Remaining misses are genuine name collisions, which we report rather than tune away
No "10x faster" claims. It's a graph: it removes exploration, it does not write code.
Usage with an AI coding agent
Inject the ability to query, not the graph. Add ~3 lines to your agent's system prompt or project instructions:
Before exploring with file search, run:
dualgraph query /path/to/project.graph.json "<task words>" 8
It returns the ranked working set; `capability:` rows are act-cited files.Keep the graph fresh in CI or a cron: dualgraph scan src ./project.graph.json (incremental by content fingerprint).
Capability catalog format
Point join at any TS/JS file containing string-keyed entries shaped like:
export const ACTIONS = {
TRANSFER_EBL: { route: 'ebl.transfer', ... },
ISSUE_INVOICE: { route: 'invoice.create', ... },
};dualgraph join ./project.graph.json --catalog src/routes/action-catalog.tsThe catalog is parsed statically — your code is never executed.
License
AGPL-3.0-only. Built by ANKR (PowerBox IT Solutions Pvt Ltd), extracted from the substrate that runs our 290-service fleet.
