Skip to content

Emit DEPENDS_ON edges for Maven/Gradle manifests (pom.xml, build.gradle) #1479

Description

@leonfin

What problem does this solve?

In a multi-repo JVM estate (~240 separately-indexed projects, each its own git repo), there is no way to answer "which projects consume artifact X?"

Concrete, measured case. A utility class ByteBufWriter lives in the repo platform-io, which publishes the Maven artifact com.acme.platform:platform-io. Other repos depend on it — but they consume it as a compiled JAR from an internal Maven repository, not as source.

Measurement Value
<dependency> blocks in platform-io's own pom.xml 23
pom.xml files across the estate declaring a dependency on platform-io 29
Distinct repos those span ~19
DEPENDS_ON edges present in any of those projects' graphs 0

Critically, the consuming projects are indexed, and they do reference the type — the reference just dangles. search_graph for ByteBufWriter in a consumer returns nodes like:

{
  "name": "mByteBufWriter",
  "label": "Field",
  "qualified_name": "consumer-repo.src.test.java.com.acme.config.SomeTest.mByteBufWriter",
  "return_type": "ByteBufWriter",
  "in_degree": 0,
  "out_degree": 0
}

The return_type is captured as a bare string. There is no edge — in either direction — to the ByteBufWriter class node that exists, fully indexed, in platform-io. in_degree and out_degree are both 0. So:

  • The type name is in the graph but is not a graph reference; it cannot be joined on.
  • trace_path stops dead at the repo boundary.
  • get_graph_schema() on these projects lists ~15 edge types — CALLS, DEFINES, USAGE, IMPORTS, INHERITS, HTTP_CALLS, … — and DEPENDS_ON is not among them. The same call on a Go or Python project would include it.

This is the surprising part: both halves of the answer are already in the index — the definition in one project, the typed reference in another — and nothing joins them, because cbm_pipeline_resolve_module has no pkgmap entry for another project's groupId.artifactId prefix and falls through to cbm_pipeline_fqn_module(ctx->project_name, module_path) (pass_pkgmap.c:1445), minting a QN inside the consumer's own namespace that matches nothing.

The dependency is fully declared and machine-readable — it is right there in all 29 pom.xml files, which pass_pkgmap already opens and reads — but the <dependencies> block is discarded. I recovered the 29 with rg -l --glob 'pom.xml' 'platform-io', which is precisely the filesystem grep the graph is meant to replace.

Root cause (from reading the source)

Three places, none of which cover Maven:

1. parse_pom_xml reads only the module's own identity, never its dependencies.
src/pipeline/pass_pkgmap.c:557 extracts <groupId> + <artifactId> and registers groupId.artifactId -> src/main/java so the module can self-register. The <dependencies> block is never parsed. pom_find_tag (:539) already tracks and skips <parent>, so the machinery for scoping XML scans exists — it just isn't applied to dependencies.

2. The dependency-manifest dispatcher has no Maven branch.
src/pipeline/pass_k8s.c:653:

if (is_gomod_file(base) || lang == CBM_LANG_GOMOD || is_requirements_file(base)) {
    ...
    handle_dep_manifest(ctx, rel, dep_src,
                        is_requirements_file(base) ? "pypi" : "gomod");

go.mod and requirements.txt are handled. pom.xml, build.gradle, and build.gradle.kts are not — even though pass_pkgmap already finds and reads all of them.

3. pass_cross_repo is runtime-only.
Per src/pipeline/pass_cross_repo.h it emits CROSS_HTTP_CALLS, CROSS_ASYNC_CALLS, CROSS_CHANNEL, CROSS_GRPC_CALLS, CROSS_GRAPHQL_CALLS, CROSS_TRPC_CALLS — all runtime connections. There is no build-time / artifact dependency equivalent.

Proposed solution

Two independently-useful tiers.

Tier 1 — Maven/Gradle parity with go.mod and requirements.txt

Make pom.xml and build.gradle[.kts] emit DEPENDS_ON edges exactly the way go.mod already does. This needs no new node label and no new edge type — it reuses the existing generic helper at pass_k8s.c:486:

static int emit_dep_edge(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t *src,
                         const char *rel_path, const char *ecosystem, const char *name)

which already builds a shared, deduplicated external Package node (%s.__%s_dep__.%s, props {"source":"<ecosystem>","external":true}) and links file --DEPENDS_ON--> Package. It is already parameterized by ecosystem string, so "maven" and "gradle" slot straight in.

Work required:

  • parse_pom_deps() — walk <dependencies>, emit groupId:artifactId per <dependency>. Must skip <dependencyManagement> and <parent> (the existing in-<parent> tracking in pom_find_tag is the pattern to follow), and should tolerate ${property} version placeholders since only the coordinate matters.
  • parse_gradle_deps() — the implementation "g:a:v" / api(...) forms.
  • Two lines in the pass_k8s.c:653 dispatcher, plus the handle_dep_manifest ternary at :617 (already at the size where a small lookup table would read better than a chained ternary).

Even alone this is a large win: it makes

MATCH (f)-[:DEPENDS_ON]->(p:Package) WHERE p.name = 'platform-io' RETURN f

work per project, so a caller can loop indexed projects and get a real consumer list without touching the filesystem.

Tier 2 — resolve external Package nodes to indexed projects

The more interesting half. Because parse_pom_xml already registers every project's own groupId.artifactId, the join key needed to turn an external Package node into a link to the project that publishes it is already being computed — it's just discarded at the project boundary.

A post-index pass structured like cbm_cross_repo_match (same "match against all indexed projects, write bidirectionally into both DBs" shape, including the target_projects / "*" convention and cancellation handling) could:

  1. Build groupId:artifactId -> project_name across indexed projects from the identities parse_pom_xml already produces.
  2. For each external Package node emitted in Tier 1, look up its coordinate.
  3. On a hit, emit a cross-project edge — CROSS_DEPENDS_ON, consistent with the existing CROSS_* naming.

That reduces the original question to:

MATCH (c)-[:CROSS_DEPENDS_ON]->(d) WHERE d.name = 'platform-io' RETURN c.name

and, combined with the existing intra-project graph, would let trace_path cross a JAR boundary for the first time.

Notes

  • Happy to take a run at Tier 1 if the direction seems right — it looks self-contained and emit_dep_edge does most of the work already.
  • Tier 1 also benefits Kotlin/Scala/Android repos, which share the Gradle path.

Alternatives considered

Index the whole estate as one project. Works only for a true single-parent-pom monorepo. My repos are separate git repos with separate lifecycles; concatenating them also defeats the per-project watcher and DB model, and the file count is far past the auto-index limit.

mvn dependency:tree and ingest the output. Correct, and it resolves transitives and property placeholders properly — but it needs a working Maven toolchain, network access to the internal repository, and a successful resolve for every repo. That is far heavier and more fragile than reading a pom.xml as text, which the index already does for pass_pkgmap. Declared direct dependencies cover the "who consumes this" question; full transitive resolution is a nice-to-have, not the blocker.

Filesystem grep over pom.xml. What I do today. It answers the question but sits entirely outside the graph, so it composes with nothing — no trace_path, no combining with call-graph data, no MCP access.

Heuristic name matching across projects. Guesswork where an exact, declared coordinate is already available. Not worth the false positives.

Confirmations

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions