feat(pnpr): store hosted packages in an S3-compatible object store#12198
Conversation
The hosted store is pnpr's source of truth — published packages plus static-served content. It can now live in an S3-compatible object store instead of a local directory, so the durable data is replicated by the provider and can be shared by several stateless pnpr replicas. Any S3-compatible endpoint works: AWS S3, Cloudflare R2, MinIO, Backblaze B2, etc. — configured via the new YAML `s3:` block. The disposable proxy cache and the install-accelerator SQLite stores always stay on local disk; only the hosted store is pluggable. Implemented with the `object_store` crate behind a `HostedStore` backend enum; the local `tokio::fs` path remains the default.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📜 Recent review details⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
📝 WalkthroughWalkthroughAdds pluggable hosted storage (Fs or S3) via object_store, implements an S3-compatible backend with staging/upload/read/delete/list operations, refactors Storage and server handlers to use the hosted abstraction, extends config parsing and error mapping, and adds unit and integration tests plus docs/examples. ChangesS3-backed hosted package storage
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Micro-Benchmark ResultsLinux |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #12198 +/- ##
==========================================
+ Coverage 87.70% 87.80% +0.10%
==========================================
Files 271 273 +2
Lines 31157 31418 +261
==========================================
+ Hits 27326 27587 +261
Misses 3831 3831 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Review Summary by QodoAdd S3-compatible object store backend for hosted packages
WalkthroughsDescription• Add S3-compatible object store backend for hosted packages - Supports AWS S3, Cloudflare R2, MinIO, Backblaze B2, Wasabi - Configured via new YAML s3: block with bucket, region, endpoint, credentials • Implement pluggable HostedStore enum routing between local filesystem and S3 - Local tokio::fs remains default; S3 backend optional - Disposable proxy cache and install-accelerator stores stay on local disk • Refactor storage layer to support streaming responses from S3 - open_tarball now returns Body instead of fs::File - Enables direct streaming from object store without buffering • Update search endpoint to work with both filesystem and S3 backends - Abstracts package name listing through storage interface Diagramflowchart LR
Config["Config with s3: block"]
Builder["build_s3_store"]
Store["Arc<dyn ObjectStore>"]
HostedStore["HostedStore enum"]
Storage["Storage layer"]
Publish["Publish flow"]
Serve["Serve flow"]
Config -->|S3Settings| Builder
Builder -->|creates| Store
Store -->|wraps in| HostedStore
HostedStore -->|powers| Storage
Storage -->|stages to| Publish
Storage -->|streams from| Serve
File Changes1. pnpr/crates/pnpr/src/s3.rs
|
Integrated-Benchmark Report (Linux)Each scenario has pacquet rows (direct install) and pnpr rows (the same client through the pnpr install accelerator), so pnpr@HEAD vs pacquet@HEAD is the pnpr-vs-direct ratio. Cold-store scenarios wipe the client store between runs (warm server); hot-store scenarios keep it warm. The pacquet@HEAD rows feed the pacquet Bencher testbed; the pnpr@HEAD rows feed the pnpr testbed. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.80564815002,
"stddev": 0.04884466138189717,
"median": 4.80399958812,
"user": 2.4446072600000006,
"system": 3.8464404199999995,
"min": 4.73288706212,
"max": 4.87763886012,
"times": [
4.87763886012,
4.73288706212,
4.84077648712,
4.80998039912,
4.73911187312,
4.84440695012,
4.77345121012,
4.7861900971199995,
4.85401978412,
4.79801877712
]
},
{
"command": "pacquet@main",
"mean": 4.78676312422,
"stddev": 0.027809225048604922,
"median": 4.7879147156199995,
"user": 2.46610446,
"system": 3.8330495199999994,
"min": 4.75122529112,
"max": 4.82353220612,
"times": [
4.82353220612,
4.79074284012,
4.8219296391199995,
4.75122529112,
4.7584788401199996,
4.81935382112,
4.77087999212,
4.78994158112,
4.7556591811199995,
4.78588785012
]
},
{
"command": "pnpr@HEAD",
"mean": 2.0634164880199997,
"stddev": 0.061859396836831,
"median": 2.04537183612,
"user": 2.57845016,
"system": 3.3561240199999993,
"min": 1.99385978812,
"max": 2.17658855712,
"times": [
2.11019388212,
2.1099351031199998,
2.04979142012,
1.99890540812,
2.03199136312,
1.99385978812,
2.17658855712,
2.00357290512,
2.11837420112,
2.04095225212
]
},
{
"command": "pnpr@main",
"mean": 2.06720349182,
"stddev": 0.07873498945466377,
"median": 2.05355439662,
"user": 2.5900123600000002,
"system": 3.3775390200000004,
"min": 1.94992998912,
"max": 2.1668633541199998,
"times": [
2.16618770412,
2.06283809412,
2.1497515531199998,
2.1668633541199998,
2.03727592512,
1.98829569212,
1.99230893412,
1.94992998912,
2.04427069912,
2.11431297312
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6704196505000001,
"stddev": 0.03433174919749147,
"median": 0.6570707896000001,
"user": 0.3670496,
"system": 1.3181934199999998,
"min": 0.6433220311000001,
"max": 0.7537090391000001,
"times": [
0.7537090391000001,
0.6906944351000001,
0.6928643991000001,
0.6558626561,
0.6434379651000001,
0.6537400841000001,
0.6582789231,
0.6449935131000001,
0.6672934591,
0.6433220311000001
]
},
{
"command": "pacquet@main",
"mean": 0.6890553167,
"stddev": 0.0930749560618876,
"median": 0.6556960201,
"user": 0.3691809999999999,
"system": 1.32927332,
"min": 0.6399807751000001,
"max": 0.9434773991000001,
"times": [
0.7290722361,
0.6731732211,
0.6565337391,
0.6481745401000001,
0.6441295181000001,
0.6436425141000001,
0.6575109231,
0.6399807751000001,
0.6548583011000001,
0.9434773991000001
]
},
{
"command": "pnpr@HEAD",
"mean": 0.7011619382000001,
"stddev": 0.09970851184965053,
"median": 0.6602271921,
"user": 0.36648610000000004,
"system": 1.33243652,
"min": 0.6531641741,
"max": 0.9698415081,
"times": [
0.7611105821,
0.6595958541000001,
0.6531641741,
0.6577103551000001,
0.6555222081000001,
0.6692738191000001,
0.6587508481000001,
0.6608585301000001,
0.6657915031,
0.9698415081
]
},
{
"command": "pnpr@main",
"mean": 0.7401420407000001,
"stddev": 0.054538695968646175,
"median": 0.7339349991,
"user": 0.3681476,
"system": 1.3292796199999999,
"min": 0.6668846711,
"max": 0.8427746941,
"times": [
0.7806623991,
0.7169659541000001,
0.6668846711,
0.6844087951000001,
0.7088693261000001,
0.7660590131,
0.7509040441,
0.6982828661,
0.7856086441000001,
0.8427746941
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.1855193533399997,
"stddev": 0.0318556954554585,
"median": 2.1767711825399996,
"user": 3.45891652,
"system": 3.03561974,
"min": 2.15799538854,
"max": 2.26139652954,
"times": [
2.15799538854,
2.17712823854,
2.16491595354,
2.16062259054,
2.22043938654,
2.1731302315399996,
2.1764141265399997,
2.17870480154,
2.18444628654,
2.26139652954
]
},
{
"command": "pacquet@main",
"mean": 2.17818929254,
"stddev": 0.016071231767408545,
"median": 2.17996101854,
"user": 3.47791682,
"system": 3.03208334,
"min": 2.1515325285399998,
"max": 2.2082254945399997,
"times": [
2.18456788054,
2.1515325285399998,
2.1814369975399996,
2.17848503954,
2.1684849055399997,
2.1763658745399996,
2.19037291054,
2.1586182095399997,
2.18380308454,
2.2082254945399997
]
},
{
"command": "pnpr@HEAD",
"mean": 2.1619777562399998,
"stddev": 0.020047158612001046,
"median": 2.1653248105399996,
"user": 3.44574582,
"system": 3.02637504,
"min": 2.1322400875399996,
"max": 2.1900016345399997,
"times": [
2.18129175654,
2.1900016345399997,
2.16085043854,
2.1322400875399996,
2.1759943275399998,
2.1608204925399996,
2.13589639454,
2.16979918254,
2.17336078154,
2.13952246654
]
},
{
"command": "pnpr@main",
"mean": 2.1654561267399997,
"stddev": 0.02128877021871887,
"median": 2.16710229054,
"user": 3.45134422,
"system": 3.0440378399999997,
"min": 2.1305836345399998,
"max": 2.18997880454,
"times": [
2.15679469654,
2.1305836345399998,
2.17988656654,
2.1327576165399997,
2.16042184354,
2.1608505715399997,
2.18936796554,
2.18997880454,
2.1733540095399997,
2.1805655585399997
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.32088156424,
"stddev": 0.018766479049382223,
"median": 1.3171620762399998,
"user": 1.4690491399999999,
"system": 1.6891475799999998,
"min": 1.29418864774,
"max": 1.35049332074,
"times": [
1.3050280557399998,
1.30794161874,
1.32130472574,
1.34993834774,
1.35049332074,
1.30953043074,
1.33019247174,
1.32717859674,
1.31301942674,
1.29418864774
]
},
{
"command": "pacquet@main",
"mean": 1.3730502216400002,
"stddev": 0.04291819858732526,
"median": 1.36851893324,
"user": 1.4437517399999997,
"system": 1.7672260799999997,
"min": 1.32903401474,
"max": 1.4814240937399998,
"times": [
1.32903401474,
1.34420649174,
1.3441823957399999,
1.34611279774,
1.4814240937399998,
1.37567969474,
1.37823085974,
1.38971156774,
1.3805621287399998,
1.36135817174
]
},
{
"command": "pnpr@HEAD",
"mean": 1.32163373774,
"stddev": 0.048633007696523456,
"median": 1.31030552524,
"user": 1.4329523399999997,
"system": 1.7003504799999998,
"min": 1.28486711474,
"max": 1.4541960567399999,
"times": [
1.32355631974,
1.3046733907399999,
1.3257471807399999,
1.4541960567399999,
1.3159376597399999,
1.3174436597399999,
1.30111375674,
1.30145091174,
1.28486711474,
1.2873513267399999
]
},
{
"command": "pnpr@main",
"mean": 1.3296497300399999,
"stddev": 0.05508079165859697,
"median": 1.30724301574,
"user": 1.4256969399999997,
"system": 1.7202080800000001,
"min": 1.28473345474,
"max": 1.46850174774,
"times": [
1.3507080887399998,
1.30816356574,
1.34955937374,
1.46850174774,
1.29221042174,
1.29016989474,
1.34667477574,
1.3063224657399999,
1.28473345474,
1.29945351174
]
}
]
} |
|
| Branch | pr/12198 |
| Testbed | pacquet |
🚨 1 Alert
| Benchmark | Measure Units | View | Benchmark Result (Result Δ%) | Upper Boundary (Limit %) |
|---|---|---|---|---|
| isolated-linker.fresh-restore.cold-cache.cold-store | Latency seconds (s) | 📈 plot 🚷 threshold 🚨 alert (🔔) | 4.81 s(+66.03%)Baseline: 2.89 s | 3.47 s (138.36%) |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 2,185.52 ms(-4.48%)Baseline: 2,287.98 ms | 2,745.58 ms (79.60%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,320.88 ms(-7.95%)Baseline: 1,434.92 ms | 1,721.90 ms (76.71%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold 🚨 view alert (🔔) | 4,805.65 ms(+66.03%)Baseline: 2,894.46 ms | 3,473.35 ms (138.36%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 670.42 ms(-0.27%)Baseline: 672.25 ms | 806.69 ms (83.11%) |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pnpr/crates/pnpr/src/server.rs (1)
568-573:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftDon't proxy past authoritative tarball read failures.
If
storage.open_tarball()fails because the hosted backend is unavailable, this branch logs and then continues into the upstream fetch path. For a package/version published here that also exists upstream, clients can get the upstream tarball instead of a 5xx, which breaks the hosted-store source-of-truth guarantee and can silently swap bytes/checksums. Please make hosted-store read errors terminal and only fall through on a definite miss; if cached read errors should stay soft,Storage::open_tarballneeds to distinguish hosted errors from cache errors.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pnpr/crates/pnpr/src/server.rs` around lines 568 - 573, The match on state.inner.storage.open_tarball(&name, filename) currently logs Err(err) and then falls through to the upstream fetch; instead make storage read errors terminal: in the Err branch (inside the request handler where open_tarball is matched) log the error and immediately return an appropriate 5xx tarball error response (e.g., internal server error / tarball error response) rather than continuing to attempt upstream fetch or returning a success; if you prefer softer behavior for cache-only errors, then modify Storage::open_tarball to return a distinct error variant for cache misses vs hosted-store failures and handle them differently here (only fall through on the definite None miss, not on Err).
🧹 Nitpick comments (2)
pnpr/crates/pnpr/tests/s3_backend.rs (1)
33-73: ⚡ Quick winAdd one S3-backed
/-/v1/searchassertion to this E2E.This test proves publish/packument/tarball wiring, but the new cross-file contract here is also
Storage::hosted_package_names()→run_local_search()→ the search handler. Exercising that route in the same object-store setup would catch a bucket-backed listing regression that this test currently misses.Based on learnings: "Port relevant pnpm tests to Rust tests whenever they translate when porting behavior from pnpm."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pnpr/crates/pnpr/tests/s3_backend.rs` around lines 33 - 73, The test publishes_to_and_serves_from_the_object_store misses exercising the hosted-package listing path (Storage::hosted_package_names() → run_local_search() → search handler). After publishing and before the tarball GET, add a GET request to the search endpoint ("/-/v1/search" or "/-/v1/search?text=mypkg" as your handler expects) against the same app instance and assert the response is StatusCode::OK and that the returned JSON includes "mypkg" (or the expected packument/entry indicating hosted packages). Use the same app/router and object-store setup and include Authorization if the search handler requires it so this E2E covers bucket-backed listing.pnpr/crates/pnpr/src/s3/tests.rs (1)
101-113: ⚡ Quick winAdd a regression test for sibling-prefix collisions in package-name listing.
Please add a case where the bucket contains keys under a similar-but-different prefix (for example, configured
packages/plus straypackages2/...) and assertlist_package_namesexcludes those entries.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pnpr/crates/pnpr/src/s3/tests.rs` around lines 101 - 113, Update the test lists_hosted_package_names to add keys under a similar-but-different prefix (e.g., create objects under "packages2/" or use store_with_prefix that simulates prefix "packages2") in addition to the normal "packages/" keys, using the same helpers (upload, write_packument) so the bucket contains stray entries; then call store.list_package_names() and assert the result does not include those stray "packages2/..." entries (i.e., only contains "`@scope/thing`" and "is-positive"), ensuring sibling-prefix collisions are excluded by list_package_names.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pnpr/crates/pnpr/src/s3.rs`:
- Around line 215-223: The listing can return keys outside self.prefix because
you fallback to the full key with unwrap_or(key); in the method that builds the
listing (calling self.store.list(scope.as_ref()) and iterating
listing.next().await) ensure you only accept keys that actually start with the
configured prefix: either construct the scope with a trailing '/' via
ObjectPath::from(self.prefix.trim_end_matches('/').to_string()) + "/" or, more
simply, after getting key = meta.location.as_ref() call
key.strip_prefix(self.prefix.as_str()) and if that returns None continue the
loop (skip non-matching keys) instead of using unwrap_or(key); then apply
rest.strip_suffix(&format!("/{PACKUMENT_FILE}")) to extract the package name.
In `@pnpr/crates/pnpr/src/storage.rs`:
- Around line 462-470: The search-walk currently masks I/O errors by using
fs::try_exists(...).await.unwrap_or(false) and if let Ok(mut inner) =
fs::read_dir(...).await which treats real errors as "package missing"; change
these to propagate errors instead: replace unwrap_or(false) with using ? on
try_exists (or call fs::metadata/try_exists and ?-propagate) so failures return
Err from the containing function, and replace the if let Ok(mut inner) =
fs::read_dir(...).await pattern with let mut inner = fs::read_dir(...).await?
(or handle Err by returning it) and bubble any errors from
inner.next_entry().await?; ensure PACKUMENT_FILE checks and the loop return
errors instead of silently continuing so the caller sees permission/IO failures.
---
Outside diff comments:
In `@pnpr/crates/pnpr/src/server.rs`:
- Around line 568-573: The match on state.inner.storage.open_tarball(&name,
filename) currently logs Err(err) and then falls through to the upstream fetch;
instead make storage read errors terminal: in the Err branch (inside the request
handler where open_tarball is matched) log the error and immediately return an
appropriate 5xx tarball error response (e.g., internal server error / tarball
error response) rather than continuing to attempt upstream fetch or returning a
success; if you prefer softer behavior for cache-only errors, then modify
Storage::open_tarball to return a distinct error variant for cache misses vs
hosted-store failures and handle them differently here (only fall through on the
definite None miss, not on Err).
---
Nitpick comments:
In `@pnpr/crates/pnpr/src/s3/tests.rs`:
- Around line 101-113: Update the test lists_hosted_package_names to add keys
under a similar-but-different prefix (e.g., create objects under "packages2/" or
use store_with_prefix that simulates prefix "packages2") in addition to the
normal "packages/" keys, using the same helpers (upload, write_packument) so the
bucket contains stray entries; then call store.list_package_names() and assert
the result does not include those stray "packages2/..." entries (i.e., only
contains "`@scope/thing`" and "is-positive"), ensuring sibling-prefix collisions
are excluded by list_package_names.
In `@pnpr/crates/pnpr/tests/s3_backend.rs`:
- Around line 33-73: The test publishes_to_and_serves_from_the_object_store
misses exercising the hosted-package listing path
(Storage::hosted_package_names() → run_local_search() → search handler). After
publishing and before the tarball GET, add a GET request to the search endpoint
("/-/v1/search" or "/-/v1/search?text=mypkg" as your handler expects) against
the same app instance and assert the response is StatusCode::OK and that the
returned JSON includes "mypkg" (or the expected packument/entry indicating
hosted packages). Use the same app/router and object-store setup and include
Authorization if the search handler requires it so this E2E covers bucket-backed
listing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c5a5f0a8-bb9e-4d96-a9f8-495b71db99b6
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (14)
Cargo.tomlcspell.jsonpnpr/crates/pnpr/Cargo.tomlpnpr/crates/pnpr/config.yamlpnpr/crates/pnpr/src/config.rspnpr/crates/pnpr/src/error.rspnpr/crates/pnpr/src/lib.rspnpr/crates/pnpr/src/s3.rspnpr/crates/pnpr/src/s3/tests.rspnpr/crates/pnpr/src/search.rspnpr/crates/pnpr/src/server.rspnpr/crates/pnpr/src/storage.rspnpr/crates/pnpr/tests/s3_backend.rspnpr/npm/pnpr/README.md
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: ubuntu-latest / Node.js 24 / Test
- GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (2)
pnpr/**/pnpr/**/*.rs
📄 CodeRabbit inference engine (pnpr/AGENTS.md)
pnpr/**/pnpr/**/*.rs: Follow the pacquet code-style guide (../pacquet/CODE_STYLE_GUIDE.md) for Rust-level conventions including imports, naming, ownership, and error handling
Follow the pacquet contributing guide (../pacquet/CONTRIBUTING.md) for test layout and Rust conventions
Files:
pnpr/crates/pnpr/src/error.rspnpr/crates/pnpr/src/lib.rspnpr/crates/pnpr/src/server.rspnpr/crates/pnpr/tests/s3_backend.rspnpr/crates/pnpr/src/s3/tests.rspnpr/crates/pnpr/src/s3.rspnpr/crates/pnpr/src/config.rspnpr/crates/pnpr/src/search.rspnpr/crates/pnpr/src/storage.rs
pnpr/**/pnpr/**/Cargo.toml
📄 CodeRabbit inference engine (pnpr/AGENTS.md)
Declare new shared dependencies in the root [workspace.dependencies] and use { workspace = true } in pnpr crate's Cargo.toml
Files:
pnpr/crates/pnpr/Cargo.toml
🧠 Learnings (42)
📓 Common learnings
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-25T12:36:42.202Z
Learning: User-visible changes (CLI flags, defaults, environment variables, lockfile/manifest/state-file formats, error codes/messages, log emissions, store layout, hook semantics) in pnpm must be mirrored to pacquet in the same PR
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pnpr/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:24.797Z
Learning: Use Conventional Commits with 'pnpr' as the scope in commit messages (e.g., feat(pnpr): ..., fix(pnpr): ...)
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/*.rs : User-facing errors go through `miette` via the `pacquet-diagnostics` crate; match pnpm's error codes and messages where pnpm defines them since error codes are part of the public contract
Applied to files:
pnpr/crates/pnpr/src/error.rs
📚 Learning: 2026-05-29T18:03:24.797Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pnpr/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:24.797Z
Learning: Applies to pnpr/**/pnpr/crates/**/Cargo.toml : New registry-only crates must be placed under pnpr/crates/<short-name>/ and named pnpr-<short-name> in Cargo.toml, never using the pacquet- prefix
Applied to files:
pnpr/crates/pnpr/src/error.rspnpr/crates/pnpr/Cargo.tomlpnpr/crates/pnpr/src/lib.rspnpr/crates/pnpr/src/config.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/Cargo.toml : Keep dependencies at the right level — add a new dependency to the specific crate that needs it, not to the workspace root or shared crate unless multiple crates depend on it
Applied to files:
Cargo.tomlpnpr/crates/pnpr/Cargo.toml
📚 Learning: 2026-05-29T18:03:24.797Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pnpr/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:24.797Z
Learning: Applies to pnpr/**/pnpr/**/Cargo.toml : Declare new shared dependencies in the root [workspace.dependencies] and use { workspace = true } in pnpr crate's Cargo.toml
Applied to files:
Cargo.tomlpnpr/crates/pnpr/Cargo.tomlpnpr/crates/pnpr/src/lib.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Do not add a dependency not already declared in `[workspace.dependencies]` without explicit human request; ask for approval and request addition to the workspace root `Cargo.toml`
Applied to files:
Cargo.tomlpnpr/crates/pnpr/Cargo.toml
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/Cargo.toml : Check whether the workspace already depends on something suitable in `[workspace.dependencies]` in the root `Cargo.toml` before adding a new dependency
Applied to files:
Cargo.tomlpnpr/crates/pnpr/Cargo.toml
📚 Learning: 2026-05-29T18:03:24.797Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pnpr/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:24.797Z
Learning: Applies to pnpr/**/pnpr/crates/**/Cargo.toml : Cargo.toml files for new registry-only crates must use the pnpr- prefix for the package name
Applied to files:
pnpr/crates/pnpr/Cargo.tomlpnpr/crates/pnpr/src/lib.rspnpr/npm/pnpr/README.md
📚 Learning: 2026-05-19T19:23:00.981Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11752
File: pacquet/crates/config/src/lib.rs:1062-1073
Timestamp: 2026-05-19T19:23:00.981Z
Learning: In `pacquet/crates/config/src/lib.rs`, `modules_dir` does not need a `!virtual_store_dir_explicit` guard on its workspace re-anchor because `modules_dir` is in pnpm's `excludedPnpmKeys` (filtered out by `WorkspaceSettings::clear_workspace_only_fields`) and therefore can only be set by workspace yaml (applied immediately after) or env vars (applied later in the cascade) — not by global `config.yaml`. `virtual_store_dir`, by contrast, IS settable from global config and requires the `if !virtual_store_dir_explicit` guard to survive the workspace-root re-anchor.
Applied to files:
pnpr/crates/pnpr/Cargo.tomlpnpr/crates/pnpr/src/config.rs
📚 Learning: 2026-05-29T18:03:24.797Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pnpr/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:24.797Z
Learning: Applies to pnpr/**/pnpr/**/*.rs : Follow the pacquet code-style guide (../pacquet/CODE_STYLE_GUIDE.md) for Rust-level conventions including imports, naming, ownership, and error handling
Applied to files:
pnpr/crates/pnpr/Cargo.tomlpnpr/crates/pnpr/src/lib.rspnpr/crates/pnpr/tests/s3_backend.rspnpr/crates/pnpr/src/s3/tests.rs
📚 Learning: 2026-05-25T12:36:42.202Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-25T12:36:42.202Z
Learning: User-visible changes (CLI flags, defaults, environment variables, lockfile/manifest/state-file formats, error codes/messages, log emissions, store layout, hook semantics) in pnpm must be mirrored to pacquet in the same PR
Applied to files:
pnpr/crates/pnpr/Cargo.tomlpnpr/crates/pnpr/config.yamlpnpr/npm/pnpr/README.mdpnpr/crates/pnpr/src/storage.rs
📚 Learning: 2026-06-04T14:40:25.306Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12189
File: pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs:435-439
Timestamp: 2026-06-04T14:40:25.306Z
Learning: In `pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs` (pnpm/pnpm repo), the pnpr install accelerator always invokes `Install` with `lockfile_only: true` (hard-coded in `pnpr/crates/pnpr/src/install_accelerator/resolve.rs`). Under `lockfile_only: true`:
1. The `PrefetchingResolver` wrapper is skipped — the bare `inner_resolver` is used instead, so `PrefetchContext { config }` is never constructed.
2. The function returns before `CreateVirtualStore` is reached, so `install_package_by_snapshot` and its `config.auth_headers` fetch path are never hit.
pnpr's tarball fetch is handled separately in `resolve::fetch_uncached`, which independently receives the request-scoped `auth_headers`. Therefore, `auth_override` only needs to be threaded into the resolver-side components (NpmResolver, TarballResolver, NamedRegistryResolver) — not into PrefetchingResolver or CreateVirtualStore — on the pnpr path. For local installs (`lockfile_only: false`), `auth_override` is always `None` a...
Applied to files:
pnpr/crates/pnpr/config.yamlpnpr/crates/pnpr/src/server.rspnpr/npm/pnpr/README.mdpnpr/crates/pnpr/src/s3.rspnpr/crates/pnpr/src/storage.rs
📚 Learning: 2026-06-04T14:55:48.516Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12189
File: pacquet/crates/cli/src/cli_args/install.rs:0-0
Timestamp: 2026-06-04T14:55:48.516Z
Learning: In `pacquet/crates/cli/src/cli_args/install.rs` (pnpm/pnpm repo), the `install_via_pnpr` function intentionally forwards the **full** `state.config.auth_headers` map to the pnpr server (not filtered to only the declared default/named registries). This is required for correctness: transitive dependencies can be scope-routed to registries not in the explicit registry list, or pinned to tarball URLs on hosts present in `.npmrc` but not a declared registry. Filtering to the declared registries silently drops tokens those sub-dependencies need, causing 401s on the server. pnpr uses the forwarded map to attach the right token per fetched URL exactly as a local install does (`AuthHeaders::for_url`). The pnpr-server's own credential is sent separately in the `Authorization` header and is excluded from the body map. Do NOT flag this as a credential-leakage issue — the rationale is documented in a comment at both call sites.
Applied to files:
pnpr/crates/pnpr/config.yamlpnpr/crates/pnpr/src/server.rspnpr/npm/pnpr/README.md
📚 Learning: 2026-05-24T08:18:06.019Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11895
File: pnpm/test/deploy.ts:91-95
Timestamp: 2026-05-24T08:18:06.019Z
Learning: In the pnpm/pnpm repository, integration tests that hit the real `registry.npmjs.org` (e.g., for `pacquet` or `pnpm/pacquet`) do NOT use a runtime env-var gate (such as `PNPM_RUN_PUBLIC_REGISTRY_TESTS`). They simply pass `--config.registry=https://registry.npmjs.org/` directly to `execPnpm` and set a higher timeout. This is the established pattern, as seen in `pnpm/test/install/pacquet.ts` and `pnpm/test/deploy.ts`. Do not suggest adding env-var guards for these tests.
Applied to files:
pnpr/crates/pnpr/config.yaml
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/*.rs : External-crate preludes such as `use rayon::prelude::*;` and root-of-module re-exports such as `pub use submodule::*;` in a `lib.rs` are allowed exceptions to the no-star-imports rule
Applied to files:
pnpr/crates/pnpr/src/lib.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/*.rs : Do not use star imports inside module bodies — write `use super::{Foo, bar}` instead of `use super::*;` for modules you control
Applied to files:
pnpr/crates/pnpr/src/lib.rs
📚 Learning: 2026-05-29T18:03:24.797Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pnpr/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:24.797Z
Learning: Prefer existing pacquet-* crates over writing new code; check pacquet-tarball, pacquet-crypto-hash, pacquet-crypto-shasums-file, pacquet-package-manifest, pacquet-network, pacquet-registry, pacquet-fs, and pacquet-diagnostics before implementing non-trivial functionality
Applied to files:
pnpr/crates/pnpr/src/lib.rspnpr/crates/pnpr/src/config.rspnpr/crates/pnpr/src/storage.rs
📚 Learning: 2026-05-25T12:36:42.202Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-25T12:36:42.202Z
Learning: Version pnpm CLI patch for bug fixes, internal refactors, and changes that don't require documentation; minor for new features/settings that should be documented; major for breaking changes
Applied to files:
pnpr/npm/pnpr/README.md
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Match how the same feature is implemented in the TypeScript pnpm CLI — any change in pacquet must match pnpm's behavior, logic, edge cases, config resolution, error messages, file/lockfile formats, and existing tests
Applied to files:
pnpr/npm/pnpr/README.md
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Do not change lockfile format, store layout, `.npmrc` semantics, or CLI surface unless pnpm changed them first
Applied to files:
pnpr/npm/pnpr/README.md
📚 Learning: 2026-05-24T21:11:04.272Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:11:04.272Z
Learning: In the pacquet Rust port (pnpm/pnpm repo), the `ResolvedPackage.optional` AND-folding on revisit intentionally mirrors pnpm's `resolveDependencies.ts:1627-1648` behavior: only the directly-revisited package's `optional` flag is updated; transitive descendants are not re-walked. pnpm CLI corrects stale optional flags downstream via `copyDependencySubGraph` BFS in `lockfile/pruner/src/index.ts:160-205`, which tracks a `nonOptional` set and re-stamps any package reachable by an all-non-optional path. Pacquet does not yet have this pruner equivalent, so the stale flags flow directly through `dependencies_graph_to_lockfile.rs:409` → `create_virtual_store.rs:762` → `installability.rs:394`. A follow-up to port `copyDependencySubGraph` is planned; until then, do not flag the resolver-layer optional propagation gap as a bug in pacquet PRs — it is intentional parity with pnpm's resolver layer.
Applied to files:
pnpr/npm/pnpr/README.md
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Port relevant pnpm tests to Rust tests whenever they translate when porting behavior from pnpm
Applied to files:
pnpr/crates/pnpr/tests/s3_backend.rspnpr/crates/pnpr/src/s3/tests.rspnpr/crates/pnpr/src/search.rs
📚 Learning: 2026-05-29T18:03:24.797Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pnpr/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:24.797Z
Learning: Applies to pnpr/**/pnpr/**/*.rs : Follow the pacquet contributing guide (../pacquet/CONTRIBUTING.md) for test layout and Rust conventions
Applied to files:
pnpr/crates/pnpr/tests/s3_backend.rspnpr/crates/pnpr/src/s3/tests.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Tests that need the mocked registry should start `pnpr` through `pacquet-testing-utils`; `cargo test` / `cargo nextest run` should not require a separate `just registry-mock launch` step
Applied to files:
pnpr/crates/pnpr/tests/s3_backend.rspnpr/crates/pnpr/src/s3/tests.rspnpr/crates/pnpr/src/search.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Use snapshot tests with `insta` and carefully review diffs when intentional changes alter snapshots; accept with `cargo insta review` only after careful review
Applied to files:
pnpr/crates/pnpr/tests/s3_backend.rspnpr/crates/pnpr/src/s3/tests.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/*.rs : Tests are documentation — do not duplicate test scenarios, edge cases, failure modes, or worked examples in prose when they are already captured by tests
Applied to files:
pnpr/crates/pnpr/tests/s3_backend.rspnpr/crates/pnpr/src/s3/tests.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Prefer real fixtures over dependency-injection seams — use `tempfile::TempDir`, the mocked registry, or integration tests spawning the actual binary for happy paths and most error paths; use the DI seam only for filesystem error kinds, deterministic time, shared process-global state, or external-service happy paths
Applied to files:
pnpr/crates/pnpr/tests/s3_backend.rspnpr/crates/pnpr/src/s3/tests.rspnpr/crates/pnpr/src/search.rs
📚 Learning: 2026-05-20T21:18:56.391Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11778
File: pacquet/crates/resolving-local-resolver/tests/resolve.rs:365-372
Timestamp: 2026-05-20T21:18:56.391Z
Learning: In `pacquet/crates/resolving-local-resolver/tests/resolve.rs`, the test `fail_when_resolving_from_not_existing_directory_an_injected_dependency` intentionally uses `injected: false`. The test is a verbatim port of the upstream pnpm TypeScript test (resolving/local-resolver/test/index.ts at ef87f3ccff). The `injected` flag only affects the file/link protocol choice for plain directory paths; when the `file:` scheme is explicit in the bare specifier, the flag has no effect on the resolution code path. The misleading test name is inherited from upstream.
Applied to files:
pnpr/crates/pnpr/tests/s3_backend.rs
📚 Learning: 2026-05-28T16:19:30.483Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12025
File: pacquet/crates/deps-path/src/link_path_to_peer_version.rs:0-0
Timestamp: 2026-05-28T16:19:30.483Z
Learning: In `pacquet/crates/deps-path/src/link_path_to_peer_version.rs`, the `link_path_to_peer_version` function intentionally deviates from upstream pnpm/JS behavior for non-BMP Unicode code points: the JavaScript `filenamify` v4 implementation sees UTF-16 code units and emits two surrogate fragments for a single non-BMP scalar, while the Rust port iterates `chars()` and emits one clean Unicode scalar. pnpm has no tests for non-ASCII link paths, so the behavior was undefined upstream; the Rust scalar form is the intentional, preferred behavior for pacquet.
Applied to files:
pnpr/crates/pnpr/tests/s3_backend.rs
📚 Learning: 2026-05-23T16:55:36.507Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11878
File: pacquet/crates/cli/tests/lockfile_verification.rs:158-162
Timestamp: 2026-05-23T16:55:36.507Z
Learning: In `pacquet/crates/cli/tests/lockfile_verification.rs`, the `trust_lockfile_skips_verification` and `trust_lockfile_cli_flag_skips_verification` tests intentionally do NOT assert `output.status.success()`. The hand-rolled fixture lockfile uses a placeholder integrity hash (`sha512-AAA…`), so the install always fails the downstream tarball integrity check regardless of the supply-chain gate. The contract being tested is "gate-skipped, not install-succeeded"; asserting success would require generating a real lockfile via the `generate_lockfile` pattern (see `hoist.rs`) which is considered not worth the extra wiring for an opt-out smoke test.
Applied to files:
pnpr/crates/pnpr/tests/s3_backend.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Follow test-logging guidance — log before non-`assert_eq!` assertions, `dbg!` complex structures, skip logging for simple scalar `assert_eq!`
Applied to files:
pnpr/crates/pnpr/src/s3/tests.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/*.rs : Follow Rust API Guidelines for naming
Applied to files:
pnpr/crates/pnpr/src/s3/tests.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Consult `plans/TEST_PORTING.md` before adding ported tests and update its checkboxes as items land; use `known_failures` modules and `pacquet_testing_utils::allow_known_failure!` at the not-yet-implemented boundary
Applied to files:
pnpr/crates/pnpr/src/s3/tests.rs
📚 Learning: 2026-05-18T20:35:22.917Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11729
File: pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs:55-57
Timestamp: 2026-05-18T20:35:22.917Z
Learning: In `pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs`, the npm attestation endpoint (`/-/npm/v1/attestations/{pkg_name}@{version}`) intentionally does NOT percent-encode the package name — the endpoint accepts literal `/` in scoped package names (e.g. `scope/pkg`). This matches upstream pnpm's `fetchAttestationPublishedAt.ts` behavior. Do not flag missing URL encoding here. By contrast, the full-metadata fetch paths (`fetch_full_metadata`, `fetch_full_metadata_cached`) DO percent-encode via the `registry_url::to_registry_url` helper, matching upstream's `toUri` behavior.
Applied to files:
pnpr/crates/pnpr/src/s3/tests.rspnpr/crates/pnpr/src/search.rs
📚 Learning: 2026-05-20T13:36:20.653Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11768
File: pacquet/crates/crypto-hash/src/lib.rs:63-69
Timestamp: 2026-05-20T13:36:20.653Z
Learning: In `pacquet/crates/crypto-hash/src/lib.rs`, `shorten_virtual_store_name` intentionally produces a 33-byte `"_<32-hex>"` result even when `max_length < 33`. This mirrors pnpm's upstream `depPathToFilename` (deps/path/src/index.ts), where JavaScript's `String.prototype.substring(0, negative)` clamps to 0, giving an empty prefix and the full `"_<hash>"` suffix. Do not suggest capping to `max_length` for small values — it would diverge from the on-disk naming contract. The upstream test suite only exercises `max_length = 120`; values below ~50 are self-defeating in practice.
Applied to files:
pnpr/crates/pnpr/src/s3/tests.rs
📚 Learning: 2026-05-20T10:51:16.296Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11760
File: pacquet/crates/resolving-npm-resolver/src/named_registry.rs:109-127
Timestamp: 2026-05-20T10:51:16.296Z
Learning: In `pacquet/crates/resolving-npm-resolver/src/named_registry.rs`, the `pick_registry_for_package` function's logic `scope_from_bare_specifier(bare_specifier).or_else(|| scope_of(pkg_name))` is intentionally correct and matches upstream pnpm's `getScope` in `config/pick-registry-for-package/src/index.ts`. When bare_specifier is an npm: alias for an unscoped package (e.g., `npm:lodash@^1`), `scope_from_bare_specifier` returns `None`, and the fallthrough to `scope_of(pkg_name)` is correct behavior — allowing a scoped local name like `private/foo` to still route through the `private` registry. Do NOT suggest changing this to an early-return on `npm:` prefix, as that would break pnpm compatibility.
Applied to files:
pnpr/crates/pnpr/src/s3/tests.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/*.rs : Search for existing shared helpers before writing new code; shared helpers tend to live in `crates/fs`, `crates/testing-utils`, and `crates/diagnostics`
Applied to files:
pnpr/crates/pnpr/src/config.rspnpr/crates/pnpr/src/search.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/*.rs : Choose owned vs. borrowed parameters to minimize copies; widen to the most encompassing type (`&Path` over `&PathBuf`, `&str` over `&String`) when it doesn't force extra copies
Applied to files:
pnpr/crates/pnpr/src/search.rs
📚 Learning: 2026-05-20T20:41:50.322Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11773
File: pacquet/crates/package-manager/src/install_package_from_registry.rs:111-114
Timestamp: 2026-05-20T20:41:50.322Z
Learning: In pacquet (pnpm/pnpm repo, Rust codebase), `install_package_from_registry` is the npm-only install path. The npm resolver always stamps `ResolveResult.id` (a `PkgResolutionId`) as `nameversion`. Parsing it back through `PkgNameVer` with `.expect()` is intentional — a parse failure means a mis-dispatch bug, not malformed external input. Per pacquet's CLAUDE.md: "Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees." Do not suggest replacing such `.expect()` calls with graceful error handling.
Applied to files:
pnpr/crates/pnpr/src/search.rs
📚 Learning: 2026-05-23T17:30:06.849Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11878
File: resolving/npm-resolver/src/createNpmResolutionVerifier.ts:381-418
Timestamp: 2026-05-23T17:30:06.849Z
Learning: In `resolving/npm-resolver/src/pickPackage.ts` (pnpm/pnpm), the resolver's `PackageMetaCache` keys by `name` (abbreviated) and `name:full` (full metadata) only — no registry component is included. This is a pre-existing limitation meaning that if two different registries serve packages of the same name in one install, the cache will only hold the first fetched entry. The `createNpmResolutionVerifier.ts` shares this same cache and inherits the limitation; a `validateSharedMeta` name-check guards against cross-package contamination but cannot distinguish same-named packages from different registries. Tightening to a registry-qualified key would require a coordinated change to the resolver's cache key shape. The Pacquet/Rust side is already registry-qualified (`{registry}\x00{name}:full`).
Applied to files:
pnpr/crates/pnpr/src/search.rspnpr/crates/pnpr/src/storage.rs
📚 Learning: 2026-05-25T14:58:11.105Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11931
File: pacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rs:560-589
Timestamp: 2026-05-25T14:58:11.105Z
Learning: In `pacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rs`, all per-`(registry, name[, version])` caches in `NpmResolutionVerifier` (`published_at`, `full_meta`, `full_meta_for_trust`, `abbreviated_meta`, `local_meta`) intentionally use the same pattern: lock → miss-check → release lock → await fetch/load → re-acquire lock → insert. This uniform pattern is deliberate; do not flag individual caches for using it. The known follow-up improvement (replacing the pattern with `tokio::sync::OnceCell` per key inside a `Mutex<HashMap<…>>`) is tracked as a future structural change to cover all five caches simultaneously.
Applied to files:
pnpr/crates/pnpr/src/search.rs
📚 Learning: 2026-05-20T22:49:17.652Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11787
File: pacquet/crates/catalogs-resolver/src/lib.rs:156-167
Timestamp: 2026-05-20T22:49:17.652Z
Learning: In pacquet's `catalogs-resolver` crate (`pacquet/crates/catalogs-resolver/src/lib.rs`), the protocol detection pattern `catalog_lookup.split(':').next().unwrap_or("")` is an intentional byte-for-byte port of pnpm's upstream JavaScript `getProtocol`/split logic in `catalogs/resolver/src/resolveFromCatalog.ts#L95`. A bare value like `"workspace"` (without a colon) is deliberately classified as the `"workspace"` protocol, matching upstream behavior. pacquet's cardinal rule is to match upstream pnpm behavior including quirks; any behavioral change must land in pnpm first and then be ported here.
Applied to files:
pnpr/crates/pnpr/src/search.rs
🪛 LanguageTool
pnpr/npm/pnpr/README.md
[grammar] ~167-~167: Ensure spelling is correct
Context: ...rites the dist.tarball URLs in served packuments, so clients fetch tarballs back through...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🔇 Additional comments (8)
cspell.json (1)
21-21: LGTM!Also applies to: 179-179
pnpr/npm/pnpr/README.md (1)
44-44: LGTM!Also applies to: 81-183
Cargo.toml (1)
112-112: LGTM!pnpr/crates/pnpr/Cargo.toml (1)
48-48: LGTM!pnpr/crates/pnpr/src/lib.rs (1)
17-17: LGTM!Also applies to: 26-28
pnpr/crates/pnpr/src/error.rs (1)
132-135: LGTM!Also applies to: 183-185
pnpr/crates/pnpr/src/config.rs (1)
4-4: LGTM!Also applies to: 7-7, 13-13, 105-122, 320-324, 423-423, 442-442, 553-558, 578-578
pnpr/crates/pnpr/config.yaml (1)
10-25: LGTM!
…ish, and error mapping
Addresses CodeRabbit review on #12198: the previous unwrap_or(key) fallback could synthesize a wrong package name for a key outside the configured prefix. Also documents the deliberate best-effort tolerance of per-entry I/O errors in the local search walk.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
pnpr/crates/pnpr/tests/s3_backend.rs (1)
136-144: ⚡ Quick winAppend trailing slash to prefix for precise S3 scoping.
The current implementation uses the raw
prefixstring (e.g.,"mypkg") which will match any key starting with those characters, including"mypkg","mypkg/", and"mypkg-dev/". To scope the listing to only objects under a package directory, append a trailing slash if not already present.♻️ Proposed fix
async fn bucket_keys(store: &Arc<dyn ObjectStore>, prefix: &str) -> Vec<String> { - let scope = ObjectPath::from(prefix); + let normalized = if prefix.is_empty() || prefix.ends_with('/') { + prefix.to_string() + } else { + format!("{}/", prefix) + }; + let scope = ObjectPath::from(normalized.as_str()); store .list(Some(&scope))🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pnpr/crates/pnpr/tests/s3_backend.rs` around lines 136 - 144, bucket_keys currently uses the raw prefix string which can overmatch (e.g., "mypkg" matches "mypkg-dev"); modify bucket_keys so it ensures the prefix ends with a trailing slash before constructing the ObjectPath (i.e., if prefix does not end_with('/') append '/' to it) so ObjectPath::from receives a directory-scoped prefix; update references inside bucket_keys (the prefix variable passed to ObjectPath::from) only—no other logic changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@pnpr/crates/pnpr/tests/s3_backend.rs`:
- Around line 136-144: bucket_keys currently uses the raw prefix string which
can overmatch (e.g., "mypkg" matches "mypkg-dev"); modify bucket_keys so it
ensures the prefix ends with a trailing slash before constructing the ObjectPath
(i.e., if prefix does not end_with('/') append '/' to it) so ObjectPath::from
receives a directory-scoped prefix; update references inside bucket_keys (the
prefix variable passed to ObjectPath::from) only—no other logic changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2a2cf810-79b7-4dbd-8a6a-bb449aace04d
📒 Files selected for processing (3)
pnpr/crates/pnpr/src/config/tests.rspnpr/crates/pnpr/src/error/tests.rspnpr/crates/pnpr/tests/s3_backend.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Lint and Test (ubuntu-latest)
- GitHub Check: Code Coverage
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pnpr/**/pnpr/**/*.rs
📄 CodeRabbit inference engine (pnpr/AGENTS.md)
pnpr/**/pnpr/**/*.rs: Follow the pacquet code-style guide (../pacquet/CODE_STYLE_GUIDE.md) for Rust-level conventions including imports, naming, ownership, and error handling
Follow the pacquet contributing guide (../pacquet/CONTRIBUTING.md) for test layout and Rust conventions
Files:
pnpr/crates/pnpr/src/error/tests.rspnpr/crates/pnpr/src/config/tests.rspnpr/crates/pnpr/tests/s3_backend.rs
🧠 Learnings (14)
📓 Common learnings
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-25T12:36:42.202Z
Learning: User-visible changes (CLI flags, defaults, environment variables, lockfile/manifest/state-file formats, error codes/messages, log emissions, store layout, hook semantics) in pnpm must be mirrored to pacquet in the same PR
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Tests that need the mocked registry should start `pnpr` through `pacquet-testing-utils`; `cargo test` / `cargo nextest run` should not require a separate `just registry-mock launch` step
Applied to files:
pnpr/crates/pnpr/src/error/tests.rspnpr/crates/pnpr/src/config/tests.rspnpr/crates/pnpr/tests/s3_backend.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/*.rs : Tests are documentation — do not duplicate test scenarios, edge cases, failure modes, or worked examples in prose when they are already captured by tests
Applied to files:
pnpr/crates/pnpr/src/error/tests.rspnpr/crates/pnpr/src/config/tests.rspnpr/crates/pnpr/tests/s3_backend.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Port relevant pnpm tests to Rust tests whenever they translate when porting behavior from pnpm
Applied to files:
pnpr/crates/pnpr/src/config/tests.rspnpr/crates/pnpr/tests/s3_backend.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/*.rs : Do not use star imports inside module bodies — write `use super::{Foo, bar}` instead of `use super::*;` for modules you control
Applied to files:
pnpr/crates/pnpr/src/config/tests.rs
📚 Learning: 2026-05-29T18:03:24.797Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pnpr/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:24.797Z
Learning: Applies to pnpr/**/pnpr/**/*.rs : Follow the pacquet code-style guide (../pacquet/CODE_STYLE_GUIDE.md) for Rust-level conventions including imports, naming, ownership, and error handling
Applied to files:
pnpr/crates/pnpr/src/config/tests.rs
📚 Learning: 2026-05-29T18:03:24.797Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pnpr/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:24.797Z
Learning: Applies to pnpr/**/pnpr/**/*.rs : Follow the pacquet contributing guide (../pacquet/CONTRIBUTING.md) for test layout and Rust conventions
Applied to files:
pnpr/crates/pnpr/src/config/tests.rspnpr/crates/pnpr/tests/s3_backend.rs
📚 Learning: 2026-05-20T21:18:56.391Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11778
File: pacquet/crates/resolving-local-resolver/tests/resolve.rs:365-372
Timestamp: 2026-05-20T21:18:56.391Z
Learning: In `pacquet/crates/resolving-local-resolver/tests/resolve.rs`, the test `fail_when_resolving_from_not_existing_directory_an_injected_dependency` intentionally uses `injected: false`. The test is a verbatim port of the upstream pnpm TypeScript test (resolving/local-resolver/test/index.ts at ef87f3ccff). The `injected` flag only affects the file/link protocol choice for plain directory paths; when the `file:` scheme is explicit in the bare specifier, the flag has no effect on the resolution code path. The misleading test name is inherited from upstream.
Applied to files:
pnpr/crates/pnpr/src/config/tests.rspnpr/crates/pnpr/tests/s3_backend.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Use snapshot tests with `insta` and carefully review diffs when intentional changes alter snapshots; accept with `cargo insta review` only after careful review
Applied to files:
pnpr/crates/pnpr/src/config/tests.rspnpr/crates/pnpr/tests/s3_backend.rs
📚 Learning: 2026-05-19T19:23:00.981Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11752
File: pacquet/crates/config/src/lib.rs:1062-1073
Timestamp: 2026-05-19T19:23:00.981Z
Learning: In `pacquet/crates/config/src/lib.rs`, `modules_dir` does not need a `!virtual_store_dir_explicit` guard on its workspace re-anchor because `modules_dir` is in pnpm's `excludedPnpmKeys` (filtered out by `WorkspaceSettings::clear_workspace_only_fields`) and therefore can only be set by workspace yaml (applied immediately after) or env vars (applied later in the cascade) — not by global `config.yaml`. `virtual_store_dir`, by contrast, IS settable from global config and requires the `if !virtual_store_dir_explicit` guard to survive the workspace-root re-anchor.
Applied to files:
pnpr/crates/pnpr/src/config/tests.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Prefer `#[cfg_attr(target_os = "windows", ignore = "...")]` (or matching `#[cfg(unix)]` gates) over runtime probe-and-skip helpers for platform-locked tools
Applied to files:
pnpr/crates/pnpr/src/config/tests.rs
📚 Learning: 2026-05-23T16:55:36.507Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11878
File: pacquet/crates/cli/tests/lockfile_verification.rs:158-162
Timestamp: 2026-05-23T16:55:36.507Z
Learning: In `pacquet/crates/cli/tests/lockfile_verification.rs`, the `trust_lockfile_skips_verification` and `trust_lockfile_cli_flag_skips_verification` tests intentionally do NOT assert `output.status.success()`. The hand-rolled fixture lockfile uses a placeholder integrity hash (`sha512-AAA…`), so the install always fails the downstream tarball integrity check regardless of the supply-chain gate. The contract being tested is "gate-skipped, not install-succeeded"; asserting success would require generating a real lockfile via the `generate_lockfile` pattern (see `hoist.rs`) which is considered not worth the extra wiring for an opt-out smoke test.
Applied to files:
pnpr/crates/pnpr/src/config/tests.rspnpr/crates/pnpr/tests/s3_backend.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Prefer real fixtures over dependency-injection seams — use `tempfile::TempDir`, the mocked registry, or integration tests spawning the actual binary for happy paths and most error paths; use the DI seam only for filesystem error kinds, deterministic time, shared process-global state, or external-service happy paths
Applied to files:
pnpr/crates/pnpr/tests/s3_backend.rs
📚 Learning: 2026-06-04T14:40:25.306Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12189
File: pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs:435-439
Timestamp: 2026-06-04T14:40:25.306Z
Learning: In `pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs` (pnpm/pnpm repo), the pnpr install accelerator always invokes `Install` with `lockfile_only: true` (hard-coded in `pnpr/crates/pnpr/src/install_accelerator/resolve.rs`). Under `lockfile_only: true`:
1. The `PrefetchingResolver` wrapper is skipped — the bare `inner_resolver` is used instead, so `PrefetchContext { config }` is never constructed.
2. The function returns before `CreateVirtualStore` is reached, so `install_package_by_snapshot` and its `config.auth_headers` fetch path are never hit.
pnpr's tarball fetch is handled separately in `resolve::fetch_uncached`, which independently receives the request-scoped `auth_headers`. Therefore, `auth_override` only needs to be threaded into the resolver-side components (NpmResolver, TarballResolver, NamedRegistryResolver) — not into PrefetchingResolver or CreateVirtualStore — on the pnpr path. For local installs (`lockfile_only: false`), `auth_override` is always `None` a...
Applied to files:
pnpr/crates/pnpr/tests/s3_backend.rs
🔇 Additional comments (13)
pnpr/crates/pnpr/src/error/tests.rs (1)
35-42: LGTM!pnpr/crates/pnpr/src/config/tests.rs (4)
2-3: LGTM!
132-137: LGTM!
139-158: LGTM!
160-164: LGTM!pnpr/crates/pnpr/tests/s3_backend.rs (8)
6-22: LGTM!
26-32: LGTM!
34-74: LGTM!
76-103: LGTM!
105-134: LGTM!
146-152: LGTM!
154-185: LGTM!
187-230: LGTM!
…hase 3) (#12206) ## What Implements the **auth half** of [#12199](#12199) (phase 3) — making pnpr's remaining per-instance state pluggable so the registry can run as stateless, horizontally-scaled replicas. ### Auth records behind config-selected backends Users + tokens now sit behind narrow async `UserBackend` / `TokenBackend` traits, built once at startup into `Arc<dyn …>` handles (the same build-once pattern #12198 used for the hosted store). Three implementations: - **Local** (default) — today's htpasswd file + SQLite token DB, or in-memory when no file is configured. Unchanged behavior. - **Networked SQLite (libsql / Turso)** — `LibsqlAuth` stores **both** records in one shared database, so several stateless replicas observe a consistent set of users and tokens. The `tokens` table DDL is shared verbatim with the local backend (a DB can migrate between them); users — which the local backend keeps in htpasswd — move into a `users` table. Selected via a new top-level YAML block: ```yaml backend: libsql: url: ${PNPR_LIBSQL_URL} authToken: ${PNPR_LIBSQL_TOKEN} # optional embedded replica for local-fast hot-path reads: replicaPath: ./auth-replica.db syncIntervalSecs: 60 ``` When the block is absent, auth stays on local disk exactly as before. ### Embedded-replica read acceleration Token lookups are on the request hot path, so against a remote primary every read would be a network round-trip. With `replicaPath` set, `LibsqlAuth` builds a libsql **embedded replica**: reads hit a local file that libsql keeps current in the background; writes go to the primary. `syncIntervalSecs` is the freshness knob that bounds token-revocation lag. ### Async access path `identify` / `enforce_access` are now async (a networked lookup is async). `enforce_access` is split into an async `resolve_identity` + a sync `authorize`, so the search endpoint resolves the caller once and authorizes each candidate synchronously (no async-in-`retain`). ### Concurrent-publish guard (cross-cutting follow-up from the issue) Closes the same-instance lost-update window in the three read-modify-write packument flows (publish, dist-tag change, partial-unpublish): a striped per-package lock serializes same-package writers on one instance while letting different packages proceed in parallel. The **cross-replica** half (S3 `If-Match` / ETag CAS) is documented in-code as the remaining piece — the issue files it under "fix when we get there," and it belongs with the multi-writer S3 publish work, not this auth branch. ## Tests All green — `cargo test -p pnpr`: - **176 lib unit tests** incl. new `LibsqlAuth` tests (run against an in-memory libsql DB — same driver + SQL, no server) and `backend.libsql` config-parsing tests (incl. the replica options). - New `concurrent_publishes_of_distinct_versions_all_survive` integration test for the publish guard. - Existing auth_persistence / auth_user_endpoints / auth_publish / server / s3_backend suites pass. - Clean under `cargo fmt`, `clippy`, `RUSTDOCFLAGS=-D warnings cargo doc`, **Dylint perfectionist**, and `taplo`. ## Docs `backend.libsql` (incl. embedded replica) documented in the bundled `config.yaml` and the `pnpr` npm README, mirroring how the S3 backend was documented in #12198.
What
Lets pnpr store its hosted packages (the ones published to it, plus static-served content) in an S3-compatible object store instead of a local directory. Because the same code targets any S3-compatible endpoint, this also covers Cloudflare R2, MinIO, Backblaze B2, Wasabi, etc.
The local
tokio::fspath remains the default — nothing changes unless you add the news3:config block.Why
The hosted store is pnpr's source of truth: durable, must be backed up, and can't be regenerated. That's exactly what belongs in object storage:
endpointgets it (and the other S3-compatibles) for free.The disposable proxy cache and the install-accelerator SQLite stores deliberately stay on local disk — they're ephemeral, latency-sensitive, and streamed/locked in filesystem-shaped ways.
How
s3.rsmodule:S3Settings(the YAMLs3:block), a client builder (object_storecrate; AWS-env credentials with explicit override, plus R2/MinIO/path-style/HTTP knobs), and anS3Storeadapter (packument get/put, streaming tarball get, staged upload, delete, prefix-scoped list).storage.rs: aHostedStore { Fs | S3 }backend enum routes the hosted ops; thecachedstore stays fs-only. Publish stages the decoded+verified tarball to local scratch, then finalize either renames (fs) or uploads (S3).open_tarballnow returns a streaming response body so S3 reads stream straight through.config.rs: parsess3:and builds the client once at config-load time (the only fallible step), soStorageconstruction stays infallible.search.rs: local search now lists package names through the storage abstraction, so it works against a bucket too.config.yaml.Example: Cloudflare R2
Tests
S3Storeadapter (against object_store's in-memory backend): packument round-trip, streaming upload+read with length, scoped keys + prefixes, idempotent delete, package listing.cargo fmt,cargo doc -D warnings, Perfectionist dylint, and taplo all clean.Notes
object_storebrings its ownreqwest 0.12alongside the workspace's0.13(one extra transitive copy) — unavoidable, harmless.Written by an agent (Claude Code, claude-opus-4-8).
Summary by CodeRabbit
New Features
Bug Fixes
Documentation