Queryable dependency graphs for Kotlin/Android codebases. A sister project of cartograph (Swift) that shares its design and exchange contracts.
The name is Kotlin + cartograph. Where cartograph maps iOS, kartograph maps Android.
kartograph builds a dependency graph from compiled Kotlin/Android code and explains why a declaration is reachable, retained, or unreachable:
- The source of truth is what the compiler recorded, not text search.
- Unused code, dependency cycles, layer rules, and architecture metrics all come from one graph.
- Every verdict carries evidence. No verdict approves a deletion.
queryandskillwere built for agent consumers from day one.
Android has one unfair advantage: "looks unused but must not be deleted" has been codified for over a decade as ProGuard/R8 keep rules. The retention knowledge the Swift side had to collect by hand already lives in this ecosystem.
The current source version is declared in VERSION. Released versions and artifacts are on GitHub Releases. The truth-source experiments settled on JVM bytecode plus official Kotlin metadata as the primary graph; the rationale is in docs/DECISION-truth-source.md.
Working today:
impactchecks the potential effect of a planned symbol edit, or of the files changed since a base commit, using captured graphs. It reports base/current paths, deletions, runtime evidence and uncertainty identically to people, agents and CI. See change impact and the scored public replays.graphrenders compiled class roots as DOT, streamedcode-graphJSON, NDJSON, or fixed-header Neo4j CSV. With--include-paths --projectit also resolves project-relative source paths. Per-root--module-name, optional external stubs, origin filtering, and bounded dispatch candidates support large exports. Repeating--classesmerges several module/variant outputs; when a JVM class appears in more than one root, the first root always wins. See graph export contracts.deadreports unreachable class declarations from Android retention roots (manifest, XML,@Keep, keep rules, inheritance hierarchies, DI/serialization annotations, JNI and framework callbacks). It supports--explain, baselines, expiring--suppressentries,--since, and machine-readable reports (text/gradle/github-actions/sarif/json/markdown). JSON, SARIF and markdown findings carry aconfidencetier (static / needs-runtime-review / runtime-observed / unmeasured) derived from the unresolved runtime channels measured in the declaration's own source file or from user-supplied runtime evidence. Recursive includes and consumer rules are supported. Keep rules that produced no retention evidence are reported asunmatched-keep-ruleinput diagnostics with file:line provenance — a measurement, not proof the rule can be removed. When findings exist, inputs that were not supplied (keep rules, dependency classpath, or a manifest with component declarations) are reported asinput-hintdiagnostics so a possibly over-reported result is visible before it is trusted. Optional--runtime-classesand--coverageinputs (user-supplied class lists and JaCoCo/Kover XML) mark findings whose class was observed asruntime-observedconfidence; they change neither findings nor exit codes.why <symbol>answers in one step why a declaration is retained, reachable, or unreachable: retention evidence with file:line provenance, the representative path from a retention root, direct callers, a test-only marker, and a measured confidence tier for unreachable declarations. The answer is a reachability fact, not a deletion approval.query/bridges/skillgive agents the users, dependencies and reachability of a single symbol, plus Flutter/React Native bridge facts, instead of a full graph dump.queryalso reports measured counts of unresolved runtime paths and conservative dispatch candidates. React Native coverage spans core@ReactModule/@ReactMethodand Expo Modules (class X : Module()withModuleDefinition { Name(...) / Function(...) / View(...) }); Expomodule-export/component-exportfacts carry"mechanism": "expo"so isthmus keeps Expo and core resolution paths separate.- Direct bytecode calls now carry optional
referenceswith the caller source file and call-site line on depth-onequeryneighbors. Kotlin inline mappings are used only when they identify the caller's own source file. Declaration locations remain separate; bytecode does not provide exact columns or offsets. New saved snapshots preserve this evidence in both normal and compact encoding, while older snapshots report its absence. dependenciescompares a declared dependency list (TSV: coordinate, scope, artifact) with bytecode references from the supplied class roots and reportsunused-dependencyfindings. It does not run the build or collect coverage; processor and runtime-only scopes are counted but not judged. See declared dependencies. Version 0.12.0 adds--libraryAPI/implementation advice,--resolved-dependenciesownership checks, all six report formats, and JVM/AndroidkartographDependenciestasks. Version 0.13.0 adds exact baselines, expiring suppressions, and capture of all observed diagnostics before filtering.- Optional processor source attribution records actual JSR-269 Filer outputs and their generating artifact through completed compiler receipts. The collector is built separately from the tagged source; attribution does not change reachability or dependency-unused decisions.
cycles/rules/metricsanalyze module/package cycles with weakest edges, fail-closed layer YAML, and Martin Ca/Ce/I/A/D metrics.- The Gradle plugin registers
kartographDead<Variant>andkartographGraph<Variant>per Android variant over the AGP public Variant API. - The Gradle plugin's
kartographSnapshotandkartographSnapshot<Variant>tasks capture JVM main/test and Android main/unit-test inputs automatically, together with compiler witnesses, for repeated impact queries. Android application variants also cover the generatedR.jarthrough aprocessResourcesproducer witness. See automatic capture and toolchain configuration and the build provenance contract. Build directories may live outside the repository, andkartograph snapshot mergecombines per-module snapshots into one verifiable snapshot forroutes/impact(multi-module capture). - Optional incremental parsing reuses unchanged class facts and dependency JAR headers. Current inputs are still checked and the analysis is rebuilt on every capture.
- The MCP stdio server exposes
query_symbol,impactandfreshnessover fixed local snapshots, using the same reports as the CLI;discover_symbolsrecovers exact selectors. - Keep-rule parsing fails closed with file and line instead of silently dropping unsupported syntax. Errors and evidence never print absolute paths.
Class loading, reflective construction, and known method/field access are connected through bounded intra-method value tracking; external dispatch uses conservative hierarchy candidates. META-INF/services registrations in class roots and in explicit CLI --service-resources inputs retain their providers. The Gradle plugin supplies the selected variant's Java resource source directories. In the external-call JSON, the matching API model and the resolution result are separate fields. Optional compiler collectors add javac/Kotlin 2.4.10 constant references and javac Dagger 2.59 selected bindings to snapshots. These collectors must be built and connected explicitly; their supported patterns and remaining gaps are documented. The primary graph and retention policy still apply. Callgraph precision remains an experiment.
See docs/LIMITATIONS.md for what the graph cannot see, and docs/PHASE2-VALIDATION.md for measured retention behavior.
The analyzer also tracks immutable arguments and String/Class return values through bounded project static helpers. In five executed comparison fixtures this recovers three previously missed reflection paths while keeping every unused control distinct. The same report compares SearchDeadCode and current R8, including optimization controls and a remaining unknown-input failure. It does not establish overall accuracy or speed superiority. Static field values and reflective reads receive additional bounded tracking, with unknown assignments and analysis limits retained. Exact private/final instance helpers, including Kotlin object/companion methods, get the same bounded String/Class return tracking. This recovers the runtime targets of four more executed Java/Kotlin cases; overridable methods and unknown receiver state remain unresolved. The expanded evaluation records concrete pre-edit review benefits on Java/Kotlin, and also the AI repair result: 6/12 passes in each condition with no graph queries. A general AI productivity gain remains unproven.
Download the CLI archive from GitHub Releases. The Gradle plugin io.github.ictechgy.kartograph becomes installable once its version appears on the Plugin Portal; a GitHub Release and Portal approval are separate events.
- Building kartograph from source is verified with JDK 17 or 21 and Gradle 9.6.1.
- Android graph/dead tasks: AGP 8.7+, Gradle 8.10+, JDK 17+ (verified with AGP 8.7.3 / Gradle 8.10.2 and with AGP 9.x).
- Automatic snapshots: JVM Java/Kotlin on Gradle 9.6.1 and JDK 17/21, plus the tested Android combinations. The Kotlin compiler adapter is verified with KGP 2.4.10; other KGP versions are not guaranteed. The minimum Android combination was tested on Gradle 8.10.2, although KGP itself recommends 8.14.4 or later.
plugins {
id("io.github.ictechgy.kartograph") version "0.20.0"
}Download kartograph-<version>.zip or .tar from a GitHub release. For 0.5.0 and later, check its SHA256 against the matching entry in SHA256SUMS before unpacking, then run bin/kartograph. Releases also include CycloneDX runtime SBOMs. Detached signatures are not published.
# Dependency graph as DOT.
cli/build/install/kartograph/bin/kartograph graph \
--classes path/to/build/tmp/kotlin-classes/debug \
--format dot
# Exchange JSON for other tools. --include-paths resolves each source file name
# against --project and reports the project-relative path with its pathKind origin.
# The kartographGraph<Variant> Gradle task writes the same document.
cli/build/install/kartograph/bin/kartograph graph \
--classes path/to/build/tmp/kotlin-classes/debug \
--format json \
--include-paths \
--project path/to/project
# Unreachable declarations. The output is a reachability fact, not a deletion approval.
cli/build/install/kartograph/bin/kartograph dead \
--classes path/to/compiled/classes \
--project path/to/project \
--manifest app/src/main/AndroidManifest.xml \
--resources app/src/main/res \
--namespace dev.example.app \
--keep-rules app/proguard-rules.pro \
--classpath path/to/dependency/classes.jar \
--test-classes path/to/test/classes \
--strict
# Pin current findings, then gate only new ones on strict.
# A relative --write path resolves against --project, not the calling shell.
cli/build/install/kartograph/bin/kartograph baseline --write .kartograph-baseline.json \
--classes path/to/compiled/classes --project path/to/project \
--manifest app/src/main/AndroidManifest.xml --resources app/src/main/res \
--namespace dev.example.app
cli/build/install/kartograph/bin/kartograph dead \
--classes path/to/compiled/classes --project path/to/project \
--manifest app/src/main/AndroidManifest.xml --resources app/src/main/res \
--namespace dev.example.app --baseline .kartograph-baseline.json \
--since origin/main --report-format sarif --strict# One symbol instead of a full graph dump.
kartograph query UserService --classes path/to/classes --project . --depth 2 --limit 100
kartograph bridges --project . --format json
# Opt-in Flutter BasicMessageChannel facts for Kotlin/JVM sources.
kartograph bridges --project . --target flutter --messages --graph-file build/reports/kartograph/main-graph.json
# Persistence relation uses for isthmus: Room/JDBC/Exposed/jOOQ, SQL-shaped literals, SQLDelight .sq/.sqm.
kartograph schema --project . --format json
# Client HTTP route calls for isthmus: declared wrappers, java.net.URL requests, Retrofit, Spring RestTemplate/RestClient/WebClient/@HttpExchange.
kartograph routes --role client --project . --wrappers http-wrappers.json app/src/main
# Spring MVC/WebFlux route declarations for isthmus; a fresh snapshot supplies bytecode values and handler usrs.
kartograph routes --role server --project . --service api --graph-file graph.json
# Multi-root reverse traversal for isthmus trace, rooted at every route-call symbol (language-traversal v1).
kartograph impact --format language-traversal --roots-from routes.json --graph-file graph.json --project .
kartograph skill --project .kartograph cycles --classes path/to/classes --strict
kartograph rules --classes path/to/classes --config .kartograph.yml --strict
kartograph metrics --classes path/to/classesThe Gradle plugin writes reports to build/reports/kartograph/<variant>.txt and graph documents to build/reports/kartograph/<variant>-graph.json:
plugins {
id("io.github.ictechgy.kartograph")
}
kartograph {
keepRules.from("proguard-rules.pro", "path/to/dependency/consumer-rules.pro")
strict.set(true)
baseline.set(layout.projectDirectory.file(".kartograph-baseline.json"))
reportFormat.set("github-actions") // gradle, github-actions, sarif, json, markdown, text
includeSourcePaths.set(true) // resolve project-relative source paths into the graph document (default false)
}./gradlew kartographDeadDebug
./gradlew kartographGraphDebugAGP does not expose dependency consumer rules as a merged file through the public Variant API, so pass those files explicitly. The dead task never reuses up-to-date/cache results, because keep-rule includes are only discovered while it runs. The graph task skips reuse only when source-path resolution is on, since that reads project sources it has not declared.
These features are included in the 0.10.1 binaries.
Use snapshot to capture the graph, retention evidence, baseline state, and measured limitations once.
Pass the same manifest/resource/namespace/keep/consumer/classpath inputs and the same private-member option as the live query.
kartograph snapshot --classes path/to/classes --project . \
--keep-rules proguard-rules.pro > graph.snapshot.json
kartograph query UserService --graph-file graph.snapshot.json --depth 2 --limit 100Saved queries do not reread current sources or rules and report a saved-graph limitation. Recapture after changes.
Ordinary graph --format json output lacks retention context and cannot be used as a query snapshot.
Mark compiled outputs that contain only generated code with --generated-classes, while still including them in --classes.
The marker is shared by dead, baseline, graph, query, and snapshot.
kartograph graph --classes path/to/normal/classes --classes path/to/generated/classes \
--generated-classes path/to/generated/classes --format jsonNodes and edges remain, with synthesized and generatedInput marking their origin. Do not mark roots that mix generated and handwritten code. In Gradle, configure kartograph.generatedClassRoots or the variant task's generatedClassRoots; each marked root must also be a project class input of that task. Class names are not used to infer this origin.
The extension applies to every variant. For variant-specific outputs, configure generatedClassRoots on the named variant tasks instead; a debug-only root at extension level cannot match the release task's inputs.
Private-member diagnostics are opt-in via dead --include-private-members (added in 0.2.0, not in 0.1.x). On top of the default class report, this adds private methods and fields/properties of reachable, non-synthesized classes. Use the same option for baselines and query. In Gradle: kartograph { includePrivateMembers.set(true) }.
This mode conservatively retains -keepclassmembers targets together with their owners, so it can report fewer class findings than class-only mode. Constructors, natives, synthesized members, compile-time constants, file facades, and members under unreachable owners are not reported. Field writes count as uses, so this is not an unread-field check. An unexplainable -keepclassmembers signature widens to all direct members of the matching classes, while ordinary -keep parsing still fails closed. Members reachable from assumed-external entry points keep their private helpers too (explained as EXTERNAL_MEMBER_ENTRY, which can under-report). Private reflection/serialization conventions are not fully proven, so review keep/consumer rules and runtime tests alongside.
To block all newly introduced diagnostics in a PR, follow the PR gate guide. The released Scripts/check-pr.py reads the base commit's baseline and also checks untouched files. --since is a changed-files filter, so it differs from the PR gate, which must catch the blast radius of a caller deletion. Measurements on public samples (Hilt/Compose/KSP), and the limits that remain, are in the public validation record.
The following development checks require a source checkout and JDK 17+.
./gradlew test
./gradlew :koverVerify
./gradlew :cli:installDist
Scripts/verify-cli-contract.sh
Scripts/verify-fixture-corpus.sh
Scripts/verify-gradle-plugin-fixture.sh
Scripts/verify-agent-surface.sh
python3 -m unittest discover -s Scripts/tests -v
python3 Scripts/verify-runtime-corpus.py # 13 Java/Kotlin cases; JDK 17
python3 Scripts/verify-runtime-contracts.py # 6 differential cases; SDK Build Tools 35.0.0
python3 experiments/compiler-references/run.py # source checkout only; JDK 17
python3 experiments/dagger-bindings/run.py # source checkout only; JDK 17
python3 experiments/callgraph-precision/run.py # source checkout only; JDK 17
Scripts/verify-release-readiness.sh # two clean builds, never publishesA finding, including unreachable, is a fact about the input graph you supplied. It never says any code is safe to delete. Reflection, JNI, dynamic registration, missing variants or classpaths, and stale build outputs can all change the result. Before changing code, review --explain, the runtime paths, and that variant's tests. The full boundaries are in docs/LIMITATIONS.md. How local inputs are handled, and what to review before publishing reports, is in SECURITY.md.
| Document | Contents |
|---|---|
docs/PRD.md |
What, for whom, how far, and what it will not do |
docs/PLAN.md |
Staged plan. Phase 0 is a source-decision experiment, not code |
docs/PHASE3-ADOPTION.md |
Baseline, --since, machine reports, and the Gradle adoption contract |
docs/PHASE4-AGENT.md |
Query, measured limitations, bridge-facts, and the agent skill contract |
docs/PHASE5-VALIDATION.md |
Cycles, layer rules, Martin metrics, self-analysis, and performance evidence |
docs/LIMITATIONS.md |
Analysis boundaries and safe reading of findings |
docs/RESEARCH.md |
Confirmed facts, unconfirmed claims, and sources |
kartograph is MIT licensed. Copyright and license texts of the dependencies bundled in distributions ship together in THIRD_PARTY_NOTICES.md and LICENSES/.
dead --external-retentions <file> reads v0 documents from isthmus 0.8.0+
retentions --for kartograph. Every actual JVM node ID must exist in the indexed graph; malformed
or unmatched input fails without partial application. The called member's containing-type chain is retained. The normal opt-in private-member
entry policy still applies; the owner expansion itself does not select sibling methods. dead --explain shows EXTERNAL_BRIDGE with the
original Dart/JS caller locations, channel, method and omitted caller count. Supply the normal
required dead arguments as well.
bridges --rn-events [--target react-native] exports explicit
getJSModule(...RCTDeviceEventEmitter::class.java).emit(...) calls (Java .class is also supported)
in a separate v2 react-native-event document. --graph-file can attach actual JVM identities.
Capture snapshots with snapshot --include-paths so bridge source paths can match indexed methods.
Expo/codegen events and emitter variables/wrappers are not resolved. This flag is separate from
Flutter --events and --messages. Both extensions are available from 0.11.0. In 0.13.0, bridge generatedAt records extraction time and optional sourceModifiedAt separately records observed source mtime; neither proves compiler freshness.
schema --project <dir> [--format json] [--graph-file <snapshot>] [--jpa-naming <profile>] (available from 0.17.0) emits a
bridge-facts document with "target": "persistence" for isthmus. It scans Kotlin and Java sources for
Room annotations (@Entity, @DatabaseView, @Query, @ColumnInfo, @ForeignKey, and the
DAO operation annotations), JDBC call arguments when a java.sql/javax.sql import gates the
file, Spring JDBC calls on receivers declared as JdbcTemplate/NamedParameterJdbcTemplate/JdbcClient
(and their *Operations interfaces), Exposed Table objects and DSL receivers, jOOQ plain-SQL calls,
SQL-shaped string literals, and SQLDelight .sq/.sqm files. Other frameworks (Ktorm, jdbi, Criteria
string paths) are not claimed. Dynamic or unresolved evidence stays visible as dynamic facts and measured
limitations; an empty scan emits "target": null. --graph-file attaches JVM symbol identities only when the
snapshot is fresh.
JPA and Spring Data (unreleased): entity mappings (@Table(name, schema), @Column, @JoinColumn(s),
@JoinTable, @Embedded/@Embeddable with @AttributeOverride(s), the three inheritance strategies with
@DiscriminatorColumn and @PrimaryKeyJoinColumn, @ElementCollection/@CollectionTable/@OrderColumn,
@Transient, @MappedSuperclass, Java getter property access) become table and column facts under the
Hibernate naming strategy detected from build and configuration files: Spring Boot 3 (Hibernate 6
CamelCaseToUnderscoresNamingStrategy + SpringImplicitNamingStrategy), Spring Boot 4 (Hibernate 7
PhysicalNamingStrategySnakeCaseImpl, which also splits before digits and leaves quoted names alone), or plain
Hibernate 6/7 defaults. --jpa-naming spring-boot-3|spring-boot-4|hibernate-6|hibernate-7 fixes the profile.
When the version is unknown, only names that differ across the candidate profiles become dynamic; a custom
strategy (settings value, strategy class or @Bean) or globally quoted identifiers leave every JPA name dynamic.
The rules are checked against real Hibernate 6 and 7 schema export in fixtures/jpa-naming/vectors.json
(experiment). Spring Data repositories (including generic base
interfaces) contribute derived query names (Spring Data PartTree property paths), @Query JPQL (entity and
alias paths resolved to tables and columns), native @Query SQL, named queries and inherited CRUD methods;
EntityManager createQuery/createNativeQuery/createNamedQuery/find calls are read too. With
--graph-file, every repository call site becomes facts located at the call and owned by the calling method's
JVM id — the only identity a handler's forward reach is guaranteed to contain, because inherited CRUD methods
have no project node. Pass that document to reach/impact --format language-traversal with
--persistence-facts <file>: calls to inherited Spring Data repository methods that it attributes to the calling
method at the call line are then not counted in unresolvedCalls (persistence-modeled-calls: reports how many).
Unmodelled mappings, unresolved query paths and non-JPA repositories are counted as limitations instead of guessed.
routes --role client --project <dir> [--wrappers <http-wrappers.json>] [--include-tests] [--service <name>] [<source-root>...]
(unreleased) emits a bridge-facts document with "target": "http" and "roles": ["client"] for the
isthmus http domain. Each route-call fact carries the HTTP method (or methodDynamic), the canonical
path template (or a dynamic fact with a proven channelPrefix), pathAnchor, and the location of the
call expression. It recognizes calls of wrappers declared in an isthmus http-wrappers v1 file (only
"language": "kotlin" entries), java.net.URL requests opened in the same function with a provable path
and method, and Retrofit verb annotations in Kotlin and Java (named elements, @HTTP, fully qualified
@retrofit2.http.*). Retrofit paths follow RFC 3986 resolution as OkHttp applies it: /x is root, x is relative
to the base URL, and ./.. segments are removed; @Url, @Path(encoded = true), a .. above an unknown base
path, and annotation constants declared in another file stay dynamic. The templates agree with the requests Retrofit 2.12.0
sent to OkHttp MockWebServer for a synthetic service corpus
(experiments/phase4-retrofit). String resolution covers literals,
same-file constants (including Java interface fields), Kotlin templates whose interpolation fills a whole segment,
and query tails (a literal ?, or a trailing local proven to start with ?). Literal URLs lose userinfo, query and fragment, and high-entropy
or webhook segments are masked. Test source sets are excluded unless --include-tests marks those facts
testSource. Other clients (OkHttp, Ktor, …), stale wrapper declarations, and undeclared sinks surface as
limitations instead of guessed facts.
Retrofit base URLs are joined by following where each service interface is created: Retrofit.Builder()…baseUrl(x)…build() .create(Api::class.java) (also Api.class, create<Api>(), and Class<T>/reified creator functions), through local vals,
properties (= …, by lazy, getters, every assignment of a var/Java field), function bodies, Dagger/Hilt @Provides
providers matched by qualifier (@Inject constructors and fields, @Provides parameters) and Koin single/factory with
get(). A base that resolves to a literal (string, template, same-file or cross-file constant, read-only property, HttpUrl
wrapper, or an in-repo buildConfigField literal of the nearest module) gives authority and a root template composed by
the RFC 3986 rules above (users/{id} + https://h/v1/ → /v1/users/{}; /x stays /x); several bases give one fact each.
Otherwise the fact keeps pathAnchor: base and carries baseRef — the source-qualified id (kt:<pkg>.<Type>.<member>) of the
declaration holding the Retrofit instance — for workspace links match.baseRefs, and unresolved-base-url: counts those
services. Runtime values and ambiguous DI bindings never yield an authority. Test-source create calls are ignored.
OkHttp request rewrites (url-rewrite-interceptors:) drop the base only of the Retrofit instances whose client provably carries
them: each URL-rewriting interceptor (class, object, lambda, anonymous object) is followed to the addInterceptor/
addNetworkInterceptor call it reaches, and each Retrofit client(…)/callFactory(…) to its OkHttpClient (builder chains,
newBuilder() copies, local builders, @Provides, Koin). Authenticators and event listeners not proven to keep the URL, and custom
Call.Factory implementations, count as rewrites. Such an instance gets its own limitation, with a client-side limitationScopes
entry bounding its hidden requests when every rewrite provably changes only scheme/host/port. If any rewrite or any client with a
resolved base cannot be bound (interceptor lists, builders passed around, unattached rewrites), every base is dropped as before.
With --graph-file, a Retrofit fact's symbol.usr is the service interface method that declares the annotation
(for an inherited method, the super-interface that declares it). Call sites invoke that method, so reverse traversal
(impact --format language-traversal --roots-from) reaches every caller, including calls through a sub-interface.
Facts left without a JVM identity are counted by missing-route-usrs:, which names a snapshot/routes
--project root mismatch when the source paths show one.
Spring server-to-server calls are recognized too: RestTemplate request methods (getForObject, exchange, ...), RestClient
and WebClient get()/post()/.../method(...) followed by .uri(...) (template strings, UriComponentsBuilder chains, URI
values, builder lambdas), on a receiver proven to be such a client, and @HttpExchange interface methods (one fact per method,
symbol.usr = the interface method). Base URLs follow builder chains (baseUrl, RestTemplateBuilder.rootUri,
DefaultUriBuilderFactory), @Bean methods chosen by type, @Qualifier, @Primary or name, and @Value("${key}") properties
from the in-repo default profile, and are joined the way Spring does it — string concatenation with // collapsed for
UriBuilderFactory (/api + users is /apiusers), root only for /-prefixed templates with rootUri — verified against Spring
Framework 6.2.19 / Boot 3.5.16 sources and an execution oracle (32 of 32 calls match). Unresolved bases keep pathAnchor: base with a
baseRef (the @Bean method, property or field supplying the client) and are counted by unresolved-base-url:; facts carry no
service, so link server members by match.hosts or match.baseRefs. Details, source citations and the two-service isthmus trace
are in Spring HTTP clients.
routes --role server --project <dir> [--graph-file <snapshot> [--input-bindings <file>]] [--service <name>] [--include-tests] [<source-root>...]
(unreleased) emits "roles": ["server"] and "dispatch": "specificity" with one route-decl fact per Spring MVC or
WebFlux handler mapping: @RequestMapping, @GetMapping…@PatchMapping, @HttpExchange/@GetExchange… on
@Controller classes, custom annotations meta-annotated with them (@AliasFor), mappings inherited from interfaces and
superclasses, class × method paths, and ANY for method-less mappings. With a fresh --graph-file the snapshot's class
roots supply the annotation values (constants already folded by the compiler) and symbol.usr is the handler's JVM id,
the same id space as impact/reach; sources always supply the annotation location. The in-repo default profile supplies
server.servlet.context-path / spring.webflux.base-path and ${key:default} placeholders (configDefault when only the
default applies). Facts carry trailingSlash (strict for Spring Boot 3+, optional for Boot 2, omitted when unknown),
narrowed, paramConstraints for regex path variables, and catch-all prefix expansion for /** and {*path}. Unresolved
paths stay dynamic; places Spring matches with an empty value (a trailing *, a partial-segment variable) also emit
empty-value variant facts. Functional routers, framework-provided routes (/error, actuator, static resources, ...), other
profiles and unresolved prefixes are reported as server-side limitations; providers whose requests can be bounded carry
isthmus http limitationScopes (for example /error for every method, static resources for GET/HEAD only), so calls
outside them stay error-judgeable. Rules, Spring sources and the actuator oracle
results (100% precision on three public Spring Boot apps and two synthetic MVC/WebFlux apps) are in
Spring server routes.
impact --format language-traversal (reverse) and reach (forward) (unreleased) emit an isthmus
language-traversal v1 document for trace: one pass over many roots, with every root that reaches each
declaration, a shortest-path witness, per-root lower-bound evidence (direct, bound, candidate) and
unresolvedCalls. Lambda and anonymous-class bodies belong to their enclosing declaration through captured
EnclosingMethod facts, so FunctionN/SAM invoke fan-out is not followed by default
(--dispatch direct|bound|candidates|all, default candidates). revision is --revision <rev> when given,
else the snapshot's revision label, else the git HEAD only when the project directory has no uncommitted or
untracked changes (omitted otherwise, so isthmus never treats an analysis of edited sources as current).
graphRevision is sha256: over the graph content — node ids and kinds, edges with their kinds and origins,
evidence tiers and lexical enclosures, locations excluded — so reverse and forward documents from one snapshot agree.
Roots and --revision containing control characters (C0, DEL, C1, U+2028, U+2029) are rejected with exit 64 because
isthmus rejects such ids. Functions that invoke or forward a lambda argument (for example a UI section running an
onClick callback) are linked to the lambda body as callback evidence (bound when every invocation is inside the
project, candidate when the value reaches library code or escapes) and are listed only for that call context, never
expanded to their other callers. Test-source declarations (src/test, src/androidTest, ...) are a separate program:
they are not traversed by default, so bound dispatch counts production implementations only (--include-tests restores
them). Snapshot inputs are verified like routes (--input-bindings for inputs outside the project). The default
impact output is unchanged. See
change impact.