Skip to content

Commit 642c223

Browse files
bomly-guyclaude
andcommitted
docs: address review — qualifiers are identity, gaps stay distinct, suffixes hash the origin
Three reviewer-directed corrections. Qualifiers stay in published IDs — the PURL spec defines them as qualifying data and container scans carry one package/version under two architectures — with only the spec's URL-valued evidence keys (repository_url, download_url, vcs_url) stripped to Origin behind the ADR-0033 gates. An unknown origin is its own occurrence, never a gap consolidation fills (manufacturing provenance); ecosystem detectors may fill gaps at detection time where their semantics justify it. Occurrence suffixes derive from a short hash of the occurrence's own normalized origin, stable under occurrence-set changes where positional ordinals would renumber published bom-refs; run-local ordinals remain only for the rare raw-evidence-only contradictions, with the run-scoped caveat stated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 834eebe commit 642c223

2 files changed

Lines changed: 60 additions & 37 deletions

File tree

dev-docs/SDK_MATURITY_PLAN.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,12 @@ One sentence per decision; the ADRs carry the detail.
104104

105105
- **Identity** (ADR-0041): a sealed typed node union — manifest, module,
106106
and dependency nodes — where a dependency node's identity is its
107-
canonical PURL (valid by the PURL specification's own rules, missing
108-
versions warned), the occurrence qualifier is the normalized origin, and
109-
comparison is a kind-scoped equals/key pair with ADR-0033 gap-filling;
110-
IDs and ordinals minted only by SDK entry points; no content address.
107+
canonical PURL with qualifiers (valid by the PURL specification's own
108+
rules, missing versions warned, URL-valued evidence qualifiers redirected
109+
to Origin), the occurrence qualifier is the normalized origin (unknown
110+
origin is its own occurrence; gap-filling is a detector decision), and
111+
comparison is a kind-scoped equals/key pair; origin-hash suffixes and
112+
ordinals minted only by SDK entry points; no content address.
111113
- **Model** (ADR-0037): every preserved SBOM assertion is a typed,
112114
`omitempty`, boundary-validated field with a declared merge class; typed
113115
edges; a per-entry document-assertions carrier on `GraphEntry`; `Metadata`
@@ -144,7 +146,7 @@ Breaking in-process changes are allowed (v0 policy); the wire stays additive
144146
|---|---|---|
145147
| 1.1 | `purlkit`: qualifiers/subpath-capable build/parse/canonicalize; the single purl-type ↔ ecosystem table (hex non-mapping recorded); `SplitEcosystemName`; canonical-ID rewrite entry point; import-boundary guard | v0.5.0 |
146148
| 1.2 | `spdxkit`: absorb `internal/licenseexpr` semantics (panic guards, `Valid`/`ValidateAll`/`Identifier`/`Compose`/`Satisfies`/`Extract`); deprecated-ID canonicalization via the audited replacement map relocated from `internal/sbom/transform.go` (the list marks deprecation; the map owns replacements); classification-by-validation; deterministic `LicenseRef-*` minting + extracted-text pairing; fix `matcherkit.NormalizeLicenseSet` to classify on write | v0.5.0 |
147-
| 1.3 | Identity (ADR-0041, superseding ADR-0036): sealed `GraphNode` union — manifest / module / dependency nodes (dependency nodes require a spec-valid canonical PURL, missing version warned; modules are the project's own artifacts; manifests structural, path-identified, never matched); kind-scoped identity equals/key over (canonical PURL, normalized origin) with ADR-0033 gap-filling; PURL identity-form readable IDs with per-PURL deterministic ordinals from 1 for coexisting occurrences; single insertion + finalization entry points; wire flat-node shape plus additive `kind` discriminator with frozen explicit/legacy/conflicting/unknown fixtures; no content address | v0.6.0 |
149+
| 1.3 | Identity (ADR-0041, superseding ADR-0036): sealed `GraphNode` union — manifest / module / dependency nodes (dependency nodes require a spec-valid canonical PURL, missing version warned; modules are the project's own artifacts; manifests structural, path-identified, never matched); kind-scoped identity equals/key over (canonical PURL, normalized origin) — unknown origin is a distinct occurrence, gap-filling is an ecosystem-detector decision; qualifier-carrying PURL readable IDs (URL-valued evidence qualifiers stripped to Origin) with stable origin-hash suffixes for coexisting occurrences and run-local ordinals only for raw-evidence-only contradictions; single insertion + finalization entry points; wire flat-node shape plus additive `kind` discriminator with frozen explicit/legacy/conflicting/unknown fixtures; no content address | v0.6.0 |
148150
| 1.4 | Model fields (ADR-0037): supplier/originator/description/homepage; `ExternalReference`; `PackageLicense` declared/concluded + extracted text; digest-algorithm registry; set-aware scope ↔ CycloneDX mapping with its scalar projection rule; typed `DependencyEdge.Kind` with a kind-preserving edge-copy/rename primitive for graph reconstruction sites; usage attribution (`PackageLocation` carries per-site scopes and relationship so the node-level union becomes derived; reachability becomes repeatable per-module-root evidence with the vulnerability annotation as derived summary, and evidence may carry optional `DependencyRefs` to the exact occurrence nodes where the analyzer can attribute — a conjunctive filter such as reachable ∧ runtime ∧ direct then joins evidence to locations within one module root, selecting one usage); a derived package → nodes reverse-index helper (the stored truth stays `Dependency.PackageRef`; the registry remains position-free); per-`GraphEntry` document assertions; per-field-class merge helpers; boundary validation codecs + fuzz targets for every new parser | v0.7.0 |
149151
| 1.5 | Metadata policy: document reserved `bomly.` prefix; deprecate `MetadataKeyDetectionLicenses` in favor of the typed license field | v0.7.0 |
150152

dev-docs/adr/0041-identity-is-the-canonical-purl-on-typed-nodes.md

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,29 @@ the same warning policy; a module that cannot mint a PURL falls back to
7878
path-based identity like a manifest, because it is the project's own
7979
record, not a registry lookup key.
8080

81-
**The occurrence qualifier is the normalized origin, and comparison
82-
preserves the gap-fill rule.** The occurrence half of identity is the
81+
**The occurrence qualifier is the normalized origin, and an unknown origin
82+
is its own occurrence.** The occurrence half of identity is the
8383
ADR-0033-normalized `Origin` already on the node — no new storage, no
84-
facet renderings, no hashes — which makes comparison wire-stable by
85-
construction, since it uses exactly the view the JSON codec persists. The
86-
SDK exposes the Java-style pair — an identity-equality predicate and an
87-
identity key for map grouping — with a three-way origin relation rather
88-
than naive equality, so ADR-0033's folding semantics survive:
84+
facet renderings — which makes comparison wire-stable by construction,
85+
since it uses exactly the view the JSON codec persists. The SDK exposes
86+
the Java-style pair — an identity-equality predicate and an identity key
87+
for map grouping — with a three-way origin relation:
8988

9089
- both records carry normalized origins and they are equal → the same
9190
occurrence; witnesses fold;
92-
- exactly one record carries an origin → a gap; the gap witness folds into
93-
the origin-bearing record (ADR-0033's fill rule, unchanged);
94-
- both carry origins and they differ → distinct occurrences that coexist.
91+
- both carry origins and they differ → distinct occurrences that coexist;
92+
- exactly one record carries an origin → distinct occurrences. An absent
93+
origin means "resolution unknown", which is a claim of its own, not a
94+
gap for consolidation to fill: folding it into whichever origin-bearing
95+
record appeared first would manufacture provenance the witness never
96+
asserted. This deliberately narrows ADR-0033's consolidation-time
97+
fill rule; where an ecosystem's semantics genuinely justify filling —
98+
one lockfile entry split across records — that ecosystem's detector
99+
fills the gap at detection time, before consolidation, where the
100+
context to justify it exists. Two records that both lack origins fold
101+
when nothing else distinguishes them, and stay distinct when raw
102+
resolution evidence proves they differ (raw evidence is legal for
103+
comparison, never for identity or publication).
95104

96105
Comparison is kind-scoped: nodes of different kinds are never equal, so a
97106
module node and a dependency node sharing a PURL — or sharing the absence
@@ -102,24 +111,35 @@ Keys are in-process comparison values, kind-prefixed, and never appear in
102111
scan JSON, SBOMs, or any published document.
103112

104113
**Readable IDs need no grammar of their own.** A dependency node's graph ID
105-
is its canonical PURL in identity form: qualifiers pass only through
106-
purlkit's identity-qualifier allowlist — empty today, so all are dropped —
107-
and the subpath is preserved. That gate exists because imported PURLs can
108-
carry resolution evidence and credentials in qualifiers
109-
(`repository_url=…`, signed download links), and node IDs are published as
110-
SBOM component identifiers; admitting the first allowlist key is a
111-
reviewed act that ships the ADR-0033 credential gates with it. When
112-
distinct occurrences of one canonical PURL must coexist, every one of them
113-
carries a deterministic run-local ordinal suffix — a single space and `o`
114-
plus a decimal. Numbering starts at 1, is scoped per canonical PURL, and
115-
is assigned in the sorted order of the occurrences' identity keys — never
116-
arrival order, never a hash of evidence; golden tests pin the assignment
117-
when the implementation lands. Raw resolution evidence (the verbatim
118-
`ResolvedURL`, tokenized URLs) never reaches an ID. Module and manifest
119-
nodes use their path-derived identities. The insertion entry point still
120-
parks contradicting records under an ephemeral in-process discriminator
121-
(NUL-marked, structurally disjoint from every readable ID) until
122-
consolidation finalizes them; the ephemeral form never reaches output.
114+
is its canonical PURL — qualifiers and subpath included, because the PURL
115+
specification defines qualifiers as qualifying data (`arch`, `distro`,
116+
`upstream`, `epoch`, `classifier`, …) and container scans genuinely carry
117+
one package/version under two architectures: dropping qualifiers would
118+
collide identities the spec keeps distinct. The exception is the
119+
specification's URL-valued evidence keys — `repository_url`,
120+
`download_url`, and `vcs_url` — whose values are resolution evidence that
121+
can embed credentials and signed links: identity normalization strips them
122+
from the PURL and their content belongs in `Origin`, behind the ADR-0033
123+
gates, so they shape the occurrence, never a published ID. When distinct
124+
occurrences of one canonical PURL must coexist, each origin-bearing
125+
occurrence carries a deterministic suffix — a single space and a short
126+
lowercase-hex hash of its kind-prefixed normalized origin. Deriving the
127+
suffix from the occurrence's own origin keeps IDs stable when the
128+
occurrence set changes: adding or removing a sibling never renumbers the
129+
others, which positional ordinals could not promise, and the normalized
130+
origin is already credential-free, so the hash publishes nothing the
131+
origin field does not. Occurrences distinguishable only by raw evidence
132+
have nothing publishable to hash; they carry run-local ordinal suffixes
133+
(`o1`, `o2`, … in sorted raw-key order) with the stated caveat that this
134+
rare class is run-scoped and not stable across evidence changes — never a
135+
hash of raw evidence, because raw resolution strings (the verbatim
136+
`ResolvedURL`, tokenized URLs) never reach an ID in any form. Module and
137+
manifest nodes use their path-derived identities. The insertion entry
138+
point still parks contradicting records under an ephemeral in-process
139+
discriminator (NUL-marked, structurally disjoint from every readable ID)
140+
until consolidation finalizes them; the ephemeral form never reaches
141+
output. Golden tests pin the suffix derivation and ordinal assignment when
142+
the implementation lands.
123143

124144
**The wire stays inside `bomly.plugin.v1`, with a specified discriminator.**
125145
Nodes keep their flat JSON shape and gain one additive `omitempty` `kind`
@@ -154,10 +174,11 @@ not lost.
154174
- The review-hardened invariants from the closed first round survive as
155175
behavior and tests, not machinery: ownership survives folds (now by
156176
construction — kinds never cross-fold, and the cross-entry merge helper
157-
must preserve module identity the same way), ordinals are never
158-
recycled, identity comparison is stable across the plugin wire, raw
159-
evidence is never provenance for a suffix, and discriminator
160-
vocabularies fold before derivation.
177+
must preserve module identity the same way), origin-derived suffixes are
178+
stable under occurrence-set changes, identity comparison is stable
179+
across the plugin wire, raw evidence is never provenance for a
180+
published suffix, and discriminator vocabularies fold before
181+
derivation.
161182
- ADR-0036 is superseded; the identity phase of `SDK_MATURITY_PLAN.md`
162183
(item 1.3) and every other identity reference in that plan are rescoped
163184
to this decision.

0 commit comments

Comments
 (0)