Queryable dependency graphs for Dart and Flutter codebases. Sister project of cartograph (Swift).
MIT licensed, and permanently free — commercial use included. There will never be paid tiers, license keys, seat or line-of-code limits, telemetry, or account sign-in.
This checked-in demo runs the fixtures/closed_app fixture from a source
checkout. See unreachable declarations, change impact, and retained-root
evidence from the real dead, impact, and query commands. The preview
summarizes actual CLI output; the asciinema v2 cast
includes the full command output plus script-added exit labels and a parsed query
summary. Playback pacing is adjusted for readability. Reproduce it with the
recording script.
The name blends Dart and cartograph.
DCM (formerly dart_code_metrics) detects unused code and files in Flutter projects — and went paid in 2023. Its free tier covers one seat and up to 50k lines of code, and the declaration-level unused-code and dependency checks closest to this tool's scope sit behind paid plans — so teams and larger projects have to pay.
dartograph fills that gap: permanently free (MIT), commercial use included — just as cartograph does for Swift after Periphery went commercial. Free alternatives now cover individual pieces — reference searches, lint rules, file-level graphs — but none combines symbol-level reachability with its evidence, pre-change impact analysis, runtime input verification, and an agent-queryable MCP surface in one tool.
- The source of truth is
package:analyzer, the Dart team's official analyzer package — not text search. - Unused code, unused files, dependency cycles, layer rules, and architecture metrics all come from one graph.
- Every answer carries its evidence. dartograph never renders a deletion verdict.
queryandskillare built in from day one, designed to be consumed by coding agents.
dartograph is a pure Dart CLI and does not require the Flutter SDK. It runs on Dart SDK 3.11 or later.
dart pub global activate dartograph 0.16.1
# or, on Dart 3.11+, an AOT-compiled install:
dart install dartograph
dartograph --versionReleases are published on pub.dev and GitHub Releases. Every release passes the full test suite, the line-coverage gate, and a package dry-run, and is dogfooded against real public Flutter plugins.
Most commands take the root of the Dart package to analyze as the last argument.
The examples below assume a global install; from a source checkout, prefix each
command with dart run (for example, dart run dartograph graph --format dot .).
# initialize configuration template
dartograph init .
# graph & dead code
dartograph graph --format dot .
dartograph graph --format html .
dartograph dead --format text .
# baseline & narrowed CI reporting
dartograph baseline --write .dartograph-baseline.json .
dartograph dead --format github-actions \
--baseline .dartograph-baseline.json --since origin/main .
# standalone apps: drop public-API retention; audit pubspec hygiene
dartograph dead --closed-app .
dartograph deps .
# symbol queries
dartograph query ApiClient --baseline .dartograph-baseline.json .
dartograph query --batch requests.json .
# change impact
dartograph compare ../before-checkout ../after-checkout
dartograph affected origin/main .
# change impact pre-check & runtime-only dependencies
dartograph impact --changed lib/api.dart .
dartograph runtime .
# duplicate code, cycles, layer rules, metrics
dartograph dup .
dartograph cycles --strict .
dartograph rules --config layers.yaml --strict .
dartograph metrics .
# Flutter bridge facts, agent skill & setup, MCP server
dartograph bridges --format json .
dartograph bridges --messages --format json .
dartograph bridges --events --format json .
dartograph schema --format json .
dartograph routes --role client --wrappers http-wrappers.json .
dartograph impact --format language-traversal --roots-from dart.http.json .
dartograph skill
dartograph setup --install .
dartograph mcpFull arguments, output formats, exit codes, and CI examples live in
doc/USAGE.md (Korean).
--sincebuilds the whole project graph first, then narrows reporting to the changed locations. It covers commits after the base ref, staged and unstaged edits, and untracked files; CI therefore needs the full Git history. Output formats aretext,json,github-actions, andsarif.--baseline <file>suppresses the exact findings recorded bybaseline --write, so known dead code does not fail CI and only new findings surface.queryanswers questions about a single symbol — neighbors in both directions, members, retention paths, baseline status, and limitations — using the same field names as cartograph, instead of dumping the whole graph.affected <git-ref>reports which libraries changed since a Git revision and which libraries transitively depend on them — each dependent backed by its shortest dependency path to a changed library as evidence.compare <before> <after>diffs two checkouts of the same package: added and removed vertices, edges, and retention roots, plus what became newly unreachable or newly reachable (a newly-unreachable declaration carries its before-path, removed edges, and removed roots as evidence). Unlike--since, it compares two whole graphs rather than filtering report locations.bridgesemits FlutterMethodChannelcreation andinvokeMethod/invokeListMethod/invokeMapMethodfacts inGRAPH-EXCHANGEv1 — the bridge-fact format that isthmus joins across platform boundaries; cartograph produces it too. Each fact carries MethodChannel provenance, lexical scope, UTF-8 positions, and UTC millisecond timestamps. Dynamic channel names remain facts; unattributed or malformed invocations, partial parses, and EventChannel/BasicMessageChannel are counted as limitations rather than read as facts in the default command.schemaemits persistencerelation-usefacts (bridge-facts v1,target: "persistence") so isthmus can join Dart code to a SQL catalog exported by schemagraph: sqflite SQL/table/column arguments, sqlite3 and postgres SQL arguments, driftTableclasses, custom queries and.driftfiles, floor@Entity/@DatabaseView/@Query, and uppercase SQL string literals. Receiver types are not resolved: common method names such asquerycount only in files that import the package. Non-literal SQL stays a dynamic fact; Isar/Hive-style stores and unsupported SQL packages are reported as limitations, not facts. The SQL reader is a port of the kartograph/cartograph extractor so every producer reads the same SQL the same way.routes --role clientemits isthmus httproute-callfacts (bridge-facts v1,target: "http",roles: ["client"]) for package:http, dio, retrofit.dart and chopper calls and for wrappers declared in an isthmushttp-wrappersv1 file. Each library's base-URL join follows its source: dio concatenates strings, retrofit.dart resolves the@RestApibase against the dio base and then joins the path like dio, and chopper joins at code generation and slash-joins with the client base.symbol.usris the enclosing declaration's dartograph id, the same id space asimpact. Unproven paths, verbs and identities stay dynamic facts or limitations. The templates agree with the requests the real libraries sent to a local mock server, and the shared isthmus conformance vectors pass 101/101 — see HTTP routes (Korean).impact --format language-traversalemits an isthmuslanguage-traversalv1 document fortrace: one pass over many roots (for example everyroute-callusr via--roots-from), with every root that reaches each declaration, a shortest-path witness and per-root lower-bound evidence (direct, orcandidatethrough overriding-member dispatch).unresolvedCallsis not reported. An unknown root is listed withroot-not-foundand exits 64.bridges --messagesandbridges --eventsare opt-in development-source producers for BasicMessageChannelsendcalls and EventChannelreceiveBroadcastStreamcalls. They emit bridge-facts v2 withtransport: "basic-message-channel"/"event-channel"; they never turn a channel construction into a send or invent a MethodChannel method. Dynamic names retain their source expression. AchannelPrefixis emitted only when the AST proves a decoded, non-empty leading literal in a string interpolation; it is a candidate prefix, not proof of a complete runtime address or instance identity.--messagesand--eventsare mutually exclusive.impactreports which declarations and tests a change touches before it lands — from--changed <file>/--symbol <id>/--since <ref>— with the dependency paths as evidence.runtimefinds inputs that only appear at run time (environment/dart-define reads, dynamic loads, config paths, assets, external URLs) and judges them against the current environment;--executeruns an entrypoint and records exit code and stderr as execution evidence.--incremental <dir>reuses cached analysis facts for CI-speed reruns,--workspaceopts into pub workspace aggregation (members listed in the root pubspec'sworkspace:are analyzed together;depsaudits each package against its own pubspec), and--record <dir>/historykeep an append-only ledger of run inputs, versions, and results.- A reusable composite action (
action.yml) wraps the CLI for GitHub Actions: it activates a published release (or a checked-out source path), analyzes a package, and writes one report file. See the pinned example workflow.github/workflows/impact-precheck.yml. skillprints a ready-to-paste skill — or installs it into a directory with--install <dir>— that teaches a coding agent how to drive dartograph for evidence-backed answers.setupprints — or installs with--install [<root>]— agent MCP integration forclaude(default),cursor,codex, oropencode(--target <agent>). For Claude it also writes a PostToolUse hook that runs an impact pre-check after Dart edits (no MCP call needed) and a managedCLAUDE.md/AGENTS.mdblock that routes graph questions to dartograph commands. Existing settings are merged, never overwritten, and--uninstallremoves only the dartograph entries.dupreports duplicated code blocks as token-structural review candidates, anddead/deps/dupaccept--kinds <csv>to narrow reported finding kinds.metricsalso reports function-level cyclomatic complexity and hot-spot rankings.cycles,rules, andmetricsonly report by default; findings become exit code 1 with--strict. Metrics are per-library Ca, Ce, instability, abstractness, and distance from the main sequence — each entry also carries its zone (main-sequence,zone-of-pain,zone-of-uselessness, orisolatedfor entries with no couplings at all) at the reported tolerance.initwrites a commenteddartograph.yamlconfiguration template to the project root (pass--forceto overwrite an existing configuration).depsaudits pubspec hygiene: dependencies declared but never imported, dev dependencies used fromlib/or never imported, andpackage:imports with no declaration. Tool-contract dependencies —executables,build.yamlbuilders,analysis_optionsincludes/plugins — count as used, and runtime/generated/asset references stay explicit limitations. Findings are a review list, never a deletion instruction.dead --closed-appturns off public-API retention for standalone apps (Flutter apps, CLI executables) — declarations unreachable frommainare reported even whenlib/<package>.dartexports them. Pair it withbaseline --write --closed-app, and never run it on a published library.dartograph mcpalso exposes MCP resources (dartograph://usage,skill,config) and prompts (impact-precheck,dead-code-review,dependency-audit,duplication-review) alongside the three query/verify tools.- A zero-build VS Code extension is on the
marketplace
(source:
editors/vscode/): it runs the CLI and surfacesdead/deps/dup/impactJSON findings as Problems diagnostics — evidence and limitations included, never a deletion verdict. Thedartograph_analysis_pluginpackage bringsdead/dupfindings intodart analyzeand IDE analysis servers asdartograph_dead_code/dartograph_duplicate_blockdiagnostics with adartograph:ignorequick fix.
A // dartograph:ignore line comment suppresses dead reporting for the
declaration it heads (retained as retentionReason: inlineIgnore) — a decision
by the repository author, recorded in the graph itself.
dartograph does not decide what is safe to delete and never deletes code. The
evidence and limitations attached to every finding require human review. It is a
whole-project reachability tool, not a reimplementation of dart analyze's
library-local unused_element.
Limitations:
- Conditional imports/exports: only the single configuration the analyzer picks is observed.
- String routes not connected to a route table are reported as limitations and never used as deletion evidence.
- Generated code older than its source is reported as a limitation; generated declarations themselves are conservatively retained.
- A package can contain several
mainfunctions. By default everymainunderlib/,bin/, andexample/is retained. Declaring the real build targets underentry_pointsindartograph.yamlnarrows retention to themainfunctions of those files (a template can be generated withdartograph init). - Local path dependencies are opt-in graph inputs through
source_packagesindartograph.yaml; each declared package root must be inside the project and containpubspec.yamlandlib/. This is useful for generated Pigeon/Dart source vendored under a project without crawling the whole pub cache. - Constructors (default, named, and factory) are modeled as their enclosing
class, not as separate nodes. A
Parser.fromJson(...)call is acalledge toParser, and uses inside constructor bodies are attributed to the class. Querying a constructor ID reportsnotFound, unused constructors are not reported bydead, andimpactworks at class granularity for constructor changes. - Public declarations and public members exported by
lib/<package-name>.dartare retained as the external consumer API. - Dynamic calls and native behavior cannot be fully proven by a static graph.
bridgesaccepts only direct imports ofpackage:flutter/services.dartas provenance. Usage through barrels that re-export Flutter services is excluded from facts and reported as theflutter-services-reexportslimitation.bridges --eventsrecords a statically identified EventChannelreceiveBroadcastStream()call as astream-listenfact, even if the returned stream is never consumed. It does not prove that the call executes, a listener is attached, a subscription is active, or an event is received.
Guarantees:
- The analysis cache lives outside the analyzed project, in the OS user cache
(
~/Library/Caches,$XDG_CACHE_HOME/~/.cache,%LOCALAPPDATA%) underdartograph/<project-root-hash>. It is invalidated automatically when project or dependency contents, mtimes, package resolution, the Dart SDK, or the analysis revision change. A missing or corrupted cache never changes results; it only costs analysis time. - For findings with more than 20 retention roots, dartograph records the total
count, the first 20 as a sample, and
retentionRootsTruncated: trueto keep output bounded. The fact that evidence was truncated is never hidden.
Repository design documents are written in Korean.
| Document | Contents |
|---|---|
doc/PRD.md |
What, for whom, how far — and what it will never do |
doc/PLAN.md |
Phase-by-phase plan |
doc/RESEARCH.md |
Confirmed facts, unconfirmed claims, sources |
doc/DECISION-analyzer.md |
Analyzer version, vertex IDs, generated code, caching decisions |
doc/USAGE.md |
Install, commands, exit codes, CI usage |
doc/GRAPH-EXCHANGE.md |
bridges bridge-facts JSON producer spec — fields, join key, limitations |
doc/MCP.md |
MCP tools, resources, prompts, error codes |
doc/TROUBLESHOOTING.md |
Common failure modes and fixes |
doc/COMPETITIVE-ANALYSIS.md |
Alternatives survey and the shipped gap list |
Contribution steps: CONTRIBUTING.md (Korean).
Vulnerability reports: SECURITY.md (Korean). Release changes:
CHANGELOG.md; a Korean version is kept in
CHANGELOG.ko.md.
MIT. Permanently free, commercial use included. This is a feature of the
project, not a footnote: the promise sits on the first screen of this README,
and the pledge never to change the license is recorded in doc/PRD.md.