feat(pacquet): port resolving/git-resolver and wire it into the install chain#11779
Conversation
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughThis PR adds a new git-resolver crate (parsing, hosted rendering, ref resolution, runners), introduces PkgResolutionId and ResolveResult.name_ver, implements Resolver for DefaultResolver to enable resolver chains, and wires npm+git resolver chains into install flows with virtual-store naming updated to use name_ver. ChangesGit resolver and resolver composition
Sequence Diagram(s)sequenceDiagram
participant WantedDep
participant DefaultResolver
participant GitResolver
participant ParseBareSpecifier
participant GitProbe
participant GitRunner
participant ResolveRef
participant PackageManager
WantedDep->>DefaultResolver: resolve(wanted_dependency)
DefaultResolver->>GitResolver: delegate resolve
GitResolver->>ParseBareSpecifier: parse_bare_specifier()
ParseBareSpecifier->>GitProbe: finalize(hosted) probe
GitResolver->>GitRunner: ls-remote (via ResolveRef)
GitRunner-->>ResolveRef: ls-remote output
ResolveRef-->>GitResolver: resolved commit
GitResolver-->>DefaultResolver: ResolveResult (id + name_ver)
DefaultResolver-->>PackageManager: resolved tree
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
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 |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #11779 +/- ##
==========================================
- Coverage 90.05% 89.67% -0.39%
==========================================
Files 163 170 +7
Lines 18851 20318 +1467
==========================================
+ Hits 16977 18220 +1243
- Misses 1874 2098 +224 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Micro-Benchmark ResultsLinux |
Integrated-Benchmark Report (Linux)Scenario: Frozen Lockfile
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.4238598509200004,
"stddev": 0.14796998878635223,
"median": 2.38213720222,
"user": 2.80677126,
"system": 3.5737683200000006,
"min": 2.29656711722,
"max": 2.80257137122,
"times": [
2.48570114922,
2.36349335722,
2.39144764822,
2.3728267562200003,
2.80257137122,
2.40070068022,
2.48631146922,
2.31301202622,
2.3259669342200002,
2.29656711722
]
},
{
"command": "pacquet@main",
"mean": 2.34743767502,
"stddev": 0.0632444987149618,
"median": 2.36546619422,
"user": 2.7577365599999992,
"system": 3.5711329199999993,
"min": 2.23835572522,
"max": 2.42164576422,
"times": [
2.38430890022,
2.2816797862200002,
2.23835572522,
2.2753686442200003,
2.38238714022,
2.41284598222,
2.3361227012200003,
2.39311685822,
2.34854524822,
2.42164576422
]
},
{
"command": "pnpm",
"mean": 4.497790352819999,
"stddev": 0.057330578151770235,
"median": 4.48447060172,
"user": 7.512217059999999,
"system": 3.97266982,
"min": 4.4146280322199996,
"max": 4.57372525922,
"times": [
4.5712933822199995,
4.4146280322199996,
4.49077588422,
4.43190812922,
4.525448648219999,
4.45890896322,
4.55978494422,
4.4732649662199995,
4.4781653192199995,
4.57372525922
]
}
]
}Scenario: Frozen Lockfile (Hot Cache)
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.70540355532,
"stddev": 0.031060851565946825,
"median": 0.6968091792200001,
"user": 0.39750566000000004,
"system": 1.5691996599999998,
"min": 0.67087535622,
"max": 0.78615518222,
"times": [
0.78615518222,
0.6913724762200001,
0.68604944922,
0.6950482292200001,
0.70899670822,
0.6985701292200001,
0.67087535622,
0.71454598522,
0.7087255422200001,
0.69369649522
]
},
{
"command": "pacquet@main",
"mean": 0.7296857136200001,
"stddev": 0.07385930297345797,
"median": 0.6959964897200001,
"user": 0.37431865999999997,
"system": 1.59666106,
"min": 0.67266505722,
"max": 0.90030861122,
"times": [
0.74807831822,
0.68815541522,
0.68939187822,
0.90030861122,
0.69359643922,
0.67266505722,
0.7133518482200001,
0.8169399582200001,
0.6983965402200001,
0.6759730702200001
]
},
{
"command": "pnpm",
"mean": 2.3890530720200003,
"stddev": 0.0756980438255328,
"median": 2.38173912522,
"user": 2.93750096,
"system": 2.17833876,
"min": 2.30936904222,
"max": 2.5718829312200002,
"times": [
2.40949393122,
2.32955478422,
2.42162822922,
2.5718829312200002,
2.41362644922,
2.33685037122,
2.36794752822,
2.33464673122,
2.30936904222,
2.39553072222
]
}
]
} |
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@pacquet/crates/package-manager/src/install_without_lockfile.rs`:
- Around line 177-187: The code registers GitResolver into the without-lockfile
resolver chain (DefaultResolver constructed with npm_resolver and git_resolver)
but Git resolutions still call expect on missing name_ver and can panic; either
stop adding GitResolver here until materialization support exists or change the
git path to propagate a typed error: update the registration in the resolver
chain (the code that constructs Box::new(DefaultResolver::new(...))) to
conditionally include Box::new(GitResolver::new(...)) based on the
materialization/feature flag, or refactor GitResolver (and any callers used by
install_without_lockfile.rs and install_package_from_registry.rs) so its
resolve/install methods return Result<..., InstallError> instead of calling
expect on name_ver and ensure the calling flow handles and forwards that error
instead of panicking.
In `@pacquet/crates/resolving-git-resolver/src/hosted_git.rs`:
- Around line 639-657: The percent_decode function currently decodes each %XX as
a char which breaks multi-byte UTF-8 sequences; change percent_decode to
accumulate raw bytes (push decoded byte values into a Vec<u8> instead of casting
to char), also push non-% bytes as their byte values, then convert the collected
Vec<u8> to a UTF-8 String (e.g., String::from_utf8 or from_utf8_lossy) before
returning; update the percent_decode implementation to use this byte-buffer
approach so multibyte UTF-8 sequences are reconstructed correctly.
In `@pacquet/crates/resolving-git-resolver/src/parse_bare_specifier.rs`:
- Around line 317-335: The percent_decode_str function currently converts bytes
to char directly which corrupts multibyte UTF-8 sequences; instead, accumulate
decoded bytes into a Vec<u8> (push decoded byte when a %HH sequence is found,
otherwise push the raw byte), then convert that byte vector to a String using a
UTF-8-safe conversion (e.g., String::from_utf8_lossy().into_owned()) so
percent-decoded multi-byte UTF-8 is preserved; update percent_decode_str to use
this byte-buffer-then-decode approach.
🪄 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: 428a7479-e0a0-4a77-8ea8-aaf5b038d510
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (23)
Cargo.tomlpacquet/crates/package-manager/Cargo.tomlpacquet/crates/package-manager/src/install_package_from_registry.rspacquet/crates/package-manager/src/install_package_from_registry/tests.rspacquet/crates/package-manager/src/install_without_lockfile.rspacquet/crates/resolving-default-resolver/src/lib.rspacquet/crates/resolving-default-resolver/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-git-resolver/Cargo.tomlpacquet/crates/resolving-git-resolver/src/create_git_hosted_pkg_id.rspacquet/crates/resolving-git-resolver/src/git_resolver.rspacquet/crates/resolving-git-resolver/src/hosted_git.rspacquet/crates/resolving-git-resolver/src/lib.rspacquet/crates/resolving-git-resolver/src/parse_bare_specifier.rspacquet/crates/resolving-git-resolver/src/resolve_ref.rspacquet/crates/resolving-git-resolver/src/runners.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rspacquet/crates/resolving-resolver-base/Cargo.tomlpacquet/crates/resolving-resolver-base/src/lib.rspacquet/crates/resolving-resolver-base/src/resolve.rspacquet/crates/resolving-resolver-base/src/tests.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: When porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(), orstreamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plainStringor&str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only viaTryFrom<String>and/orFromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallibleFrom<String>(andFrom<&str>when convenient).
If upstream occasionally constructs without validation, exposefrom_str_uncheckedas an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization.
Use#[derive(derive_more::From)]and#[derive(derive_more::Into)]for mechanical conversion impls. Fall back to manualimplonly when conversion needs custom logic.
String-literal unions should becomeenums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Pathover&PathBuf,&strover&String) when it doesn't force extra copies.
PreferArc::clone(&x)/Rc::clone(&x)overx.clone()for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;. Two forms stay allowed: external-crate preludes likeuse rayon::prelude::*;and root-of-module re-...
Files:
pacquet/crates/resolving-git-resolver/src/create_git_hosted_pkg_id.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/package-manager/src/install_package_from_registry/tests.rspacquet/crates/package-manager/src/install_package_from_registry.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rspacquet/crates/resolving-default-resolver/src/tests.rspacquet/crates/resolving-resolver-base/src/resolve.rspacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rspacquet/crates/resolving-resolver-base/src/tests.rspacquet/crates/resolving-git-resolver/src/lib.rspacquet/crates/resolving-resolver-base/src/lib.rspacquet/crates/resolving-default-resolver/src/lib.rspacquet/crates/resolving-git-resolver/src/runners.rspacquet/crates/package-manager/src/install_without_lockfile.rspacquet/crates/resolving-git-resolver/src/parse_bare_specifier.rspacquet/crates/resolving-git-resolver/src/git_resolver.rspacquet/crates/resolving-git-resolver/src/resolve_ref.rspacquet/crates/resolving-git-resolver/src/hosted_git.rs
🧠 Learnings (1)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.
Applied to files:
pacquet/crates/resolving-git-resolver/src/create_git_hosted_pkg_id.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/package-manager/src/install_package_from_registry/tests.rspacquet/crates/package-manager/src/install_package_from_registry.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rspacquet/crates/resolving-default-resolver/src/tests.rspacquet/crates/resolving-resolver-base/src/resolve.rspacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rspacquet/crates/resolving-resolver-base/src/tests.rspacquet/crates/resolving-git-resolver/src/lib.rspacquet/crates/resolving-resolver-base/src/lib.rspacquet/crates/resolving-default-resolver/src/lib.rspacquet/crates/resolving-git-resolver/src/runners.rspacquet/crates/package-manager/src/install_without_lockfile.rspacquet/crates/resolving-git-resolver/src/parse_bare_specifier.rspacquet/crates/resolving-git-resolver/src/git_resolver.rspacquet/crates/resolving-git-resolver/src/resolve_ref.rspacquet/crates/resolving-git-resolver/src/hosted_git.rs
🔇 Additional comments (20)
pacquet/crates/resolving-resolver-base/src/resolve.rs (1)
14-63: LGTM!Also applies to: 237-247
pacquet/crates/resolving-resolver-base/Cargo.toml (1)
17-20: LGTM!pacquet/crates/resolving-resolver-base/src/lib.rs (1)
30-33: LGTM!pacquet/crates/resolving-resolver-base/src/tests.rs (1)
137-140: LGTM!pacquet/crates/resolving-git-resolver/Cargo.toml (1)
1-30: LGTM!pacquet/crates/resolving-git-resolver/src/lib.rs (1)
36-50: LGTM!pacquet/crates/resolving-git-resolver/src/runners.rs (1)
31-35: LGTM!Also applies to: 37-71, 86-96, 98-113, 115-148
pacquet/crates/resolving-git-resolver/src/create_git_hosted_pkg_id.rs (1)
15-29: LGTM!Also applies to: 35-94
pacquet/crates/resolving-git-resolver/src/git_resolver.rs (1)
30-39: LGTM!Also applies to: 41-59, 62-95, 98-141, 146-174, 226-317
pacquet/crates/resolving-default-resolver/src/lib.rs (1)
20-22: LGTM!Also applies to: 81-112
pacquet/crates/resolving-default-resolver/src/tests.rs (1)
18-19: LGTM!Also applies to: 41-45
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs (1)
145-150: LGTM!pacquet/crates/resolving-deps-resolver/src/tests.rs (1)
47-53: LGTM!pacquet/crates/resolving-git-resolver/src/resolve_ref.rs (1)
270-279: ⚡ Quick winThis suggestion is incorrect and contradicts SemVer semantics.
The review proposes retrying prerelease versions against their base MAJOR.MINOR.PATCH when
range.satisfies()rejects them. This is not how pnpm or semantic versioning works. Per the SemVer specification, prerelease versions are intentionally excluded from range matching unless explicitly requested. Retrying a prerelease against its base version after rejection would violate the spec's contract—it would force an unstable version upon a user who has not opted in. pnpm relies on standard semver logic to exclude unintended prerelease versions. The current implementation using onlyrange.satisfies(&parsed)is correct.> Likely an incorrect or invalid review comment.pacquet/crates/package-manager/src/install_package_from_registry.rs (1)
107-112: Duplicate: same panic-path concern already raised ininstall_without_lockfile.rs.This
expectis part of the same resolver/install mismatch and should be fixed together to avoid runtime panics.pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs (1)
346-347: LGTM!Also applies to: 374-374
pacquet/crates/resolving-npm-resolver/src/npm_resolver/tests.rs (1)
118-121: LGTM!Also applies to: 155-155, 253-255, 283-286
Cargo.toml (1)
42-42: LGTM!pacquet/crates/package-manager/Cargo.toml (1)
14-40: LGTM!pacquet/crates/package-manager/src/install_package_from_registry/tests.rs (1)
155-157: LGTM!
…ll chain
Adds `pacquet-resolving-git-resolver`, the Rust port of pnpm's
`@pnpm/resolving.git-resolver`. Recognises GitHub / GitLab / Bitbucket
shortcut forms and full `git+ssh:` / `git+https:` / `ssh:` / plain
`https://…/repo.git` URLs, runs `git ls-remote` to pin the commit
(partial commit search, annotated-tag dereference, semver-range matching),
and emits either a git-hosted tarball resolution or a `Git{repo,commit}`
resolution. Production runners shell out to the system `git` binary via
`tokio::task::spawn_blocking` and use the install-wide
`ThrottledClient` for the HEAD probe.
Widens the resolver-base contract so URL-shaped IDs fit: adds a
`PkgResolutionId` newtype (rule-3 branded string, infallible
`From<String>`/`From<&str>`/`From<&PkgNameVer>`), changes
`ResolveResult.id` to that type, and adds `name_ver: Option<PkgNameVer>`
so callers that need the structured `name@version` form keep working.
npm-resolver fills both fields; git-resolver leaves `name_ver` `None`
(the install path that consumes git resolutions hasn't landed yet, so
those call sites panic with a TODO message until then).
`DefaultResolver` now implements `Resolver` too (returns `Ok(None)`
when no resolver in the chain claims), letting `resolve_dependency_tree`
accept the chain directly. The install-side wiring in
`install_without_lockfile.rs` constructs
`DefaultResolver::new(vec![Box::new(npm_resolver), Box::new(git_resolver)])`
with `RealGitProbe` / `RealGitRunner`, mirroring upstream's
`createResolver` chain order.
Test coverage: 51 unit tests in the new crate, including the full
SCP-style URL repair matrix ported from `parsePref.test.ts` and the
GitLab `/-/archive/` tarball regression for pnpm #11533. Full
workspace `cargo nextest run` is green at 1635 tests.
---
Written by an agent (Claude Code, claude-opus-4-7).
…port * Reorder `#[derive(...)]` on `PkgResolutionId` to match the `prefix_then_alphabetical` rule the dylint Perfectionist lint enforces (`From` last after `Serialize`/`Deserialize`). * Add `()` to function intra-doc links that collide with same-named modules (`create_git_hosted_pkg_id`, `parse_bare_specifier`) so rustdoc's `broken-intra-doc-links` lint stops treating them as ambiguous. --- Written by an agent (Claude Code, claude-opus-4-7).
CI's `just ready` doesn't surface Perfectionist (it runs only as a
dedicated dylint job on a nightly toolchain). Fixes:
* Rename single-letter generics `P`/`R` → `Probe`/`Runner` on
`GitResolver`, `PartialSpec::finalize`, `from_hosted_git`, and
`resolve_ref`.
* Rename single-letter closure / function / let-binding params
(`s`/`h`/`c`/`p`/`i`/`g`/...) to descriptive names.
* Replace Unicode ellipsis (`…`, U+2026) with ASCII `...` in comments.
* Add trailing commas to multi-line `assert_eq!` / `assert!`
invocations, and remove the stray trailing comma on a single-line
one.
Also fix follow-on JSR-resolver test cases that still read
`result.id.{name,suffix}`: switch them to `result.name_ver.as_ref()...`
to match the post-widening `ResolveResult` shape.
---
Written by an agent (Claude Code, claude-opus-4-7).
* Replace the two `.expect()` calls on `ResolveResult.name_ver` in the install path with `.ok_or_else()` that surfaces a typed error: `InstallPackageFromRegistryError::UnsupportedResolution` and a new `InstallWithoutLockfileError::UnsupportedInstallResolution`. Now that the git resolver is in the chain, a git/tarball/local resolution reaching the without-lockfile install path returns an error end-to-end instead of panicking. Add a regression test pinning the contract. * Make `percent_decode` (in `hosted_git.rs`) and `percent_decode_str` (in `parse_bare_specifier.rs`) UTF-8 aware: collect decoded bytes into a `Vec<u8>` and reassemble via `String::from_utf8`, falling back to the original input on malformed UTF-8 (matches Node's `decodeURIComponent` throwing a `URIError` that upstream's `try/catch` swallows). The byte→`char` cast was corrupting any multi-byte sequence (e.g., `%E2%80%A6` → ellipsis); regression test added. --- Written by an agent (Claude Code, claude-opus-4-7).
Main's `feat(pacquet): peer-dependency resolution stage` reworked `install_without_lockfile.rs` to derive the virtual-store name from the resolved depPath via `pacquet_deps_path::dep_path_to_filename` instead of reading `result.name_ver`. That removed the `.expect()` / `.ok_or_else()` site this error variant was added for; with no remaining callers, drop the dead variant. --- Written by an agent (Claude Code, claude-opus-4-7).
Summary
@pnpm/resolving.git-resolverto a newpacquet-resolving-git-resolvercrate: GitHub / GitLab / Bitbucket shortcut parsing,git+ssh:/git+https:/ssh:/ plainhttps://…/repo.gitURLs,git ls-remoteref resolution (partial commits, annotated-tag dereference,#semver:range matching), and tarball-vs-Git{repo,commit}resolution selection. Production runners shell out to the systemgitbinary viatokio::task::spawn_blockingand use the install-wideThrottledClientfor the HEAD probe.ResolveResult.idto a newPkgResolutionIdnewtype on resolver-base (rule-3 branded string) so URL-shaped IDs from git / tarball / local resolvers fit. AddsResolveResult.name_ver: Option<PkgNameVer>for callers that need the structuredname@versionform; npm-resolver fills both, git-resolver leavesname_verNone.ResolverforDefaultResolverand uses it ininstall_without_lockfile.rsto chain npm + git resolvers, mirroring upstream'screateResolverorder.Paired with the Stage-1
pacquet-git-fetcher; resolutions emitted here are exactly whatGitFetcher/GitHostedTarballFetcherconsume.What is not in this PR
install_without_lockfile.rs/install_package_from_registry.rsstill need a structuredname@version, so they.expect()result.name_verand panic with a TODO message when the field isNone. Lockfile-driven installs are unaffected (they don't go through the resolver).currentPkg && !updateshort-circuit. Pacquet doesn't threadcurrentPkgthrough the resolver seam yet — every resolve re-runsls-remote.gisthost, the upstreambrowse/bugs/docs/file/gitURL templates, and proxy / TLS plumbing on the HEAD probe (it uses the install-wideThrottledClientdirectly).Test plan
cargo nextest run --workspace— 1635 tests pass (51 new inpacquet-resolving-git-resolver, including the full SCP-style URL repair matrix ported fromparsePref.test.tsand the GitLab/-/archive/regression for Errors when a dependency is specified as https://gitlab.com/owner/repo" #11533).cargo clippy --workspace --all-targets -- --deny warnings— clean.cargo fmt --all+taplo format— clean.just integrated-benchmarkagainst npm-only manifests (sanity check the chain wiring doesn't regress the existing npm path).pacquet install zkochan/is-negative) against a real registry, once the install path that consumes a gitResolveResult.name_ver = Noneis wired.Written by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit
New Features
Bug Fixes / Improvements