v1.0.0 · Node ≥ 20 · No build step

arch-graph

Static architecture graph for NestJS monorepos.
Local multilingual semantic search runs alongside, fully offline.
The Self-Injected AI Operating System.
Deterministic. Benchmarked.
Zero LLM tokens on build AND query.
Cuts LLM context costs by up to 90%.

  • 90% token savings via Surgical Reads
  • 14.3× fewer tokens on structural queries
  • 74.8% RU semantic hit-rate vs graphify 20.4%
  • 0 LLM tokens to build indexes

The challenge

Twelve services in. Nobody knows what depends on what.

You're 12 services into a NestJS monorepo. Who publishes user.created? Which service depends on users_table? Every question is a 15-minute grep session. But for AI Agents, the problem is worse: Large codebases drown LLMs in context. Reading full files burns thousands of tokens per turn, and without explicit guidance, the AI guesses your project's "unwritten rules," leading to architectural drift.

arch-graph solves both by providing a deterministic knowledge sidecar. Human developers get millisecond answers; AI agents get compressed "Proof Packets" and extracted policies. The result: 100% architectural fidelity with 90% fewer tokens.

How it works

Sources flow in. A typed graph grows out.

Every @Module, @InjectQueue, @MessagePattern, configured nats.subscribeDecorators / rmq.subscribeDecorators, Nest command object ({ cmd: Enum.X }), this.someCmd override, @InjectRepository, axios.get() and import statement gets parsed by ts-morph, classified by domain extractor, resolved to typed nodes and edges, and emitted as graph.json.

Level 0 Context Injection

The Self-Injected AI Operating System.

Stop bloating your LLM prompts with static instructions. arch-graph acts as a native plugin for your AI agents (Claude Code, Cursor, Gemini), automatically waking them up with a perfect mental map of your monorepo and enforcing your project's unique architectural rules.

0 Level 0 Orientation

When you launch an agent, the SessionStart.sh hook automatically feeds it a high-level "Empire Map": apps, libs, index health, and top coding policies. The LLM never starts blind.

1 Surgical Reads

Instead of reading 1000-line files (10,000 tokens), the LLM uses get_file_outline to get exact line and endLine boundaries. It reads only the 20 lines it needs. 90% token savings.

2 Gold Standard Blueprints

The engine ranks symbols by quality (JSDocs, tests, decorators). When creating a new feature, the LLM requests a blueprint and mimics your project's absolute best code, not generic boilerplate.

3 Policy Mining

arch-graph automatically infers your team's unwritten rules (e.g. "@ManyToOne is always paired with @CustomFK"). It feeds these to the LLM as mandatory policies.

4 Dependency Guardrails

Before the LLM writes code, it runs validate_proposal. If it tries to import a Repository into a Controller, the tool blocks it and demands it route through a Service.

5 Placement Engine

Monorepos get messy when LLMs guess where files go. The suggest_placement tool analyzes your folder clusters and tells the LLM exactly where a new service should live.

AI Layer Workflow: Implementing PromoCode Feature
User
Implement a new PromoCodeService with a validate() method.
Agent
Session start hook. Calling get_orientation()...
{ "projectSummary": "NestJS monorepo with 5 apps and 12 libs.", "freshness": "2026-05-22T14:30:00.000Z", "symbols": 27588, "topPolicies": [ "DTO location: libs/shared/src/dto/*.ts", "Entity location: libs/shared/src/db/entities/*.ts" ], "agentHint": "Use 'get_project_policies' for the full list of architectural conventions." }
Agent
Checking project rules. Calling get_project_policies()...
{ "policies": [ { "rule": "When using @ManyToOne, also use @CustomFK", "confidence": 1.0 }, { "rule": "Controllers must NOT import Repositories", "kind": "guardrail" } ] }
Agent
Finding correct folder. Calling suggest_placement("PromoCodeService", "class")...
{ "suggestions": [{ "path": "apps/api/src/modules/promos/PromoCodeService.ts", "reason": "Existing feature services follow this cluster pattern." }] }
Agent
Learning from best examples. Calling get_blueprint("class")...
{ "compositeGuide": "Ideal Service: Uses @Injectable, implements BaseService, and pairs @ManyToOne with @CustomFK. See DiscountService.ts:12-45.", "patterns": ["Services 100% located in **/services/*.ts"] }
Agent
Strategy ready. I will implement PromoCodeService in apps/api/src/modules/promos/ using @CustomFK and BaseService inheritance as per local standards.

Deterministic Code Analysis

Deep Code-Intel Sidecar.

While semantic search handles fuzzy intent, code-intel provides 100% accurate, deterministic facts. It builds a specialized index of symbols, calls, and data flows that the LLM can query to understand complex execution chains across service boundaries.

Data-Flow facts

Trace parameters from @Body, through mappers, to the Database sink. AST-proven facts, no guessing.

Cross-Service Flow

Don't stop at boundaries. trace_message_flow links emitters to remote handlers across NATS/RMQ.

Impact Analysis

Changing a DTO or DB Entity field? impact_contract instantly finds every affected consumer.

Call Graph Scenarios

Ask "What happens when this is called?". trace_scenario returns internal execution chains, conditions, and bubbling exceptions.

Route Resolution

See GET /api/v1/orders/42 in a log? resolve_url maps it back to the exact handler — folding in setGlobalPrefix + URI version the controller never shows. Forward too.

Code Metrics

Ranked deterministic hotspots — fan-in/out, branch complexity, god-class — via code_metrics. Raw facts, no arbitrary "smell" thresholds.

Deterministic Query: Trace Scenario
User
What happens exactly when a new order is created? Trace the logic.
Agent
I will trace the execution path. Calling trace_scenario("POST /orders")...
{ "entry": "OrdersController.create", "calls": [ { "callee": "OrdersService.register", "conditions": ["dto.valid"] }, { "callee": "OrderMapper.toEntity", "conditions": ["dto.valid"] }, { "callee": "InventoryService.checkStock", "conditions": ["dto.valid"] }, { "callee": "TransactionsRepository.save", "conditions": ["dto.valid", "stock > 0"] } ] }
// Logic branch 'stock > 0' was automatically detected and merged into the trace.
Intelligence vs Navigation

Why architecture facts beat raw LSP indexing

Legacy LSP (Microscope)
arch-graph (Radar)
Scope
Syntax & types in current workspace.
System-wide intent (NATS, DB, BullMQ).
Efficiency
Raw 200+ reference lists.
Compressed Proof Packets (90% savings).
Context
Stops at file boundaries.
Cross-boundary (NestJS ➔ React).

Semantic layer

Local multilingual semantic search.

A dense-vector index sits next to the structural graph and answers fuzzy or natural-language queries — in Russian, English, or any of 50+ supported languages. The embedder runs in-process via transformers.js; no API key, no GPU, no network roundtrip. Build cost: a single CPU pass once per graph rebuild. Query cost: a cosine-similarity scan over ~1k–10k node vectors, sub-100ms on a typical monorepo.

Search by intent, not by name

Query"where do we send email to customers"

Matchprovider:EmailSender · queue:email · db-table:email_logs

Same index answers English, Russian, and 50+ other languages. Useful when the team mixes languages or doesn't remember the exact identifier.

How it works

Embeddermultilingual-e5-base

Dim · Rank768 · dense+BM25

Xenova/multilingual-e5-base via transformers.js with passage/query prefixes. Ranking fuses dense cosine with BM25 lexical hits via Reciprocal Rank Fusion, so exact symbols and table names stay visible. Schema v2 with per-node contentHash enables incremental re-embed. Persisted as arch-graph-out/semantic/embeddings.jsonl + manifest.json.

Three semantic MCP tools

code_searchcode only

docs_searchdocs only

semantic_search covers the union. Split exists because mixing buckets drops A_find recall: 80% → 30%. All three support minScore, kindQuotas, and kindBoosts; bucket filters stay tool-owned. Baseline hit-rate: 47% → 67% → 75% with e5-base. Semantic is one of four MCP surfaces — see 30+ tools in the CLI/MCP section below for the full set.

vs graphify on the same suite

RU queries74.8% · 20.4%

EN strict75.4% · 56.5%

Both are local-first graph tools. graphify uses LLM subagents at build time for semantic extraction; arch-graph uses zero LLM tokens at both build and query. On Russian queries arch-graph leads by +54.4 pp (multilingual e5-base vs English-keyword BFS); on EN-keyword strict apples-to-apples it leads by +18.9 pp (e5-base default, 2026-05-19). Fair-scope note: graphify by default indexes .next/, tmp/, and nested .worktrees/ directories into its BFS graph; those add noise nodes that inflated graphify's prior lenient scores. The numbers above are measured after excluding those paths on the graphify side — arch-graph already excludes them by convention via appsGlob/libsGlob. Full 103-query memo →

arch-graph semantic build                          # one-time index build
arch-graph semantic search "email notifications"   # multilingual fuzzy query
arch-graph code-search "PaymentHandler retry"      # code bucket only
arch-graph docs-search "how to set up MSSQL agent" # docs bucket only

Measured

Measured on 3 real NestJS monorepos.

Head-to-head re-run via arch-graph compare on four private reference monorepos. 40 auto-generated questions total (10 per project) sampled from real nodes in each graph. Same compression, same cl100k_base encoder, same questions for both tools. Recall is a substring-presence necessary-condition heuristic, not an end-to-end LLM eval. This benchmark predates the semantic sidecar — it measures arch-graph's structural-only retrieval. A fresh post-semantic head-to-head on 103 fuzzy-intent queries with e5-base (2026-05-19) is in docs/comparisons/2026-05-19-arch-graph-vs-graphify-eval.mdon Russian queries arch-graph leads 74.8% vs 20.4% (multilingual handling matters when 80%+ of queries are non-English); under apples-to-apples strict scoring on English-keyword queries arch-graph leads by +18.9 pp at 75.4% vs 56.5% (graphify with full LLM semantic extraction). arch-graph stays at zero LLM tokens on both build and query; graphify uses LLM subagents at build time.

Avg tokens per question

arch-graph39,779

graphify569,924

14.3× fewer tokens. Same 40 questions, same compression.

Mean recall (substring-presence)

arch-graph100%

graphify39%

~2.5× higher recall on architecture queries. Necessary-condition, not an LLM eval.

Total tokens · 40 questions

arch-graph1.59M

graphify22.8M

Same compression aggressiveness, same cl100k_base encoder, same questions.

Build cost

arch-graphdeterministic

graphifyLLM-driven

ts-morph only. No network. No LLM at build time.

Per-project breakdown

Project Size arch nodes / edges arch tokens arch recall graphify nodes / edges graphify tokens graphify recall
A large 805 / 1,517 62,460 100% 11,414 / — 715,430 50%
B large 897 / 1,220 63,059 100% 10,874 / — 724,785 37%
C medium 358 / 547 23,552 100% 5,163 / — 302,999 9%
D small 144 / 239 10,046 100% 6,128 / — 536,483 61%

Recall uses a substring-presence heuristic: for each question, we check whether every ground-truth label appears in the tool's compressed context. It's a permissive necessary-condition — "did the tool's output even contain the answer". Projects B and D had graphify rebuilt fresh; the build ran in AST-only mode (LLM semantic pass was unavailable in the skill context where it was invoked), producing a larger, less-curated graph than a full graphify run would. This inflates graphify's token count and may overstate arch-graph's token advantage on those two projects. Recall, however, is measured on whatever was actually in the graph — that's the honest comparison. Projects A and C used pre-existing graphify-out (build mode unknown). Full methodology: bench/report.md →

Skeptical of our numbers? Reproduce the same comparison on your own repo: arch-graph compare --graphify path/to/graphify-out. Auto-generates 10 questions from your real graph, writes arch-graph-out/compare-report.md. Without --graphify you still get a graph-size-only summary.

We also ran 4 other tools on the same 40 questions

Short version — different tools answer different questions. Headline numbers from the same bench harness:

  • @nestjs/devtools-integration (official, runtime): 65,395 tokens · 9.2% mean recall. 87.5% on module-imports (its native domain), 0% on NATS / BullMQ / TypeORM / cross-service. Needed DB / NATS / JWT env stubs to bootstrap — in CI without secrets, 3 of 4 projects would fail at boot.
  • nestjs-spelunker (runtime, DI-only): 13,535 tokens · 10.6% mean recall. Cheapest in tokens — it captures only the module-import layer. Needed a harness bypassing onModuleInit hooks (real hooks hit live external services).
  • @riaskov/nst-graph-visualizer (advertised static, in practice runtime): partial · 1.3% mean recall. Failed to load the main app module on 3 of 4 projects — bidirectional TypeORM forwardRef in entity barrels created CJS require cycles. arch-graph's true-static AST mode doesn't hit this.
  • dependency-cruiser (static, generic): nominal 40.8% recall, but that's substring noise — by default it doesn't parse TypeScript in directory mode, so the "matches" are file paths like caniuse-lite/broadcastchannel.js. 0% on real NestJS-architecture questions.

None of these are "broken" — they're built for different scopes. arch-graph wins where their scopes don't overlap: cross-cutting message-bus + data-layer edges, statically, with file:line. Read the full breakdown with per-tool caveats on the comparison page →

Coverage at a glance

Numbers above show how well arch-graph answers. This compact matrix shows what it sees that adjacent tools structurally don't — the uniqueness-defining rows from the full comparison. first-class, partial / inferred, not covered by construction.

Capability arch-graph graphify @nestjs/devtools-integration @riaskov/nestjs-graph-visualizer dependency-cruiser
NATS pub/sub edges
BullMQ producer/consumer
TypeORM @InjectRepository → table
TypeORM @ManyToOne/@OneToManydb-relation
HTTP inter-service calls
NestJS DI graph ✓ static ✓ runtime ✓ static
@UseGuards / @UseInterceptors / @UsePipes ✓ runtime
TS imports (static + dynamic + CJS require) ✓ best
Import cycle detection (diagnostics.cycles)
file:line on every edge
Static (no boot required) ◐ AST
Deterministic (no LLM)
Semantic search (opt-in) ✓ 3 tools
Markdown docs indexed ✓ doc-section

Only the uniqueness-defining rows shown — full comparison → covers 22 capabilities × 9 tools, with per-tool analysis and honest "where arch-graph loses" framing.

Open invitation

Help us widen the benchmark.

Three ways to contribute, from one command to a full evaluator suite. The published numbers come from three NestJS monorepos — every additional data point narrows the confidence interval, especially on non-NestJS shapes (Node monoliths, GraphQL backends, mixed-language teams).

1 One-shot: anonymous numbers

Run arch-graph compare --share on your repo. Counts and ratios only — no code, no service IDs. Preview shows the exact payload before anything leaves your machine.

See what's sent →

2 Custom evaluator suite

Bring your own queries.json (RU / EN / mixed), run the standard eval harness, submit per-category hit-rates. Schema and step-by-step in CONTRIBUTING.md.

Example query set →

3 Code, fixes, new extractors

Branch off main, follow conventional commits, add vitest tests. New extractor? Read src/extractors/docs/ first — it's the cleanest shape reference.

PR workflow →

Quick start

