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.md — on 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
Mean recall (substring-presence)
arch-graph100%
graphify39%
Total tokens · 40 questions
arch-graph1.59M
graphify22.8M
Build cost
arch-graphdeterministic
graphifyLLM-driven
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/@OneToMany → db-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 |
✓ |
✗ |
✗ |
✗ |
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.
-
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
-
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
-
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)
-
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
-
@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.
-
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
-
HttpService, axios, fetch call sites. Env-ref URLs resolved against internal services; external hosts emit http-external.
-
Static import, dynamic import(), and CommonJS require(...). Aliases resolved via tsconfig.paths. File-level + service-level aggregation.
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.