Commit 2df7fe6
feat(scan): subproject & module presentation with per-module manifests (#270)
* feat(output): derive project/subproject/module hierarchy from scan manifests
Adds output.BuildHierarchy/ClassifyManifest: a pure presentation-layer
grouping of scan manifests into project → subproject (independently
discovered nested directory) → module (workspace/reactor member whose
manifest path sits beneath its subproject directory). No schema changes
— the hierarchy derives entirely from the existing manifest subproject
and path fields.
Consumers wired in this commit:
- Text report: renders a grouped manifest tree when a scan spans
subprojects or modules; flat scans keep the compact report. The
"in M manifests" count now uses the real manifest count instead of
graph roots (which undercounts once module roots have inbound edges),
and the misleading "Discovered 1 subproject: . (npm)" banner on plain
root scans is gone (BuildSubprojectSummary removed; the tree
supersedes it).
- Markdown: manifest table grouped by hierarchy location; package
totals and the dependency inventory now dedup shared packages by
PURL/ID instead of double-counting across manifests.
- MCP compact scan: additive subprojects/modules summary counts
(omitted for flat scans; mcp/1 unchanged).
- Progress: resolved-subproject children show a manifest count when a
resolution produced multiple entries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tui): subproject and module nodes in scan and diff views
The scan components tree and manifest list now group manifests using the
derived hierarchy: root-level manifests attach directly under the
project node, with sibling "subproject" nodes (independently discovered
nested directories) and "module" nodes (workspace/reactor members),
each holding their own manifests and components. Flat single-root scans
keep exactly the previous tree shape. Group nodes are string-keyed
("subproject:<dir>", "module:<dir>"), default expanded, and collapsible
like any other node; the tree glyph helper is now ancestors-aware
instead of hardcoding two levels.
Also: project node title carries subproject/module counts; manifest
details rename "Relative path" to "Subproject" and add a "Module dir"
line; the Source tab gains a subprojects section and per-manifest
subproject/module attributes; the diff components tab group-by cycle
gains a "subproject" axis (status → manifest → subproject → ecosystem)
whose details pane lists the manifests the group spans.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(npm,pnpm): emit one manifest per workspace member
Workspace lockfiles now yield one manifest entry per member alongside
the root entry, so views and JSON present each workspace member as a
module with its own manifest path (<member-dir>/package.json) and its
reachable dependency subtree.
- Shared detectors.SubgraphFrom(g, rootID) helper extracts a module
root's reachable subtree from a merged graph (cycle- and
diamond-safe).
- npm: the v2/v3 packages-map parser is now workspace-aware. Member
keys (non-node_modules paths) become application root nodes,
node_modules link entries resolve onto the member node instead of
synthesizing duplicate versionless packages, and member
devDependencies produce edges with development scope (previously all
dropped or mangled).
- pnpm: non-"." importers are wired (previously silently dropped —
member direct-dependency edges never attached), each with its own
application root named from the member package.json; workspace
"link:" version specs resolve to the target importer's root node.
- Non-workspace lockfiles keep exactly one entry (regression-guarded);
consolidation counts a shared transitive dependency once across
module entries (regression test).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(cargo): emit one manifest per workspace member
Cargo workspaces now yield one manifest entry per member
(<member-dir>/Cargo.toml plus the member's reachable dependency
subtree). Single-package projects keep exactly one entry.
- cargo metadata path: partitions the resolved graph by
workspace_members using each package's manifest_path; the synthesized
virtual workspace root never leaks into entries.
- Cargo.lock path (bug fix included): the root Cargo.toml's
[workspace] members are parsed (inline/multiline arrays, glob
expansion) and member-aware resolution kicks in. With cargo on PATH
it prefers `cargo metadata --locked` (deterministic given the lock);
without it, each member's Cargo.toml is parsed and the lock graph is
partitioned by member package names, with member manifest dependency
lists annotating direct-edge scopes. Virtual workspace roots
(workspace-only Cargo.toml) previously failed the lock path with
"cargo.toml does not contain a package name" and fell back to syft —
they now resolve natively.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(maven): emit one manifest per reactor module
Multi-module Maven reactors now yield one manifest entry per module
(<module-dir>/pom.xml plus the module's reachable dependency subtree,
including inter-module dependencies) alongside a root entry for
unmatched graph roots such as the aggregator pom's own node.
`mvn dependency:tree -DoutputType=tgf` output carries no directory
paths, so a new pom walk (walkPomModules, internal/detectors/maven/
modules.go) recursively reads <modules> declarations, resolving each
module pom's groupId:artifactId (groupId falls back to <parent>),
depth-capped and cycle-safe. Graph roots are matched to modules by
coordinates; unmatched roots stay in the root entry so nothing is ever
dropped, and any walk failure degrades to today's single merged
manifest. Per-module pom positions are attached from each module's own
pom.xml.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(smoke): workspace and reactor smoke cases with per-module goldens
Four new pinned fixture repos (all tagged v1.0.0):
- bomly-dev/example-javascript-npm-workspaces — npm workspace: root dep,
two members sharing pinned vulnerable deps, workspace link, member
devDependency. Golden shows apps/web + packages/lib manifest entries
alongside the root lockfile.
- bomly-dev/example-javascript-pnpm-workspaces — same shape through
pnpm-lock.yaml importers.
- bomly-dev/example-rust-cargo-workspace — virtual workspace root (no
[package]) with two members, one depending on the other; exercises the
Cargo.lock partitioning path without a cargo binary and proves virtual
roots resolve natively.
- bomly-dev/example-java-maven-multimodule — pom-packaging parent with
core + web modules, web depending on core; golden shows per-module
entries plus the parent root entry.
Existing single-module goldens (scan-npm, scan-cargo, scan-maven,
scan-recursive-monorepo) verified unchanged against the new detectors.
Also fixes maven module matching to consider all graph nodes, not only
roots — a module consumed by a sibling (web -> core) has inbound edges
and would otherwise lose its manifest entry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: subproject and module presentation
- SCAN_TARGETS: new "Subprojects and modules in scan output" section —
terminology, which detectors emit per-module manifests vs a merged
root manifest, and the JSON derivation rule.
- MODELS: manifests collection carries one entry per workspace/reactor
module; hierarchy derivation documented for JSON consumers.
- MCP: compact summary subprojects/modules counts.
- TUI: components tree grouping (tab label was stale as "Packages").
- Architecture decision log: subproject/module terminology + view-side
derivation; per-module emission lives in detectors (consolidation
stays select/dedup/rebase) with the deferred-detector list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tui): merge project/module nodes with their manifests
A project/module and its manifest are two faces of the same thing, so
presenting them as separate tree levels was confusing. In the scan
components tree and manifest list, a directory holding exactly one
manifest now renders as a single node: labeled with the package's own
name (web, core-lib) plus a directory hint, subtitled by its kind
(project/subproject/module), with the full manifest, detector, and
dependency metadata moved into the details pane. Components nest
directly beneath the merged node — including for flat single-manifest
scans, where the project node absorbs the root manifest.
Directories holding several manifests (e.g. a root with a lockfile and
GitHub Actions workflows) keep explicit manifest child rows for
disambiguation. The component glyph helper gains a subtreeLast
dimension so a merged project's component block draws correct
continuation bars when module nodes follow it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(render): merged node lines and all-module top-level dependencies
Text report: single-manifest groups render as merged nodes named by the
package itself with the manifest path as a hint —
"dev.bomly.example:core (module, maven) — 2 packages [core/pom.xml]" —
matching the interactive tree's merged model.
Top-level dependencies now cover every module: the direct-dependency
classification treats graph roots plus all application-type nodes as
top-level parents, so a module consumed by a sibling (web -> core) no
longer hides its direct dependencies as "transitive". Maven reactor
module roots are typed as applications when matched (TGF output carries
no type), which the npm/pnpm/cargo member roots already were; scan JSON
is unaffected (dependency type is not serialized) and existing goldens
verified unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(render): markdown Name column and project-structure mermaid diagrams
Markdown manifest table gains a Name column carrying the package's own
name per merged node (derived via output.ManifestRootName), matching
the merged project/module presentation of the interactive and text
views.
Architecture docs gain mermaid diagrams making the model explicit:
docs/ARCHITECTURE.md shows how projects discover subprojects, how
workspace/reactor expansion produces modules, and how each manifest's
graph root leads to dependency instances that deduplicate into
PURL-keyed packages; dev-docs/MODELS.md carries the SDK-level variant
with the exact types and the ClassifyManifest derivation rule.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(render): nest modules under their parent manifest in the text tree
- BuildHierarchy resolves which parent manifest natively produced each
module (matched by package manager, falling back to ecosystem, then a
sole parent manifest) via the new AttachedManifest field; hidden
directories (.github/workflows) no longer classify as modules.
- Text tree: modules nest under the manifest that resolves them, and
manifest lines are named after their package with the path bracketed:
"dev.bomly.example:multimodule-parent — 1 package, 2 modules
[pom.xml]" with core/web nested beneath. Proper pluralization.
- Report header: relationship and scope distributions share one line
("(2 direct, 2 transitive · runtime 5, dev 0)"), and the Enriched
line is blank-line separated from the counts block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tui): absorb module roots into merged nodes; overview and target polish
- Components tree: a merged project/module node now stands in for its
graph root — expanding it reveals the root's direct dependencies as a
top-level forest instead of a duplicative "name@version ROOT" row
(the project's own ROOT row is unchanged). Merged node brackets carry
the manifest path; a subproject/module with child modules merges too,
nesting them beneath it before its dependency forest, mirroring the
text tree.
- Overview: the Relationship and Scope Distribution panes label rows by
full relative manifest path — three reactor "pom.xml" rows are now
distinguishable.
- Git URL targets name themselves after the repository (last URL
segment, .git trimmed) instead of the full URL; the URL remains the
descriptor's Path. Applies everywhere the project name shows (TUI
top bar, Target pane, reports).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(render,tui): dependency counts always sum consistently
The report header counted every graph node as a "package" while the
relationship split excluded roots and the scope split counted scoped
nodes only, so "7 packages (2 direct, 2 transitive · runtime 5, dev 0)"
never added up. Project and module nodes are structure, not packages:
- Header total is now direct + transitive (structural nodes — graph
roots and application-type module roots — excluded), and scope counts
cover exactly the same node set with an "unscoped N" bucket shown
when non-zero. total = direct + transitive = runtime + dev
(+ unscoped) by construction.
- Manifest tree lines count dependencies only ("multimodule-parent —
0 packages, 2 modules"; the web module no longer counts the core
module it depends on as one of its packages).
- TUI merged project/module nodes count the components beneath them,
excluding the absorbed root row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tui): modules branch from their parent root; parent-first ordering
- Components tree: module nodes now branch out of the ROOT component
row of the manifest that resolves them (matched per package manager
via the group's attachedTo), instead of hanging off the project node.
The parent root leads the tree and defaults to expanded when it
carries modules, so the reactor/workspace structure is visible at a
glance; collapsing the parent hides its modules. Roots hidden by the
active relationship/scope/severity filters fall back to the flat
forest. The same nesting applies inside multi-manifest groups and the
Manifests tab list.
- Manifest rows sort shortest-path-first, so the parent manifest
(pom.xml) precedes its modules (core/pom.xml, web/pom.xml) in the
Overview Relationship/Scope Distribution panes and every other
manifest listing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tui): project node describes the target; root node owns the manifest
- The PROJECT node's details pane now describes the scan target only:
the target info plus a Manifests section listing everything found
inside it (root manifests with their module nodes nested, subprojects
with theirs). The root manifest's Manifest/Detector/Dependencies
sections no longer leak onto the project node.
- The merged project's ROOT component row owns the root manifest: its
details pane carries the component sections, the full manifest and
detector metadata, and a Modules section listing the module nodes
branching out of it.
- Dropped the inaccurate "Press Enter to view components for this
manifest." hint from manifest details (tree nodes expand with →).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tui): unified details layout for all merged nodes
The project root, module, and subproject nodes are the same thing — an
internal package standing on a manifest — so their details panes now
share one layout (mergedNodeDetails): node identity (name, directory,
components, description), a compact "Root package" section for the
internal application itself (name, PURL, licenses, vulnerability
summary — internal packages are rarely enriched, but a custom plugin
wired to an internal advisory source can attach data, so both surface
when present), the manifest and detector metadata, dependency counts,
and a Modules section for nodes carrying module children.
This replaces the project root's previous details dump, which stacked
the full component sections (dependencies, dependents, vulnerabilities,
licenses) on top of the manifest sections.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent b8cf529 commit 2df7fe6
57 files changed
Lines changed: 5456 additions & 298 deletions
File tree
- dev-docs
- docs
- internal
- cli
- opts
- render
- detectors
- cargo
- maven
- node
- npm
- pnpm
- testdata/lockfiles
- npm-v3-workspaces
- pnpm-v9-workspaces
- apps/web
- packages/lib
- engine/consolidation
- mcp
- output
- tui
- test/smoke
- testdata/golden
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
135 | 143 | | |
136 | 144 | | |
137 | 145 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
29 | 69 | | |
30 | 70 | | |
31 | 71 | | |
| |||
205 | 245 | | |
206 | 246 | | |
207 | 247 | | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
208 | 259 | | |
209 | 260 | | |
210 | 261 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
67 | 90 | | |
68 | 91 | | |
69 | 92 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
111 | | - | |
| 111 | + | |
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
80 | 91 | | |
81 | 92 | | |
82 | 93 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
23 | | - | |
| 23 | + | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
| 32 | + | |
31 | 33 | | |
32 | 34 | | |
33 | 35 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
59 | 65 | | |
60 | 66 | | |
61 | 67 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
457 | 457 | | |
458 | 458 | | |
459 | 459 | | |
460 | | - | |
| 460 | + | |
461 | 461 | | |
462 | 462 | | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
463 | 466 | | |
464 | 467 | | |
465 | 468 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
| 55 | + | |
| 56 | + | |
55 | 57 | | |
56 | 58 | | |
57 | | - | |
| 59 | + | |
58 | 60 | | |
59 | 61 | | |
60 | 62 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
44 | | - | |
| 44 | + | |
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
| |||
218 | 218 | | |
219 | 219 | | |
220 | 220 | | |
221 | | - | |
| 221 | + | |
222 | 222 | | |
223 | 223 | | |
224 | 224 | | |
| |||
0 commit comments