# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Development environment

All Perl/build operations run inside the Docker dev container. Do not install Perl, cpanm, or build deps on the host.

```sh
docker ps                                    # confirm params-util-dev-1 is running
docker exec params-util-dev-1 sh -ec 'cd /workspace && <command>'
```

The container mounts the repo read-write at `/workspace`. The `perl-5.30`...`perl-5.42` Compose services exist for cross-version testing but mount the workspace **read-only** (and `cp -a` to `/tmp/build` first), so use the `dev` service for active work.

## Common commands

```sh
# Inside the container (`docker exec params-util-dev-1 sh -ec 'cd /workspace && ...'`):
perl Makefile.PL                # regenerates t/pp and t/xs from t/inline (see below)
make                            # compiles XS
make test                       # full suite incl. xt/ author tests
make manifest                   # regenerate MANIFEST after adding files
make realclean                  # nukes generated tests + build artifacts

prove -lbv t/xs/02_main.t       # single test file (after `make`)
prove -lbv t/inline/10_leaks.t  # leak tests need Test::LeakTrace

perltidy -b <file>              # in-place tidy; remove the .bak afterwards
podchecker lib/Params/Util/<f>  # POD validation
```

## Architecture

### Dual PP/XS implementation

`Params::Util` ships both a Pure-Perl backend (`lib/Params/Util/PP.pm`) and an XS backend (`Util.xs`). The loader in `lib/Params/Util.pm` falls back to PP for any function not present in the XS module:

```perl
Params::Util->can($_) or *$_ = Params::Util::PP->can($_) for (@EXPORT_OK);
```

`PERL_PARAMS_UTIL_PP=1` forces PP at runtime. **Any new predicate must exist in PP**; XS is an optional accelerator.

### XS uses ALIAS groups (ADR-005)

Functions that share logic differing only by the called Perl method or by a small validation switch live in one C body, dispatched via `ix`:

- Group 1 (`_IDENTIFIER`/`_CLASS`) — string validators
- Group 2 (`_POSINT`/`_NONNEGINT`) — numeric validators
- Group 3 (`_CLASSISA`/`_CLASSDOES`/`_CLASSCAN`/`_SUBCLASS`) — class + `call_method`
- Group 4 (`_INSTANCE`/`_INSTANCEDOES`/`_INSTANCECAN`) — `sv_isobject` + `call_method`
- Group 5 (`_INVOCANT`/`_INVOCANTCAN`) — invocant check + optional `call_method`

When adding a sibling predicate to one of these groups, add an `ALIAS:` line and an entry in the static `methods[]` table — do **not** copy a whole new XSUB.

`_HANDLE`, `_DRIVER`, `_SET`, `_SET0` are intentionally PP-only (see ADR-005).

### Tests are generated — `t/inline/` is canonical (ADR-001)

`t/pp/` and `t/xs/` are gitignored. They are regenerated by `Sandbox/Tumble.pm` at every `perl Makefile.PL` run and shipped in `MANIFEST` so CPAN consumers do not need `Test::WriteVariants`.

**Edit only `t/inline/*.t`.** Variants differ solely in the `BEGIN { $ENV{PERL_PARAMS_UTIL_PP} = 0|1; }` preamble that Tumble injects.

After adding a test file: `make manifest` to register `t/inline/`, `t/pp/`, and `t/xs/` versions.

The test fixture classes at the tail of `t/inline/02_main.t` are deliberately asymmetric:

- `Foo` — has `sub foo`
- `Bar` — `@ISA = 'Foo'` (real inheritance, inherits `foo`)
- `Baz` — overrides `isa` to fake `isa('Foo')` but has **no** `foo` method
- `Bad` — unrelated, only has `sub bad`

`Baz` is the discriminator between `_ISA` (which trusts the faked `isa`) and `_CAN`/`_DOES` (which see no actual method). Tests that exercise `_CAN`/`_DOES` against `@objects` must filter — e.g. `grep { $_->can('foo') } @objects` — or they will fail on the three Baz blessings.

### Architecture Decision Records

`lib/Params/Util/ADR-NNN.pod` (Nygard format: Status / Context / Decision / Consequences). Existing ADRs cover Test::WriteVariants (001), multi-method `_CAN`/`_DOES` deferral (002), `Params::Util::Assert` deferral (003), `Test::Params::Util` as separate dist (004), XS ALIAS consolidation (005). When making a non-trivial design choice, write an ADR rather than burying the rationale in a commit.

## Author-test gates

`make test` runs `xt/*.t` which can fail on otherwise-correct code:

- **`xt/perltidy.t`** — generated `t/pp`/`t/xs` are excluded; `Sandbox/`, inline tests, and ADR pods are checked. Run `perltidy -b <file>` to fix; perltidy's brace placement is the authority, do not hand-format.
- **`xt/spelling.t`** — every novel proper noun (`ADR`, `arity`, `validators`, ...) needs a stopword in `xt/spelling.t`'s `__DATA__` block.
- **`xt/manifest.t`** — run `make manifest` after adding/renaming files.
- **`xt/pod.t`/`pod-coverage.t`** — keep the SYNOPSIS predicate list in `lib/Params/Util.pm` synchronised with `@EXPORT_OK`.

POD with em-dashes/curly quotes fails podchecker without an `=encoding` line. Stick to ASCII (`--`, `'`, `"`) in pods.

## Conventions

- **Commits** use `git commit --signoff`, Chris Beams style (imperative subject ≤ 50 chars, body explains *why*). Pass multi-line messages via a `.git-commit-msg` file (HEREDOC corrupts the backticks); delete the file after committing. Include the `Co-Authored-By: Claude Opus <noreply@anthropic.com>` trailer.
- **No emoji** in code, comments, or pods unless explicitly requested.
- The leak-test suite (`t/inline/10_leaks.t`) must stay green for every XS change that touches `call_method`/stack handling.
