Skip to content

remove, rollback and get treat case-distinct packages as one: remove pkg:npm/jsonstream also removes JSONStream's patch #1292

Description

[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:

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

Dependencies

Activity

  1. added
    bugSomething isn't working
    arch-auditFiled by a scheduled architecture audit routine (see the architecture review discussion)
    on Oct 9, 2026
  2. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [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


    Generated by Claude Code

  3. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1294.


    Generated by Claude Code

  4. added a commit that references this issue on Oct 9, 2026
    9c7a1da
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:claimedagent:triagedarch-auditFiled by a scheduled architecture audit routine (see the architecture review discussion)bugSomething isn't workingpriority:p1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions