feat: deterministic package to repo confidence scoring (CM-1306) - #4544
Draft
joanagmaia wants to merge 1 commit into
Draft
feat: deterministic package to repo confidence scoring (CM-1306)#4544joanagmaia wants to merge 1 commit into
joanagmaia wants to merge 1 commit into
Conversation
Signed-off-by: Joana Maia <jmaia@contractor.linuxfoundation.org>
This was referenced Sep 1, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
Introduces centralized, deterministic package-to-repository confidence scoring across the packages pipeline.
Changes:
- Adds the scoring migration, backfill procedure, and claim types.
- Migrates registry writers and readers to shared scoring and ordering.
- Adds rescore tooling, tests, and ADR documentation.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
backend/src/osspckgs/migrations/V1788307200__package_repo_confidence_scoring.sql |
Adds schema, scoring, indexes, and backfill procedure. |
docs/adr/0020-package-repo-confidence-scoring.md |
Records the scoring architecture decision. |
docs/adr/README.md |
Indexes ADR-0020. |
services/apps/packages_worker/package.json |
Adds rescore commands. |
services/apps/packages_worker/src/cargo/enrich.ts |
Uses centralized scoring for Cargo links. |
services/apps/packages_worker/src/deps-dev/queries/packageReposSql.ts |
Exports deps.dev provenance. |
services/apps/packages_worker/src/deps-dev/workflows/ingestRepos.ts |
Scores deps.dev links during merge. |
services/apps/packages_worker/src/enricher/updateEnrichedRepos.ts |
Triggers scoring after enrichment. |
services/apps/packages_worker/src/go/activities.ts |
Migrates Go link writes. |
services/apps/packages_worker/src/maven/runMavenEnrichmentLoop.ts |
Migrates Maven link writes. |
services/apps/packages_worker/src/npm/upsertPackage.ts |
Migrates npm link writes. |
services/apps/packages_worker/src/nuget/runNuGetEnrichmentLoop.ts |
Migrates NuGet link writes. |
services/apps/packages_worker/src/packagist/upsertPackageInfo.ts |
Migrates Packagist link writes. |
services/apps/packages_worker/src/packagist/__tests__/persistPackageInfo.test.ts |
Updates Packagist mocks and assertions. |
services/apps/packages_worker/src/pypi/upsertProject.ts |
Migrates PyPI link writes. |
services/apps/packages_worker/src/rubygems/runRubyGemsCoreLoop.ts |
Migrates RubyGems link writes. |
services/apps/packages_worker/src/scripts/rescorePackageRepos.ts |
Adds backfill and tie-check tooling. |
services/apps/packages_worker/src/security-contacts/ingestSingle.ts |
Uses shared best-link ordering. |
services/libs/data-access-layer/src/osspckgs/api.ts |
Consolidates repository-link ordering. |
services/libs/data-access-layer/src/osspckgs/repos.ts |
Removes the legacy Maven upsert. |
services/libs/data-access-layer/src/osspckgs/sqlFragments.ts |
Adds shared ordering SQL. |
services/libs/data-access-layer/src/osspckgs/stewardships.ts |
Uses shared ordering. |
services/libs/data-access-layer/src/osspckgs/types.ts |
Removes the obsolete upsert type. |
services/libs/data-access-layer/src/packages/index.ts |
Exports confidence utilities. |
services/libs/data-access-layer/src/packages/repoConfidence.test.ts |
Tests labels, claims, and SQL generation. |
services/libs/data-access-layer/src/packages/repoConfidence.ts |
Defines claims and scoring SQL helpers. |
services/libs/data-access-layer/src/packages/repoConfidenceScoring.integration.test.ts |
Tests database scoring behavior. |
services/libs/data-access-layer/src/packages/repos.ts |
Centralizes link upserts and rescoring. |
Suppressed comments (2)
backend/src/osspckgs/migrations/V1788307200__package_repo_confidence_scoring.sql:117
repo_id % 1000000does not make confidence unique: two links whose repo IDs differ by one million receive the same offset whenever their source priority and adjusted base match. That contradicts the total-ranking/no-ties invariant and can still leave downstream consumers with ambiguous scores. Preserve the full repo ID in a sufficiently precise bounded offset, or retain a separate deterministic tie-break key across every consumer.
-- Two repos on the same package collide only if their ids are congruent mod 1e6
-- and their sources share a priority band.
offset_units := source_priority::bigint * 1000000 + COALESCE(p_repo_id, 0) % 1000000;
RETURN LEAST(base + offset_units * 0.000000001, 0.999999999);
backend/src/osspckgs/migrations/V1788307200__package_repo_confidence_scoring.sql:163
- The procedure has the same invalid
UPDATE ... FROM LATERALcorrelation:pris the update target, not a precedingFROMitem visible to the lateral subquery. Consequently the first backfill batch fails instead of rescoring any rows. Move the scoring call into theSETexpression.
SET confidence = s.confidence
FROM batch b, packages p, repos r,
LATERAL (
SELECT package_repo_confidence(
pr.source, p.ecosystem, pr.signal, pr.ownership_match, pr.provenance,
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+69
to
+72
| it('scores the source tiers', async () => { | ||
| expect(await score({ source: 'manual' })).toBeCloseTo(0.99, 3) | ||
| expect(await score({ source: 'deps_dev', provenance: 'SLSA_ATTESTATION' })).toBeCloseTo(0.99, 3) | ||
| expect(await score({ source: 'deps_dev', provenance: 'GO_ORIGIN' })).toBeCloseTo(0.9, 3) |
Comment on lines
+134
to
+142
| `UPDATE package_repos pr | ||
| SET confidence = s.confidence | ||
| FROM packages p, repos r, | ||
| LATERAL ( | ||
| SELECT ${packageRepoConfidenceCall('p', 'r', claimFromRow('pr'))} AS confidence | ||
| ) s | ||
| WHERE p.id = pr.package_id | ||
| AND r.id = pr.repo_id | ||
| AND pr.repo_id = ANY($(repoIds)::bigint[])`, |
Comment on lines
+17
to
+20
| -- Ownership evidence for a declared link. Real values written by CM-1394; | ||
| -- 'no_evidence' until then, which is the no-op default for scoring. | ||
| ADD COLUMN IF NOT EXISTS ownership_match text NOT NULL DEFAULT 'no_evidence' | ||
| CHECK (ownership_match IN ('matched', 'unmatched', 'no_evidence')), |
Comment on lines
+88
to
+90
| IF p_disabled IS TRUE THEN | ||
| RETURN 0.05; | ||
| END IF; |
Comment on lines
253
to
+257
| ON CONFLICT (package_id, repo_id) DO UPDATE SET | ||
| confidence = GREATEST(EXCLUDED.confidence, package_repos.confidence), | ||
| verified_at = NOW() | ||
| source = CASE WHEN EXCLUDED.confidence > package_repos.confidence | ||
| THEN EXCLUDED.source ELSE package_repos.source END, | ||
| signal = CASE WHEN EXCLUDED.confidence > package_repos.confidence | ||
| THEN EXCLUDED.signal ELSE package_repos.signal END, |
Comment on lines
+105
to
+109
| CROSS JOIN LATERAL ( | ||
| SELECT ${packageRepoConfidenceCall('p', 'r', { | ||
| source: `'deps_dev'`, | ||
| signal: `'primary'`, | ||
| ownershipMatch: `'no_evidence'`, |
Comment on lines
+76
to
+79
| await rescorePackageReposForRepos( | ||
| qx, | ||
| (updated as { id: string }[]).map((r) => r.id), | ||
| ) |
Comment on lines
+84
to
+87
| scored AS ( | ||
| SELECT ${packageRepoConfidenceCall('p', 'r')} AS confidence | ||
| FROM packages p, repos r | ||
| WHERE p.id = $(packageId)::bigint AND r.id = $(repoId)::bigint |
Comment on lines
+64
to
+72
| // Confidence is never passed in — package_repo_confidence() (V1788307200) is the only | ||
| // path that produces one. Callers describe the claim (source, which manifest field it | ||
| // came from, what ownership evidence backs it) and the function scores it against the | ||
| // package's ecosystem and the repo's current state. | ||
| // | ||
| // Conflict policy, uniform across every writer: keep the highest-scoring claim and adopt | ||
| // that claim's provenance. Writer order is irrelevant — a routine registry refresh cannot | ||
| // downgrade a link a stronger source (manual, an attested deps.dev row) already owns, and | ||
| // a stronger claim arriving later takes the row over completely. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part 1 of 3 for CM-1392.
Stack: #4544 (this PR) → #4545 → #4546. Merge in that order.
Problem
package_reposis the seam every consumer (Akrites, Insights, criticality, blast radius) reads to answer "which repo does this package come from". The read side takes the highest-confidence link, but all nine registry writers hardcoded0.8and deps.dev carried its own provenance→confidence CASE. A squatting package and the legitimate publisher were indistinguishable, and links that tied on0.8resolved arbitrarily. Repos accumulated inflatedpackages_publishedcounts, and every metric derived from those aggregates inherited the error.What this does
One IMMUTABLE SQL function,
package_repo_confidence(...), becomes the only producer of apackage_repos.confidencevalue. Every writer calls it — the eight registry loops, the deps.dev staging merge, and the enricher rescore.source(+provenancefor deps.dev): manual 0.99 · SLSA 0.99 · rubygems/pypi attestation 0.95 ·GO_ORIGIN0.90 · other deps.dev 0.50 · declared 0.85 (maven 0.80) · heuristic 0.30.signal='secondary'−0.10,ownership_match='unmatched'−0.25 /'no_evidence'−0.10, archived −0.20, fork −0.10, disabled → flat 0.05, non-GitHub host with a competing GitHub link −0.05.signal,ownership_match,provenance. The first two default to their no-op value and are only written by the two follow-up PRs, so this change is behaviour-neutral on merge apart from the tier/penalty values themselves.source_priority * 1e6 + repo_id % 1e6scaled1e-9(max0.003999999), bounded below the 0.05 tier gap and below both label boundaries, so ranking is total without ever crossing a tier or a High/Medium/Low label. Reachable range0.05…0.993999999.confidence = GREATEST(EXCLUDED.confidence, package_repos.confidence), and the descriptive columns only move when the new score wins. A weaker source can no longer overwrite a stronger one.bestRepoLinkOrderBy/BEST_REPO_LINK_JOINreplace five inline copies with divergent tie-breakers. The fragment keepsROUND(confidence, 2), so API payloads stay at two decimals.archived/disabled/is_forkflip.Migration — needs a maintenance window
confidenceis widened in place,numeric(3,2)→numeric(12,9). The scale change rewrites the table underACCESS EXCLUSIVE, andpackage_reposcarriesREPLICA IDENTITY FULLplus a Sequin publication (V1781009234).Run with the Sequin and Tinybird sinks paused. The migration header says so too.
Existing rows keep their values until
rescore_package_repo_confidence()backfills them — keyset-paged,COMMITper chunk, session advisory lock, run out-of-band viascripts/rescorePackageRepos.ts, which also reports the no-ties invariant.Decision record
ADR-0020 — includes why the scoring lives in SQL rather than TypeScript, and why the dual-column
confidence_scorealternative was dropped in favour of the in-place widening.