One command to get going.

  1. 1

    Install + (optionally) init

    Clones into ~/.arch-graph, symlinks arch-graph into ~/.local/bin, then asks whether to initialise the current directory. If yes, chains straight into the wizard — writes arch-graph.config.ts, optionally installs the Claude Code skill, optionally adds a git hook, and runs the first build. Requires Node ≥ 20.

    cd path/to/your/nestjs-monorepo
    curl -fsSL https://roman-dubovik.github.io/arch-graph/install.sh | sh

    Prefer to read the script first? Same effect: git clone https://github.com/roman-dubovik/arch-graph ~/.arch-graph && bash ~/.arch-graph/scripts/install.sh. If you skip the init prompt, run arch-graph init later in any project — and add arch-graph-out/ to .gitignore so build output stays local.

  2. 2

    Init separately (if you skipped above)

    Same interactive wizard, run on demand in any project. Asks about config, Claude skill, git hook, and offers to run the first build right away.

    cd path/to/your/nestjs-monorepo
    arch-graph init
  3. 3

    Stays in lockstep automatically

    The init wizard installs a pre-commit hook by default. On every commit it validates a fresh structural pass (ts-morph only, no LLM) and leaves generated graph artifacts local. Full semantic indexing is explicit via arch-graph semantic build; the model is cached locally and incremental rebuilds reuse unchanged vectors. Each build emits graph.json, diagnostics.json, validation.json, graph.mermaid, plus an advisory recall table per domain.

    arch-graph hook install              # pre-commit: validate structural graph; artifacts stay local
    arch-graph build                     # manual structural; advisory exit 0
    arch-graph semantic build            # manual semantic; incremental by default
    arch-graph semantic build --full     # full re-embed (model swap / schema bump)
    arch-graph build --strict            # CI gate: exit 3 if recall drops below floor

What it knows

Eight domains. Gated on every build.

