@@ -18,6 +18,15 @@ function blankToNull(s: string | null | undefined): string | null {
1818 return t || null
1919}
2020
21+ // versions.licenses is documented as a deterministically-sorted SPDX array (schema
22+ // comment, V1779710880__initial_schema.sql) — Composer allows dual/multi-licensed
23+ // releases, so unlike npm/pypi's near-always-single-element input, the same license
24+ // set can arrive from the registry in a different order between fetches. Sorting once
25+ // here keeps semantically identical arrays byte-identical, avoiding false audit noise.
26+ function sortLicenses ( licenses : string [ ] | null | undefined ) : string [ ] | null {
27+ return licenses && licenses . length > 0 ? [ ...licenses ] . sort ( ) : null
28+ }
29+
2130export function normalizePackagistStats ( pkg : PackagistPackageInfo ) : NormalizedPackagistStats {
2231 const description = blankToNull ( pkg . description )
2332 const repositoryUrl = blankToNull ( pkg . repository )
@@ -146,7 +155,7 @@ export function buildPackagistVersionRows(versions: PackagistExpandedVersion[]):
146155 publishedAt : v . time ?? null ,
147156 isLatest : false , // Will be set below
148157 isPrerelease : isPackagistPrerelease ( v . version_normalized ?? v . version ) ,
149- licenses : v . license && v . license . length > 0 ? v . license : null ,
158+ licenses : sortLicenses ( v . license ) ,
150159 } ) )
151160
152161 // Find latest: prefer stable over prerelease; within each group use Composer ordering
@@ -176,7 +185,7 @@ export function buildPackagistVersionRows(versions: PackagistExpandedVersion[]):
176185 const latestReleaseAt = times [ times . length - 1 ] ?? null
177186
178187 // Licenses and homepage come from the latest row
179- const licenses = kept [ latestIdx ] . license ?. length ? ( kept [ latestIdx ] . license ?? null ) : null
188+ const licenses = sortLicenses ( kept [ latestIdx ] . license )
180189 const homepage = blankToNull ( kept [ latestIdx ] . homepage )
181190
182191 return {
0 commit comments