AGENTS.md
pnpm/AGENTS.mdAGENTS.md
Quality
69/100
Scores the file, not the repository.Length
3,207 words
21 headings · 6 code blocksRepository
36k
— · pushed 0 days agoLast changed
2 days ago
First indexed 2 days ago.1# AGENTS.md (pacquet)23Guidance for AI coding agents working in `pnpm/`.45**Read [`../AGENTS.md`](../AGENTS.md) first.** It covers the conventions that apply across the whole monorepo — GitHub PR workflow, signing agent-authored content, conventional commit messages, code-reuse philosophy, "never ignore test failures," and the PR-conflict resolution script. This file specializes those rules for pacquet's Rust code and adds pacquet-only ones.67## What this project is89`pacquet` is the [pnpm](https://pnpm.io) CLI implemented in Rust. It is one of10two parallel implementations of the same package manager — the other is the11TypeScript pnpm CLI (the workspaces outside `pnpm/`). The two are kept12behaviorally identical: the same commands, flags, defaults, error codes, file13formats, lockfile shape, and directory layout. pacquet is not a downstream port14that trails the TypeScript CLI; it is a source of truth in its own right, at15near-complete feature parity, and the two stacks are developed together.1617## The cardinal rule1819**pacquet and the TypeScript pnpm CLI must stay behaviorally identical.**20They are parallel implementations of one package manager, developed together at21near-complete feature parity. Any user-visible change — a command, flag,22default, error code or message, lockfile/manifest/state-file format, log23emission parsed by `@pnpm/cli.default-reporter`, store layout, or hook24semantic — must land in both stacks at the same time. The repo-wide statement25of this obligation lives in26[`../AGENTS.md`](../AGENTS.md#keep-pnpm-and-pacquet-in-sync); this section is the27pacquet-side detail.2829Neither stack is downstream of the other. You are not "porting from" the30TypeScript code: when you implement or change behavior in pacquet, make the31equivalent change in the TypeScript workspaces in the same PR, and vice versa.32If you genuinely can't (different expertise, scope too large, or the other33stack hasn't grown the surrounding feature yet), ship your side and say so in34the PR description so the matching commits can follow before it lands.3536Working rules:37381. **Keep the two implementations in agreement.** When you touch behavior in39 pacquet, find the counterpart in the TypeScript workspaces — they live at40 the repo root (`pnpm/` for the CLI entry, `pkg-manager/`, `resolving/`,41 `lockfile/`, `store/`, `fetching/`, `config/`, `hooks/`, and so on; see the42 [repo-structure section](../AGENTS.md#repository-structure)) — and change it43 there too. The two must agree on logic, edge cases, config resolution, error44 messages, and file/lockfile formats.452. **Match observable behavior, not structure.** Structural similarity (similar46 function decomposition and names) is a convenience for cross-referencing, not47 a requirement. What must match is what a user or a downstream tool can48 observe.493. **Don't diverge unilaterally.** Do not add a feature, flag, or quirk to one50 stack without the other, and do not "fix" a behavior in only one. A genuine51 bug present in both is fixed in both.524. **Log emissions are part of behavioral identity.** A function that fires53 `pnpm:<channel>` events through the reporter must use the same call site,54 payload, and ordering in both stacks so `@pnpm/cli.default-reporter` parses55 pacquet's NDJSON the same way it parses the TypeScript CLI's. See56 [Reporter / log events](./CODE_STYLE_GUIDE.md#reporter--log-events)57 in the style guide for the convention (channel mapping, threading58 `R: Reporter`, emit-site placement, recording-fake tests).595. **Prefer real fixtures; reach for the dependency-injection seam60 only when they can't cover the branch.** Most happy paths and61 error paths should be tested with a `tempfile::TempDir`, the62 mocked registry, or an integration test that spawns the actual63 binary. Use the DI seam — a capability trait on the `Host`64 provider, threaded as `Sys: <Bounds>` — only for branches a real65 fixture can't reach portably: filesystem error kinds66 (`PermissionDenied`, `ENOSPC`, …), deterministic time, shared67 process-global state a test would otherwise mutate68 (`env::set_var`, `set_current_dir`, the umask, …), or the69 external-service happy paths in features like `pnpm login` (2FA)70 and `pnpm publish` (OIDC / provenance) when those land. See71 [Dependency injection for tests](./CODE_STYLE_GUIDE.md#dependency-injection-for-tests)72 in the style guide for the gating rule, the names (`Sys`, `Host`,73 `Fs*`, `Clock`, `EnvVar`, …), the eight principles, and the74 `modules-yaml` worked example.7576If the intended behavior is unclear or looks wrong, stop and ask the user77rather than guessing.7879## Modeling branded string types8081TypeScript pnpm leans on *branded* string types. A branded string is a82plain string narrowed by a phantom property (for example,83`type PkgName = string & { __brand: 'PkgName' }`), so the type system can84track intent that the runtime cannot see. Some brands are stamped through85a validating constructor. Others are minted with a bare `as` type assertion and86have no runtime check at all. Both stacks must preserve that distinction,87because it is part of the public contract pnpm exposes through manifest,88lockfile, state, and config files. The TypeScript brand and the Rust newtype89must agree on validation policy.9091Rules for a Rust newtype standing in for a branded string type ("the92TypeScript side" below is its TypeScript counterpart):93941. **Declare a newtype wrapper.** Do not collapse the brand into a plain95 `String` or `&str`. Give the type its own struct so misuse is a type96 error in pacquet too.972. **If upstream always validates before construction, validate too.**98 When every brand site in pnpm runs through a checking factory, pacquet's99 wrapper must construct only via `TryFrom<String>` and/or `FromStr`. Do100 not provide an infallible public constructor that takes an arbitrary101 string.1023. **If upstream never validates, just brand for type-safety.** Some103 upstream brands exist purely to keep the type system from confusing104 one string slot with another. For example, a brand may exist to prevent105 a `PkgId` from being passed where a `PkgName` is expected, even though106 the value is never validated at runtime. In that case the Rust wrapper107 should expose an infallible `From<String>` (and `From<&str>` when108 convenient). The type-safety win is the whole point, and no validator109 is needed.1104. **If upstream occasionally constructs without validation, expose111 `from_str_unchecked`.** When pnpm sometimes mints the brand via a bare112 `as` assertion, skipping its validator, add a `from_str_unchecked` (or113 similarly named) constructor on the Rust side so callers can opt into114 the same unchecked path explicitly. Keep the validating constructor as115 well. `from_str_unchecked` is the escape hatch, not the default.1165. **Match upstream serde behavior.** If the branded type crosses a117 JSON, YAML, or INI boundary (manifest files, lockfiles, state files,118 config files, and similar), wire the wrapper into serde so the119 validation policy survives serialization:120 - `#[serde(try_from = "String")]` for deserialization, so121 deserialized values go through the validator.122 - `#[serde(into = "String")]` for serialization.123 Use both when the type is round-tripped.1246. **Derive simple conversions with `derive_more`.** When the conversion125 impls implied by the rules above are mechanical (a one-liner that126 wraps or unwraps the inner field), use `#[derive(derive_more::From)]`127 and `#[derive(derive_more::Into)]` rather than handwriting an `impl`128 block. Fall back to a manual `impl` only when the conversion needs129 custom logic, such as validation or normalization. `derive_more` is130 already a workspace dependency.1317. **String-literal unions become `enum`s.** If upstream uses a string132 literal type or a union of string literals (for example,133 `'auto' | 'always' | 'never'`), model it as a Rust `enum`, not a134 newtype wrapper. The set of valid values is closed, so encode that.1358. **Template literal types are branded strings.** If upstream uses a136 string template literal type (for example,137 ``` `${string}@${string}` ```), treat it the same as a branded string138 type. Use a newtype wrapper with the validation discipline from rules139 2 through 5 above.140141## Follow the project guides1421431. Follow the contributing guide in [`CONTRIBUTING.md`](./CONTRIBUTING.md), and **ALWAYS** double-check before committing. It covers commit message format, writing style, setup, and the automated checks to run before committing.1442. Follow the code style guide in [`CODE_STYLE_GUIDE.md`](./CODE_STYLE_GUIDE.md), and **ALWAYS** double-check before committing. It covers code-level conventions not enforced by tooling: imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, `pipe-trait`, error handling, test layout, logging during tests, and cloning of `Arc` and `Rc`.145146## Repo layout (inside `pnpm/`)147148- `crates/` — library and binary crates that make up pacquet.149 - `cli`, `package-manager`, `package-manifest`, `lockfile`, `store-dir`,150 `tarball`, `registry`, `network`, `npmrc`, `fs`, `executor`,151 `diagnostics`, `testing-utils`.152- `tasks/` — developer tooling: `integrated-benchmark`, `micro-benchmark`,153 `registry-mock`.154- `CONTRIBUTING.md` — commit-message format, writing style, setup, and the155 automated checks to run before submitting. Read it before submitting code.156- `CODE_STYLE_GUIDE.md` — manual code-style conventions beyond what `cargo157 fmt`, `taplo`, and clippy enforce: imports, modules, naming, ownership158 and borrowing, trait bounds, pattern matching, `pipe-trait`, error159 handling, test layout, and `Arc`/`Rc` cloning. Read it before submitting160 code.161162The Rust workspace (`Cargo.toml`, `Cargo.lock`, `rust-toolchain.toml`,163`justfile`, `.cargo/`, `.taplo.toml`, etc.) lives at the **repo root**, not164inside `pnpm/`. Run `cargo` and `just` from the repo root.165166## Commands167168Prefer `just` recipes when one fits; drop down to `cargo` / `taplo` / etc.169directly when you need flags the recipe doesn't expose (e.g. filtering tests170by crate or name — see below).171172- `just ready` — run the same checks CI runs (typos, fmt, check, test, lint).173 Run this before declaring a task complete.174- `just test` — `cargo nextest run`.175- `just lint` — `cargo clippy --locked --workspace --all-targets -- --deny warnings`.176- `just check` — `cargo check --locked --workspace --all-targets`.177- `just fmt` — `cargo fmt` + `taplo format`.178- `just cli -- <args>` — run the pacquet binary.179- `just registry-mock <args>` — manage the mock registry used by tests.180- `just integrated-benchmark <args>` — compare revisions or compare against181 pnpm itself (see `CONTRIBUTING.md`).182183Warnings are errors (`--deny warnings` in lint). Do not silence them with184`#[allow(...)]` unless there is a specific, justified reason.185186## Tests187188- Tests live alongside the code they exercise (standard Cargo layout) plus189 integration tests under each crate's `tests/`. Shared pacquet fixtures live190 under `crates/testing-utils/src/fixtures/`; registry package fixtures live191 under `../pnpr/.fixtures/packages/`.192- Snapshot tests use `insta`. When an intentional change alters a snapshot,193 review the diff carefully, then accept with `cargo insta review`. Never194 accept snapshot changes blindly.195- Tests that need the mocked registry start `pnpr` through196 `pacquet-testing-utils`; `cargo test` / `cargo nextest run` should not197 require a separate `just registry-mock launch` step.198- When a behavior change spans both stacks, keep their tests in sync — give199 pacquet a Rust test for the same scenario the TypeScript stack covers (and200 vice versa) whenever it translates. Matching test coverage is the easiest201 way to prove behavioral parity.202- The active test-porting plan lives in203 [`plans/TEST_PORTING.md`](./plans/TEST_PORTING.md). It enumerates the204 upstream TypeScript tests scheduled to be ported (with file paths and line205 numbers) and the conventions expected of the ports — `known_failures`206 modules, `pacquet_testing_utils::allow_known_failure!` at the207 not-yet-implemented boundary, and the practice of temporarily breaking the208 subject under test to verify the ported test actually catches the209 regression. Consult it before adding ported tests, and update its210 checkboxes as items land.211- When temporarily breaking an implementation (to prove a test catches the212 regression, or for any other experiment), revert with `git restore <file>`,213 never by moving a saved backup copy into place. Cargo's freshness check is214 mtime-based, and an mtime-preserving restore leaves the binary compiled215 from the broken source looking fresh — later test runs then fail in216 impossible-looking, "flaky" ways with nothing pointing at the stale217 artifact. Details in the "Test the tests" section of218 [`plans/TEST_PORTING.md`](./plans/TEST_PORTING.md). If test outcomes ever219 flip with no code change, `touch` the implementation file and rerun before220 debugging anything else.221222### No "tolerant" tests for missing tools223224Tests must not be tolerant of a missing build / runtime environment by225silently `return`-ing early when a tool isn't found. Patterns like:226227```rust228fn skip_if_no_git() -> bool {229 if std::process::Command::new("git").arg("--version").output().is_err() {230 eprintln!("skipping: `git` not on PATH");231 return true;232 }233 false234}235236#[test]237fn my_test() {238 if skip_if_no_git() {239 return;240 }241 // ...242}243```244245are forbidden. If the test needs a tool, just call into it and let the246existing `.unwrap()` / `.expect(...)` panic when the tool is absent — a247failing test in an under-provisioned environment is the correct signal.248Tolerance defeats the purpose of testing: if the environment really249doesn't have the required tools, that's the *environment's* fault and it250needs to be fixed.251252This applies in particular to `git`, `node`, and `npm` — git is ubiquitous253on developer machines, and Node.js is a documented prerequisite for254building pnpm. There is no realistic environment in which pacquet's tests255should run *and* these tools should be absent.256257The only marginally acceptable exception is platform-locked tools — APIs258or binaries that exist on one OS but not another. Even then, prefer259`#[cfg_attr(target_os = "windows", ignore = "...")]` (or the matching260`#[cfg(unix)]` gate already used in this crate for `/bin/sh` shims) over a261runtime probe-and-skip helper. The gate is visible to `cargo test` and262shows up in the test report; a silent `return` does not.263264### Running tests narrowly265266Running the full suite is slow. While iterating, target what you're working267on:268269```sh270# One crate271cargo nextest run -p pacquet-lockfile272273# One test by name substring274cargo nextest run -p pacquet-lockfile <name_substring>275276# One integration test file277cargo nextest run -p pacquet-lockfile --test <file_stem>278```279280Run `just ready` (full suite) before handing the PR off.281282## Style283284`CODE_STYLE_GUIDE.md` is the source of truth. Highlights:285286- Choose owned vs. borrowed parameters to minimize copies; widen to the most287 encompassing type (`&Path` over `&PathBuf`, `&str` over `&String`) when it288 doesn't force extra copies.289- Prefer `Arc::clone(&x)` / `Rc::clone(&x)` over `x.clone()` for reference-290 counted types, so the cost is visible at the call site.291- Follow the test-logging guidance in the style guide — log before non-292 `assert_eq!` assertions, `dbg!` complex structures, skip logging for simple293 scalar `assert_eq!`.294- Follow [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/naming.html)295 for naming.296- **No star imports inside module bodies.** Write `use super::{Foo, bar}`297 instead of `use super::*;`, and the same for any other glob whose298 target is a module you control. Two forms stay allowed: external-crate299 preludes such as `use rayon::prelude::*;` and root-of-module300 re-exports such as `pub use submodule::*;` in a `lib.rs`. See the301 "No star imports" section in `CODE_STYLE_GUIDE.md`.302303### Comments304305Same baseline as [`../AGENTS.md`](../AGENTS.md#comments): write code that explains itself; comments are for the non-obvious *why*, not a translation of the *what*.306307Rust-specific additions:308309- **Doc comments (`///`, `//!`) are rustdoc-visible API documentation.** Use them for item contracts. Put implementation-only rationale in regular `//` comments.310- **Tests are documentation. Do not duplicate them in prose.** If a behavioral scenario, edge case, failure mode, or worked example is already captured by a test (its name, its setup, its assertions), do not also narrate it in the doc comment on the implementation. The doc comment should state the contract once; the test demonstrates the behavior. The same applies in reverse: a test's own doc comment should not re-explain what the asserts already say, only the *why* if it is not obvious.311- **`// SAFETY:`, `// TODO:`, and similar prefixes are the exception.** They signal hidden invariants or known follow-ups that a reader cannot recover from the code alone.312313Prefer renaming, restructuring, or extracting a helper over leaving a comment. Reach for prose only when the right names and types genuinely cannot carry the information.314315### Preserve existing method chains316317When editing existing code, do not break a method chain (including `pipe-trait`318`.pipe(...)` chains) into intermediate `let` bindings unless you can justify319the rewrite. Valid justifications include a chain that fails to compile after320your edit, a borrow checker rejection, a meaningful performance win from321splitting it up, or any other concrete reason the chain cannot stay as it is.322Refactoring for style alone is not a justification when the task is something323else. Keep the surrounding code shape intact and confine your edits to what324the task asks for.325326When the change you need can fit inside the existing chain, keep it there.327For example, swapping a `PathBuf::from` allocation for a `Path::new` borrow:328329```diff330 output331 .stdout332 .pipe(String::from_utf8)333 .expect("convert stdout to UTF-8")334 .trim_end()335- .pipe(PathBuf::from)336+ .pipe(Path::new)337 .parent()338 .expect("parent of root manifest")339 .to_path_buf()340```341342Do not flatten the chain just because you happen to be editing nearby:343344```diff345-output346- .stdout347- .pipe(String::from_utf8)348- .expect("convert stdout to UTF-8")349- .trim_end()350- .pipe(PathBuf::from)351- .parent()352- .expect("parent of root manifest")353- .to_path_buf()354+let stdout = String::from_utf8(output.stdout).expect("convert stdout to UTF-8");355+Path::new(stdout.trim_end()).parent().expect("parent of root manifest").to_path_buf()356```357358If you do need to break a chain, state the justification in your reply, the359commit message, or the PR description so a reviewer can confirm the rewrite360was warranted. If the rewrite is purely stylistic, raise it with the user as361its own change rather than including it in an unrelated edit.362363## Code reuse (pacquet specifics)364365The general "search before you write / extract shared code / prefer mature366crates / keep deps at the right level" rules from367[`../AGENTS.md`](../AGENTS.md#code-reuse-and-avoiding-duplication) apply.368Pacquet-specific notes:369370- Shared helpers tend to live in `crates/fs`, `crates/testing-utils`, and371 `crates/diagnostics` — check there first.372- Check whether the workspace already depends on something suitable (see373 `[workspace.dependencies]` in the root `Cargo.toml`) before adding a new374 dependency.375- **Keep dependencies at the right level.** Add a new dependency to the376 specific crate that needs it, not to the workspace root or to a shared377 crate unless multiple crates actually depend on it.378379## Errors and diagnostics380381User-facing errors go through `miette` via the `pacquet-diagnostics` crate.382Match pnpm's error codes and messages where pnpm defines them — error codes383are part of the public contract, not implementation detail. See384<https://pnpm.io/errors> for the canonical list.385386## Commit and PR hygiene387388- Keep commits focused. A bug fix commit should not also refactor or389 reformat unrelated code.390- When a change has a counterpart in the TypeScript pnpm CLI, land both391 together; if they must be split, cross-reference the matching PR so a392 reviewer can confirm the two stacks stay in sync.393- Run `just ready` before pushing.394- The repo-wide husky `pre-push` hook runs `pnpm/scripts/pre-push-rust.sh`,395 which checks `rustfmt`, `taplo`, `cargo clippy` (with `--all-targets -D396 warnings`), `cargo doc` (with `RUSTDOCFLAGS=-D warnings`), and `cargo397 dylint`. Make sure your environment398 can run cargo (the hook needs it) before pushing; `cargo-dylint` is399 detected at runtime and skipped with a warning if not installed.400401### Commit messages402403Conventional Commits applies (see404[`../AGENTS.md`](../AGENTS.md#commit-messages) for the full type list). Use405a scope that names the crate or area being touched, matching the existing406history (`git log --oneline` for examples). Pacquet adds one type beyond the407standard list:408409- `bench`: benchmark-only changes.410411Examples (from this repo's history):412413```414fix(network): set explicit timeouts on default reqwest client415feat(lockfile): support npm-alias dependencies in snapshots416perf(store-dir): share one read-only StoreIndex across cache lookups417```418419## Things not to do420421- Do not add a feature, flag, or behavior to one stack without making the422 same change to the other. The two move together.423- Do not change lockfile format, store layout, `.npmrc` semantics, or CLI424 surface in only one stack — those are the shared contract and must change425 in both at once.426- A dependency that is already declared in `[workspace.dependencies]` in the427 root `Cargo.toml` may be added to any crate that needs it.428- Do not add a dependency that is not already declared in the workspace429 without an explicit human request. If there is a clear benefit and430 justification for pulling in a new third-party crate, ask the human to431 approve it and to add it to `[workspace.dependencies]` rather than adding432 it yourself. Consult `deny.toml` when evaluating candidates.433- Do not introduce `unsafe` without a clear justification and review.434- Do not disable lints, tests, or CI checks to make a PR green.435
Also in pnpm/pnpm
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 199k | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| aaif-goose/gooseAGENTS.md · 52k | AGENTS.md | setupbuildtestlint-format+6 | 100/100 | 3 days ago | |
| wpscanteam/wpscanAGENTS.md · 9.7k | AGENTS.md | setupbuildteststyle+6 | 100/100 | 2 days ago | |
| SkeneTechnologies/skene-cookbookAGENTS.md · 51 | AGENTS.md | setupbuildtestlint-format+7 | 100/100 | 2 days ago | |
| duckduckgo/content-scope-scriptsspecial-pages/AGENTS.md · 70 | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| mui/material-uiAGENTS.md · 99k | AGENTS.md | setupbuildtestlint-format+9 | 100/100 | 3 days ago | |
| trick77/agents-md-syncAGENTS.md · 2 | AGENTS.md | setupbuildteststyle+5 | 100/100 | 3 days ago | |
| TryGhost/Ghoste2e/AGENTS.md · 55k | AGENTS.md | setupteststylearch+2 | 100/100 | 3 days ago |
