feat(git-fetcher): real npm-packlist semantics with .npmignore + bundleDependencies (#436 follow-up)#468
Conversation
📝 WalkthroughWalkthroughRewrites packlist to use the ChangesPacklist algorithm upgrade with ignore crate integration
Sequence DiagramsequenceDiagram
participant Caller
participant PacklistFn as packlist()
participant Matcher as files matcher
participant IgnoreWalker as ignore crate walker
participant BundleHandler as bundled deps
Caller->>PacklistFn: call(pkg_dir, manifest)
PacklistFn->>Matcher: compile manifest.files into Gitignore matcher
PacklistFn->>IgnoreWalker: walk tree with .npmignore/.gitignore
IgnoreWalker->>PacklistFn: stream entries (filtered)
PacklistFn->>Matcher: check files_field inclusion
PacklistFn->>PacklistFn: re-add root always-included files
PacklistFn->>PacklistFn: force-include main and bin paths
PacklistFn->>BundleHandler: read bundleDependencies keys and splice results
BundleHandler->>PacklistFn: return bundled subtree entries under node_modules/<name>/
PacklistFn->>Caller: return combined file list
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 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)
Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #468 +/- ##
==========================================
- Coverage 88.64% 88.57% -0.08%
==========================================
Files 116 116
Lines 9952 10048 +96
==========================================
+ Hits 8822 8900 +78
- Misses 1130 1148 +18 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Replaces the MVP files-field-only packlist in crates/git-fetcher with a faithful port of npm-packlist / pnpm fs/packlist. The walker now honors per-directory .npmignore/.gitignore, treats the files field as a gitignore-style allowlist, force-includes the standard always-included files plus main/bin, and recurses into bundleDependencies (and the legacy bundledDependencies / true shorthand). The hand-rolled glob matcher is removed in favor of the ignore crate.
Changes:
- New 4-pass
packlist()usingignore::WalkBuilderandgitignore::Gitignore, withbundleDependenciesrecursion undernode_modules/<name>/. - Adds
ignore = "0.4.25"workspace dependency and wires it intopacquet-git-fetcher. - Expands
packlist/tests.rswith 7 new unit tests covering.npmignore/.gitignore, always-included overrides, nested.npmignore, both bundle-deps spellings, and a missing-bundle-dir case.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
crates/git-fetcher/src/packlist.rs |
Rewrites the packlist algorithm to use ignore + four explicit passes, including bundleDependencies recursion. |
crates/git-fetcher/src/packlist/tests.rs |
Adds 7 tests covering the new .npmignore, always-included, bundleDependencies, and missing-dir behaviors. |
crates/git-fetcher/Cargo.toml |
Adds ignore workspace dep. |
Cargo.toml |
Adds ignore = "0.4.25" to [workspace.dependencies]. |
Cargo.lock |
Lockfile entry for ignore 0.4.25 and its transitive deps. |
Comments suppressed due to low confidence (1)
crates/git-fetcher/src/packlist.rs:204
- There is no cycle guard on bundleDependencies recursion. If a bundled dep at
node_modules/aitself bundlesbwhich is materialized atnode_modules/a/node_modules/b/node_modules/a/..., the recursivepacklist()will not terminate. Consider tracking visited canonicalpkg_dirpaths and short-circuiting on repeats, or imposing a depth limit, to keep a malformed git-hosted package from hanging the fetcher.
for bundle_name in bundle_dep_names(manifest) {
let bundle_pkg_dir = pkg_dir.join("node_modules").join(&bundle_name);
if !bundle_pkg_dir.is_dir() {
tracing::debug!(
target: "pacquet::git_fetcher::packlist",
bundle_name = %bundle_name,
pkg_dir = %pkg_dir.display(),
"bundleDependencies entry not present under node_modules/; skipping",
);
continue;
}
let bundle_manifest = safe_read_package_json_from_dir(&bundle_pkg_dir)
.ok()
.flatten()
.unwrap_or_else(|| Value::Object(serde_json::Map::new()));
let nested = packlist(&bundle_pkg_dir, &bundle_manifest)?;
for rel in nested {
out.insert(format!("node_modules/{bundle_name}/{rel}"));
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…leDependencies (#436 follow-up) Replaces the MVP `files`-field-only packlist with a faithful port of [`npm-packlist`](https://github.com/npm/npm-packlist) / [`pnpm/fs/packlist`](https://github.com/pnpm/pnpm/blob/94240bc046/fs/packlist/src/index.ts). Four passes: 1. Walk via `ignore::WalkBuilder` configured for `.npmignore` + `.gitignore` per-directory inheritance. 2. Apply the `files`-field allowlist on top, compiled into a single `Gitignore` matcher. 3. Always-include `package.json`, `README*`, `LICEN[SC]E*`, `CHANGES*`, `CHANGELOG*`, `HISTORY*`, `NOTICE*` at the root, plus `main` / `bin` paths — these override `.npmignore` *and* the `files`-field filter, matching npm-packlist's `alwaysIncluded`. 4. Recurse into `bundleDependencies` (and the legacy `bundledDependencies`) under `node_modules/<name>/`. Each bundled dep gets its own `packlist()` pass and the result splices in under its name. Accepts both `[<name>]` arrays and `true` (bundle every entry in `dependencies`, npm's lesser-known shorthand). New runtime dep: `ignore = "0.4.25"` (BurntSushi, used by ripgrep; MIT/Unlicense, `cargo deny` clean). Replaces the hand-rolled `*` / `**` / `?` glob matcher in the old packlist.rs — the new matcher uses `ignore::gitignore::GitignoreBuilder::add_line`, which gives us full gitignore semantics (negation, anchored vs unanchored, directory-suffix rules) for free. Two intentional divergences from upstream documented in the module header: - When both `.npmignore` and `.gitignore` exist in the same directory, `ignore` combines their rules; npm-packlist would use only `.npmignore`. Same outcome for the common case (a `.npmignore` that's a strict superset); divergence only when `.npmignore` explicitly includes a path `.gitignore` excludes (rare in published packages). - `.git/info/exclude` and global `~/.gitignore` are NOT honored. The fetcher operates on a clean tarball / checkout, not the user's working tree. Tests: 7 new packlist tests cover `.npmignore` exclusion, `.gitignore` fallback when no `.npmignore`, always-included override of `.npmignore`, per-subdir `.npmignore` inheritance, `bundleDependencies` subtree inclusion, legacy `bundledDependencies` spelling, and the missing-bundle-dir graceful skip. All 7 existing packlist tests still pass under the new implementation (the `*` / `**` / `?` glob assertions still hold because the `ignore` crate's globset is a superset of what the old matcher did).
Micro-Benchmark ResultsLinux |
cf50582 to
8aa2341
Compare
…versal, dir-vs-basename excludes (#468 review) Four CodeRabbit findings on the npm-packlist port: - **`WalkBuilder::parents` defaults to `true`**. `ignore` would search above `pkg_dir` for `.gitignore` / `.npmignore` and apply them to the walk; for a packlist the pack must depend only on the package's own contents. Adding `.parents(false)` to the builder isolates the walk to `pkg_dir`. Test: `npmignore_in_parent_dir_does_not_leak_in` plants a `.gitignore` in the temp parent and asserts `index.js` still ships. - **`bundleDependencies` path traversal**. `bundle_name` was joined into `pkg_dir/node_modules/<name>` without validation; a malicious manifest with `bundleDependencies: ["../../etc"]` could escape. New `is_safe_bundle_name` runs the same `Component`-based check `cas_io::join_checked` uses: only `Normal` and `CurDir` components pass; `ParentDir`, `RootDir`, `Prefix` are refused. Scoped names like `@scope/foo` still pass (their `/` resolves under `node_modules`, not above it). Test: `bundle_dependencies_rejects_path_traversal`. - **`should_always_exclude` over-eager segment walk**. The single list mixed dir-typed exclusions (`.git`, `.svn`, `.hg`, `CVS`) with file-typed exclusions (`.npmrc`, lockfiles, debug logs). Splitting into `ALWAYS_EXCLUDED_DIR_SEGMENTS` (matched at any segment) and `ALWAYS_EXCLUDED_BASENAMES` (basename-only) keeps the VCS-state filter while removing the dead double-fire on files. Test: `always_excluded_dir_segments_only_match_vcs` plants both a `lib/CVS/Root` (must exclude) and `lib/cvs-tools.txt` (must include). - **`files_field_includes` leaf fallback was redundant *and* wrong**. `Gitignore::matched` correctly handles unanchored patterns at any depth (verified empirically: `cli` matches `bin/cli`). The leaf fallback was dead code. But it also failed to handle the directory-include case (`files: ["cli"]` should include `lib/cli/index.js` if `lib/cli` is a directory matching the pattern). Switch to `matched_path_or_any_parents`, which walks ancestor segments and returns `Ignore` when any segment matches. Test: `files_field_bare_basename_matches_at_depth` pins root + nested file + directory-match cases. No production code touched outside `packlist.rs`. 18 packlist tests pass (14 from the first commit + 4 new regressions). `just ready`: 925 → 925 passes locally.
…ain/bin (#468 review round 2) Two more CodeRabbit findings: - **Recursive `packlist()` had no depth limit or cycle detection.** A bundled dep whose own `bundleDependencies` points back at itself (or any symlink loop in `node_modules`) would stack-overflow the fetcher. Refactor `packlist` into an inner `packlist_inner` that threads a `HashSet<PathBuf>` of canonical visited paths plus a `depth: u32` counter through every recursion. `fs::canonicalize` resolves symlinks so a loop shows up as the same path; the `MAX_BUNDLE_DEPTH = 32` is a belt-and-braces guard against any cycle the canonical-path check can't see (filesystem mount tricks, etc.). Test: `bundle_dependencies_self_cycle_is_caught` plants a symlink loop through `node_modules/self/node_modules/self`, declares a self-bundling manifest, asserts the recursion is cut after one level. - **Pass 3 force-included `main` / `bin` without consulting `should_always_exclude`.** A manifest with `"main": "package-lock.json"` would re-add the lockfile after pass 1 filtered it. Run both `main` and `bin` paths through `should_always_exclude` before insertion — the always-excluded set wins over manifest fields, matching npm-packlist's silent override. Tests: `main_field_pointing_at_always_excluded_basename_is_refused` (lockfile case) and `bin_field_pointing_at_vcs_segment_is_refused` (`.git/`-prefixed path case). No production code changes outside `packlist.rs`. Local test count: 925 → 928. `cargo deny`, dylint, doc-deny-warnings all green.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/git-fetcher/src/packlist.rs (1)
393-415: 💤 Low valueMinor edge case: bundle name
.would be accepted.If
bundleDependenciescontains".", the validation passes (CurDir is allowed), andpkg_dir.join("node_modules").join(".")resolves topkg_dir/node_modulesitself. This would attempt to recurse intonode_modules/as if it were a package.In practice this is unlikely to occur since
.is not a valid npm package name, andnode_modules/package.jsontypically doesn't exist. The primary defense against..traversal is working correctly. Consider rejecting names that resolve to empty or consist only of.components if you want to tighten this further:🛡️ Optional tightening
fn is_safe_bundle_name(name: &str) -> bool { if name.is_empty() { return false; } let path = std::path::Path::new(name); if path.is_absolute() { return false; } + let mut has_normal = false; for component in path.components() { match component { - std::path::Component::Normal(_) => {} + std::path::Component::Normal(_) => { has_normal = true; } std::path::Component::ParentDir | std::path::Component::RootDir | std::path::Component::Prefix(_) => { return false; } // `.` components are stripped silently — `./foo` resolves // the same as `foo` on every platform. std::path::Component::CurDir => {} } } - true + has_normal }🤖 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 `@crates/git-fetcher/src/packlist.rs` around lines 393 - 415, The function is_safe_bundle_name currently allows a bare "." name via std::path::Component::CurDir; update is_safe_bundle_name to explicitly reject names that are just "." or that normalize to only CurDir components: after the empty and absolute checks, return false if name == "." or if path.components().all(|c| matches!(c, std::path::Component::CurDir)); keep the existing disallow list for ParentDir/RootDir/Prefix and preserve other behavior.
🤖 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 `@crates/git-fetcher/src/packlist.rs`:
- Around line 393-415: The function is_safe_bundle_name currently allows a bare
"." name via std::path::Component::CurDir; update is_safe_bundle_name to
explicitly reject names that are just "." or that normalize to only CurDir
components: after the empty and absolute checks, return false if name == "." or
if path.components().all(|c| matches!(c, std::path::Component::CurDir)); keep
the existing disallow list for ParentDir/RootDir/Prefix and preserve other
behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: ffad2d7e-5aee-4486-a345-5c6a441a1e2e
📒 Files selected for processing (2)
crates/git-fetcher/src/packlist.rscrates/git-fetcher/src/packlist/tests.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). (6)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Code Coverage
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (ubuntu-latest)
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Lint and Test (macos-latest)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs
📄 CodeRabbit inference engine (AGENTS.md)
**/*.rs: Preserve existing method chains andpipe-traitchains; do not break them into intermediateletbindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Pathover&PathBuf,&strover&String) when it does not force extra copies.
PreferArc::clone(&x)andRc::clone(&x)overx.clone()for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;for any glob whose target is a module you control. External-crate preludes (e.g.,use rayon::prelude::*;) and root-of-module re-exports (e.g.,pub use submodule::*;inlib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plainStringor&str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only viaTryFrom<String>and/orFromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallibleFrom<String>andFrom<&str>constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bareasassertion), add afrom_str_unchecked(or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...
Files:
crates/git-fetcher/src/packlist/tests.rscrates/git-fetcher/src/packlist.rs
🧠 Learnings (2)
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.
Applied to files:
crates/git-fetcher/src/packlist/tests.rs
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.
Applied to files:
crates/git-fetcher/src/packlist/tests.rscrates/git-fetcher/src/packlist.rs
🔇 Additional comments (21)
crates/git-fetcher/src/packlist.rs (13)
1-49: LGTM!
51-84: LGTM!
90-131: LGTM!
145-195: LGTM!
197-238: LGTM!
240-278: LGTM!
283-325: LGTM!
327-342: LGTM!
344-357: LGTM!
359-366: LGTM!
368-383: LGTM!
417-437: LGTM!
439-463: LGTM!crates/git-fetcher/src/packlist/tests.rs (8)
1-44: LGTM!
46-94: LGTM!
96-158: LGTM!
159-241: LGTM!
243-305: LGTM!
307-393: LGTM!
395-476: LGTM!
478-529: LGTM!
Integrated-Benchmark Report (Linux)Scenario: Frozen Lockfile
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.1416566493399998,
"stddev": 0.0904564328222664,
"median": 2.1177318321399996,
"user": 2.71823808,
"system": 2.12927274,
"min": 2.02783760864,
"max": 2.31478877564,
"times": [
2.2334458906399997,
2.02783760864,
2.1255782066399997,
2.0902521246399997,
2.0347339226399996,
2.09713628364,
2.19247074764,
2.19043747564,
2.10988545764,
2.31478877564
]
},
{
"command": "pacquet@main",
"mean": 2.11319864914,
"stddev": 0.07543856045353234,
"median": 2.10035476164,
"user": 2.68716948,
"system": 2.14028554,
"min": 2.02687580564,
"max": 2.23488773364,
"times": [
2.08446285364,
2.02687580564,
2.0355271026399997,
2.18484261264,
2.06095341864,
2.0423744356399998,
2.20517345564,
2.1162466696399997,
2.14064240364,
2.23488773364
]
},
{
"command": "pnpm",
"mean": 5.339317037340001,
"stddev": 0.04858982432602792,
"median": 5.33115434464,
"user": 8.61042938,
"system": 2.69370734,
"min": 5.27132011364,
"max": 5.42717210264,
"times": [
5.42717210264,
5.38037035364,
5.391129766640001,
5.3041227516400005,
5.353317474640001,
5.29728051264,
5.30614860864,
5.27132011364,
5.32402734464,
5.33828134464
]
}
]
}Scenario: Frozen Lockfile (Hot Cache)
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.5387498851799999,
"stddev": 0.045955947753293945,
"median": 0.51873091208,
"user": 0.36655486,
"system": 0.9549635999999999,
"min": 0.50544199758,
"max": 0.65830282958,
"times": [
0.65830282958,
0.51977137058,
0.51176597758,
0.50544199758,
0.51769045358,
0.56285927058,
0.51559844658,
0.51663126758,
0.55496728558,
0.52446995258
]
},
{
"command": "pacquet@main",
"mean": 0.62935082258,
"stddev": 0.06198814461213402,
"median": 0.61905508658,
"user": 0.37442756,
"system": 0.9554202,
"min": 0.57180072258,
"max": 0.78946010958,
"times": [
0.78946010958,
0.6243984325799999,
0.63499201458,
0.64466974858,
0.61371174058,
0.64489044258,
0.58712170958,
0.57180072258,
0.60043092558,
0.58203237958
]
},
{
"command": "pnpm",
"mean": 2.1053951213800004,
"stddev": 0.06938621196694894,
"median": 2.10458274608,
"user": 2.79143266,
"system": 1.2785768999999998,
"min": 2.0009420285800004,
"max": 2.2120577795800003,
"times": [
2.0997278135800004,
2.0976499525800003,
2.17409619958,
2.2120577795800003,
2.0009420285800004,
2.0028287115800003,
2.10943767858,
2.1607501675800003,
2.13378072358,
2.06268015858
]
}
]
} |
Summary
Replaces the MVP
files-field-only packlist with a faithful port ofnpm-packlist/pnpm/fs/packlist. Four passes:.npmignore+.gitignorewalk viaignore::WalkBuilderwith per-directory inheritance. A.npmignoreinlib/applies tolib/**only; the root.gitignoreapplies to the whole tree.parents(false)keeps ignore-file lookup confined topkg_dirso a.gitignoreabove the package can't leak into the published file set.files-field allowlist compiled into a singleGitignorematcher. Usesmatched_path_or_any_parentsso a pattern likeclimatches bothbin/cli(file match) andlib/cli/index.js(directory match, wherelib/cliis a dir).package.json,README*,LICEN[SC]E*,CHANGES*,CHANGELOG*,HISTORY*,NOTICE*at the root, plusmain/binpaths. Override.npmignoreand thefiles-field filter, matching npm-packlist'salwaysIncluded.mainandbinare vetted through the always-excluded check before insertion — a manifest with"main": "package-lock.json"or"bin": ".git/hook"does not get to re-add cruft.bundleDependencies/bundledDependenciesrecursion with cycle detection: each bundled dep gets its own packlist pass undernode_modules/<name>/. Both spellings accepted plus thebundleDependencies: trueshorthand. Cycle protection uses aHashSet<PathBuf>of canonical visited paths + aMAX_BUNDLE_DEPTH = 32belt-and-braces cap so a symlink loop or self-referencing bundle can't stack-overflow the fetcher.bundleDependenciesnames are validated against the same path-traversal disciplinecas_io::join_checkeduses (Component::Normal/CurDironly; rejects.., root, drive prefix).Always-excluded set, split by type
ALWAYS_EXCLUDED_DIR_SEGMENTS—.git,.svn,.hg,CVS. Matched at any path segment, so VCS state is dropped at any depth.ALWAYS_EXCLUDED_BASENAMES—.npmrc,npm-debug.log,.DS_Store,package-lock.json,yarn.lock,pnpm-lock.yaml. Basename-only match, so a regular file likelib/cvs-tools.txtships even though it mentionsCVS.ALWAYS_EXCLUDED_SUFFIXES—.origfamily.New runtime dep
ignore = "0.4.25"(BurntSushi, used by ripgrep;Unlicense OR MIT,cargo denyclean). Replaces the hand-rolled*/**/?matcher in the oldpacklist.rs— the new matcher usesignore::gitignore::GitignoreBuilder::add_line, which gives full gitignore semantics (negation, anchored vs unanchored, directory-suffix rules) for free.Intentional upstream divergences (documented in the module header)
.npmignoreand.gitignoreexist in the same directory,ignorecombines their rules; npm-packlist would use only.npmignore. Same outcome for the common case (a.npmignorethat's a strict superset); divergence only when.npmignoreexplicitly includes a path.gitignoreexcludes — rare in published packages..git/info/excludeand global~/.gitignoreare NOT honored. The fetcher operates on a clean tarball / checkout, not the user's working tree.Out of scope (still on #436)
${filesIndexFile}\traw+ final) for skipping the re-import when packlist == raw. Pure perf optimization.skip-if-no-npmhelper) + PATH-shim shallow-fetch test + Stage 2 resolver-side install tests.Test plan
cargo nextest run -p pacquet-git-fetcher— 21 packlist tests pass:- 7 carried over from the MVP packlist (files-field, *.orig, glob edge cases, etc.).
- 7 new for
.npmignore/.gitignore/bundleDependenciescore behaviour:npmignore_excludes_listed_paths,gitignore_excludes_when_no_npmignore,npmignore_does_not_drop_always_included_files,npmignore_in_subdir_applies_to_subtree_only,bundle_dependencies_subtree_is_included,bundled_dependencies_legacy_spelling_works,bundle_dependency_missing_dir_is_silently_skipped.- 4 new safety regressions from review round 1:
npmignore_in_parent_dir_does_not_leak_in,bundle_dependencies_rejects_path_traversal,always_excluded_dir_segments_only_match_vcs,files_field_bare_basename_matches_at_depth.- 3 new safety regressions from review round 2:
bundle_dependencies_self_cycle_is_caught(symlink loop),main_field_pointing_at_always_excluded_basename_is_refused,bin_field_pointing_at_vcs_segment_is_refused.just ready— 928 tests, 928 pass.RUSTDOCFLAGS=-D warnings cargo doc --no-deps --workspace --document-private-items— clean.taplo format --checkjust dylintcargo deny check— license + bans + advisories clean for the newignoredep.Written by an agent (Claude Code, claude-opus-4-7).