Skip to content

feat: ownership-evidence signal for package repo links (CM-1394) - #4546

Draft
joanagmaia wants to merge 1 commit into
feat/CM-1393-secondary-manifest-signalfrom
feat/CM-1394-ownership-evidence
Draft

feat: ownership-evidence signal for package repo links (CM-1394)#4546
joanagmaia wants to merge 1 commit into
feat/CM-1393-secondary-manifest-signalfrom
feat/CM-1394-ownership-evidence

Conversation

@joanagmaia

@joanagmaia joanagmaia commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

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/linux produced a link indistinguishable from the kernel's own. That is how unrelated packages inflate a repo's packages_published count.

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_match and priced it (unmatched −0.25, no_evidence −0.10); this PR produces the value.

What this does

matchOwnership({ namespace, maintainers, repoOwner }) returns matched, unmatched, or no_evidence, and every declared writer calls it before persisting a link. Namespace evidence first, maintainer logins as fallback. no_evidence is deliberately distinct from unmatched: 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, so io.github.resilience4j matches resilience4j. Identities match on equality or on prefix when the shorter is ≥4 characters — tokio-rstokio and langchain-ailangchain match, ab against abcdef stays unmatched.

Ecosystem Namespace evidence Maintainer evidence
npm scope maintainers
pypi maintainer/author names
packagist vendor from name maintainers
nuget owners + authors
maven groupId developer/contributor usernames
cargo maintainer GitHub logins
go module path owner, VCS hosts only
rubygems none at the link-writing loop

Go derives an owner only for module paths rooted at a known VCS host — a vanity path would otherwise produce a false unmatched. Rubygems stays no_evidence for 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 yields no_evidence rather than unmatched.

Cargo gets a SQL twin of the matcher — package_repo_owner_key(text) (IMMUTABLE) and package_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 on cargo_sync.repo_choice from CM-1393. The SQL expression was validated read-only against packages-db case-by-case, including the -io suffix edge, to confirm it agrees with normalizeIdentity.

Before merge

Record the matched/unmatched/no_evidence distribution per ecosystem, so an unexpectedly large unmatched share is caught before the scores reach downstream consumers.

Decision record

ADR-0022 — includes why unmatched links are scored down rather than rejected, and why the matcher exists in both TypeScript and SQL.

Signed-off-by: Joana Maia <jmaia@contractor.linuxfoundation.org>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 unmatched does not update existing declared links: every TypeScript caller passes this into upsertPackageRepo, whose keep-highest conflict policy retains the old no_evidence score (0.75) over the new unmatched score (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 as https://git.kernel.org/pub/scm/... therefore treats pub as an owner and emits a false unmatched instead of no_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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants