Skip to content

Compare purls through one PurlKey type - #1045

Merged
Mikola Lysenko (mikolalysenko) merged 11 commits into
mainfrom
arch-fix/purl-key
Oct 8, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 11 commits into
mainfrom
arch-fix/purl-key

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

Problem

Architecture audit §3.B (purl equality). At least six relations answered "do these two purls name the same package release", and they disagreed:

  • purl_eq folded percent-encoding and composer only;
  • purl_identity_key stripped qualifiers and did composer only;
  • two different functions were both named canonical_base_purl (one in vex::discover and one in rollout);
  • policy::canon and package_spec_matches each had their own folding;
  • the ledger had five ownership matchers with different canonicalization (covers_purl, purl_keys_cover, lookup_entry, apply's Go --check set, the hosted_pin_of / hosted_vendored_overlap keys);
  • there were several local normalize_purl(strip_purl_qualifiers(..)) closures used as keys.

The canonical_purl doc comment also claimed an identity it doesn't provide.

User-visible effects:

  • B20: scan --prune / --sync dropped a live manifest entry, and GC'd its blobs, when the API spelled the purl pkg:nuget/Newtonsoft.Json@13.0.3 and the NuGet global-cache crawl reported pkg:nuget/newtonsoft.json@13.0.3. Update detection and redirect-candidate matching missed the same entry. docker_e2e_nuget.rs worked around this by serving a lowercase purl from the fixture. A PEP 503 spelling (typing_extensions vs typing-extensions) was pruned the same way.
  • B73: remove / rollback pkg:pypi/typing_extensions@4.12.2 found nothing when the recorded key was typing-extensions. The same happened for a NuGet case variant.

Related: #553 (the takeover ledger lookup now uses the same key, but there's no e2e for it yet), #748 / C20 (this is the identity half; the builders are untouched), #484 (Go + vs %2B is folded by the key).

Change

  • New utils::purl_key
    • PurlKey is the release identity. It strips qualifiers and subpath, percent-decodes, lowercases the type, PEP 503-folds PyPI names, case-folds NuGet names and versions, case-folds Composer names, and keys Composer versions by release identity (3.0.2 = v3.0.2 = 3.0.2.0).
    • PurlKey::qualified keeps release variants apart.
    • canonical_base_purl moved here. It is the display spelling the key is built from, and VEX product purls still use it.
  • Call sites. Every purl identity comparison in core and the CLI now builds PurlKeys (the exceptions are listed under Deferred):
    • ledger ownership;
    • prune GC;
    • update detection;
    • redirect candidates;
    • lockfile-only checks (one predicate; the duplicate notInstalled check now calls it);
    • takeover ledger drop;
    • hosted pins;
    • VEX source matching;
    • rollout budget;
    • policy reports;
    • get presence;
    • Go-patches orphan prune;
    • vlt heal targets;
    • Gradle pin rows;
    • the hosted memory engine's batch-search owner map, lock_inventory::lookup, the vendored blob harvest, apply's mismatch-blob record filter, rollback's superseded_by_hosted, and the rollout RecordedIndex (review round, see below).
  • Vendored key sets are now HashSet<PurlKey>, so the compiler routes every construction through the key.
  • package_spec_matches compares the folded key case-insensitively. User filter specs stay as lenient as before, and a versioned Composer spec still matches by exact release identity.
  • canonical_purl keeps only its display role. Its doc no longer claims identity.
  • Docker e2e: docker_e2e_nuget.rs now serves the API's real mixed-case spelling for the patch, and scan --sync must keep that entry.

Duplicate copies deleted

Purl equality relations went from 11 to 1 (PurlKey, with canonical_base_purl as its display spelling). Deleted:

  • utils::purl::purl_eq
  • composer_version::{purl_identity_key, composer_purl_identity, composer_purls_equivalent, composer_bases_equivalent}
  • rollout::canonical_base_purl
  • policy::canon
  • vex::discover::{canonical_base_purl, same_package}
  • rollout::stage::qualified_key (a copy of PurlKey::qualified)
  • lock_inventory::lookup's own PEP 503 / exact-version matcher
  • vex_sources::same_package
  • policy's canonical_pypi_purl

The ledger matchers (covers_purl, purl_keys_cover, lookup_entry_kv) are now one-line PurlKey comparisons. The ad-hoc key closures in scan/mod.rs, ledgers.rs, list.rs, hosted.rs, get.rs and apply.rs are gone.

Behavior notes

  • Policy filtered[] / retained[] purls and rollout base_purls are the PurlKey spelling, so PyPI and NuGet names there now appear folded. Composer already showed its identity form.
  • VEX output is unchanged except for one case: a Composer dev- branch name keeps its case in canonical_base_purl. Before this PR it was lowercased.

Review round (rebased onto main @ 431b818)

Core API break (for the release notes; CHANGELOG untouched)

The following socket-patch-core public items were removed or moved:

  • utils::purl::purl_eq
  • vex::canonical_base_purl (re-export) and rollout::canonical_base_purl; use utils::purl_key::canonical_base_purl
  • composer_version::{composer_purl_identity, purl_identity_key, composer_purls_equivalent}
  • rollout::stage::qualified_key

These signatures changed from HashSet<String> to HashSet<PurlKey>:

  • vendor::vendored_purl_keys
  • VendorState::purl_keys
  • vendor::purl_keys_cover

lock_inventory::lookup now also matches composer padding and NuGet/composer name case. Nothing else in the workspace references the removed items.

Testing

Failing first: on main plus only the new tests, all three regression tests failed. On this branch they pass.

  • cargo test -p socket-patch-core --lib -- folds_pep503_and_nuget_case nuget_case_spellings_are_one_vendored_package: 2 failed.
  • cargo test -p socket-patch-cli --lib -- detect_prunable_keeps_case_and_pep503: failed. It pruned both Newtonsoft.Json and typing_extensions.

New tests:

Commands run on this branch (macOS):

  • cargo test -p socket-patch-core -p socket-patch-cli --lib
  • cargo test -p socket-patch-core --tests --no-fail-fast: all 36 integration suites pass. The lib failure is the same pre-existing one.
  • cargo test -p socket-patch-cli with these integration tests: scan, remove, rollback, apply, get, vendor, cli, e2e_scan, e2e_nuget, e2e_composer, e2e_composer_version_identity, e2e_socket_yml_policy, scan_rollout_e2e, hosted_memory_{rollout,engine,parity}, covgap_commands_{scan_mod,scan_hosted,rollback,get,vendor,vex}, global_scope_project_state, in_process_{remove_repair_lifecycle,rollback_vendored,vendor}, e2e_vex, e2e_vex_vendor, e2e_vex_redirect, e2e_embedded_vex, coverage_fix_scan_hosted_dryrun_vendored. All passed.
  • cargo clippy -p socket-patch-core -p socket-patch-cli --all-targets -- -D warnings: no findings in any touched file.
    • The local clippy 1.93 reports pre-existing lints in untouched files, such as jvm_jar.rs, prebuilt_common/mod.rs and a macOS-only unused variable in python_crawler.rs.
    • I rustfmt'd only the changed hunks. Files that were already unformatted on main keep their old formatting.
  • After the rebase: focused unit tests for purl, gc, state, policy and rollout pass.
  • After the review-round rebase and fixes (macOS):
    • cargo build -p socket-patch-core -p socket-patch-cli --all-targets
    • cargo clippy -p socket-patch-core -p socket-patch-cli --all-targets: 0 findings in touched files. The pre-existing lints in untouched files listed above remain.
    • cargo test -p socket-patch-core --lib: 5641 passed (the digest failure is gone now that Fix main CI red on stale digest pending-list entries #1016 is on main).
    • cargo test -p socket-patch-cli --lib --test scan --test vendor --test rollback --test apply --test get --test remove --test gradle_agent_cli --test in_process_rollback_vendored --test in_process_vendor: all passed (lib 872, scan 118, vendor 91, rollback 38, apply 106, get 81, remove 94 (incl. Fix remove/rollback missing PyPI name spellings (#1024) #1025's PyPI spelling tests), gradle_agent_cli 34, in_process_rollback_vendored 16, in_process_vendor 121).
    • New regression tests: batch_search_credits_a_respelled_response_only_to_its_asker, lookup_matches_by_purl_identity, overlap_reports_one_entry_per_release_across_spellings, covers_every_spelling_of_the_release (unused_vendored_manifest_keys), and sentinel_free_key_keeps_rejected_spellings_apart. The first three fail against the pre-fix code: a spelling-keyed owner falls back to every chunk root, exact-version lookup misses 3.0.2.0, and spelling dedup gives 4 entries.
  • Not run locally: docker_e2e_nuget (Linux/Docker). It is the end-to-end proof for B20 (mixed-case manifest key against the lowercase global-cache crawl, through scan --sync, GC, apply and agent VEX). It must pass in CI before this lands.

Deferred

  • Name-level PyPI comparisons inside lock parsers (canonicalize_pypi_name(a) == canonicalize_pypi_name(b)) are left alone. They compare names, not purls.
  • Same-source qualifier grouping in vendor.rs / apply.rs (variant_groups, vendored_bases over manifest keys) is left as-is, since both sides already share one spelling.
  • Moving Ecosystem and canonicalize_pypi_name to core (E39 / Move canonicalize_pypi_name and the PEP 508 name scanner out of crawlers and vendor into one PyPI name module #883) is a separate refactor.
  • The four target grammars (scan --package, get fuzzy, remove/rollback, UUID shortcut) are untouched.
  • Left as literal spelling comparisons, deliberately:
    • get.rs's failure dedup on normalize_purl is display-only.
    • ecosystem_dispatch::merge_qualified matches a crawler's echo of the exact base purl it was asked for, so both sides are the same string.
    • The Gradle/Maven scan keys use canonical_base_purl. Maven coordinates are case-sensitive, so the canonical spelling is the identity, and the lock-file GAVs they are checked against are built as strings.
  • The vendored blob harvest matches PurlKey::new(base_purl), not lookup_entry. It keeps the old qualifier-insensitive match for manifest keys, whereas lookup_entry is qualifier-sensitive.
  • No maintainer decision was needed.

🤖 Generated with Claude Code


Note

Medium Risk
Wide refactor of identity comparisons across apply, scan, GC, rollback, and policy paths; behavior changes are intentional but any missed call site could still mis-match or over-prune patches.

Overview
Introduces PurlKey as the single release-identity for package URLs (encoding, qualifiers, NuGet/PyPI/Composer spelling variants) and routes prune GC, lockfile-only detection, vendor ownership, rollout, policy, VEX, and hosted batch search through it instead of a dozen overlapping helpers (purl_eq, policy::canon, duplicate canonical_base_purl, composer purl identity helpers, etc.).

User-visible fixes: scan --prune/--sync no longer drops live manifest entries when the API uses mixed-case NuGet or alternate PyPI spellings vs the crawl; remove/rollback identifiers now match those recorded keys. Vendored key sets become HashSet<PurlKey>; dry-run prune preview shares wet vendor GC manifest-key logic via unused_vendored_manifest_keys; hosted memory batch search credits respelled API purls only to the asking root.

Several removed/moved socket-patch-core public APIs (purl_eq, old canonical_base_purl locations, composer identity exports); vendored helpers now take HashSet<PurlKey>.

Reviewed by Cursor Bugbot for commit daa5ef8. Configure here.


Generated by Claude Code

@mikolalysenko Mikola Lysenko (mikolalysenko) added the arch-refactor PR opened by the scheduled architecture refactor routine label Oct 7, 2026
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
Review of #1045 found purl equality checks that still bypassed the key:

- hosted memory batch_search matched response packages to their asking
  roots by spelling, so a `Newtonsoft.Json` / `typing_extensions` /
  composer `@3.0.2.0` answer fell back to every root in the chunk;
- lock_inventory::lookup had its own matcher (PEP 503 only, exact
  version), so a composer API purl `@3.0.2.0` missed the lock's `3.0.2`;
- the vendored blob harvest and apply's mismatch-blob record filter
  compared qualifier-stripped strings;
- rollback's superseded_by_hosted (new on main) used the deleted
  canonical_base_purl / composer_purls_equivalent pair;
- rollout::stage::qualified_key duplicated PurlKey::qualified (deleted);
  RecordedIndex now keys by PurlKey;
- Gradle scan keys use canonical_base_purl (Maven is case-sensitive, so
  the canonical spelling is the identity there).

Regression tests: a mixed-case NuGet batch answer is credited only to
its asker; lookup matches composer padding and PEP 503 spellings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 7, 2026 16:47
One type answers "do these two purls name the same package release":
qualifiers and subpath stripped, components percent-decoded, the type
lowercased, PyPI names PEP 503-folded, NuGet names and versions
case-folded, Composer names case-folded and Composer versions keyed by
release identity (3.0.2 = v3.0.2 = 3.0.2.0). PurlKey::qualified keeps
release variants apart. canonical_base_purl (moved here from
vex::discover) stays the display spelling the key is built from.

Property tests check that every spelling variant of a release shares
one key, that no other release does, and that the Composer half equals
release equivalence over the whole shared vector file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Six "same package" relations disagreed, so a NuGet or PyPI spelling
difference between the API purl, the crawl and the ledger could prune a
live manifest entry, skip a takeover or miss a remove target. Every one
now compares PurlKeys, and the copies are gone:

- deleted utils::purl::purl_eq, composer_version::{purl_identity_key,
  composer_purl_identity, composer_purls_equivalent,
  composer_bases_equivalent}, rollout::canonical_base_purl,
  policy::canon, vex::discover::{canonical_base_purl, same_package},
  vex_sources::same_package and policy's canonical_pypi_purl;
- the ledger matchers (covers_purl, purl_keys/purl_keys_cover,
  lookup_entry_kv), the hosted/vendored overlap, the hosted pin lookup,
  apply --check's Go set, the takeover ledger drop and every local
  normalize_purl(strip_purl_qualifiers(..)) closure used as a key now
  build PurlKeys; vendored key sets are HashSet<PurlKey>.

Fixes B20: scan --prune no longer GCs a NuGet entry the API spelled
Newtonsoft.Json while the global-cache crawl reports newtonsoft.json
(update detection and redirect candidates use the same key). Fixes B73:
remove/rollback identifiers fold PEP 503 and NuGet case. canonical_purl
keeps only its display role and its doc no longer claims identity.

Policy filter specs stay case-insensitive (they compare the folded key
lowercased), and policy/rollout report purls are the PurlKey spelling,
so PyPI/NuGet names there appear folded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The wiremock fixture had to serve the crawler's lowercase purl so scan's
GC pass would not prune the manifest entry. Serve the API's real
mixed-case Newtonsoft.Json spelling for the patch instead; scan --sync
must now keep it, and agent VEX names the manifest key's spelling.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review of #1045 found purl equality checks that still bypassed the key:

- hosted memory batch_search matched response packages to their asking
  roots by spelling, so a `Newtonsoft.Json` / `typing_extensions` /
  composer `@3.0.2.0` answer fell back to every root in the chunk;
- lock_inventory::lookup had its own matcher (PEP 503 only, exact
  version), so a composer API purl `@3.0.2.0` missed the lock's `3.0.2`;
- the vendored blob harvest and apply's mismatch-blob record filter
  compared qualifier-stripped strings;
- rollback's superseded_by_hosted (new on main) used the deleted
  canonical_base_purl / composer_purls_equivalent pair;
- rollout::stage::qualified_key duplicated PurlKey::qualified (deleted);
  RecordedIndex now keys by PurlKey;
- Gradle scan keys use canonical_base_purl (Maven is case-sensitive, so
  the canonical spelling is the identity there).

Regression tests: a mixed-case NuGet batch answer is credited only to
its asker; lookup matches composer padding and PEP 503 spellings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- `scan --prune --dry-run` previewed the vendor GC's manifest drop with a
  qualifier-strip relation while the wet pass used PurlKey, so NuGet case,
  PEP 503 and composer padding variants were pruned for real but not in
  the preview. Both now call vendor::unused_vendored_manifest_keys.
- LockfileSupplement.purls is a HashSet<PurlKey> built once, so the
  lockfile-only predicate is one hash lookup instead of re-keying the
  whole set on every miss.
- Ledgers::hosted_vendored_overlap deduplicates by PurlKey, so two
  spellings of one release give one takeover warning.
- get's hosted-claim set (rebased onto #940's new code) is a
  HashSet<PurlKey>; the new gem takeover pin lookup uses PurlKey::same.
- composer_version: pin that the sentinel-free key PurlKey uses never
  lets a rejected spelling collide with an accepted one, and document it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Lockfile-only skips installed spelling variants
    • Changed lockfile_supplement to use PurlKey normalization for exclusion checks, matching the membership check behavior and preventing spelling variants from being incorrectly classified as not installed.

Create PR

Or push these changes by commenting:

@cursor push aae44b9bf5
Preview (aae44b9bf5)
diff --git a/crates/socket-patch-cli/src/commands/scan/discovery.rs b/crates/socket-patch-cli/src/commands/scan/discovery.rs
--- a/crates/socket-patch-cli/src/commands/scan/discovery.rs
+++ b/crates/socket-patch-cli/src/commands/scan/discovery.rs
@@ -75,7 +75,7 @@
     if entries.is_empty() {
         return out;
     }
-    let crawled_purls: HashSet<&str> = crawled.iter().map(|p| p.purl.as_str()).collect();
+    let crawled_purls: HashSet<PurlKey> = crawled.iter().map(|p| PurlKey::new(&p.purl)).collect();
     let in_scope = |purl: &str| {
         only.is_none_or(|list| {
             socket_patch_core::crawlers::Ecosystem::from_purl(purl)
@@ -83,7 +83,7 @@
         })
     };
     for entry in entries {
-        if crawled_purls.contains(entry.purl.as_str()) || !in_scope(&entry.purl) {
+        if crawled_purls.contains(&PurlKey::new(&entry.purl)) || !in_scope(&entry.purl) {
             continue;
         }
         let Some(pkg) = crawled_from_purl(&entry.purl, &common.cwd) else {

You can send follow-ups to the cloud agent here.

Comment thread crates/socket-patch-cli/src/commands/scan/discovery.rs Outdated
Comment thread crates/socket-patch-cli/src/commands/vendor.rs
lockfile_supplement excluded installed packages by literal purl but
keyed the remainder by PurlKey, so a lock spelling that differed from
the crawl only in NuGet case, PEP 503 form or composer padding was
recorded as lockfile-only and lockfile_only_contains then flagged the
live install package_not_installed (agent apply skip, vendor baseline
pre-verify, [NOT INSTALLED] marker). Exclude crawled packages by
PurlKey, the same relation the lookup uses, via a testable
lockfile_only_packages helper.

unused_vendored_manifest_keys only keyed the ledger key, but a golang
ledger key can keep the module proxy's !x case encoding while the
manifest holds the decoded spelling, which PurlKey does not bridge. The
earlier wet GC also matched entry.base_purl; restore that through
VendorEntry::covers_purl in both the wet GC and the scan --prune
--dry-run preview so a reverted !burnt!sushi entry no longer leaves its
BurntSushi manifest record and blobs behind.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@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 daa5ef8. Configure here.

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

Copy link
Copy Markdown
Collaborator Author

[agent] Ready for review at daa5ef8bfba15b90700b40b8680235601206098f.

  • CI: required ci-ok green. 527 success / 6 skipped / 0 failing; 30 non-required macOS/Windows legs still queued or running. yarn-berry 4.1.0 (ubuntu) failed on attempt 1 with a corepack yarn@2.4.3 unavailable fetch race (a sibling yarn@2.4.3 test passed 0.6s later) and passed on re-run.
  • Bugbot reviewed daa5ef8: no unresolved findings. No open review threads.
  • Mergeable, no conflicts.
  • Reviewer focus: PurlKey equality and normalization, which now backs every purl comparison.

Generated by Claude Code

@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Oct 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 8, 2026
Resolve conflicts against main's rustfmt pass and the containment-helper
refactor (#1042): keep main's formatting, drop the removed `canon` /
composer-identity helpers in favour of PurlKey, and route main's new
apply-failure purl matching in get.rs (#955) through PurlKey::qualified /
PurlKey::same so it agrees with the rest of the purl identity.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pick up #1093 so PR CI runs without the macOS legs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Oct 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 8, 2026
Resolve conflicts with #1035 (supersede lifecycle), #1038 (shared repo-root
walk), and #1033 (unwired VEX copies):

- #1035 added `vex::discover::same_release` and a second
  `canonical_base_purl` there. `PurlKey::same` already treats composer
  version spellings as one release, so every `same_release` call
  (vex_sources, rollback, discover, and `ledgers::hosted_pins_matching`)
  now uses `PurlKey::same`, and the duplicate helpers are dropped.
- Both sides' new ledgers tests are kept.
- The repo-root lookup in `policy` takes main's `utils::repo_root` version.
  `canonical_pypi_purl` stays deleted (nothing calls it now).
- `vex` re-exports main's `UnattestedKind`. `UnwiredCopy::covers` compares
  purls with `PurlKey::same` rather than raw string equality.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Oct 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 8, 2026
Resolve the apply `--check` conflict with #1029. Main hoisted the vendored
key set into `run_check`'s outer scope (`vendored_purl_keys`, already a
`PurlKey` set via `purl_keys_cover`). The Go redirect check now reuses that
set instead of loading the vendor ledger a second time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Upstream moved setup-php's `v2` tag to d52fc211, so the `# v2` comment on
the f3e473d1 pin no longer matches. zizmor's ref-version-mismatch now
fails the org-required "Audit GitHub Actions" check on every PR. f3e473d1
is tag 2.37.2, the label composer-compatibility.yml already uses. This is
the same one-line change as #1118, carried here so this PR can merge;
whichever lands first, the other merges cleanly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 628542d Oct 8, 2026
456 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the arch-fix/purl-key branch October 8, 2026 10:30
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 8, 2026
Resolve conflicts with #1038 (shared repo-root walk, Option m2_repo) and
#1045; route #1035's superseded-checksum path and the hosted engine's
Gradle root files through vendor::jvm::layout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 8, 2026
Resolve the hosted.rs conflict with #1045: this branch moved the
takeover out of hosted.rs into hosted/takeover.rs, so keep that layout
and port #1045's change there. The takeover's ledger drop now compares
PurlKeys and the local canonical_purl key helper is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 8, 2026
Resolve get.rs and scan/vendor_flow.rs against #1045's PurlKey: the
hosted takeover set is now a HashSet<PurlKey> shared by the fetch gate
and the dry-run preview.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 8, 2026
get.rs: keep this branch's split (the download engine lives in
agent_download.rs, short_uuid tests in ui/text.rs) and carry main's
changes to the moved code over to agent_download.rs: store_verified_blob
in write_blob_entry (#726), PurlKey comparisons in lock_text_refusals_for
/ apply_key_covers / fold_apply_failures (#1045), and the base64 engine.
main's new get.rs tests (verified blob writes, linked blob dirs, base64
tolerance, the pnpm-lock FIFO guard) are kept.
list.rs: keep ui::sentence_case (main only reformatted the line).
scan/hosted.rs: add main's yarn_classic_outer closure (#1083).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 8, 2026
Conflicts resolved:
- vex discover: main's UnattestedKind (#1033) replaces this branch's
  parallel UnattestedWhy; the #899 shrinkwrap case becomes
  UnattestedKind::NpmShrinkwrapOnly, mapped through vex_sources'
  unattested_note like the other kinds.
- scan/policy.rs: the #812 shared-copy keys use PurlKey (#1045) instead
  of the removed canon().
- CLI_CONTRACT.md: main's contested/unattested bullets, plus this
  branch's #828 withheld-ref sentence, #899 shrinkwrap bullet and npm
  warning rows.
- redirect npm.rs / scan mod.rs: imports and key types from both sides.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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

Projects

None yet

3 participants