feat: ownership-evidence signal for package repo links (CM-1394) - #4546
Draft
joanagmaia wants to merge 1 commit into
Draft
feat: ownership-evidence signal for package repo links (CM-1394)#4546joanagmaia 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
joanagmaia
force-pushed
the
feat/CM-1394-ownership-evidence
branch
from
September 1, 2026 14:37
5ab6373 to
4d59421
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Adds ownership evidence scoring to package-to-repository links across package ecosystems.
Changes:
- Adds shared TypeScript ownership matching and ecosystem integrations.
- Adds Cargo’s SQL-based ownership matcher.
- Documents the ownership-evidence decision and adds unit coverage.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
utils/ownershipMatch.ts |
Implements identity normalization and matching. |
utils/__tests__/ownershipMatch.test.ts |
Tests matching and owner extraction. |
pypi/upsertProject.ts |
Adds PyPI ownership evidence. |
packagist/upsertPackageInfo.ts |
Adds Packagist ownership evidence. |
packagist/__tests__/persistPackageInfo.test.ts |
Updates persistence expectations. |
nuget/runNuGetEnrichmentLoop.ts |
Adds NuGet ownership evidence. |
npm/upsertPackage.ts |
Adds npm ownership evidence. |
maven/runMavenEnrichmentLoop.ts |
Adds Maven ownership evidence. |
go/activities.ts |
Derives ownership from VCS module paths. |
cargo/normalizeRepos.ts |
Stages canonical repository owners. |
cargo/enrich.ts |
Computes Cargo ownership during bulk enrichment. |
docs/adr/README.md |
Indexes ADR-0022. |
docs/adr/0022-package-repo-ownership-evidence.md |
Records the architecture decision. |
V1788393600__package_repo_owner_match.sql |
Adds SQL ownership matching functions. |
Suppressed comments (2)
services/apps/packages_worker/src/utils/ownershipMatch.ts:60
- Returning
unmatcheddoes not update existing declared links: every TypeScript caller passes this intoupsertPackageRepo, whose keep-highest conflict policy retains the oldno_evidencescore (0.75) over the newunmatchedscore (0.60). As a result, previously ingested squatting links remain unpenalized indefinitely. Same-source refreshes need replacement semantics, or this PR needs a backfill that writes the computed evidence before rescoring.
return candidates.some((c) => isSameIdentity(c, repoOwner)) ? 'matched' : 'unmatched'
services/apps/packages_worker/src/utils/ownershipMatch.ts:66
CanonicalRepo.host === 'other'does not guarantee an owner/repository path: the canonicalizer preserves every path segment for unknown hosts. A valid primary URL such ashttps://git.kernel.org/pub/scm/...therefore treatspubas an owner and emits a falseunmatchedinstead ofno_evidence. Only derive owners for hosts with a known owner/path contract, while retaining an explicit allowlist for other supported VCS hosts.
export function repoOwnerFromCanonical(repo: CanonicalRepo): string | null {
const path = repo.url.replace(/^https?:\/\/[^/]+\//, '').split('/')
return path.length >= 2 ? path[0] : null
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+33
to
+34
| const segments = namespace.split(/[./]/).filter(Boolean) | ||
| return [namespace, ...segments].map(normalizeIdentity).filter(Boolean) |
Comment on lines
+245
to
246
| SELECT rc.package_id, r.id, $(source), rc.signal, om.match, NULL, | ||
| s.confidence, NOW(), NOW() |
| - Squatting links are separated from legitimate ones by score alone — the read | ||
| side needs no filtering, and `ORDER BY confidence DESC LIMIT 1` starts | ||
| returning the right repo for the epic's named cases. | ||
| - `no_evidence` keeps ecosystems with thin metadata from being punished for it. |
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 3 of 3 for CM-1392.
Stack: #4544 → #4545 → #4546 (this PR). Based on #4545 — review in stack order.
Problem
A package can name any repository it likes. Nothing in the ingestion path checked whether the entity publishing the package has anything to do with the entity owning the repo, so a package declaring
github.com/torvalds/linuxproduced a link indistinguishable from the kernel's own. That is how unrelated packages inflate a repo'spackages_publishedcount.The evidence is already ingested — npm scopes, Maven groupIds, packagist vendors, Go module paths, maintainer/owner/author logins for most registries. CM-1306 reserved
ownership_matchand priced it (unmatched−0.25,no_evidence−0.10); this PR produces the value.What this does
matchOwnership({ namespace, maintainers, repoOwner })returnsmatched,unmatched, orno_evidence, and every declared writer calls it before persisting a link. Namespace evidence first, maintainer logins as fallback.no_evidenceis deliberately distinct fromunmatched: a registry that exposes nothing to compare must not be treated as a failed comparison.Normalisation — trim, lowercase, strip a leading
@, strip one trailing vanity suffix (-ai,-io,-team,-labs,-oss,-dev, only when more than one character survives), drop non-alphanumerics. Reverse-DNS namespaces expand into candidates, soio.github.resilience4jmatchesresilience4j. Identities match on equality or on prefix when the shorter is ≥4 characters —tokio-rs→tokioandlangchain-ai→langchainmatch,abagainstabcdefstaysunmatched.maintainersnameGo derives an owner only for module paths rooted at a known VCS host — a vanity path would otherwise produce a false
unmatched. Rubygems staysno_evidencefor now (owners are fetched in the critical loop, not the core loop that writes the link), and maven's backfill caller passes no evidence, so it yieldsno_evidencerather thanunmatched.Cargo gets a SQL twin of the matcher —
package_repo_owner_key(text)(IMMUTABLE) andpackage_repo_owner_match(repo_owner, candidates[])— because its pipeline is set-based SQL; streaming millions of staged rows through Node to compute a three-valued enum is not worth it. The repo owner rides along oncargo_sync.repo_choicefrom CM-1393. The SQL expression was validated read-only against packages-db case-by-case, including the-iosuffix edge, to confirm it agrees withnormalizeIdentity.Before merge
Record the matched/unmatched/no_evidence distribution per ecosystem, so an unexpectedly large
unmatchedshare is caught before the scores reach downstream consumers.Decision record
ADR-0022 — includes why
unmatchedlinks are scored down rather than rejected, and why the matcher exists in both TypeScript and SQL.