feat(pnpm-policy): origins — group dependencies by the repository they publish from - #118
Merged
Conversation
…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.
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.
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
Real output against
constructive:That reproduces by tool what I worked out by hand for #117 — including that the
graphqlpackages 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.--fromshows 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'srepositoryfrom the registry and parsesowner/repoout of the several shapes that field takes (git+https://,git@host:,ssh://, bareowner/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'srepositorywhen the top level omits it, which older publishes do.cli.ts—--ownerand--from, both repeatable.--outwrites 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
repositoryfield 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
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