Skip to content

feat(manifest): record each subproject's dependency tree in JVM facts - #1585

Merged
Jeppe Fredsgaard Blaabjerg (jfblaa) merged 14 commits into
v1.xfrom
jfblaa/facts-dependency-tree
Oct 12, 2026
Merged

Jeppe Fredsgaard Blaabjerg (jfblaa) merged 14 commits into
v1.xfrom
jfblaa/facts-dependency-tree

Conversation

@jfblaa

@jfblaa Jeppe Fredsgaard Blaabjerg (jfblaa) commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

LLM Description written by Claude Code:claude-opus-5-5

JVM facts merged every resolution root's edges into one graph keyed by coordinate. A package's dependencies in one subproject therefore leaked into every other subproject that also uses it, mis-attributing dependency chains and manifests. This records each subproject's own dependency tree instead.

Merge gate: release with a Coana bump that contains coana-tech/coana-package-manager#2544, whose socket fix reads per-project trees. depscan already reads the format (SocketDev/depscan#27464, merged).

Format

{
  "components": [ { "type": "maven", "namespace": "…", "name": "jackson-databind", "version": "2.15.2", "id": "…" }, … ],
  "dependencies": [
    { "component": 1 },
    { "component": 2, "children": [0] },
    { "component": 0 },
    { "component": 2, "children": [2, 0] }
  ],
  "projects": [
    { "id": "g:worker:1", "name": "worker", "subprojectDir": "worker", "manifestFiles": [{ "file": "worker/pom.xml" }], "children": [1] },
    { "id": "g:api:1",    "name": "api",    "subprojectDir": "api",    "manifestFiles": [{ "file": "api/pom.xml" }],    "children": [3] }
  ]
}
  • dependencies (new, top level): a component with one exact subtree. component indexes components; children indexes dependencies. Positions with identical subtrees share an entry.
  • projects[].children replaces projects[].dependencies: the subproject's direct dependencies, the roots of its tree. A resolved dependency the direct ones don't reach fails the run.
  • components no longer carry dependencies or manifestFiles. A consumer attributes every component to the facts file, and a direct component to the manifestFiles of each project whose children include it.
  • projects[].manifestFiles is the build file the tool reports for the project: Maven's POM, sbt's files defining the project's settings, and Gradle's configured build file even when absent on disk (Gradle doesn't expose where a dependency was declared). This replaces the per-dependency declaring-script marks of feat(manifest): attribute Gradle direct dependencies to their declaring build script #1593.
  • One tree per subproject with its configurations merged; dev stays a per-component flag.
  • No version marker: a file has either the old edge shape or the tree.

Also in this PR

  • The compute-artifacts sidecar keeps its contract (per-project dependencies and classpath, keyed by project id) and is still passed to Coana by socket fix and reachability, since the Coana version can be chosen independently (--fix-version, --reach-version). An older Coana gets reachability as today; its socket fix knows from the sidecar which subprojects resolve a package but can't read direct dependencies or edges from a tree.
  • socket fix accepts Socket facts files already in the repo instead of refusing to run. With --dynamic-sbom-inference they are backed up before generation and restored afterwards, also when generation fails.
  • sbt build modules come from sbt's own inter-project classpath traversal, so dependencies of a version-conflict winner are no longer dropped and sibling subprojects appear only in the configurations that depend on them.

Verification

  • Assembler tests cover divergent and shared subtrees, merged configurations, cycles, and unreached nodes failing the run; each piece of the encoder was disabled in turn and broke a test.
  • Records from 8 open-source Maven, Gradle and sbt builds (9 to 448 subprojects): every subproject's tree reproduces the union of its configurations' graphs, output is deterministic, and files are about half today's raw size and similar or smaller after brotli.
  • sbt compat 1.4.9, 1.6.2 and 1.9.9 pass; 0.13.18 couldn't run because the sbt launcher failed to download sbt itself. A multi-project sbt build and a Gradle build with root-configured subprojects (Gradle 5.6.4 and 9.2.1) produce the expected trees and build files.
  • Unit tests pass apart from a socket fix CLI test that depends on the local API token.

🤖 Generated with Claude Code

Facts merged every resolution root's edges into one graph keyed by
coordinate, so a package's dependencies in one subproject leaked into
every other subproject that also uses it.

Edges move out of components into a top-level dependencies list: each
entry is a component with one exact subtree, shared by every position
whose subtree is identical, and each project lists the entries at the
roots of its own tree (its configurations merged). Identical subtrees
are found by partition refinement, so cycles merge exactly too.

The compute-artifacts sidecar keeps its contract: projects still carry
their direct dependency ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the dependency-tree shape, a subproject's own build files go on
projects[].manifestFiles, and components no longer carry manifestFiles.
Consumers attribute a direct component to the build files of the
projects whose children include it, and every component to the facts
file itself, so neither is written per component.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sbt facts plugin read lm-coursier's update report too literally, which
left resolved nodes unreachable from a project's direct dependencies. The
assembler then rooted those nodes in projects[].children, mixing them with
real direct dependencies.

- Match callers by org:name. lm-coursier stamps a caller with its declared
  version rather than the resolved one, so exact-GAV matching dropped edges
  from conflict winners.
- Take sibling subprojects from the build structure, following sbt's own
  Classpaths.interSort. The update report lists every inter-project
  dependency in every configuration, and cross-versioning can merge
  distinct siblings into one module.
- Fail on unreached nodes instead of rooting them, so projects[].children
  is exactly the direct dependencies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With per-subproject trees in .socket.facts.json, Coana's socket fix reads
each project's direct dependencies from the facts file itself.

- Stop refusing an existing .socket.facts.json; upload it like any other
  manifest. --dynamic-sbom-inference regenerates it.
- Stop writing the resolved-paths sidecar for socket fix and drop
  --compute-artifacts-sidecar, along with runDynamicSbomInference's now
  unused sidecar option.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
children holds exactly the subproject's direct dependencies; the
assembler fails on unreached nodes instead of rooting them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jdalton

Copy link
Copy Markdown
Collaborator

This draft targets the correct branch, v1.x. Keep the coordinated backend and Coana changes as merge prerequisites.

High: preserve existing facts before generation

The PR removes the pre-existing facts guard from src/commands/fix/coana-fix.mts. In src/commands/fix/generated-socket-facts.mts, generation runs before copyFile creates backups. Existing facts can therefore be overwritten before their original contents are saved; cleanup can also remove those files.

Suggestion 💡: back up existing files before generation and restore them on success and failure. Add a regression that begins with an existing facts file and checks its original bytes after cleanup.

Integration check: confirm the replacement for the fix sidecar

The patch removes --compute-artifacts-sidecar and stops requesting the sidecar during fix generation. The current fix path uses that sidecar for each project's classpath. The PR description says the sidecar contract remains.

Suggestion 💡: link the coordinated Coana change that replaces this input, or retain the sidecar until it lands. Verify an actual fix application, as well as facts ingestion, before merging.

…cy-tree

# Conflicts:
#	CHANGELOG.md
#	src/commands/fix/coana-fix.mts
#	src/commands/manifest/scripts/assemble.mts
#	src/commands/manifest/scripts/assemble.test.mts
#	src/commands/manifest/scripts/facts.mts
#	src/commands/manifest/scripts/sidecar.mts
#	src/commands/manifest/scripts/sidecar.test.mts
#	src/commands/manifest/scripts/socket-facts.plugin.scala
…rence

Facts files present before generation were overwritten before any
backup, and removed by the cleanup after a fix or a failed generation.
Back them up first and restore them afterwards; upload a regenerated
file only once.
…roject graph

Build each subproject's merged graph once and read projects[].children,
the sidecar's direct dependencies and its classpath from it. Also drop a
dead build-module ext lookup in the sbt plugin and pass socket fix's
facts generation a single facts-file finder.
…d file

A facts tree names each project's direct dependencies, so the build file
on projects[].manifestFiles only has to mark the subproject. Gradle's API
does not say which script declared a dependency, so use the project's
configured build file, present or not, and drop the declaring-script
records recovered from stack frames.
@jfblaa

Copy link
Copy Markdown
Contributor Author

[agent] Thanks, both addressed:

  • Preserve existing facts: fixed in 9ead569. Facts files already in the repo are backed up before generation, and restored after the fix and when generation fails. generated-socket-facts.test.mts covers this, including the original bytes surviving cleanup.
  • Fix sidecar replacement: coana-tech/coana-package-manager#2544 replaces it. Coana's socket fix takes each project's direct dependencies and closure from the facts trees, so it no longer needs the per-project classpath. The sidecar stays for reachability. This PR merges together with the Coana bump that contains #2544. Coana's tests cover fixes planned from tree-format facts. An end-to-end socket fix run against a real scan still needs to happen with that bump before merge.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but none of the reported issues met your autofix severity threshold.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 0361123. Configure here.

Comment thread src/commands/manifest/scripts/socket-facts.plugin.scala
Comment thread src/commands/fix/coana-fix.mts
socket fix can run an older Coana (--fix-version) that reads each
project's classpath from the sidecar rather than from facts trees. A
Coana that reads facts trees ignores the sidecar for them.
@jfblaa

Copy link
Copy Markdown
Contributor Author

[agent] Update to my reply above: socket fix passes the compute-artifacts sidecar to Coana again (57d97b5), since the Coana version can be chosen with --fix-version. A Coana with coana-tech/coana-package-manager#2544 uses the facts trees and ignores the sidecar for them; an older one still gets per-project classpaths from it.

@mtorp Martin Torp (mtorp) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ✅
Don't know if Oskar Haarklou Veileborg (@BarrensZeppelin) has some comments since he reviewed the Coana PR.

The bare .socket.facts.json is Coana's report, not producer output, so
socket fix skips it as reachability scans already do.
@jfblaa
Jeppe Fredsgaard Blaabjerg (jfblaa) merged commit e3e513b into v1.x Oct 12, 2026
15 checks passed
@jfblaa
Jeppe Fredsgaard Blaabjerg (jfblaa) deleted the jfblaa/facts-dependency-tree branch October 12, 2026 13:19
@jfblaa

Copy link
Copy Markdown
Contributor Author

[agent] End-to-end check with the published socket 1.8.0 and Coana 15.12.5: socket fix --dynamic-sbom-inference --id <GHSA> from a clean tree for each case.

  • Gradle multi-project demo, 12 advisories (Kotlin and Groovy DSL, version catalog, gradle.properties, buildSrc, root extra property, transitive constraints, eachDependency, strictly, ranges, Spring dependency management, dependency locking, root subprojects {}): every fix changes exactly the expected files, including commons-io fixed in lib and legacy but not in app, which only gets it through project(":lib").
  • Maven reactor: commons-text fixed in the declaring module only (not the module that depends on it), commons-io in its module, log4j through the parent property.
  • sbt multi-project: each fix lands on the declaring project in build.sbt.

No generated facts files are left behind after any run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants