Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dev-docs/adr/0033-package-origin-is-detector-asserted.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ The invariant runs when a detector records a value, again at the JSON boundary i

Origins are never merged, reconciled, or disputed. Two records of one package are either witnesses of one resolution or distinct occurrences, and the rule is uniform: **identical records fold, a gap fills from whichever record has an origin, and contradicting records stay distinct nodes — no tiebreak ever picks a winner over a contradiction.** One-node-per-PURL is the registry's constraint, not the graph's: many graph nodes may share a PURL, all linked to one registry package through `PackageRef`. Where a lockfile gives records positional identity (cargo's source-qualified package IDs, bun's keys), the detector keeps distinct nodes and `normalizeGraphPackageIdentity` preserves them through the canonical-PURL rewrite instead of collapsing them; across manifests, `preserveContradictingOccurrences` re-IDs a contradicting record before the SDK graph merge, so the merge folds only witnesses and fills only gaps. Where a lockfile references by bare name (uv, poetry, pipenv groups), one graph position exists and the deterministic first/last record wins as a whole — no field-level mixing; scope, relationship, and locations still union everywhere, being usage facts rather than assertions. Node insertion itself goes through one helper, `detectors.EnsureNode`, which deliberately merges nothing — records that differ deserve distinct nodes under distinct IDs, and records that are the same need nothing merged. `TestNodeInsertionGoesThroughTheSharedHelper` fails if a hand-written lookup-then-insert reappears; that guard found four sites nobody had reported when it was introduced. Generalizing positional identity beyond origin contradictions is #399.

> **Amended 2026-09-12 (issue #454):** the helper is `detectorkit.EnsureNode` in bomly-sdk; the CLI-local `detectors.EnsureNode` was deleted when bomly-sdk v0.9.0 shipped. Since bomly-sdk v0.10.0 it is a thin wrapper over `Graph.InsertNode`, which folds two records of one identity into one node and unions their usage facts — scope, relationship, locations — and their origins, so "merges nothing" is no longer literally true. That fold also supersedes the sentence above about contradicting records staying distinct nodes: under [ADR-0041](0041-identity-is-the-canonical-purl-on-typed-nodes.md) identity is the canonical package URL, so two records of one package that assert different origins are one node whose origins list has two elements (`TestConsolidateGraphsFoldsContradictingResolutionsKeepingBoth`), a fact to display rather than a reason to split identity. What survives unchanged, and is the rule this decision actually rests on, is that no tiebreak ever picks a winner: both origins are kept, neither is reconciled into the other, and export projects what the detectors asserted. `TestNodeInsertionGoesThroughTheSharedHelper` now names the SDK helper.

One consequence worth stating: origin does not appear in `scan`/`diff`/`explain` payloads, because those documents are built from explicit projections rather than from the SDK types. It is provenance for the SBOM, which is where users read it. (While origin rode on metadata, keeping it out took an explicit prefix filter in `output.cloneRefMetadata`; the typed field made that unnecessary.)
2 changes: 1 addition & 1 deletion internal/detectors/cargo/origin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func TestSetCargoOriginBySourcePrefix(t *testing.T) {
// "pkg:cargo/helper@1.0.0" and keeping them apart would produce two components
// with byte-identical identity. Nothing is lost -- the folded node carries both
// repositories as origins, which says more than two indistinguishable nodes
// did (ADR-0041; the reasoning is recorded in detectors.EnsureNode).
// did (ADR-0041; the reasoning is recorded in the SDK's `Graph.InsertNode`, which `detectorkit.EnsureNode` wraps).
func TestCargoDuplicateCrateSourcesFoldWithBothOrigins(t *testing.T) {
metadata := []byte(`{
"packages": [
Expand Down
Loading