Skip to content

feat(pnpm-policy): origins — group dependencies by the repository they publish from - #118

Merged
pyramation merged 1 commit into
mainfrom
feat/origins-command
Aug 6, 2026
Merged

feat(pnpm-policy): origins — group dependencies by the repository they publish from#118
pyramation merged 1 commit into
mainfrom
feat/origins-command

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Bakes the analysis we just did by hand into the tool, so "which projects am I actually depending on" is a command rather than an afternoon.

Why

Deciding what to exempt from a release-age quarantine means deciding which projects you trust. npm only offers accounts, and an account is as wide as everything its owner will ever publish.

We hit that: an upstream account added for one stack silently exempted graphql — the reference implementation, six maintainers — along with the GraphiQL packages. Nothing in the tooling would have shown that; you had to go read npm metadata package by package.

Usage

pnpm-policy origins                      # every resolved package, grouped by repo owner
pnpm-policy origins --from postgraphile  # only the subtree that one dependency dragged in
pnpm-policy origins --owner acme         # just that owner's packages
pnpm-policy origins --owner acme --out acme.inventory.json   # written as an inventory

Real output against constructive:

$ pnpm-policy origins --from postgraphile,grafast,graphile-build,grafserv
packages under that subtree: 190

radix-ui  (29)
<no repository metadata>  (20)
graphile  (15)
graphql  (8)
thekashey  (8)

That reproduces by tool what I worked out by hand for #117 — including that the graphql packages are a separate project — and it took one command.

Three pieces, pure TS, each usable on its own

graph.ts — reads the lockfile into a name-keyed dependency graph and walks it. --from shows what a single decision pulled in rather than surveying the whole lockfile. Versions collapse to names deliberately: two copies of a package are the same trust decision, and exemption lists are keyed by name. Cycles terminate.

origins.ts — resolves each package's repository from the registry and parses owner/repo out of the several shapes that field takes (git+https://, git@host:, ssh://, bare owner/repo), lowercased so owner comparisons are not case-sensitive. One request per package — the honest cost, since search does not return repository metadata. A 404 becomes an origin with no repository rather than aborting a survey of hundreds. Falls back to the latest version's repository when the top level omits it, which older publishes do.

cli.ts--owner and --from, both repeatable. --out writes an inventory with no maintainers and no scopes: the result is a reviewed list of names, and a scope glob would re-widen it to whatever gets published into that scope next.

Everything is exported (readLockfileGraph, reachableFrom, packageOrigins, groupByOwner, namesFromOwners, repositorySlug), so it is usable as a library without going through the CLI.

Honest limitation

The repository field is self-reported and a compromised publish can claim anything. This is a proxy for provenance, not proof of it — it answers "which project is this package from", not "is this package safe". Said plainly in both the README and the module doc.

Verification

83 tests pass (73 existing, 10 new)
tsc --noEmit clean

New tests cover URL shapes, case-insensitive owners, roots including devDependencies, subtree extraction that excludes unrelated roots, cycle termination, repository present only on the latest version, unknown packages not aborting the run, grouping, and owner filtering.

Independent of #116 and #117 — no ordering constraint.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CEswUi4ANuB58rva48aHge

…ish from

Deciding what to exempt from a release-age quarantine means deciding
which projects you trust. npm only offers accounts, and an account is as
wide as everything its owner will ever publish — the maintainer of a
library you want may also co-maintain something far larger you did not
mean to exempt. We hit exactly that: an upstream account added for one
stack silently exempted `graphql` and the GraphiQL packages.

`origins` answers the question npm does not.

    pnpm-policy origins                      # grouped by repo owner
    pnpm-policy origins --from postgraphile  # only that dependency's subtree
    pnpm-policy origins --owner acme --out acme.inventory.json

Three pieces, all pure TS and reusable on their own:

  graph.ts    reads the lockfile into a name-keyed dependency graph and
              walks it, so --from shows what one decision dragged in
              rather than surveying everything. Versions collapse to
              names: two copies are the same trust decision, and
              exemption lists are keyed by name. Cycles terminate.

  origins.ts  resolves each package's repository from the registry and
              parses owner/repo out of the several shapes that field
              takes (git+https, git@host:, ssh://, bare owner/repo). One
              request per package; a 404 becomes an origin with no
              repository rather than aborting a survey of hundreds.

  cli.ts      --owner and --from, repeatable; --out writes an inventory
              with no maintainers and no scopes, because the result is a
              reviewed list and a glob would re-widen it.

Verified against constructive's real lockfile: the subtree under
postgraphile/grafast/graphile-build/grafserv is 190 packages, of which 15
are graphile-owned and 8 belong to the graphql org — reproducing by tool
what was worked out by hand, including that the graphql packages are a
different project.

The repository field is self-reported, so this is a proxy for provenance,
not proof. It says which project a package is from, not that it is safe.

83 tests pass, 10 new: URL shapes, case-insensitive owners, roots
including devDependencies, subtree extraction, cycle termination,
repository found only on the latest version, unknown packages, grouping
and owner filtering.
@pyramation
pyramation merged commit 398638c into main Aug 6, 2026
59 checks passed
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.

1 participant