dartograph 0.7.0
dartograph: ^0.7.0 copied to clipboard
Evidence-backed dependency graph analysis for Dart and Flutter codebases.
Changelog #
A Korean version of this changelog is kept in CHANGELOG.ko.md.
0.7.0 #
-
Added
graph --format anonfor privacy-preserving graph export (parity with dependency-cruiser'sanonreporter). Package-relative paths and file paths are anonymized using deterministic, injective identifiers (s0,s1, ...) while preserving graph topology, file extensions, and a whitelist of standard Dart idiomatic vocabulary. Analyzer limitation strings are sanitized in a single alternation pass to prevent double-replacement leaks -
Added
dead --report-redundant-publicto identify public declarations that are never referenced outside their defining library (parity with Periphery's redundant public accessibility check). Follows the informational contract of--report-test-only(findings emitted asinfo, command exits with code 0). Does not combine with--explain,--baseline, or--report-test-only(usage 64);--sinceand machine-readable output formats are allowed. Conservative exclusions preserve retention roots, enum constants, override implementers, operators, and private container members -
Added architectural zone classification to
metricsJSON output (parity with cartograph'sMetricsZone). Each component metric entry now includes azonefield (main-sequence,zone-of-pain,zone-of-uselessness, orisolated). The boundary aligns with--strictthreshold calculations -
In
graph --format dot, vertices participating in circular dependencies are now highlighted in red (color="#d9383a",fontcolor="#d9383a") (parity with madge). Detected via Tarjan's SCC algorithm; DOT output for acyclic graphs remains byte-identical -
GraphNodenow carries an explicitisLibraryboolean flag instead of relying on.dart::string heuristics in HTML exports and projections. Libraries whose file names contain::are now correctly classified. Analysis cache serialization schema version incremented tov3(cache identity unchanged) for automatic re-analysis of prior caches without changing serialized JSON, DOT, or Mermaid outputs -
Fixed grammatical agreement in
bridgeslimitations across all three diagnostic families (unscanned-*plural nouns for N ≥ 2 and dynamic-* singular verb agreement for N = 1)
0.6.0 #
-
Breaking change (library API): the public library surface (
package:dartograph/dartograph.dart) was trimmed to what is actually supported. CLI behavior and exit codes are unchanged.querySymbolwas removed — a one-shot wrapper aroundSymbolQuerySessionused only bytool/benchmarks. Construct aSymbolQuerySessionand callqueryinsteadCodeGraph.usageEdgesFromwas removed (no product caller; filterCodeGraph.edgesinstead)SymbolQuerySession.analysis(aReachabilityResultfield) is no longer public. The session now exposesdeadDeclarations(an unmodifiableList<DeadFinding>) for thequery --baselineflow, so no public member references an unexported type. Reachability questions are answered bySymbolQuerySession.query, whose document carries each symbol's reachability state.ReachabilityResult/ReachabilityExplanationstay internal;DeadFindingis now exported
-
bridgespub workspace detection now validates membership. A package whose pubspec declaresresolution: workspacejoins the nearest ancestorworkspace:root when that root plausibly lists it — explicit paths are matched after URL normalization, while glob entries and non-listworkspace:values are conservatively accepted. Only a well-formed explicit-path list that omits the package falls back to the scan root, with a newpub-workspace-member-not-listedlimitation so a misconfigured workspace never silently skews the isthmus join basis -
SARIF output no longer corrupts already-absolute source URIs. A dead finding whose source is
file:(an out-of-root path) orpackage:(a dependency) is passed through unchanged instead of being split on/and re-encoded (which turnedfile:///a.dartintofile%3A///a.dart). Project-relative sources keep the existing per-segment encoding (backslash- and percent-safe) -
HTML graph output now classifies a library whose file name contains
::correctly, using the.dart::declaration boundary instead of any::. Such a library is no longer shown as a member, and its display name is no longer truncated at the first:: -
README revised for clarity (English original and Korean twin); no behavior change
0.5.0 #
-
bridgesgained--project <shared-root>and pub workspace auto-detection (isthmus monorepo join request #38; GRAPH-EXCHANGE delegates the shared-root declaration to producer options)--projectkeeps the scan on the positional package root while the document'sprojectfield andlocation.pathbecome relative to the shared root (POSIX realpath; it must be an existing directory containing the package root, else usage 64). Sibling packages of a pub monorepo (e.g. a*_platform_interfaceholding the MethodChannel and the plugin package holding the native side) can now emit the exact sameprojectstring the isthmus strict-equality join requires — no hand-rewriting documents, which breaks provenance- A package whose pubspec declares
resolution: workspacepicks up its pub workspace root (nearest ancestor pubspec with aworkspace:key — the Melos definition) without--project. Precedence:--project> workspace detection > scan root. Detection failures fall back to the scan root and reportpub-workspace-root-not-found/pub-workspace-pubspec-unparsedlimitations so a skewed join basis is never silent - Without a workspace declaration or
--projectthe output is byte-identical to before (project = realpath of the scan root, paths relative to it) — existing bridge goldens pass unmodified - The bridges control-character rejection message now reads "a fact value
or source path contains control characters" — message-only, no behavior
change: source paths have been validated since 0.3.0, and empty names are
skipped into the
empty-bridge-nameslimitation rather than throwing, so the old "is empty" attribution was unreachable/wrong
-
Indexing got measurably faster with byte-identical output (audit P1/P2/P9/P10; measured with the new
tool/benchmark_index.dartA/B harness — sha256 of graph, dead, query, retention, and test-only outputs identical before/after):- Element-to-ID resolution is memoized per relationship-collector pass; it used to recompute path normalization and name chains for every identifier visit
CodeGraph.nodes/edgesread views are cached and invalidated on mutation instead of re-sorting on every access;_addPublicApiRootshoists its node-id list out of the per-export loop- The edge comparator is shared between
CodeGraphandGraphSnapshot(single determinism implementation), pubspec.yaml is read once per index, and declaration source paths are computed once per declaration - Synthetic benchmark (600 files, 4,923 nodes / 14,726 edges, Dart 3.13.3, macos_arm64, min of 3 cold runs): index 1456 ms -> 1053 ms (-28%). Machine- specific; relative comparison only, not an SLA
-
Two more measured hot-path fixes with byte-identical output (audit P4/P8; harness now also times
rulesevaluation and hashes its violations):LayerRuleEvaluatorcaches compiled glob RegExps per pattern instead of recompiling for every node × layer × pattern (first-match assignment scans all patterns for unmatched nodes). Benchmark: rules evaluation on 4,923 nodes 10.8 ms -> 3.0 ms (-72%), violations hash identicaldead --sinceresolves each unique finding source's symlink once instead of once per finding (findings of one file shared the syscall before)
-
Reachability/query hot loops got indexed with byte-identical output (audit P3/P5/P6; same A/B harness, all six artifact hashes identical):
ReachabilityResultgainsisReachable(set lookup) andreachableMemberOf(dot-prefix witness index built once, deterministic first-in-sorted-order semantics preserved) —querybatches andcomparelosses no longer run a linear scan per query/loss. Synthetic benchmark: 100-query batch on 4,923 nodes 10.7 ms -> 1.7 ms (-84%)- The double sort of reachable ids per
analyzeis gone (5.7 ms -> 4.7 ms on the benchmark), andcompareGraphshoists its limitation dedup+sort out of the per-loss loop
0.4.1 #
--since/affectedGit change matching is now bidirectional for symlinked sources: both the link path itself (the link file changed or was retargeted) and the resolved physical target (the target changed) are matched against the changed set — previously only the resolved path was compared, so a changed link file silently fell out of scope (measured)- A
dartograph.yamldeclaringentry_pointsnow reports anentry-points: main retention roots narrowed to N declared build target(s)limitation — a configuration added by a pull request can no longer hide dead code indistinguishably from a clean repository - SECURITY.md documents the symlink ingestion channel: dartograph follows links inside the analysis tree like the analyzer does, so a repository that plants links can pull same-user files from outside itself into its graph output and CI artifacts
- Output fidelity fixes from the audit (all additive or misinformation-only changes)
dead --format jsongains areportfield (dead/test-only) so a stored artifact is machine-classifiable without the exit code — the four report formats are now lossless-symmetricdead --format github-actionsemits a::notice ... suppressed by baselineline when the baseline suppressed findings (the other three formats already reported the count; zero stays byte-identical)graph --format jsonnodes carryisEnumConstant: truefor enum constants (conditional-field convention likeline/column; the cache document already had it — the public exchange format can now reproduce the enum-constant retention decision)- SARIF file findings no longer invent a
regionof line 1 column 1 —regionis optional in SARIF and fabricating position evidence violates the evidence contract (declaration findings keep their real region) - The determinism contract now names its second declared exception: the
generated-code-stalenesslimitation is an mtime observation (git does not preserve mtimes, so its presence can differ across fresh clones; findings, nodes, and edges are unaffected) — documented in USAGE and lib/AGENTS.md
- The analysis cache key now hashes every
.dartfile under the package root (excluding.dart_tool/.git/build), not only the five standard source directories — the analyzer also reads outside the standard directories through the import closure (e.g.tool/), and a key that missed them reused a stale analysis after such a file changed (reproduced: resolution limitations vanished). Hidden directories (e.g..fvmtoolchain links) are pruned from the walk so key computation cannot balloon into hashing a whole Flutter SDK, and nested-package discovery widens to pubspecs outside the standard directories. Relative imports that leave the root remain a documented coverage boundary.toolVersionis part of the cache identity, so upgrading to 0.4.1 invalidates existing caches automatically (one reanalysis) - Hardened the CLI error boundary:
Errors (TypeError, RangeError, …) escaping from analyzer/yaml internals are contained at the command boundary as an analysis failure (exit 2) — no more stack traces echoing internal paths or an undocumented exit 255. This also closes per-command catch asymmetries (e.g._runBaselinehad noon ArgumentError; compare/query leaked ArgumentErrors) - The bridges control-character policy rejection now reports "Bridges extraction failed: …" instead of "unable to index the package" (correct attribution — the rejection is an extraction policy, not an indexing failure)
- Non-UTF8
gitoutput (possible filenames on Linux) folds into the ChangedFilesException diagnosis ("Changed files could not be computed…") instead of surfacing as a misattributed indexing failure - Unified the control-character and injection policy across every human and CI
output surface (audit follow-up: newline-bearing filenames could cut a Mermaid
label into two statements, forge text diagnostic lines, and pass ESC/bidi
characters through to GitHub Actions logs)
- Mermaid: CR·LF become the documented entity codes (
#13;·#10;) so labels always stay on one physical line; a literal#10;in the source round-trips thanks to the leading#→#35;substitution - DOT: raw CR is now escaped like LF (a display-level line break; statement structure is preserved)
- text reports: C0·DEL characters in paths, ids, evidence, and limitations
become visible escapes (
\n·\r·\t·\xNN) — a secondpath:line:col:diagnostic line can no longer be forged and terminal ANSI injection is neutralized - GitHub Actions: percent encoding extends beyond the spec minimum (
%, CR, LF,:·,in properties) to C0·DEL·C1, U+2028/2029, and the bidi controls (U+202A–202E·U+2066–2069) — same%0D/%0Aconvention, normal inputs unchanged - SARIF: the artifact
uriis built from per-path-segment encoding instead ofUri(path:), which silently corruptedback\slash.dartintoback/slash.dartand misattributed a literal%41.dartasA.dart - The policy table is documented on the export module
(
lib/src/export/graph_exporter.dart). Only pathological inputs change bytes; normal paths and ids are byte-identical
- Mermaid: CR·LF become the documented entity codes (
0.4.0 #
- Added
// dartograph:ignoreinline comments (absorbing Periphery's comment command)- When the body of a line comment above a declaration starts with the marker
(doc comments and block comments are not directives), the declaration's dead report
is suppressed. Prose that merely mentions the marker does not trigger suppression, and a
reason can follow as in
// dartograph:ignore — reason. A trailing comment at the end of a line (void foo() {} // dartograph:ignore) is not interpreted as suppressing the next declaration (misattribution guard). For variables and fields, the marker on the enclosing declaration applies - A suppressed declaration becomes a retention root with
retentionReason: inlineIgnore, sodead --explain,query, and compare answer with that evidence. The user directive takes precedence over other retention reasons. Because it is a reachability root, what the suppressed declaration references also disappears from the report — use a baseline to suppress a single finding. Only the declaration itself is suppressed; it does not propagate to members or files - Retention-root extraction semantics changed, so the analysis cache identity is bumped to v5. Old caches are reanalyzed automatically (serialization format unchanged)
- When the body of a line comment above a declaration starts with the marker
(doc comments and block comments are not directives), the declaration's dead report
is suppressed. Prose that merely mentions the marker does not trigger suppression, and a
reason can follow as in
- Added
--level <file|type|symbol>and--collapse <n>tograph(absorbing cartograph'sgraph --leveland dependency-cruiser's--collapse)--level filefolds declarations into their libraries,typefolds members into top-level declaration containers, andsymbol(the default) draws the graph as is. Self-loops created by folding are dropped and edges are deduplicated after representative substitution. The default output is byte-for-byte identical to before- dartograph analyzes a single package, so cartograph's module resolution does not exist
(folding to the package yields one vertex) —
fileis the coarsest level --collapse <n>summarizes the file-level graph to the leading n path segments (project:lib/src/a.dartbecomesproject:lib/srcat n=2). Folder vertices are aggregates without location fields. Combining with a level other than--level file, a missing value, duplicates, an unknown resolution, values below 1, and non-integers are usage errors (64)
- Added
htmltograph --format(absorbing cartograph'sgraph --format html)- A single self-contained HTML file with no external CDN, script, or font references at
all. It opens on air-gapped networks and in CI artifacts; the graph facts ride in a
<script type="application/json">payload and render with an inline canvas force-directed layout, search, pan, and zoom - Above 400 vertices the most connected vertices are kept first, and the truncation is
stated on the page and in the payload (
truncatedFrom). Use--format dotfor the full graph <in the payload becomes\u003c(a valid JSON escape) so dartograph's own node IDs like<no-library>cannot break script-tag tokenization. Limitations ride both in a collapsible header list and in the payload
- A single self-contained HTML file with no external CDN, script, or font references at
all. It opens on air-gapped networks and in CI artifacts; the graph facts ride in a
- Added the
affected <git-ref> <package-root>command (absorbing dependency-cruiser's--affected)- Seeds the libraries to which files changed since a Git reference (commit, branch, tag,
HEAD~1, …) are attributed, walks import/export edges in reverse, and answers with the transitively dependent libraries as JSON. Changes to part files are attributed to the host library - Each affected library carries
pathanddepthevidence — the shortest dependency chain to the nearest changed library — andchanged(seeds) andaffected(dependents) do not overlap. The impact radius is a library (file) level observation - Changed Dart files that belong to no analyzed library are reported as the
changed-dart-files-without-librarylimitation. Git failures get the same diagnosis and exit code 2 as--since; a successful report exits 0 regardless of the impact count
- Seeds the libraries to which files changed since a Git reference (commit, branch, tag,
- Added
dead --report-test-only(cartograph parity)- Recomputes reachability without the test-directory (
test/,integration_test/, …) retention roots and selects production declarations reached only from tests. This is not dead code — it is the observation that "tests are the only caller" - Reported at
infoseverity (textinfo:, github-actions::notice, sarifnote, ruleIdtest-only-declaration), and the exit code is 0 even with findings (it never fails the build). Declarations inside test directories and@visibleForTestingproduction declarations are conservatively excluded - Does not combine with
--explain(a single-target query) or--baseline(which suppresses dead findings) — usage 64;--sinceand--formatare allowed
- Recomputes reachability without the test-directory (
- Added
cycles --explain <symbol-id>andrules --explain <symbol-id>(cartograph evidence parity)cycles --explainemits as JSON the cycles a vertex takes part in as a strongly connected component and each cycle'sbreakCandidate(candidate edge to break). A vertex belongs to at most one strongly connected component, so the answer is 0 or 1 cyclesrules --explainemits as JSON the layer the vertex is assigned to, thematchedPatternandmatchedCandidatethat decided the assignment, and therulesstarting from that layer. With no matching layer those fields are null/empty lists (--configis still required)- Both
--explainforms are single-vertex queries and do not combine with--strict(usage 64). An ID absent from the graph is answered withknown: falseand exit code 64; a known ID exits 0 regardless of participation
- Added
--depth <n>and--limit <n>toquery(cartograph SymbolQueryDocument parity)--depth(default 1) follows usage relations (usedBy,dependsOn) with BFS up to n hops; each neighbor'sdepthfield says how many steps it is from the queried symbol. All edge kinds reaching one neighbor ride inedges, and a neighbor reachable through several paths is reported once at its shortest depth--limitcaps the number of neighbors per direction; when it omits neighbors, that direction'struncatedbecomestrueand omitted neighbors are not expanded further. Containment (members,declaredIn) is always one hop- The defaults (depth 1, no limit) preserve the previous output, with one narrow
breaking change: usage edges pointing at a symbol itself (recursion) are excluded
from that symbol's own neighborhood, matching cartograph (the old 1-hop walk included
them). Values below 1, non-integers, missing values, and duplicate flags are usage
errors (64). Combines with
--batchand--baseline
- Fixed a false positive that reported
operatordeclarations in use as dead- Operator syntax (
a + b,a[i],-a,a++,a += b) resolves through tokens rather than identifiers, so no usage edges were created; operator declarations on classes and extension types were wrongly reported unreachable while being consumed - Resolved operators are now recorded as
calledges. Thea[i] = vwrite keeps its existing reference path, and the[]/[]=that compound assignment and increment (m[i] += v,m[i]++) read and write through are recorded as operator calls too. Built-in operators (dart:core) have no nodes and gain no edges; unused operators and read operators consumed only by writes keep being reported (bidirectional corpus regression) - Extraction semantics changed, so the analysis cache identity is bumped to v4. Old caches are reanalyzed automatically (serialization format unchanged)
- Operator syntax (
- Input error messages are now distinguished per cause so they never point at the wrong one
- A missing baseline file yields "Baseline is invalid: create it with dartograph baseline --write" instead of "unable to index the package"
- A missing or invalid
rules --configfile yields "Analysis failed: unable to read the rules configuration." (distinguished from indexing failure; theAnalysis failed:prefix is kept) - A
baseline --writewrite failure (destination creation, permissions) yields "Baseline write failed: unable to write the baseline file." instead of "unable to index the package" (indexing already succeeded)
- Dead-file findings for percent-encoded filenames keep their file-level limitations
%20and friends preserved byUri.pathare decoded so they match the source limitations the analyzer built from real paths; previously those files' limitations silently vanished from findings
- Mermaid output escapes
<,>,&,",\, and#in its own node IDs with HTML entity codes- Fixes Mermaid mistaking
<no-library>and<unnamed-extension@...>for HTML tags, caused by reusing the DOT backslash escape - Quotes cut a quoted string in half and break label structure, so they become the
entity codes Mermaid documents (
#quot;, backslash#92;,#itself#35;)
- Fixes Mermaid mistaking
--helpnow states that--explainrequires--format jsonand does not combine with--baselineor--since- The agent
skilloutput gains anaffectedentry andinlineIgnoreevidence wording (updated alongside the new features)
0.3.0 #
- Fixed unreachable false positives for enum constants consumed only through
.values- Enum constants have only enum→constant
memberedges, which do not count as usage, and container rescue runs member→container only, so constants could not be saved. With consumption likeStatus.valuesthat never references individual constants, even constants of a reachable enum were wrongly reporteddead - A reachable enum now retains its constants, and
explainreturns theretained by its reachable enumevidence together with a real path to the enum. If the enum itself is unreachable, its constants keep being reported - Node serialization gains the enum-constant flag, bumping the cache schema to v2. Old caches are rejected at decode and reanalyzed
- Enum constants have only enum→constant
- Fixed silent-miss and total-failure defects in Flutter channel fact extraction
- When
static final _channel = MethodChannel(...)was declared after its uses inside a class or other declaration body, method-invoke facts were dropped entirely and downgraded tounresolved-receiver-invocationscounts without position or symbol. Declaration-body fields are now prescanned, so resolution no longer depends on declaration order; shadowing by a same-named top-level channel is still respected - A single empty channel or method name such as
MethodChannel('')used to fail the wholebridgesdocument without file or line information. Only that fact is now skipped and aggregated into theempty-bridge-nameslimitation while the remaining facts are emitted normally. Names containing control characters are still rejected entirely
- When
- Fixed
dead --sincemissing changed files underdiff.relative=trueand in sub-packagesgit diffprints cwd-relative paths, which were joined with the repository root, misaligning paths and silently dropping all findings (exit 0).gitnow runs with-c diff.relative=falseso paths are always repository-root relative
- Fixed option-shaped values being accepted as paths
baseline --write --force .actually created a file named--forceand reported success; it is now rejected as a usage error (64) before indexing and writing- Calls with a missing value now yield usage errors (64) instead of analysis failures
(2): the package root of
query,bridges, andgraph, and the values ofrules --configanddead --baseline/--since compareonly checked for--, so an existing short option likecompare -h .was accepted as a path; single-dash values are rejected too- Narrow breaking change: calls that used to work by passing a path starting with
-(e.g.baseline --write -b.json .) now exit 64. Pass./-nameinstead.bridgeskeeps the existing--escape. This was an undocumented implicit allowance, andquery --batchhad the same restriction from the start
- Fixed
dead --explainasserting a container retained by a member was unreachableexplainanswered "unreachable from all retention roots" with exit code 1 for declarations thatdeadin the same run excluded from findings- It now returns the
retained by a reachable memberevidence with the witness member and a real path to that member, and exits 0 query'sretainedByMemberstate andcompare's witness notation are unchanged
- Symlinked Dart sources are included in analysis-cache inputs and the bridge scan
- The analyzer follows both file and directory links, but they were missing from the input list, so editing a link target left the cache key unchanged and returned a stale graph; fixed
- Platform-channel facts of linked sources, missed for the same reason, are now extracted too
- Directory links record the already-followed real paths so cycles do not loop forever
- Broken links keep being excluded (there is no target)
- Added the
dartograph.yamlentry_pointsoption to declare real build targets and narrowmainretention roots- Without it, the previous conservative policy (every
mainunderlib/,bin/,example/) is kept - Empty documents and comment-only documents declare no entry points and are treated the same (default policy); only a non-empty non-mapping document is rejected
- Empty, absolute, outside-root, non-string, out-of-scope (not under
lib/,bin/,example/), missing, or non-.dartpaths are reported as analysis failures instead of being silently ignored - An entry point that exists but has no
mainis reported as theconfigured-entry-point-without-mainlimitation - Retention-root semantics changed, so the analysis-cache identity is bumped to v3 and the configuration is included in the cache key
- Without it, the previous conservative policy (every
0.2.0 #
- Per-source limitations (analysis errors, unresolved invocations, conditional configurations) attached to findings
query --batchand the library-facingSymbolQuerySession, sharing one index and reachability computationcompare, explaining graph changes between two checkouts and the reachability paths that disappeared- Supported Dart declaration names added to bridge facts, with isthmus round-trip evidence verified in both directions
- Source-mutation regression evaluation and query-session performance measurement tooling
0.1.1 #
- UTC millisecond generation timestamps and UTF-8 byte positions per isthmus GRAPH-EXCHANGE v1
- MethodChannel extraction following Flutter services import provenance and lexical scope
- cascade and invokeListMethod/invokeMapMethod support, dynamic-name facts, and unattributed/invalid-call limitations
- EventChannel, BasicMessageChannel, conditional imports, and re-exports reported as limitations instead of false facts
- Usage through Flutter services re-exports is preserved toward the documented miss instead of being guessed
- explain's not-found and file evidence hardened; analyzer identity, generated code, and nested-dependency cache decisions hardened
- Invalid CLI calls distinguished from Git failures; option-shaped skill paths rejected
0.1.0 #
- Deterministic symbol- and file-level dependency graphs on Dart analyzer 14.3.0
dead,query,cycles,rules, andmetricswith evidence and limitations- DOT, Mermaid, JSON, text, GitHub Actions, and SARIF output
--sincecombining a baseline with a Git change scope- Corruption-tolerant persistent fact cache keyed by contents and analyzer identity
- Retention rules: package-barrel public API, multiple mains, overrides, generated code, tests, plugins
bridges, exporting Flutter platform-channel exchange factsskill, printing and installing safety guidance for agents- Exit codes
0/1/2/64, a false-positive corpus, and a 90% line-coverage gate - Retention-root evidence for large projects bounded by count, a 20-item sample, and a truncation flag
dartograph is MIT licensed and permanently free, commercial use included. It provides no deletion verdicts and no automatic deletion.