Airlock
view release on metacpan or search on metacpan
misc/agent-context/.claude/skills/getty-perl-core/SKILL.md view on Meta::CPAN
- **`my ( $self, $x ) = @_;`** as the first line â explicit destructure, spaces inside the parens. Never `my $self = shift;` as argument unpacking.
- **One-liners skip unpacking** and use `shift->` or `$_[0]->`: `sub trace { shift->_logger->trace(@_) }`. This is the one place `shift` is right.
- **A builder that ignores `$self` fits on one line:** `sub _build_readonly { 0 }`.
- **`_` prefix marks private** subs and attributes. Builders for private attributes double up: `sub _build__mp`.
### Methods, not bare subs
- **In a class, every helper is a method on `$self`** â not `sub _foo {...}` invoked as `_foo($self->config, $x)`.
- **Per-process caches go on the singleton as an attribute** (`has _cache => ( is => 'ro', default => sub { {} } )`), not a `my %CACHE` package variable.
- **No package-level state** unless it is a true constant (an `%ENGINE_CLASS` lookup table counts; a per-call cache does not).
- Bare subs are fine in **non-class utility modules** imported as functions. Once a file says `use Moose`/`use Moo`, every `sub` is a method.
Why: bare subs hide what the call needs, can't be overridden or mocked, and force every caller to thread state by hand.
### Dispatch through the invocant, never a hardcoded class name
Route a class's own methods, and every collaborator it reaches for, through the invocant â never through a literal package name. Perl passes the invocant as the first argument for both `Foo->method` and `$obj->method`, so a class name is itself a v...
- **A method on the same class:** `$self->value_to_bytes($v)`, never `My::Value->value_to_bytes($v)` from inside `My::Value`.
- **A collaborator in another class:** name the class once in an overridable resolver, then dispatch through it â never a literal at the call site.
```perl
package My::Pipe;
sub backend_class { 'My::Backend::Age' } # override point, named once
sub decrypt {
my ( $self, %arg ) = @_;
my $key = $self->backend_class->unwrap( $arg{header} ); # not My::Backend::Age->unwrap
return $self->_finish( $key ); # not My::Pipe->_finish
}
package My::Pipe::Vault;
use parent -norequire, 'My::Pipe';
sub backend_class { 'My::Backend::Vault' } # a subclass swaps it; the pipeline follows
```
This holds for class-method-only modules too: write every method's first line as `my ( $self, ... ) = @_;` and dispatch through `$self` whether or not the class is ever instantiated. The module becomes subclassable for free, and callers that write `T...
## Errors
- **`croak`, never `die`.** Errors report the caller's line, not ours.
- **Import it:** `use Carp qw( croak );` and call `croak(...)` bare.
- **Name the origin in the message:** `croak __PACKAGE__."->state too many args"` â or whatever identifies the operation in that module's DSL.
## Strings
- **Concatenate, do not interpolate:** `'Adding '.$f.' with '.$length.' bytes'`. Interpolate only where concatenation would be unreadable.
- **Single quotes by default.** `'...'` and `"..."` are genuinely different in Perl â `"` interpolates and processes escapes, `'` does not. Reach for `"` when you need that, not by habit.
- **Import lists as `qw( croak confess )`** â spaces inside the parens. Never rely on default exports.
## Control flow
- **Postfix `if`/`unless`** for guards and short conditions: `croak(...) if $self->readonly;`
- **`unless $x`** instead of `if !$x`.
- **Guard clauses return bare:** `return unless $res->is_success;` â not `return undef;`.
- **Nested ternaries** for a return that picks between expressions, instead of an if/elsif chain.
## Data
- **`{ %hash }` and `\%hash` are different operations, not two styles.** `{ %h }` builds a new anonymous copy; `\%h` references the existing hash. Return a copy when the caller must not mutate your state; return the reference when sharing is the poin...
- **`Path::Tiny`** for every file operation â not `File::Spec`, not bare `open`. `path(...)->child(...)->slurp_utf8`.
- **`JSON::MaybeXS`** always â never `JSON::PP`, `JSON::XS`, `Cpanel::JSON::XS` directly. Encoders get `canonical => 1, convert_blessed => 1`.
- **Every serialiser is deterministic.** MessagePack `->canonical`, DBIC `serializer_options => { canonical => 1 }`. Same rule, every format.
- **Booleans: `JSON->true` / `JSON->false`.** `use JSON::MaybeXS;` covers codec and booleans.
- `$YAML::XS::Boolean = 'JSON::PP'` is one of YAML::XS's fixed mode names, not a module choice â leave it alone.
- **Align `=>` in multi-line hash literals** when keys are of similar length.
- **Optional pairs inline:** `$cond ? ( experimental => 1 ) : (),`
## Configuration
Config comes from environment variables prefixed with the project name
(`$ENV{MYPROJECT_TIME_ZONE}`), each with a default in code. Where many
attributes share that shape, write a generator that wraps `has` rather than
repeating the declaration.
## DBIC-ish result classes
- Column defs via **`DBIx::Class::Candy`** or **`DBIO::Candy`** â `primary_column` / `column` macros, not `__PACKAGE__->add_column(...)`.
- **`keep_storage_value => 1`** on enum and integer columns that shouldn't be inflated/deflated.
- **`\'NOW()'`** (literal scalar ref) for DB-side timestamp defaults.
## Style, comments, structure
- **2-space indentation.** Not 4. Not tabs. Every Getty Perl file.
- **No trailing commas** at the end of multi-line lists (unlike Python).
- **Section long files with a figlet banner** as a comment block. Pick from `standard`, `slant`, `small`, `banner`. Where figlet is unavailable or the file is short, a `#### <Name>` rule does the job.
- **Commented-out debug lines stay** (`#use DDP; p($res);`). They mark where debugging was needed before â deleting them as dead code removes a warning sign, and sometimes the precaution it guards.
## cpanfile
- **A `cpanfile` carries the requirements** â that is the file, not `dist.ini` prereq blocks.
- **`requires 'Module::Name';`** â the version argument is optional, omit it when unpinned. Never write `'0'`.
- **A version means "or higher".** `requires 'Foo', '5.0';` already accepts 5.1 â never write `'>= 5.0'`.
- **Alphabetical order**, phase blocks (`on test => sub {...}`) at the end.
### Getty-authored dependencies â CRITICAL
Getty's `dist.ini` uses `[@Author::GETTY]`, which sets `$VERSION` in the repo to the **next, unreleased** version (`0.402` while CPAN is at `0.401`). The repo is ALWAYS one ahead of CPAN.
1. **Pin what the code actually needs, not the number that happened to be in front of you.** Opening a sibling repo, reading its `$VERSION` and pinning that â while depending on nothing that version introduced â is the mistake this section exists...
2. **Check `cpanm --info Module::Name`** for the released version. If the released one carries what you use, that is the pin.
3. **Pinning the next, unreleased version is correct when the change spans both repos** â the sibling gained what this code calls, or a release is being prepared and the distributions are tested together from their working trees. It commits you to ...
4. **Pin every Getty-authored distribution.** Not stale, not omitted â current.
5. **Re-check on upgrade.**
```bash
cpanm --info Module::Name | tail -1
# â GETTY/Module-Name-1.234.tar.gz â pin to 1.234
```
Getty-authored (non-exhaustive): `Langertha`, `IO::K8s`, `Kubernetes::REST`, `WWW::Crawl4AI`, `Net::Async::Crawl4AI`, `Net::Async::WebSearch`, `Catalyst::Plugin::ChainedURI`, `Locale::Simple`, `DBIO::*`, `WWW::Zitadel`, `WWW::PayPal`, `WWW::Chain`.
## Every file carries its own `$VERSION`
**Each module under `lib/` and each Perl executable under `bin/` needs its own
`our $VERSION = '...';`**, set to the version that will be released NEXT â one
higher than what is on CPAN (or higher). A Perl source file without a `$VERSION`
ships versionless and breaks consumers that pin against it. Bash and other
non-Perl executables are still installed from `bin/`, but are deliberately
excluded from the PPI-based version rewrite.
( run in 0.938 second using v1.01-cache-2.11-cpan-b554fb43c41 )