Skip to content

feat!: adopt the typed graph node union (bomly-sdk v0.9.0, ADR-0041) - #423

Merged
bomly-guy merged 33 commits into
mainfrom
claude/cli-2.1-2.3-adoption
Sep 5, 2026
Merged

feat!: adopt the typed graph node union (bomly-sdk v0.9.0, ADR-0041)#423
bomly-guy merged 33 commits into
mainfrom
claude/cli-2.1-2.3-adoption

Conversation

@bomly-guy

@bomly-guy bomly-guy commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Adopts bomly-dev/bomly-sdk v0.9.0 across the CLI: the dependency graph is a sealed union of manifest, module, and dependency nodes, and a node's identity is a canonical package URL minted at construction (ADR-0041). Also bumps the nine bomly-plugin-* modules to v0.2.0.

This is phase 2.1–2.3 of the SDK maturity program. The SDK side is released; this is the consumer.

What changes in the pipeline

The project's own artifacts are module nodes. Every detector that used to mint a dependency node and then mark it — FirstParty on coordinates, a forced workspace:<dir> ID, DependencySourceWorkspace as a stand-in — now builds a module node whose ID carries the declaring manifest path. Ownership is the node kind; there is nothing to set afterwards. This covers npm, pnpm, yarn, bun, cargo (both the lock and metadata paths), maven, gradle, and the Python detectors.

Duplicate identities fold. The occurrence machinery is gone. Two records that resolved one name@version from different places become one node whose Origins list carries both — a stronger dependency-confusion signal than two nodes with byte-identical identity. The Cargo case (one crate from two git remotes) is the one that made this worth checking rather than assuming; the reasoning is recorded next to the fold, in internal/detectors/cargo/lock_index.go.

Application-typed imports are ordinary dependency nodes, so they diff and match like any other package (ADR-0015).

The stopgaps are gone

An earlier revision of this branch carried three CLI-local helpers with SDK issues filed against them. v0.9.0 shipped all three, so the helpers are deleted and the call sites point at the model:

was is
internal/nodes sdk.NodeCoordinates / NodeDisplayName / NodeVersion / AsDependencyNode / DependencyNodesOf / IsProjectOwned
detectors.EnsureNode, PromoteToModule, PropagateScopes detectorkit.*
detectors.NewDependencyFrom sdk.NewDependencyNodeFrom
detectors.NewDependencyOrGeneric sdk.NewDependencyNode (the generic fallback is the constructor's, and records a warning)
detectors.RefineOrigins sdk.MergeOrigins (which drops a superseded origin itself)

internal/testnodes keeps only what is CLI test ergonomics — fixture builders that panic rather than take a testing.TB, so a table entry stays one expression. Its label lookups delegate to bomly-sdk/testkit, and its copy of the matching rules is gone: two answers to "which node is this" is how they drifted the first time.

The two structural guards stay CLI-side, in internal/detectors/guards_test.go, because they police this tree. TestNodeInsertionGoesThroughTheSharedHelper now names detectorkit.EnsureNode and exempts nothing — there is no local copy left to reach for.

Version case is no longer folded

The SDK used to lowercase every version containing a letter. That corrupted Maven's 1.0-SNAPSHOT into 1.0-snapshot — a different version, Maven versions being case sensitive. v0.9.0 delegates version spelling to packageurl-go, which case-folds exactly one type (huggingface). The Maven, Gradle and reachability goldens show the correction: 3.7.0.Final, 3.1.1.RELEASE, 1.0-SNAPSHOT.

It costs an incidental PyPI fold, since PEP 440 does normalize version case and packageurl-go does not implement it. That is bomly-dev/bomly-sdk#39, and TestPythonVersionCaseIsNotFoldedYet pins the current behaviour so the change is visible rather than silent.

Review findings fixed

Fifteen inline findings, each verified against the behaviour before being believed, and each with the assertion that was missing. The ones that mattered most:

  • Remediation stopped recognising project roots. executableRootOf narrowed to *DependencyNode, so a module root — what a normal graph now has — was rejected, inferredPlacement found no root, and every dependency whose detector left Relationship unset degraded to manual review. That is most of a normal scan. The reachability goldens show the fix across three ecosystems: manual-review becomes direct-bump and transitive-override.
  • Recursive scans folded same-named nested projects. A module's ID carries its declaring manifest path, written relative to the detector's own working directory, so two nested projects sharing a package name both minted module:package.json#pkg:npm/app@1.0.0 and merged into one node holding both projects' edges. Rebased in consolidation, where the subproject path is already known.
  • Explain paths could not be joined to anything. Path packages published name@version while the focused dependency published its canonical node ID.
  • Exported SBOMs published only the first origin, discarding the dependency-confusion signal at the export boundary. CycloneDX emits one external reference per origin; SPDX, which defines a single download location, emits the rest as OTHER-category references.
  • A manifest root decided the document's primary component, so a project with two top-level packages described itself as one of them.
  • The pre-push hook accepted a stale stamp on Linux, GNU stat -f meaning "filesystem" and succeeding.

Verification

  • make verify SMOKE=1 green: fmt, lint, vet on every build-tag variant, go test ./..., generated-doc drift check, and the full networked smoke suite.
  • Smoke goldens are regenerated in this PR and the diff assessed. Three classes of change: the Maven/Gradle version-case correction, the review fixes above, and two upstream drifts unrelated to this branch — a new npm advisory (GHSA-4mjr-xmp4-gh2g) and a SwiftPM transitive resolving 1.6.0 → 1.7.0.
  • Every new test mutation-checked: each fails when the behaviour it claims to pin is broken.
  • make verify now compiles the smoke suite. It is behind a build tag, so go vet ./... never saw it, and this branch pushed a smoke file that did not compile.

Smoke coverage

Re-assessed against the feature surface. Two gaps found; one closed here:

  • SARIF had no smoke coverage at all, though it is what GitHub code scanning reads and how Guard annotates a pull request. The new case pins a real rule, result, severity and repo-relative location. It is driven by a denied package rather than advisory data: the shared mock OSV server answers every query with no vulnerabilities, so a vulnerability-auditor case would have pinned an empty document and caught nothing.
  • MCP has no end-to-end case. The compact mcp/1 projection has 49 unit tests, so the gap is the binary-driven path — server startup, tool registration, stdio protocol — which needs a JSON-RPC harness. Left out deliberately rather than rushed in here.

Open against the SDK

Not blockers for this PR; filed with reproductions:

Phases 2.4–2.8 (document assertions to the codec, strict json/v2 ingest, full scope-set export, helper consolidation, usage attribution) follow as separate PRs.

🤖 Generated with Claude Code

bomly-sdk v0.8.0 makes the dependency graph a sealed union of manifest,
module, and dependency nodes, with identity minted as a canonical package
URL at construction (ADR-0041). This is the CLI's adoption of that model.

What changes in the pipeline:

- The project's own artifacts are module nodes. Every detector that used to
  mint a dependency node and then mark it -- FirstParty on coordinates, a
  forced "workspace:<dir>" ID, DependencySourceWorkspace as a stand-in --
  now builds a module node whose ID carries the declaring manifest path.
  Ownership is the node kind; there is nothing to set afterwards.
- Duplicate identities fold. The occurrence machinery is gone: two records
  that resolved one name@version from different places become one node whose
  Origins list carries both, which is a stronger dependency-confusion signal
  than two nodes with byte-identical identity.
- Application-typed imports are ordinary dependency nodes, so they diff and
  match like any other package (ADR-0015).

Three helpers absorb what the migration would otherwise have scattered:

- internal/nodes reads a node of any kind -- coordinates, display fields,
  narrowing. The GraphNode interface exposes only what every kind has, so
  every renderer needed the same type switch; written per caller it
  disagrees with itself about what a manifest looks like. The SDK is the
  deeper home (ADR-0040) and v0.8.0 has no coordinates accessor, so this
  delegates once bomly-dev/bomly-sdk#33 ships.
- detectors.EnsureNode is generic in the node type, so inserting a module
  returns a module. A survivor of another kind is an error rather than a
  silent nil.
- detectors.PropagateScopes replaces three copies of the same BFS scope
  walk, one per Python lockfile detector. Only the seed differed, so that is
  the parameter.

Tests are migrated in the following commit.
Fixtures can no longer be struct literals with a hand-chosen ID, and graph
traversal yields the union rather than dependency nodes, so every test that
built or read a node had to change. Two helpers keep that from becoming a
type switch per assertion:

- internal/testnodes takes the fixture shapes the tests already used and
  routes them through the real constructors, panicking rather than returning
  an error -- a fixture whose coordinates cannot mint an identity is a broken
  test, not a condition under test.
- a package-local mustDep narrows a node the case is asserting about, failing
  instead of panicking when the graph holds another kind.

Three suites tested behaviour that no longer exists and were rewritten rather
than adjusted, because adjusting them would have left tests that pass while
pinning nothing:

- The cargo dual-source cases asserted two nodes for one crate resolved from
  two remotes. They now assert the fold and both origins on the survivor,
  with the reasoning recorded in detectors.EnsureNode.
- The consolidation origin suite was almost entirely about occurrence-ID
  minting and its order-independence. It now covers what folding must not
  lose: both resolutions, scopes, locations, the stronger relationship, and
  the separation between a project module and an external package that names
  the same coordinates.
- detectors.EnsureOccurrence's test became a fold test over the same inputs.

Node IDs in assertions are canonical package URLs now; lookups that keyed on
the old "name@version" string either use the PURL or find the node by name.
…ey set

The npm, pnpm, yarn, and bun lockfile parsers each described a package as one
struct literal and passed only its coordinates to the constructor, so
ResolvedURL, Source, the integrity digests, and the npm metadata stopped
reaching the graph -- silently, in four places, for the same reason.
detectors.NewDependencyFrom is that copy written once.
Two user-visible losses the union adoption would otherwise have shipped:

- A dependency path rendered only its dependency nodes, so every explain
  path started at the first consumed package and never said which module
  pulled it in -- which is the question explain answers. Structural nodes
  render too now, through output.PackageFromGraphNode.
- The scan document's manifest listing walked dependency nodes only, so
  every depends_on chain was headless. Modules are listed alongside
  packages; manifests are not, being what the listing is about.

Also: explain resolves a target by its ecosystem-native name as well as its
bare one. Normalization splits a qualified name into org and name, so
"bomly explain golang.org/x/text" stopped matching anything.
…url fork

go mod tidy demotes github.com/anchore/packageurl-go to an indirect
dependency: nothing in this repository imports it any more, which is the
removal ADR-0041 slated. purlkit over the official packageurl-go is the
parser now.
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 211 files, which is 111 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 60399655-60d2-4bb5-ad64-cd879fbc9466

📥 Commits

Reviewing files that changed from the base of the PR and between 79b38c9 and c357d4a.

⛔ Files ignored due to path filters (69)
  • docs/schemas/diff.md is excluded by !docs/schemas/**
  • docs/schemas/diff.schema.json is excluded by !docs/schemas/**
  • docs/schemas/explain.md is excluded by !docs/schemas/**
  • docs/schemas/explain.schema.json is excluded by !docs/schemas/**
  • docs/schemas/scan.md is excluded by !docs/schemas/**
  • docs/schemas/scan.schema.json is excluded by !docs/schemas/**
  • go.sum is excluded by !**/*.sum
  • test/smoke/testdata/golden/audit-sarif.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/container-diff-alpine.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/container-explain-alpine.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/container-scan-alpine-audit.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/container-scan-alpine.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/container-scan-debian.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/diff-go-audit.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/diff-go.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/diff-npm.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/diff-sbom-detail-change.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/diff-sbom.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/explain-go-enrich.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/explain-go.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/finding-baseline-workflow.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/lite-diff-go.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/lite-explain-go.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/lite-scan-go.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/lite-scan-sbom-cyclonedx.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/lite-scan-sbom-spdx.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/plugin-scan-archive.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/plugin-scan-dev.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/sbom-export-cyclonedx.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/sbom-export-spdx.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-bun.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-bundler.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-cargo-workspace.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-cargo.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-cocoapods.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-composer.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-cpp-conan.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-github-actions.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-go-audit-high.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-go-audit.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-go-enrich.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-go-reachability.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-go.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-gradle-multimodule.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-gradle.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-java-maven-reachability.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-maven-multimodule.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-maven.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-mix.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-npm-audit.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-npm-reachability.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-npm-scope-runtime.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-npm-workspaces.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-npm.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-nuget.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-pnpm-workspaces.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-pnpm.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-pub.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-python-pip-reachability.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-python-pip.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-python-pipenv.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-python-poetry.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-python-uv.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-recursive-monorepo.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-sbom-cyclonedx.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-sbom-spdx.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-sbt.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-swiftpm.golden.json is excluded by !**/*.golden.json, !**/testdata/**
  • test/smoke/testdata/golden/scan-yarn.golden.json is excluded by !**/*.golden.json, !**/testdata/**
📒 Files selected for processing (211)
  • .githooks/pre-push
  • .github/workflows/smoke.yml
  • .github/workflows/update-smoke-goldens.yml
  • .gitignore
  • AGENTS.md
  • CLAUDE.md
  • CONTRIBUTING.md
  • Makefile
  • go.mod
  • internal/auditors/license/auditor.go
  • internal/auditors/license/auditor_test.go
  • internal/auditors/license/spdx_policy_matrix_test.go
  • internal/auditors/package/auditor.go
  • internal/auditors/package/auditor_test.go
  • internal/auditors/vulnerability/auditor.go
  • internal/auditors/vulnerability/auditor_test.go
  • internal/auditors/vulnerability/policy_matrix_test.go
  • internal/baseline/identity_round_trip_test.go
  • internal/benchmark/run.go
  • internal/benchmark/run_test.go
  • internal/benchmark/summary.go
  • internal/cli/explain_cmd.go
  • internal/cli/mcp_cmd.go
  • internal/cli/render/reachability_test.go
  • internal/cli/render/remediation_projection_test.go
  • internal/cli/render/scan.go
  • internal/cli/render/scan_warnings_test.go
  • internal/cli/root_cmd_test.go
  • internal/cli/scan_cmd_test.go
  • internal/cli/scan_output.go
  • internal/cli/scan_output_test.go
  • internal/detectors/cargo/detector.go
  • internal/detectors/cargo/detector_test.go
  • internal/detectors/cargo/identity_test.go
  • internal/detectors/cargo/lock_index.go
  • internal/detectors/cargo/origin.go
  • internal/detectors/cargo/origin_test.go
  • internal/detectors/cargo/positions.go
  • internal/detectors/cargo/workspace.go
  • internal/detectors/cargo/workspace_test.go
  • internal/detectors/cocoapods/detector.go
  • internal/detectors/cocoapods/detector_test.go
  • internal/detectors/cocoapods/positions.go
  • internal/detectors/composer/detector.go
  • internal/detectors/composer/detector_test.go
  • internal/detectors/composer/positions.go
  • internal/detectors/conan/detector.go
  • internal/detectors/conan/detector_test.go
  • internal/detectors/conan/positions.go
  • internal/detectors/ensure_node.go
  • internal/detectors/ensure_node_test.go
  • internal/detectors/githubactions/detector.go
  • internal/detectors/githubactions/detector_test.go
  • internal/detectors/githubactions/positions.go
  • internal/detectors/gomod/detector.go
  • internal/detectors/gomod/detector_test.go
  • internal/detectors/gomod/fixture_test.go
  • internal/detectors/gradle/detector.go
  • internal/detectors/gradle/detector_test.go
  • internal/detectors/gradle/fixture_test.go
  • internal/detectors/gradle/positions.go
  • internal/detectors/guards_test.go
  • internal/detectors/maven/detector.go
  • internal/detectors/maven/detector_test.go
  • internal/detectors/maven/fixture_test.go
  • internal/detectors/maven/modules.go
  • internal/detectors/maven/modules_test.go
  • internal/detectors/maven/positions.go
  • internal/detectors/mix/detector.go
  • internal/detectors/mix/detector_test.go
  • internal/detectors/mix/positions.go
  • internal/detectors/node/bun/bun_lockfile_parser.go
  • internal/detectors/node/bun/bun_lockfile_parser_test.go
  • internal/detectors/node/bun/bun_native_parser.go
  • internal/detectors/node/bun/bun_native_test.go
  • internal/detectors/node/common.go
  • internal/detectors/node/common_test.go
  • internal/detectors/node/graph_relationships.go
  • internal/detectors/node/graph_relationships_test.go
  • internal/detectors/node/lockfile_integration_test.go
  • internal/detectors/node/nodetest/fuzz.go
  • internal/detectors/node/npm/npm_lockfile_parser.go
  • internal/detectors/node/npm/npm_lockfile_parser_test.go
  • internal/detectors/node/npm/npm_lockfile_workspaces_test.go
  • internal/detectors/node/npm/origin_test.go
  • internal/detectors/node/npm/positions.go
  • internal/detectors/node/origin_integration_test.go
  • internal/detectors/node/pnpm/pnpm_lockfile_parser.go
  • internal/detectors/node/pnpm/pnpm_lockfile_parser_test.go
  • internal/detectors/node/pnpm/pnpm_lockfile_workspaces_test.go
  • internal/detectors/node/pnpm/positions.go
  • internal/detectors/node/yarn/positions.go
  • internal/detectors/node/yarn/yarn_lockfile_parser.go
  • internal/detectors/node/yarn/yarn_lockfile_parser_test.go
  • internal/detectors/nuget/detector.go
  • internal/detectors/nuget/detector_test.go
  • internal/detectors/nuget/positions.go
  • internal/detectors/positions_extractors_test.go
  • internal/detectors/pub/detector.go
  • internal/detectors/pub/detector_test.go
  • internal/detectors/pub/origin_test.go
  • internal/detectors/pub/positions.go
  • internal/detectors/pub/pub_native.go
  • internal/detectors/python/common.go
  • internal/detectors/python/detector_test.go
  • internal/detectors/python/lockfile_integration_test.go
  • internal/detectors/python/origin.go
  • internal/detectors/python/origin_test.go
  • internal/detectors/python/pip.go
  • internal/detectors/python/pip_resolution_test.go
  • internal/detectors/python/pipenv.go
  • internal/detectors/python/piplock.go
  • internal/detectors/python/piplock_test.go
  • internal/detectors/python/poetry.go
  • internal/detectors/python/poetrylock.go
  • internal/detectors/python/positions_loose.go
  • internal/detectors/python/roots_guard_test.go
  • internal/detectors/python/uv.go
  • internal/detectors/python/uvlock.go
  • internal/detectors/ruby/detector.go
  • internal/detectors/ruby/detector_test.go
  • internal/detectors/ruby/origin_test.go
  • internal/detectors/ruby/positions.go
  • internal/detectors/sbom/detector.go
  • internal/detectors/sbom/detector_test.go
  • internal/detectors/sbt/detector.go
  • internal/detectors/sbt/detector_test.go
  • internal/detectors/sbt/positions.go
  • internal/detectors/sbt/sbt_native.go
  • internal/detectors/swiftpm/detector.go
  • internal/detectors/swiftpm/detector_test.go
  • internal/detectors/swiftpm/origin_test.go
  • internal/detectors/swiftpm/positions.go
  • internal/detectors/swiftpm/swiftpm_native.go
  • internal/engine/consolidation/consolidation.go
  • internal/engine/consolidation/consolidation_fallback_test.go
  • internal/engine/consolidation/consolidation_test.go
  • internal/engine/consolidation/enrichment.go
  • internal/engine/consolidation/enrichment_test.go
  • internal/engine/consolidation/locations.go
  • internal/engine/consolidation/locations_test.go
  • internal/engine/consolidation/manifest.go
  • internal/engine/consolidation/modules.go
  • internal/engine/consolidation/modules_test.go
  • internal/engine/consolidation/origin_test.go
  • internal/engine/diff/diff.go
  • internal/engine/diff/diff_test.go
  • internal/engine/engine_test.go
  • internal/engine/explain/why.go
  • internal/engine/explain/why_test.go
  • internal/engine/graph_accounting_invariants_test.go
  • internal/engine/match_eligibility.go
  • internal/engine/match_eligibility_test.go
  • internal/engine/pipeline.go
  • internal/engine/pipeline_explain.go
  • internal/engine/pipeline_fallback_test.go
  • internal/engine/pipeline_logger_test.go
  • internal/engine/pipeline_network_intent_test.go
  • internal/engine/pipeline_resolve.go
  • internal/engine/pipeline_test.go
  • internal/graphview/graphview.go
  • internal/mcp/compact_limits_test.go
  • internal/mcp/remediation.go
  • internal/mcp/remediation_test.go
  • internal/output/dependencies_graph_test.go
  • internal/output/hierarchy_test.go
  • internal/output/output_test.go
  • internal/output/sarif.go
  • internal/output/sarif_test.go
  • internal/output/types.go
  • internal/output/view.go
  • internal/output/view_projection_test.go
  • internal/output/view_test.go
  • internal/plugin/github_release_test.go
  • internal/plugin/plugin_test.go
  • internal/registry/remediation_ownership_test.go
  • internal/remediation/derive.go
  • internal/remediation/derive_test.go
  • internal/sbom/cyclonedx.go
  • internal/sbom/enrichment_test.go
  • internal/sbom/export_quality_test.go
  • internal/sbom/graph.go
  • internal/sbom/graph_test.go
  • internal/sbom/identity.go
  • internal/sbom/license_emission_test.go
  • internal/sbom/model.go
  • internal/sbom/origin_test.go
  • internal/sbom/primary_component_test.go
  • internal/sbom/sbom_test.go
  • internal/sbom/spdx23.go
  • internal/sbom/transform.go
  • internal/testnodes/boundary_test.go
  • internal/testnodes/testnodes.go
  • internal/tui/diff.go
  • internal/tui/diff_aggregations_test.go
  • internal/tui/focus_test.go
  • internal/tui/posture.go
  • internal/tui/posture_test.go
  • internal/tui/scan.go
  • internal/tui/scan_hierarchy.go
  • internal/tui/scan_test.go
  • internal/tui/tui.go
  • internal/tui/tui_test.go
  • internal/tui/utils.go
  • scripts/verify-snapshot.sh
  • test/smoke/fixture_compile_test.go
  • test/smoke/helpers_test.go
  • test/smoke/plugin_minversion_test.go
  • test/smoke/plugin_test.go
  • test/smoke/sarif_golden_test.go
  • test/smoke/sbom_export_golden_test.go

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread go.mod Fixed
Comment thread go.mod Fixed
Comment thread go.mod Fixed
Comment thread go.mod Fixed
Comment thread go.mod Fixed
Comment thread go.mod Fixed
Comment thread go.mod Fixed
Comment thread go.mod Fixed
Comment thread go.mod Fixed
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Bomly Diff Summary

Compared 79b38c9d259554b1821ce4686d529d70f42cce73 to c357d4a27ba93cd62b695d2dcd1b532ab4a851c6.

Overview

Status Manifests Dependencies Findings Duration
✅ Pass +0 / ~1 / -0 0 added / 11 version changed / 1 detail changes / 0 removed 0 introduced / 0 persisted / 0 resolved 1m 23s

Dependency Changes

Summary: 0 added, 11 version changed, 1 detail change, 0 removed.

Changed Dependencies

Change Package Version Direct? Scope Licenses
changed github.com/bomly-dev/bomly-plugin-depsdev-license-matcher v0.1.0 → v0.2.0 Yes runtime Apache-2.0
changed github.com/bomly-dev/bomly-plugin-govulncheck-analyzer v0.1.0 → v0.2.0 Yes runtime Apache-2.0
changed github.com/bomly-dev/bomly-plugin-grype-matcher v0.1.0 → v0.2.0 Yes runtime Apache-2.0
changed github.com/bomly-dev/bomly-plugin-jsreach-analyzer v0.1.0 → v0.2.0 Yes runtime Apache-2.0
changed github.com/bomly-dev/bomly-plugin-jvmreach-analyzer v0.1.0 → v0.2.0 Yes runtime Apache-2.0
changed github.com/bomly-dev/bomly-plugin-osv-matcher v0.1.0 → v0.2.0 Yes runtime Apache-2.0
changed github.com/bomly-dev/bomly-plugin-pyreach-analyzer v0.1.0 → v0.2.0 Yes runtime Apache-2.0
changed github.com/bomly-dev/bomly-plugin-scorecard-matcher v0.1.0 → v0.2.0 Yes runtime Apache-2.0
changed github.com/bomly-dev/bomly-plugin-syft-detector v0.1.0 → v0.2.0 Yes runtime Apache-2.0
changed github.com/bomly-dev/bomly-sdk v0.4.2 → v0.9.0 Yes runtime Apache-2.0
changed github.com/package-url/packageurl-go v0.1.5 → v0.1.7 No runtime MIT

Dependency Detail Changes

Review: 0 of 1 detail change needs extra review.

Attention Package Version Changes
Info github.com/anchore/packageurl-go v0.2.0 relationship: direct → transitive

Vulnerabilities

✅ No vulnerability changes.

License Changes

✅ No license changes.

Project Posture

✅ No project posture changes (--matchers +scorecard was not selected).

Policy Findings

✅ No policy differences were identified.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 662568440f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/sbom/transform.go Outdated
Comment thread internal/remediation/derive.go
Comment thread internal/cli/render/scan.go Outdated
Comment thread internal/sbom/transform.go Outdated
Five ineffectual assignments and one empty branch, each a leftover from a
rewrite rather than a style nit -- and one was a swallowed error: cargo's
root construction returned an err the next if-statement shadowed, so a
node that failed to build would have been added as nil.

The consolidation helper that searched for an uncolliding synthesized root
ID is deleted with them: a manifest node's ID is minted from its path by a
grammar that cannot collide with a package URL or a module ID.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 920b2f536d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/detectors/node/npm/npm_lockfile_parser.go
Comment thread internal/engine/consolidation/manifest.go Outdated
Comment thread internal/engine/consolidation/locations.go Outdated
Comment thread internal/engine/pipeline.go
The migration inserted imports at the top of each block, so the SDK and
internal packages landed among the standard library. gofmt sorts within a
group but never regroups, and nothing in .golangci.yml checks it.
Comment thread internal/remediation/derive.go Outdated
Comment thread internal/sbom/transform.go Outdated
Comment thread internal/cli/scan_output.go
Regenerating locally surfaced three ways the goldens recorded the machine
that produced them rather than anything Bomly decided. Each would have made
a local regeneration break CI, and each was already latent:

- Container scans baked in the runner's architecture. A multi-arch image
  resolves to the host, so amd64 on CI and arm64 on a laptop differ on every
  package in the image. Normalized to <arch>, with the alternation listing
  both spellings a package manager uses for one machine (amd64/x86_64) so a
  genuinely wrong arch still shows up.
- Absolute temp paths leaked through the reachability evidence added in
  bomly-sdk v0.8.0: module_root carried /var/folders/... on macOS and /tmp
  on Linux. The random suffix was normalized; the prefix was not.
- Strings inside arrays were never normalized at all -- only map values were
  -- so a package URL in a depends_on list kept the host arch while the same
  URL as a map value did not.

And one that hid real coverage: the golden filter dropped every entry whose
ID was not a package URL. That was written when synthetic roots had ad-hoc
IDs; every node ID is minted by a grammar now, so the filter was silently
removing every module and manifest node -- the project's own code, which is
exactly what a workspace or reactor-build case exists to check.

The min-version plugin fixture gets its own source. That test builds the
example plugin against the oldest SDK release whose binaries must keep
loading, and the guarantee is about the wire, not the source API: the node
constructors replaced sdk.NewDependency, so one source cannot compile
against both v0.1.0 and the pin. Both sources are now compile-checked
against the release each targets.
Adding a golden for the exported SBOM found this on the first run: a module
component carried module:apps/web/package.json#pkg:npm/web@1.0.0 in the
purl field, which both formats define as a Package URL and no consumer can
parse. The bom-ref is where the node identity belongs; the purl is the one
its coordinates mint.

The export had assertions but no golden -- it is the artifact most consumers
actually read, and nothing pinned its shape, so a change to which components
appear at all went unchecked. Both formats are now exported from one scan and
compared, so they cannot drift apart.

Documents get their own golden path. normalizeJSON knows the scan/diff/explain
response schema and edits it on sight -- it zeroes metadata.duration_ms, and a
CycloneDX document has a metadata object too -- so running it over an SBOM
invented a field neither side produced.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 28414831b7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .githooks/pre-push Outdated
Comment thread test/smoke/sbom_export_golden_test.go
Comment thread test/smoke/sbom_export_golden_test.go
Comment thread internal/sbom/transform.go Outdated
bomly-guy and others added 3 commits September 3, 2026 23:49
bomly-sdk v0.9.0 ships what this branch had been carrying locally, so the
stopgaps go and the call sites point at the model.

Deleted, each replaced by its SDK counterpart:

  internal/nodes                    -> sdk.NodeCoordinates, NodeDisplayName,
                                       NodeVersion, AsDependencyNode,
                                       DependencyNodesOf, IsProjectOwned
  detectors.EnsureNode              -> detectorkit.EnsureNode
  detectors.PromoteToModule         -> detectorkit.PromoteToModule
  detectors.PropagateScopes         -> detectorkit.PropagateScopes
  detectors.NewDependencyFrom       -> sdk.NewDependencyNodeFrom
  detectors.NewDependencyOrGeneric  -> sdk.NewDependencyNode (the generic
                                       fallback is the constructor's now)
  detectors.RefineOrigins           -> sdk.MergeOrigins (which drops a
                                       superseded origin itself)

internal/testnodes keeps only what is CLI test ergonomics -- fixture builders
that panic instead of taking a testing.TB, so a table entry stays one
expression. Its label lookups delegate to bomly-sdk/testkit, and its copy of
the matching rules is gone: two answers to "which node is this" is how the
version fold drifted in the first place. Its hand-maintained field list goes
too, DepFrom now being NewDependencyNodeFrom.

The two structural guards move to internal/detectors/guards_test.go and stay
CLI-side, because they police this tree:
TestNodeInsertionGoesThroughTheSharedHelper now names detectorkit.EnsureNode
and exempts nothing, there being no local copy left to reach for.

The Cargo fold decision moves to internal/detectors/cargo/lock_index.go, next
to the fold it explains, rather than being deleted with the helper it was
written above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seven findings from review, each verified against the behavior before being
believed, and each with the assertion that was missing:

Remediation stopped recognizing project roots. executableRootOf narrowed to
*DependencyNode, so a module root -- what a normal graph now has -- was
rejected before executableRoot ran. inferredPlacement then found no root and
every dependency whose detector left Relationship unset degraded to manual
review, which is most of a normal scan. executableRoot's own module clause was
dead for the same reason and is gone.

The provider graph clone kept only dependency nodes, dropping the modules,
manifests and root edges, and ignored AddEdge errors. Providers may inspect
RemediationHintRequest.Detection, so a disconnected clone changes the plugin
contract. It now clones every kind through CopyEdgesInto, edge kinds included.

Explain paths published "name@version" while the focused dependency published
its canonical node ID, so nothing could join a path entry to a dependency,
package, or finding -- and the short form collides across ecosystems. The
legacy rewrite is deleted and the function renamed for what it still does.

A module component published no group: componentOrg parsed NodeID, which for a
module is the structural "module:<path>#<purl>" grammar and no package URL at
all. It reads componentPURL now, so group and purl agree.

A manifest root decided the document's primary component. The manifest is not
a component, so the encoder silently promoted whichever package sorted first
to be the subject of the whole document -- a project with two top-level
packages described itself as one of them. Structural roots now resolve to the
exported nodes beneath them, which lets the synthesized project root form.

Module locations were never rebased onto the subproject path, so a recursively
discovered module reported "pom.xml" where the repository holds
"apps/service/pom.xml", and scan JSON published it.

The enrichment INFO log counted every structural node as an excluded package,
contradicting the comment above it.

Also: make verify now compiles the smoke suite. It is behind a build tag, so
`go vet ./...` never saw it, and this branch pushed a smoke file that did not
compile. Running smoke needs the network; compiling it costs a second.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rest of the review findings, plus the smoke coverage they exposed.

Exported SBOMs published only the first origin. ADR-0041 folds equal-identity
records and keeps their disagreement as a list, which is the
dependency-confusion signal the fold exists to preserve -- so publishing one
described a package that resolved from two registries as though it came from
one, backwards for the case that matters most. Component carries the whole
list now: CycloneDX emits an external reference per origin, and SPDX, which
defines one download location per package, emits the rest as OTHER-category
references under a document-defined type. The SPDX category and refType
strings are the library's constants rather than transcriptions of them.

Module identities were never rebased onto the subproject path. A module's ID
carries its declaring manifest path, and a detector writes that relative to
its own working directory, so with --recursive two nested projects sharing a
package name both minted "module:package.json#pkg:npm/app@1.0.0" and folded
into one node holding both projects' edges. Consolidation rebases them where
it already rebases manifest paths and locations; the monorepo smoke golden now
shows "module:fixtures/webapp/package.json#...".

ensureEntryRoot attached only dependency roots, leaving an independent module
or manifest root loose -- so the entry still had several roots and every
root-based projection downstream got an ambiguous graph, which is the
condition that function exists to remove.

Non-root module nodes were missing from the top-level parents, so a module
another module depends on had its own direct dependencies reported as
transitive. The existing test missed it by building its "modules" as
application-typed dependency nodes; the new one uses real module nodes and
fails without the fix.

The pre-push hook accepted a stale stamp on Linux. GNU stat reads -f as
"filesystem" and succeeds, so choosing the BSD spelling by "did stdout come
back non-empty" filled newest with an inode report, and the comparison then
errored inside a condition the hook did not check. It probes the flag now.

Smoke: the SBOM export case ran against a mutable branch, so its golden
tracked the fixture repository rather than Bomly -- pinned to the tag, like
its sibling. The CycloneDX tool version is a separate field the string
normalizer could not reach, so the golden carried a literal release number
that would fail on the next bump.

SARIF had no smoke coverage at all, though it is what GitHub code scanning
reads and how Guard annotates a pull request. The new case pins a real rule,
result, severity and repo-relative location, driven by a denied package rather
than advisory data: the shared mock OSV server answers every query with no
vulnerabilities, so a vulnerability-auditor case would have pinned an empty
document and caught nothing.

Goldens regenerated. The reachability goldens show the remediation fix
end-to-end -- manual-review becomes direct-bump and transitive-override across
Go, npm and Maven -- and explain paths now publish canonical package URLs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bomly-guy bomly-guy changed the title feat!: adopt the typed graph node union (bomly-sdk v0.8.0, ADR-0041) feat!: adopt the typed graph node union (bomly-sdk v0.9.0, ADR-0041) Sep 4, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75f4889bb4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/detectors/python/pipenv.go Outdated
Comment thread internal/tui/scan.go Outdated
…r real PURL

Two findings from Codex's review of 75f4889.

Four Python parsers each hard-coded "requirements.txt" as the module's
declaring manifest. A Pipenv project is declared by Pipfile and a Poetry
project by pyproject.toml, and the declaring path is part of a module's
identity now -- published in scan JSON, SBOM references and explain paths --
so the literal was both a wrong cross-reference and a fold risk between two
projects declared by different files on matching coordinates.

The rule gets one home, pythonModuleRoot, which derives the manifest from the
package manager already on the coordinates, and
TestPythonRootsGoThroughTheSharedConstructor fails if a direct NewModuleNode
call reappears in the package. Routing uv through it exposed that its
coordinates never stated their package manager -- it had the right literal by
hand -- so it states it now and keeps pyproject.toml. pip and the pip-inspect
synthetic root keep requirements.txt, so their identities are unchanged.

The TUI rendered NodeID under a column labelled "PURL". For a module that is
the structural "module:<path>#<purl>" grammar and for a manifest there is no
package URL at all, so an interactive scan showed a value no consumer can
parse while scan JSON and both SBOM exports had it right. Both TUI paths now
use output.PurlFromGraphNode.

That projection belongs on the SDK's GraphNode (ADR-0040) and is filed as
bomly-dev/bomly-sdk#43. Until it ships there are two copies, and deliberately
so: internal/sbom keeps the codec's own rather than importing the CLI's output
layer, which would be backwards. Both are commented with the issue and
converge on the accessor when it lands.

Goldens: only the pipenv and poetry cases change, and neither could be
regenerated here -- pipenv, poetry and uv are not installed on this machine,
so those five smoke cases skip. Both parsers are pure, so
TestPythonParserRootsNameTheDeclaringManifest asserts the new identities
directly instead. Smoke runs in the merge queue, not on pull requests, so
those two goldens need an Update Smoke Goldens dispatch before merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Comment thread internal/detectors/python/common.go Outdated
Comment thread internal/detectors/python/roots_guard_test.go
Comment thread .githooks/pre-push Outdated
…hole tree

Three findings from review of 49b50a0.

The manager-aware declaring manifest only reached the lockfile parsers.
baseDetector.resolveGraph is the pip-inspect path shared by pip, Pipenv,
Poetry and uv, and it built the root without saying which manager it spoke
for, so every successful pip-inspect graph declared itself from
requirements.txt -- and a Pipenv project got its correct Pipfile identity only
when it fell back to the pure lock parser. One project, two identities,
decided by which strategy happened to succeed. baseDetector carries its
Manager now, and filterPythonToolPackages takes it too, because that function
looks the root up by ID and would otherwise miss it and silently skip
re-parenting orphans. TestPythonRootIdentityAgreesAcrossResolutionStrategies
pins the two strategies to one answer.

The Python smoke goldens are regenerated, and all five had been stale for the
whole migration rather than only the two I claimed: every project root was
still a dependency node. They now read module:Pipfile#...,
module:pyproject.toml#... for Poetry and uv, and module:requirements.txt#...
for pip -- the correct manifests only because of the fix above, which the
regeneration is the evidence for. Each inventory loses exactly one package,
its own project root, which is what a module node means; no dependency moved.

The pre-push hook compared modification times of a hand-written file list
covering 541 of 816 tracked files. Editing a shell script, a workflow, an npm
wrapper source or a nested testdata fixture left the stamp looking fresh, a
deleted file vanished from the list rather than invalidating anything, and a
restored mtime read as unchanged -- each reporting a passing verification for
work that was never tested, which is the one thing the hook exists to prevent.
It compares a digest of HEAD plus the full diff against it now, so git decides
what the repository contains rather than a list that has to be kept in step.
scripts/verify-snapshot.sh defines it once and `make verify` records it.
Exercised against all four previously invisible cases.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee5b2f91d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/tui/scan.go Outdated
Both halves of the interactive explain view narrowed to dependency nodes, so
the node that answers "which of my modules pulled this in" was dropped --
which is the question explain exists to answer.

explainRelationships labelled the union's direct parents but walked only
dependency nodes when labelling everything else. A normal graph's root is a
module node, and for a transitive target it is not a direct parent, so it fell
into that loop and was skipped: no label, no count, and a header reporting
"Roots: 0" for a scan that plainly has one. Nested workspace modules were
hidden the same way.

buildExplainComponentListModel repeated the narrowing, leaving the project's
own module out of the component list -- inconsistent with the ordinary
component tree beside it, which walks the union and renders whatever kind it
finds.

Both iterate Graph.Nodes now. The new test drives the transitive case, where
the root is an ancestor rather than a parent; restoring either narrowing
reproduces "Roots: 0". Interactive rendering only, so no golden moves.

Also corrects the verification snapshot added in the previous commit. It
folded HEAD into the digest, so `git commit` invalidated a verification that
was still perfectly valid -- the content had not changed, only which side of
the HEAD boundary it sat on. A gate that fails on every commit is one that
teaches people to pass --no-verify, which is worse than no gate. It digests
the index plus the worktree diff against it instead: unchanged across a
commit, and still changing on any edit or deletion. All three properties
exercised.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 262043ac06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/sbom/graph.go Outdated
Comment thread internal/output/types.go Outdated
Two findings from review of 262043a. Both are ways this migration could return
a smaller answer than the input described, without saying so.

SBOM ingest dropped a component whose identity could not mint a well-formed
package URL, along with every relationship naming it, and returned no error.
A document carrying one malformed Maven purl therefore produced a smaller
graph than it described, and a scan of it read clean while a genuinely
vulnerable dependency was simply absent from the answer. For a tool whose
whole output is "what are you shipping and is it vulnerable", quietly
returning fewer dependencies than the input listed is the worst available
failure. It is an error now, naming the component and the purl so the author
can find it -- the same rule ADR-0041 applies at the plugin wire: no lenient
path, no pkg:generic coercion for an identity that was asserted and is
invalid. Reproduced before fixing; the full smoke suite, SBOM ingest cases
included, is unchanged by it.

DependenciesFromGraph published a manifest node's ID in depends_on while
deliberately omitting manifests from the listing, so a workspace path
(module -> child manifest -> child module) left a reference no consumer could
resolve. Dropping the ID instead would have severed the workspace, so the hop
is stepped through: the parent module depends on the child module, expressed
only in IDs the document defines. The walk is bounded against structural
cycles.

Also corrects the verification snapshot for the second time, and this time
against the property rather than a symptom. It read git's bookkeeping -- first
HEAD, then the index -- and both invalidated a verification that was still
valid, because `git add` and `git commit` move content across those boundaries
without changing a byte of it. It digests the worktree tree of everything
.gitignore does not exclude, built in a throwaway index, so it changes when a
file's bytes change, when one appears, and when one is deleted, and at no
other time. All five properties exercised; it costs 0.16s.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

for _, r := range g.Roots() {

P2 Badge Treat every module as a directness parent

In the merged graph used by the diff TUI, a workspace or reactor module can have an incoming edge and therefore not appear in Roots(). Its immediate package children are never labeled direct here and fall through as transitive, corrupting the relationship summary and relationship-based component filters. Although renderDirectDepsTable now contains fresh module-aware handling for this exact graph shape, this independent classifier remains roots-only; centralize and reuse the module-aware parent rule.

AGENTS.md reference: AGENTS.md:L137-L140

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/sbom/transform.go Outdated
Comment thread internal/tui/scan.go Outdated
Three findings from review of 33b9baf, all the same shape: a projection over
graph nodes existed once per surface, and the copies had each learned
different things.

The SBOM export named structural nodes it does not export. A workspace is
module -> child manifest -> child module, and the two edges type differently
-- the first derives depends-on, the second describes -- so publishing the
first left CycloneDX with a dependsOn pointing at no bom-ref, while filtering
the second dropped the hop and cut the child module's subtree loose.
Reproduced both before fixing. Scan JSON had this same defect fixed one commit
earlier, in its own local copy, which is the whole argument.

The diff TUI's directness classifier read graph roots only, so a workspace
module that another module depends on was never a parent and its immediate
packages read transitive -- corrupting the relationship summary and every
filter built on it. renderDirectDepsTable had already been taught this; the
classifier beside it had not.

The raw Relationships view iterated dependency nodes as parents, so it omitted
every module-to-package edge: for a project with only direct dependencies it
rendered empty while the count beside it reported the edges it was not showing.

internal/graphview now owns all three -- the package URL a node publishes, the
children a document can name, and which nodes count as top-level parents. It
is a leaf, SDK only, so the codec, the renderers and the TUI reach it without
depending on each other; that was the obstacle that left two copies of the
purl projection behind last time, and this removes it. Every mutation was
checked with the tree still compiling, after an earlier attempt "passed"
because the mutations broke the build instead of the tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@bomly-guy

Copy link
Copy Markdown
Collaborator Author

The third finding from that review — internal/tui/diff.go classifying directness from graph roots only — is fixed in c357d4a alongside the other two. It came through as a review-summary link rather than an inline thread, so there was nowhere to reply in place.

classifyRelationships and renderDirectDepsTable now share one rule, graphview.TopLevelParentIDs: graph roots, every module node, and every application-typed dependency node. That was the substance of the finding — the two classifiers had diverged, and only one had learned about module nodes. New test asserts a package under a non-root module classifies as direct; removing module nodes from the shared rule fails both that test and the render one.

🤖 Addressed by Claude Code

@bomly-guy
bomly-guy merged commit 909825d into main Sep 5, 2026
16 checks passed
@bomly-guy
bomly-guy deleted the claude/cli-2.1-2.3-adoption branch September 5, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants