Dependency resolution detects ecosystems only at the scan root and then reads only root-level manifests. Source enumeration is repo-wide, but dependency enumeration is not.
Evidence
docs/license-tool-design.md:56 describes dependency resolution by manifest presence.
internal/resolve/resolve.go:31 to internal/resolve/resolve.go:41 detects resolvers only at the provided root path.
internal/resolve/maven.go:71 to internal/resolve/maven.go:76 reads only <root>/pom.xml.
internal/resolve/npm.go:56 to internal/resolve/npm.go:64 reads only <root>/package.json and <root>/node_modules.
internal/resolve/gradle.go:56 to internal/resolve/gradle.go:60 scans only root Gradle manifests.
Reproduction
Create a repository with supported source files and a nested package, Maven module, or Gradle subproject under a subdirectory.
Run audit from the repository root.
Observe that source files from the nested project are included, but dependency licenses for the nested manifest are absent.
Why It Matters
Common multi-module repositories can appear cleaner than they are. The audit reports repo-wide source status while silently omitting nested dependency surfaces.
Suggested Fix
Add manifest discovery as a first-class step that finds nested ecosystem roots. Feed each resolver explicit manifest roots, and add fixture tests for Maven modules, npm workspaces or nested packages, and Gradle subprojects.
## Finding
Dependency resolution detects ecosystems only at the scan root and then reads only root-level manifests. Source enumeration is repo-wide, but dependency enumeration is not.
## Evidence
- `docs/license-tool-design.md:56` describes dependency resolution by manifest presence.
- `internal/resolve/resolve.go:31` to `internal/resolve/resolve.go:41` detects resolvers only at the provided root path.
- `internal/resolve/maven.go:71` to `internal/resolve/maven.go:76` reads only `<root>/pom.xml`.
- `internal/resolve/npm.go:56` to `internal/resolve/npm.go:64` reads only `<root>/package.json` and `<root>/node_modules`.
- `internal/resolve/gradle.go:56` to `internal/resolve/gradle.go:60` scans only root Gradle manifests.
## Reproduction
1. Create a repository with supported source files and a nested package, Maven module, or Gradle subproject under a subdirectory.
2. Run audit from the repository root.
3. Observe that source files from the nested project are included, but dependency licenses for the nested manifest are absent.
## Why It Matters
Common multi-module repositories can appear cleaner than they are. The audit reports repo-wide source status while silently omitting nested dependency surfaces.
## Suggested Fix
Add manifest discovery as a first-class step that finds nested ecosystem roots. Feed each resolver explicit manifest roots, and add fixture tests for Maven modules, npm workspaces or nested packages, and Gradle subprojects.
Status: confirmed — dependency resolution detects and reads manifests only at the scan root, while source enumeration is repo-wide; reproduced behaviorally.
Severity: high (label says bug/dependencies). The defect produces a silently incomplete audit — a multi-module repo reports its nested dependency surfaces as if they don't exist, with no warning. For a tool whose stated value is "an unresolved dependency is an honest gap, not a fabricated answer" (internal/resolve/resolve.go:9-14), silently omitting whole manifests is a correctness/trust failure, not a cosmetic one. Not critical only because it under-reports rather than mis-asserting a license.
Verified root cause: the asymmetry lives in the audit pipeline wiring, not in the resolvers themselves.
cmd/license-tool/commands.go:387-396 (buildAuditPipeline's ResolveDeps): iterates resolve.Detected(root) and calls r.Resolve(root, ...) against the single root path only — never recurses.
internal/resolve/resolve.go:34-42 (Detected): detects ecosystems at exactly one path.
Each resolver then reads only <path>/<manifest>: internal/resolve/maven.go:72 (filepath.Join(path, "pom.xml")), internal/resolve/npm.go:57,64 (<path>/package.json, <path>/node_modules), internal/resolve/gradle.go:58-60 (scanGradleManifest(filepath.Join(path, m), ...)).
Contrast with source enumeration, which is repo-wide: internal/enumerate/enumerate.go:80-91 lists files via gitListFiles / walkListFiles (the latter being filepath.WalkDir at enumerate.go:269). So the same nested pom.xml is read as a source file but never as a manifest.
Reproduction (built from current main): a repo with root pom.xml (com.root:rootdep) plus sub/module/pom.xml (com.nested:nesteddep) — audit . enumerated 4 source files including sub/module/mod.go and sub/module/pom.xml, but reported exactly 1 dependency (com.root:rootdep); the nested com.nested:nesteddep was silently absent.
Suggested fix: introduce a manifest-discovery step that walks the repo (reusing the enumeration walk / git ls-files, honoring the same .gitignore + --exclude semantics so the two surfaces stay symmetric) and yields every directory containing a known manifest, then run resolve.Detected(dir) + r.Resolve(dir, ...) per discovered root. The natural seam is ResolveDeps in cmd/license-tool/commands.go:387-396; the resolvers already take a per-manifest path and need no change. This must touch all three resolver detection paths consistently (Maven pom.xml, npm package.json, Gradle build.gradle[.kts]) — like the v0.2.1 universal-shebang fix, the discovery table should enumerate the manifest filenames in one place (mirror gradleManifests / the Detect predicates) rather than special-casing one ecosystem. Deduplicate so the root manifest isn't resolved twice. To keep the 100% gate green, add fixture tests for: a Maven reactor with a nested module, an npm nested package / workspace, and a Gradle subproject, asserting nested deps appear; plus a test that excluded/ignored directories are not descended into.
Corrections to the issue:
The design-doc citation is off by three lines. The dependency-resolution description is at docs/license-tool-design.md:59 (Data-flow step 4, "detect ecosystems by manifest presence"), not :56 (which is "Resolve config", step 1). Note also that the doc text itself does not promise recursion — so this is arguably an under-specification in the design doc as much as a code bug; a maintainer may want to clarify intended scope there too.
npm nuance: even within a single resolved root, npm resolution only reads the direct declared deps' installed package.json (npm.go:63,72), not the full transitive node_modules tree. The issue frames the gap purely as "nested manifest roots," but a workspaces repo also hides transitive/sibling-workspace packages. The fix scope should state whether transitive coverage is in or out.
Otherwise the evidence line refs (resolve.go:31-41, maven.go:71-76, npm.go:56-64, gradle.go:56-60) are all accurate.
Open questions:
Discovery scope: walk the whole repo for manifests, or only roots reachable from the top-level manifest's declared modules (<modules> in Maven, workspaces in npm, include in Gradle settings)? Whole-repo walk is simpler and matches source enumeration, but may surface vendored/example manifests.
Should discovery honor --exclude / .gitignore identically to source enumeration (recommended for symmetry), and should there be a flag to opt out / cap depth for huge monorepos?
De-dup / aggregation semantics: report nested deps grouped per-module, or flattened with de-duplication across modules?
Out of scope confirmation: is npm transitive-dependency coverage in scope for this fix, or strictly nested manifest roots?
Overlaps: none that duplicate this. #22 (Gradle tool tier advertised-but-ignored) and #25 (invalid resolver-tier values silently accepted) also touch internal/resolve/ but are independent defects; a fix here neither depends on nor resolves them. #11 (docs gap) is unrelated.
Automated maintainer review.
**Status**: confirmed — dependency resolution detects and reads manifests only at the scan root, while source enumeration is repo-wide; reproduced behaviorally.
**Severity**: high (label says `bug`/`dependencies`). The defect produces a silently incomplete audit — a multi-module repo reports its nested dependency surfaces as if they don't exist, with no warning. For a tool whose stated value is "an unresolved dependency is an honest gap, not a fabricated answer" (`internal/resolve/resolve.go:9-14`), silently omitting whole manifests is a correctness/trust failure, not a cosmetic one. Not critical only because it under-reports rather than mis-asserting a license.
**Verified root cause**: the asymmetry lives in the audit pipeline wiring, not in the resolvers themselves.
- `cmd/license-tool/commands.go:387-396` (`buildAuditPipeline`'s `ResolveDeps`): iterates `resolve.Detected(root)` and calls `r.Resolve(root, ...)` against the single `root` path only — never recurses.
- `internal/resolve/resolve.go:34-42` (`Detected`): detects ecosystems at exactly one `path`.
- Each resolver then reads only `<path>/<manifest>`: `internal/resolve/maven.go:72` (`filepath.Join(path, "pom.xml")`), `internal/resolve/npm.go:57,64` (`<path>/package.json`, `<path>/node_modules`), `internal/resolve/gradle.go:58-60` (`scanGradleManifest(filepath.Join(path, m), ...)`).
Contrast with source enumeration, which is repo-wide: `internal/enumerate/enumerate.go:80-91` lists files via `gitListFiles` / `walkListFiles` (the latter being `filepath.WalkDir` at `enumerate.go:269`). So the same nested `pom.xml` is read as a *source file* but never as a *manifest*.
Reproduction (built from current `main`): a repo with root `pom.xml` (`com.root:rootdep`) plus `sub/module/pom.xml` (`com.nested:nesteddep`) — `audit .` enumerated 4 source files including `sub/module/mod.go` and `sub/module/pom.xml`, but reported exactly **1** dependency (`com.root:rootdep`); the nested `com.nested:nesteddep` was silently absent.
**Suggested fix**: introduce a manifest-discovery step that walks the repo (reusing the enumeration walk / `git ls-files`, honoring the same `.gitignore` + `--exclude` semantics so the two surfaces stay symmetric) and yields every directory containing a known manifest, then run `resolve.Detected(dir)` + `r.Resolve(dir, ...)` per discovered root. The natural seam is `ResolveDeps` in `cmd/license-tool/commands.go:387-396`; the resolvers already take a per-manifest `path` and need no change. This must touch **all three** resolver detection paths consistently (Maven `pom.xml`, npm `package.json`, Gradle `build.gradle[.kts]`) — like the v0.2.1 universal-shebang fix, the discovery table should enumerate the manifest filenames in one place (mirror `gradleManifests` / the `Detect` predicates) rather than special-casing one ecosystem. Deduplicate so the root manifest isn't resolved twice. To keep the 100% gate green, add fixture tests for: a Maven reactor with a nested module, an npm nested package / workspace, and a Gradle subproject, asserting nested deps appear; plus a test that excluded/ignored directories are not descended into.
**Corrections to the issue**:
- The design-doc citation is off by three lines. The dependency-resolution description is at `docs/license-tool-design.md:59` (Data-flow step 4, "detect ecosystems by manifest presence"), not `:56` (which is "Resolve config", step 1). Note also that the doc text itself does not promise recursion — so this is arguably an under-specification in the design doc as much as a code bug; a maintainer may want to clarify intended scope there too.
- npm nuance: even within a single resolved root, npm resolution only reads the *direct* declared deps' installed `package.json` (`npm.go:63,72`), not the full transitive `node_modules` tree. The issue frames the gap purely as "nested manifest roots," but a workspaces repo also hides transitive/sibling-workspace packages. The fix scope should state whether transitive coverage is in or out.
- Otherwise the evidence line refs (`resolve.go:31-41`, `maven.go:71-76`, `npm.go:56-64`, `gradle.go:56-60`) are all accurate.
**Open questions**:
- Discovery scope: walk the *whole* repo for manifests, or only roots reachable from the top-level manifest's declared modules (`<modules>` in Maven, `workspaces` in npm, `include` in Gradle settings)? Whole-repo walk is simpler and matches source enumeration, but may surface vendored/example manifests.
- Should discovery honor `--exclude` / `.gitignore` identically to source enumeration (recommended for symmetry), and should there be a flag to opt out / cap depth for huge monorepos?
- De-dup / aggregation semantics: report nested deps grouped per-module, or flattened with de-duplication across modules?
- Out of scope confirmation: is npm transitive-dependency coverage in scope for this fix, or strictly nested manifest roots?
**Overlaps**: none that duplicate this. #22 (Gradle tool tier advertised-but-ignored) and #25 (invalid resolver-tier values silently accepted) also touch `internal/resolve/` but are independent defects; a fix here neither depends on nor resolves them. #11 (docs gap) is unrelated.
_Automated maintainer review._
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Finding
Dependency resolution detects ecosystems only at the scan root and then reads only root-level manifests. Source enumeration is repo-wide, but dependency enumeration is not.
Evidence
docs/license-tool-design.md:56describes dependency resolution by manifest presence.internal/resolve/resolve.go:31tointernal/resolve/resolve.go:41detects resolvers only at the provided root path.internal/resolve/maven.go:71tointernal/resolve/maven.go:76reads only<root>/pom.xml.internal/resolve/npm.go:56tointernal/resolve/npm.go:64reads only<root>/package.jsonand<root>/node_modules.internal/resolve/gradle.go:56tointernal/resolve/gradle.go:60scans only root Gradle manifests.Reproduction
Why It Matters
Common multi-module repositories can appear cleaner than they are. The audit reports repo-wide source status while silently omitting nested dependency surfaces.
Suggested Fix
Add manifest discovery as a first-class step that finds nested ecosystem roots. Feed each resolver explicit manifest roots, and add fixture tests for Maven modules, npm workspaces or nested packages, and Gradle subprojects.
Status: confirmed — dependency resolution detects and reads manifests only at the scan root, while source enumeration is repo-wide; reproduced behaviorally.
Severity: high (label says
bug/dependencies). The defect produces a silently incomplete audit — a multi-module repo reports its nested dependency surfaces as if they don't exist, with no warning. For a tool whose stated value is "an unresolved dependency is an honest gap, not a fabricated answer" (internal/resolve/resolve.go:9-14), silently omitting whole manifests is a correctness/trust failure, not a cosmetic one. Not critical only because it under-reports rather than mis-asserting a license.Verified root cause: the asymmetry lives in the audit pipeline wiring, not in the resolvers themselves.
cmd/license-tool/commands.go:387-396(buildAuditPipeline'sResolveDeps): iteratesresolve.Detected(root)and callsr.Resolve(root, ...)against the singlerootpath only — never recurses.internal/resolve/resolve.go:34-42(Detected): detects ecosystems at exactly onepath.<path>/<manifest>:internal/resolve/maven.go:72(filepath.Join(path, "pom.xml")),internal/resolve/npm.go:57,64(<path>/package.json,<path>/node_modules),internal/resolve/gradle.go:58-60(scanGradleManifest(filepath.Join(path, m), ...)).Contrast with source enumeration, which is repo-wide:
internal/enumerate/enumerate.go:80-91lists files viagitListFiles/walkListFiles(the latter beingfilepath.WalkDiratenumerate.go:269). So the same nestedpom.xmlis read as a source file but never as a manifest.Reproduction (built from current
main): a repo with rootpom.xml(com.root:rootdep) plussub/module/pom.xml(com.nested:nesteddep) —audit .enumerated 4 source files includingsub/module/mod.goandsub/module/pom.xml, but reported exactly 1 dependency (com.root:rootdep); the nestedcom.nested:nesteddepwas silently absent.Suggested fix: introduce a manifest-discovery step that walks the repo (reusing the enumeration walk /
git ls-files, honoring the same.gitignore+--excludesemantics so the two surfaces stay symmetric) and yields every directory containing a known manifest, then runresolve.Detected(dir)+r.Resolve(dir, ...)per discovered root. The natural seam isResolveDepsincmd/license-tool/commands.go:387-396; the resolvers already take a per-manifestpathand need no change. This must touch all three resolver detection paths consistently (Mavenpom.xml, npmpackage.json, Gradlebuild.gradle[.kts]) — like the v0.2.1 universal-shebang fix, the discovery table should enumerate the manifest filenames in one place (mirrorgradleManifests/ theDetectpredicates) rather than special-casing one ecosystem. Deduplicate so the root manifest isn't resolved twice. To keep the 100% gate green, add fixture tests for: a Maven reactor with a nested module, an npm nested package / workspace, and a Gradle subproject, asserting nested deps appear; plus a test that excluded/ignored directories are not descended into.Corrections to the issue:
docs/license-tool-design.md:59(Data-flow step 4, "detect ecosystems by manifest presence"), not:56(which is "Resolve config", step 1). Note also that the doc text itself does not promise recursion — so this is arguably an under-specification in the design doc as much as a code bug; a maintainer may want to clarify intended scope there too.package.json(npm.go:63,72), not the full transitivenode_modulestree. The issue frames the gap purely as "nested manifest roots," but a workspaces repo also hides transitive/sibling-workspace packages. The fix scope should state whether transitive coverage is in or out.resolve.go:31-41,maven.go:71-76,npm.go:56-64,gradle.go:56-60) are all accurate.Open questions:
<modules>in Maven,workspacesin npm,includein Gradle settings)? Whole-repo walk is simpler and matches source enumeration, but may surface vendored/example manifests.--exclude/.gitignoreidentically to source enumeration (recommended for symmetry), and should there be a flag to opt out / cap depth for huge monorepos?Overlaps: none that duplicate this. #22 (Gradle tool tier advertised-but-ignored) and #25 (invalid resolver-tier values silently accepted) also touch
internal/resolve/but are independent defects; a fix here neither depends on nor resolves them. #11 (docs gap) is unrelated.Automated maintainer review.
Resolved on
developin commit742c124e6ec2afa9312075db3112bc2def3c91ff.Validation:
Closing as fixed.