Skip to content

Look up NuGet packages by normalized version in the crawler (#1202) - #1239

Open
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
mainfrom
arch-refactor/1202-nuget-crawler-identity
Open

Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
mainfrom
arch-refactor/1202-nuget-crawler-identity

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Refs #1202 (the crawler slice; #1230 is the PurlKey slice; the formats::nuget move remains).

Summary

NuGet's package identity is the normalized version (1.0.0.0 = 1.0.0 = 1.00.0). The vendored feed, the lock match and upstream restore already use vendor::nuget_feed::normalize_nuget_version, but the NuGet crawler's find_by_purls (agent-mode apply, rollback and the VEX installed lookup) only lowercased. So a purl at @1.0.0.0 never found the global folder's foo/1.0.0/, and a packages.config folder Foo.1.0.0.0/ was invisible to a purl at @1.0.0. This PR routes the crawler's lookup through the same normalizer.

Why (leverage)

What changed

  • find_by_purls_sync: the global layout tries <id lower>/<version lower>/ first (what main tried), then <id lower>/<normalized>/ when that spelling differs. The exact-case legacy <Name>.<Version>/ probe is unchanged.
  • find_legacy_dir_case_insensitive became find_legacy_dir_by_identity, through a new legacy_dir_is: at any . boundary, the id matches case-insensitively and the non-empty version normalizes to the purl's. It's a superset of the old dir.to_lowercase() == "<name>.<version>".to_lowercase() match. The old match runs first over the listing, and the identity match only runs when it finds nothing, so a root holding both foo.1.0.0 and Foo.1.0.0.0 keeps main's pick. The verification gate is unchanged.
  • The rows keep the requested purl, name and version, as before.

Deleted

git diff --stat: production +47/−19 (the lowercase-only global path and target-string match), tests +116/−4 (3 call sites renamed).

Behavior

For spellings that already resolved: none. Every probe main made runs first, in main's order, so the same directory is found, even in a cache holding two spellings of one release (034c189, after the final review: test_find_by_purls_as_written_spelling_wins_over_normalized). The new behavior: a non-normalized version (4-part with a zero revision, zero-padded segments, +build metadata) now finds the package that NuGet considers the same release. Foo@1.0.0.1 is still not 1.0.0, and Foo.Bar.1.0.0 is still not Foo. The test-only oracle.rs keeps main's rule. Its randomized versions are all normalized, so the equivalence test still passes, which shows nothing changes for normalized spellings.

Test evidence

  • Red→green: with the lookup reverted to main's lowercase rule, test_find_by_purls_global_cache_normalizes_version, test_find_by_purls_legacy_layout_normalizes_version and legacy_dir_is_matches_identity_only fail. They pass on the branch. test_find_by_purls_global_cache_as_written_version_still_found pins the fallback to the old path.
  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo test -p socket-patch-core --lib: 5891 passed, plus 4 root-only failures that also fail on main in this sandbox (copy_tree::relax_loop_must_not_traverse_symlinked_root, vlt_heal::an_unremovable_hidden_lock_keeps_every_store_entry, pypi_poetry::wire_write_failure_maps_error_and_leaves_lock_untouched, pypi_requirements::wire_failure_rolls_back_already_written_files).
  • crawler_nuget_e2e 28, e2e_nuget 21, ecosystem_dispatch_e2e 40, in_process_remote_ecosystems_apply 12, in_process_rollback_all_ecosystems 27, in_process_scan 27 and e2e_vex 38: all passed. There's no .NET SDK in the sandbox, so e2e_nuget_dotnet_build runs in CI.

Risk

Low. The change is one lookup function and is strictly additive for spellings that already matched. The fallback listing is only consulted after the direct probes miss, as before.

🤖 Generated with Claude Code

https://claude.ai/code/session_014zP8cfveTMRsL71USbtgAx


Note

Low Risk
Scoped to NuGet directory lookup in find_by_purls; prior probe order is preserved for spellings that already matched, with additive fallbacks only.

Overview
NuGet find_by_purls now resolves packages using NuGet’s normalized version identity, aligning the crawler with the vendored feed and lock matching (normalize_nuget_version).

For the global cache, lookup tries the lowercased PURL version path first, then falls back to the normalized folder (e.g. @1.0.0.0 → foo/1.0.0/). For legacy packages/<Id>.<Version>/, find_legacy_dir_case_insensitive is replaced by find_legacy_dir_by_identity, which still prefers an exact case-insensitive spelling match, then matches folders whose id and normalized version equal the PURL (splitting at every . for dotted ids). As-written spellings still win when both exist; returned rows keep the requested PURL.

Extensive unit tests cover normalization, legacy cross-spelling, and regression guards.

Reviewed by Cursor Bugbot for commit 034c189. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko Mikola Lysenko (mikolalysenko) added arch-refactor PR opened by the scheduled architecture refactor routine refactor Structural change: duplicated code or logic, missing abstraction, layering, dead code labels Oct 9, 2026
Agent-mode apply, rollback and VEX locate a NuGet package's directory
through the crawler's find_by_purls, which keyed versions by lowercase
only. NuGet's identity is the normalized version, so a project pinned
as 1.0.0.0 never found the global folder's foo/1.0.0/, and a
packages.config folder Foo.1.0.0.0/ was invisible to a purl at 1.0.0.

The lookup now goes through the same normalize_nuget_version the
vendored feed and lock match use: the global folder under the
normalized version (then the as-written one), and the legacy folder
fallback by case-insensitive id plus normalized version. Spellings
that already matched still match the same directory.

Refs #1202

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 9, 2026 07:13
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 9, 2026
Assisted-by: Claude Code:claude-opus-5-5

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 9, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at 387935382.

  • CI: every check suite on the head is green (no failures, no main-wide failures).
  • Bugbot: reviewed 387935382, no findings; no open review threads.
  • Mergeable against main (e03a666d), no CHANGELOG changes.
  • Slack announcement: not sent this run (Slack send tool unavailable); next run retries.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does: The NuGet crawler's find_by_purls_sync now looks packages up by NuGet's normalized version, using the same normalize_nuget_version that #1230's PurlKey uses, instead of only lowercasing. In the global cache it probes <id>/<normalized>/ first, then the as-written spelling. In the legacy packages/<Id>.<Version>/ layout, the case-insensitive fallback matches on identity: the id ignoring case, plus a normalized version. So @1.0.0.0 finds foo/1.0.0/.

Risk: low. The change is lookup-only and confined to one function. is_safe_name_version still checks the raw version, and the normalized string is derived from it: it only drops +build, strips leading zeros and lowercases. The verification gate and the direct-probe order are unchanged. It's consistent with #1230 and doesn't duplicate it (#1230 only touched purl_key.rs).

Look here:

Verified:

  • cargo test -p socket-patch-core --lib nuget_crawler: 44 passed, including the oracle-equivalence test. rustfmt --check is clean.
  • git merge-tree against current main is clean. Main hasn't touched this file since the branch point.
  • CI 377/377 green on 3879353 (ci-ok, clippy). Bugbot passed. No CHANGELOG.md change, no unresolved threads.

Changes I made: none.

Open questions (non-blocking):

  • In a degenerate cache holding both foo/1.0.0/ and foo/1.0.0.0/, @1.0.0.0 now resolves to 1.0.0/ where main picked 1.0.0.0/. So the description's "no change for spellings that already resolved" isn't strictly true.
  • Legacy fallback: if case-mismatched foo.1.0.0 and Foo.1.0.0.0 both exist, the pick follows readdir order. Preferring an exact case-insensitive match first would keep main's choice. This is cheap to add in a follow-up if wanted.

Auto-merge is armed: approving sends this straight to the merge queue.


Generated by Claude Code

A cache or packages/ folder holding both spellings of one release
(foo/1.0.0.0/ and foo/1.0.0/) now resolves to the folder main always
picked, the as-written one, and only falls back to the normalized
spelling. The legacy fallback likewise prefers the case-insensitive
<name>.<version> match over an equal release spelled differently.

Refs #1202

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] Both open questions in the final review were right: the description overstated the "no change" claim. Fixed in 034c189 rather than left for a follow-up:

  • Global layout: <id>/<as-written lower>/ is probed first (main's only probe), and <id>/<normalized>/ only after it. With both foo/1.0.0/ and foo/1.0.0.0/, @1.0.0.0 resolves to 1.0.0.0/ as on main.
  • Legacy fallback: main's case-insensitive <name>.<version> match runs first over the listing. The identity match only runs when that match finds nothing.
  • New test: test_find_by_purls_as_written_spelling_wins_over_normalized (both layouts). nuget_crawler lib tests (45), crawler_nuget_e2e (28) and workspace clippy pass. The PR body is updated.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 9, 2026
Assisted-by: Claude Code:claude-opus-5-5

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 034c189. Configure here.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief (updated for 034c189)

What it does: The NuGet crawler's find_by_purls_sync also finds packages by NuGet's normalized version (the same normalize_nuget_version #1230's PurlKey uses). Main's lookups still run first: in the global cache it probes <id>/<as-written lower>/, then <id>/<normalized>/. In the legacy packages/<Id>.<Version>/ layout, the case-insensitive <name>.<version> match runs first, and the identity match (id ignoring case, normalized version) runs only when that finds nothing. So @1.0.0.0 now finds foo/1.0.0/, and every spelling that resolved on main resolves to the same folder.

Risk: low. Lookup-only, confined to one function. is_safe_name_version still checks the raw version and the verification gate is unchanged. Since my previous brief, 034c189 addressed both of its open questions by putting main's probes first.

Look here:

  • nuget_crawler.rs:163-166:`` global-cache probe order (as-written, then normalized)
  • nuget_crawler.rs:333 find_legacy_dir_by_identity (exact case-insensitive spelling first) and :361 legacy_dir_is
  • nuget_crawler.rs:1470:`` new test, both spellings present in both layouts

Verified: reviewed the 3879353..034c189 delta line by line against my earlier open questions. cargo test -p socket-patch-core --lib nuget_crawler: 45 passed. CI 305/305 green on 034c189 (ci-ok, clippy). Bugbot: no findings on this head. No CHANGELOG.md change, no open threads.

Changes I made: none.

Open questions: none.

Auto-merge is armed: approving sends this straight to the merge queue.


Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-refactor PR opened by the scheduled architecture refactor routine Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review refactor Structural change: duplicated code or logic, missing abstraction, layering, dead code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants