You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
remove, rollback and get treat case-distinct packages as one: remove pkg:npm/jsonstream also removes JSONStream's patch #1292
[agent] Filed by the scheduled architecture audit routine (CLI and core). Register: discussion #560 register.
Kind: bug. Source: new finding, register row C80 (follows C62/#1034 and C63/#1045).
Problem
#1034 gave get, remove and rollback one target grammar, utils::target::Target. Purl and name targets match through policy::package_spec_matches, the lenient filter matcher behind scan --package and socket.yml, which lowercases names in every ecosystem. PurlKey, the purl identity #1045 introduced, folds case only for PyPI, NuGet and Composer. So Target uses two equivalences, depending on whether the purl carries a version:
Versionless purls and names compare lowercased. These are Target::matches_package/name_matches and the versionless arm of matches_patch, which both go through package_spec_matches. Its doc says "npm forbids uppercase", but legacy npm names such as JSONStream are uppercase and distinct from jsonstream.
The ambiguity guard is blind to case.package_identity lowercases the whole purl, so Target::settle counts pkg:npm/JSONStream and pkg:npm/jsonstream, or github.com/Sirupsen/logrus and github.com/sirupsen/logrus, as one package and never refuses them.
CLI_CONTRACT.md promises: "get, remove and rollback act on one package per name, so a name whose last-segment rule reaches several packages … is refused". It also says a purl without a version selects "every version" of that package, not of a different package.
Proof by execution (main @ 31383f5, run twice, identical results). The test used a debug build and a manifest holding pkg:npm/JSONStream@1.3.5 and pkg:npm/jsonstream@0.0.1, run with remove <target> --skip-rollback --json:
target
removed
left
pkg:npm/jsonstream
both
none
jsonstream (name; the contract says ambiguous)
both, exit 0, no ambiguous_target
none
pkg:npm/jsonstream@0.0.1
jsonstream only
JSONStream@1.3.5
pkg:npm/JSONStream@1.3.5
JSONStream only
jsonstream@0.0.1
A unit probe of Target:
matches_package is true and matches_patch false for pkg:npm/Lodash@4.17.21, pkg:gem/Rails@7.0.0, pkg:cargo/Serde@1.0.0, pkg:golang/github.com/Foo/bar@v1.0.0 and pkg:maven/Org.X/y@1.0 against their lowercase records. So get selects an installed package whose patch the same spelling can't remove or rollback.
The versionless spelling of each of those matches both.
Target::parse("logrus").ambiguity([Sirupsen@v1.0.0, sirupsen@v1.8.1]) and Target::parse("jsonstream").ambiguity([JSONStream, jsonstream]) both return None.
rollback <target> selects through the same matches_patch (find_patches_to_rollback), so it reverts both packages' files.
Symptoms and impact
Symptoms: no open issue reports this.
Impact: a destructive command acts on a second, distinct package. remove drops its patch record and, without --skip-rollback, restores its files. rollback reverts it. get searches and applies to it.
Who it reaches: Go projects that carry both Sirupsen and sirupsen spellings of logrus (a well-known case in older module graphs), npm legacy mixed-case names, and Maven groupIds.
The inconsistency: the same package selects differently depending on whether a version is typed.
Proposed change
In utils/target.rs, compare packages by PurlKey identity (canonical_base_purl with the version dropped), not by a blanket to_lowercase:
package_identity returns the PurlKey base. This deletes its private lowercase + PEP 503 fold.
A versionless purl target matches a record or installed purl when the PurlKey bases are equal (in matches_package and matches_patch), the same relation the versioned arm already uses.
A name may still be typed in any case. settle counts distinct PurlKey identities, so jsonstream beside JSONStream is refused as ambiguous_target. An exact-case full name wins, the way the full name already wins over last-segment matches today.
package_spec_matches keeps its lenient behaviour for scan --package and socket.yml, which the contract documents as filters. Target stops calling it for purl targets. Correct its "npm forbids uppercase" comment.
Size and scope
Files:utils/target.rs (~40 production lines changed, plus tests) and possibly policy/mod.rs (comment only).
Out of scope:
the scan --package/socket.yml filter semantics;
get's API search spelling;
any PurlKey folding change.
Acceptance criteria
Target::parse("pkg:npm/jsonstream").matches_patch("pkg:npm/JSONStream@1.3.5", _) is false. The same holds for Go Sirupsen/sirupsen and a Maven groupId pair.
Target::parse("jsonstream").settle([JSONStream, jsonstream]) is Err. Target::parse("JSONStream").settle(...) settles on JSONStream alone.
Versioned and versionless purl targets agree on case for every ecosystem: a table test over npm, golang, maven, cargo, gem, pypi, nuget and composer.
CLI regression test: remove pkg:npm/jsonstream --skip-rollback --json on the two-record manifest above removes only jsonstream, and remove jsonstream --json exits 1 with ambiguous_target.
cargo test -p socket-patch-core utils::target plus the get, remove and rollback CLI suites stay green.
[agent] Claiming this issue for the architecture refactor routine (highest leverage: Target matches packages through two case equivalences; moving it onto the one PurlKey identity fixes a destructive cross-package remove/rollback, and its file is free of open PRs). Branch: arch-refactor/1292-target-purlkey-identity. Claim-ID: 2026-10-09T15:56:17Z-24970f
[agent] Filed by the scheduled architecture audit routine (CLI and core). Register: discussion #560 register.
Kind: bug. Source: new finding, register row C80 (follows C62/#1034 and C63/#1045).
Problem
#1034 gave
get,removeandrollbackone target grammar,utils::target::Target. Purl and name targets match throughpolicy::package_spec_matches, the lenient filter matcher behindscan --packageand socket.yml, which lowercases names in every ecosystem.PurlKey, the purl identity #1045 introduced, folds case only for PyPI, NuGet and Composer. SoTargetuses two equivalences, depending on whether the purl carries a version:Target::matches_package/name_matchesand the versionless arm ofmatches_patch, which both go throughpackage_spec_matches. Its doc says "npm forbids uppercase", but legacy npm names such asJSONStreamare uppercase and distinct fromjsonstream.matches_patchcompare throughpurl_matches_identifier→PurlKey, which is case-preserving for npm, Go, Maven, Cargo and gem.package_identitylowercases the whole purl, soTarget::settlecountspkg:npm/JSONStreamandpkg:npm/jsonstream, orgithub.com/Sirupsen/logrusandgithub.com/sirupsen/logrus, as one package and never refuses them.CLI_CONTRACT.md promises: "
get,removeandrollbackact on one package per name, so a name whose last-segment rule reaches several packages … is refused". It also says a purl without a version selects "every version" of that package, not of a different package.Proof by execution (main @
31383f5, run twice, identical results). The test used a debug build and a manifest holdingpkg:npm/JSONStream@1.3.5andpkg:npm/jsonstream@0.0.1, run withremove <target> --skip-rollback --json:pkg:npm/jsonstreamjsonstream(name; the contract says ambiguous)ambiguous_targetpkg:npm/jsonstream@0.0.1jsonstreamonlyJSONStream@1.3.5pkg:npm/JSONStream@1.3.5JSONStreamonlyjsonstream@0.0.1A unit probe of
Target:matches_packageis true andmatches_patchfalse forpkg:npm/Lodash@4.17.21,pkg:gem/Rails@7.0.0,pkg:cargo/Serde@1.0.0,pkg:golang/github.com/Foo/bar@v1.0.0andpkg:maven/Org.X/y@1.0against their lowercase records. Sogetselects an installed package whose patch the same spelling can'tremoveorrollback.Target::parse("logrus").ambiguity([Sirupsen@v1.0.0, sirupsen@v1.8.1])andTarget::parse("jsonstream").ambiguity([JSONStream, jsonstream])both returnNone.rollback <target>selects through the samematches_patch(find_patches_to_rollback), so it reverts both packages' files.Symptoms and impact
removedrops its patch record and, without--skip-rollback, restores its files.rollbackreverts it.getsearches and applies to it.Sirupsenandsirupsenspellings of logrus (a well-known case in older module graphs), npm legacy mixed-case names, and Maven groupIds.Proposed change
utils/target.rs, compare packages byPurlKeyidentity (canonical_base_purlwith the version dropped), not by a blanketto_lowercase:package_identityreturns thePurlKeybase. This deletes its private lowercase + PEP 503 fold.PurlKeybases are equal (inmatches_packageandmatches_patch), the same relation the versioned arm already uses.settlecounts distinctPurlKeyidentities, sojsonstreambesideJSONStreamis refused asambiguous_target. An exact-case full name wins, the way the full name already wins over last-segment matches today.package_spec_matcheskeeps its lenient behaviour forscan --packageand socket.yml, which the contract documents as filters.Targetstops calling it for purl targets. Correct its "npm forbids uppercase" comment.Size and scope
utils/target.rs(~40 production lines changed, plus tests) and possiblypolicy/mod.rs(comment only).scan --package/socket.yml filter semantics;get's API search spelling;PurlKeyfolding change.Acceptance criteria
Target::parse("pkg:npm/jsonstream").matches_patch("pkg:npm/JSONStream@1.3.5", _)is false. The same holds for GoSirupsen/sirupsenand a Maven groupId pair.Target::parse("jsonstream").settle([JSONStream, jsonstream])isErr.Target::parse("JSONStream").settle(...)settles onJSONStreamalone.typing_extensions), NuGet and Composer case/spelling folds still match (existingget <name>doesn't PEP 503-normalise PyPI names, soget typing_extensionsorget ruamel.yamlreports "No packages matching" (exit 0) for an installed, patchable package #926/removeandrollbackdon't PEP 503-normalise PyPI purl identifiers, soremove pkg:pypi/typing_extensions@4.7.1exits 1 "No patch found" whilegetaccepts the same identifier #1024 tests stay green).remove pkg:npm/jsonstream --skip-rollback --jsonon the two-record manifest above removes onlyjsonstream, andremove jsonstream --jsonexits 1 withambiguous_target.cargo test -p socket-patch-core utils::targetplus theget,removeandrollbackCLI suites stay green.Dependencies
PurlKey).