@@ -105,8 +105,12 @@ for map grouping — with a three-way origin relation:
105105Comparison is kind-scoped: nodes of different kinds are never equal, so a
106106module node and a dependency node sharing a PURL — or sharing the absence
107107of an origin — can never fold into each other, whatever the insertion
108- order. Module nodes compare by canonical PURL when both carry one and by
109- (declaring manifest path, name) otherwise; manifest nodes compare by path.
108+ order. Module nodes always compare with their declaring manifest path as
109+ part of the identity — beside the canonical PURL when one exists, beside
110+ the module name otherwise — because a recursive scan can legitimately
111+ discover two unrelated projects carrying the same ecosystem, name, and
112+ version, and a PURL-only comparison would fold those separate roots and
113+ union their dependency edges. Manifest nodes compare by path.
110114Keys are in-process comparison values, kind-prefixed, and never appear in
111115scan JSON, SBOMs, or any published document.
112116
@@ -120,15 +124,33 @@ specification's URL-valued evidence keys — `repository_url`,
120124` download_url ` , and ` vcs_url ` — whose values are resolution evidence that
121125can embed credentials and signed links: identity normalization strips them
122126from 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
127+ gates, so they shape the occurrence, never a published ID. Redirection
128+ means passing the value through the ADR-0033 origin constructors, never
129+ stapling it on: those constructors reject a query-carrying artifact URL
130+ outright — a signed or tokenized download link is discarded entirely, not
131+ sanitized into something publishable — so a credential embedded in a
132+ qualifier can reach neither a published ID nor an exported origin field. When distinct
124133occurrences of one canonical PURL must coexist, each origin-bearing
125134occurrence carries a deterministic suffix — a single space and a short
126135lowercase-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
136+ suffix from the occurrence's own origin means a contested occurrence keeps
137+ one suffix for as long as it is contested — adding or removing a sibling
138+ never renumbers the others, which positional ordinals could not promise —
139+ and the normalized origin is already credential-free, so the hash
140+ publishes nothing the origin field does not. A singleton keeps the bare
141+ PURL, and gaining a first sibling does change its rendered ID; that is
142+ deliberate, because readable IDs carry the reference semantics the major
143+ SBOM formats define: CycloneDX ` bom-ref ` and SPDX element IDs are
144+ document-local handles required only to be unique within one document,
145+ with cross-document identity carried by the data fields (the PURL,
146+ external references) rather than the handle. Bomly follows that industry
147+ contract — IDs are unique within a run's outputs and as human-meaningful
148+ as uniqueness allows, while cross-run identity lives in the identity
149+ fields (canonical PURL, normalized origin) that diff, baselines, and
150+ matching already join on. Suffixing every origin-bearing occurrence
151+ unconditionally was considered and rejected: it would trade the common
152+ case's readability for stability of a handle the formats define as
153+ document-local. Occurrences distinguishable only by raw evidence
132154have nothing publishable to hash; they carry run-local ordinal suffixes
133155(` o1 ` , ` o2 ` , … in sorted raw-key order) with the stated caveat that this
134156rare class is run-scoped and not stable across evidence changes — never a
0 commit comments