Each card is one extractor pass — a typed walk over the NestJS decorators / call shapes that domain uses. Coverage is boolean here (the extractor exists or it doesn't); per-domain recall is measured separately in the bench above and gated by arch-graph build --strict.

  • NATS

    Subject-level message bus. Standard Nest decorators plus configured aliases such as NatsMessagePattern; command objects like { cmd: Enum.X } and inherited this.someCmd properties resolve to literal or pattern subjects.

    • nats-publish
    • nats-subscribe
    • nats-request
    • nats-reply
  • TypeORM

    Repository → entity → table chain. Relation edges carry type, owner-side, inverse property, join-table and selected option metadata, including configured decorator aliases.

    • db-access
    • db-read · db-write
    • db-relation
  • BullMQ

    Queue boundaries. @InjectQueue producers and @Processor consumers tied to a named queue node. Queue meta includes concurrency, defaultDelay, defaultAttempts, defaultBackoff, hasRepeat (extras-v1); jobData type fields and workerConcurrencyEnvVar / workerConcurrencyFallback (types-v1, requires --with-types). Modern @nestjs/bullmq pattern: WorkerHost.process() override AND class heritage type-args (extends BaseWorkerHost<T,R>) both detected (realworld-v1) + recursive walk for 2-level inheritance (realworld-v2) + bullmq-default concurrency injection with source marker (realworld-v3).

    • queue-produce
    • queue-consume
    • queue-fails-into (DLQ — MUST registerQueue.failOver, MAY catch-block .add())
    • queue-event-listener (queue.on / worker.on)
    • queue-repeat (queue.add(…, {repeat:{cron}})cron-schedule)
  • React / Next FE

    Pages, routes, React Router JSX routes, components, hooks, JSX renders, imports, and i18n. Next Pages Router roots are marker-gated; feature folders named pages/ stay out of route recall.

    • fe-routes-to
    • fe-renders
    • fe-imports
  • Cron schedule cron-v1

    @nestjs/schedule decorators (@Cron, @Interval, @Timeout) plus dynamic SchedulerRegistry.add* registrations. Each site becomes a cron-schedule node with the expression resolved against CronExpression aliases when possible. Per-site diagnostics (unresolved, unresolvedOptions, filteredByReceiver) surface non-literal arguments without losing visibility.

    • cron-triggers
  • NestJS DI

    Module wiring plus the filter chain. Guards, interceptors, pipes attach to controllers and handlers as first-class edges.

    • di-import · di-provides · di-exports
    • di-controller
    • di-guard · di-interceptor · di-pipe
  • HTTP

    HttpService, axios, fetch call sites. Env-ref URLs resolved against internal services; external hosts emit http-external.

    • http-call
    • http-external
  • TS imports

    Static import, dynamic import(), and CommonJS require(...). Aliases resolved via tsconfig.paths. File-level + service-level aggregation.

    • ts-import
    • lib-usage

Per-build the extractor also runs a recall gate against ground truth it derives from your code (NATS handlers vs senders, TypeORM @InjectRepository resolution, BullMQ producer/consumer roles, DI references). Floors are ≥ 95% per domain (≥ 80% for TS imports, since alias resolution is best-effort). arch-graph build --strict exits non-zero on regression. Additional diagnostic: import cycles across ts-import / lib-usage / di-import layers, surfaced in diagnostics.json.

14 features shipped.

  • code-intel-heritage-v1

    Inheritance-aware code intelligence. The extractor models TypeScript extends chains: class heritage fields, super-call edges, and override classification (delegation/augmented/replaced). get_type_definition returns inheritedMembers; find_references follows super-call edges; self_check filters pure-delegation collisions so only real silent-wrong-answer risks surface.

  • code-intel-stabilization-v1

    Honest health verdict: self_check degrades only on real silent-wrong-answer risks (skipped files, class/method or type name collisions). Composite file-qualified symbol IDs disambiguate when short names repeat. Atomic CLI writes, torn-write tolerant MCP loader. Cursor markers auto-migrate from legacy on re-install; uninstall is markdown-aware and preserves user content.

  • ai-runtime-layer-v1

    Direct agent integration (Claude, Cursor, Gemini). Includes Orientation hooks, Style Guardians, and Guardrails to ensure AI writes architecturally sound code.

  • cross-service-flow-v1

    Unified trace across NATS/RMQ boundaries. Links emitters to remote handlers for a single, end-to-end event chain.

  • member-map-intelligence

    New get_type_definition tool returns exhaustive member maps (fields, decorators, methods) for any symbol without reading files.

  • code-intel-v1

    Deterministic code intelligence: symbols, deep call traces, and interprocedural data-flow facts extracted directly from AST via ts-morph.

  • surgical-reads-v1

    Symbol range detection (line to endLine). Allows LLMs to read only specific methods instead of full files, saving 90% in token costs.

  • doc-section-v1

    Markdown files are indexed as graph nodes. Semantic search now covers documentation alongside source code for better high-level intent retrieval.

  • code-vs-docs-v1

    Semantic search split into dedicated code_search and docs_search tools to prevent result dilution. Hit-rate up to 75% with e5-base.

  • ui-uplift-v1

    Frontend component embeddings now include Tailwind tokens and i18n strings for highly precise UI-specific retrieval.

  • openapi-enrich-v1

    Swagger/OpenAPI route descriptions and parameter summaries are automatically folded into backend endpoint node metadata.

  • fe-i18n-multi-enum-v1

    Support for multi-file locales and automated TS enum-member resolution in NestJS @Controller path templates.

  • nats-command-resolver-v1

    NATS subjects now resolve Nest command objects ({ cmd: EAudit.X }) and base-class sender methods expanded through overrides.

  • bullmq-types-v1

    Resolves Job<DataType> generics via ts-morph type-checker. High-fidelity queue data-flow extraction for producers and workers.

Three ways to query

CLI, MCP, or Claude Code skill — equal first-class paths.

The same graph backs all three. Pick the integration that matches the consumer: terminals → CLI, MCP-aware editors → MCP server, agents in Claude Code → skill.

Primary path

CLI

Ten subcommands. --json by default, --table for humans. Exit code 4 on not-found. Fastest for agents — no stdio overhead, no server lifecycle.

arch-graph who-publishes user.created --table
arch-graph deps-of order-api --table
arch-graph path service:auth service:billing
arch-graph stats

For MCP-aware clients

MCP server

Stdio MCP server exposing 30+ specialized tools — 10 structural (subject_publishers, queue_consumers, table_users, path, …), 3 semantic (code_search, docs_search, semantic_search), 16 code-intel tools for symbols, members, references, exception traces, and impact (4 marked EXPERIMENTAL: get_blueprint, get_project_policies, suggest_placement, validate_proposal), plus 2 natural-language fallback tools (explain, query). Wire it into any MCP host.

// ~/.claude/.mcp.json
{
  "mcpServers": {
    "arch-graph": { "command": "arch-graph", "args": ["mcp"] }
  }
}

Auto-trigger in Claude Code

Claude Code skill

Installs ~/.claude/skills/arch-graph/SKILL.md + a delimited block in ./CLAUDE.md. Semantic strategy instructions are written as an idempotent marked block, or to CLAUDE.md.arch-graph-snippet.md when you choose the separate review file. The skill knows when to fire — Claude consults the graph automatically on architecture questions.

arch-graph claude install --skill
arch-graph hook install            # pre-commit, keeps graph in lockstep

Honest comparison

Other tools you might consider.

arch-graph isn't the only graph extractor in this space, and on some questions it isn't the best one. If you're picking a tool, weigh these honestly against arch-graph's wedge (static + cross-cutting NATS/BullMQ/TypeORM edges + file:line provenance).

  • @nestjs/devtools-integration

    Official, runtime-based. Boots your app via NestFactory.create() and snapshots the live module/provider graph. More authoritative than any static tool on what DI actually wires up — including conditional bootstrap. Different category (live runtime vs static). Doesn't extract NATS subjects, BullMQ queues, or TypeORM table edges as typed edges.

  • @riaskov/nestjs-graph-visualizer

    Static + Nest-aware — methodologically closest to arch-graph. Narrower scope: NestJS module DI only, no cross-cutting NATS / BullMQ / inter-service HTTP. Output is Mermaid / DOT / SVG (not JSON).

  • dependency-cruiser

    Generic TypeScript import graph. Battle-tested across module systems. Doesn't see NestJS semantics — all decorators collapse to plain imports. arch-graph won't dominate it on raw file-imports; we'd expect a near-tie there, and a win by construction on any NATS / BullMQ / TypeORM question dep-cruiser is structurally incapable of answering.

Honesty

What it isn't. When it doesn't work.

arch-graph is a static extractor. It does not see runtime configuration, container env values, or dynamically constructed identifiers. The bench numbers are measured on our reference suite — your project's results depend on how closely your code matches the NestJS conventions the extractor recognises.

  • Static, not runtime

    If your subject name only exists at runtime (env vars, DI factories), arch-graph won't invent an edge. It records the gap in diagnostics.json.

  • Dynamic subjects → diagnostics

    publish(`subject.${userId}`) becomes a diagnostic with file:line, not a guessed edge. No hallucinated topology.

  • Custom wrappers must be declared

    If you wrap @nestjs/microservices or BullMQ in your own client, you have to declare the wrapper API in arch-graph.config.ts — otherwise the extractor sees no decorator it recognises and silently misses the calls. The diagnostics output is the gap report.

  • Single monorepo

    No cross-repo graph yet. Multi-repo deployments need a separate stitching pass that isn't built.

  • NestJS-specific

    The extractors are tuned to NestJS decorators and a small set of wrapper conventions. General-purpose code analysis isn't the goal — graphify is.

  • Bench ≠ your codebase

    We measure ourselves on our 5 reference NestJS monorepos. We do not promise the same recall on yours — your numbers depend on which conventions you follow and what's declared in your config. The per-build recall gate is what tells you whether arch-graph is matching reality on your code.