Skip to content
Back to skills

Getty Perl Core

ASecurity

Load on any Perl edit in a Getty project — the house choices for module loading, errors, control flow, cpanfile and per-file $VERSION.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 4, 2026
developmentpythongobashexpresskubernetesdebuggingapibackend

Works with

  • api

Security analysis

A100/100

Scanned October 4, 2026

npx -y skills add Getty/skills --skill getty-perl-core --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Getty Perl Core?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Getty Perl Core
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/getty-getty-perl-core/badge)](https://www.skillsdirectory.com/skills/getty-getty-perl-core)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: getty-perl-core
description: "Load on any Perl edit in a Getty project — the house choices for module loading, errors, control flow, cpanfile and per-file $VERSION."
---

# Perl Core — Getty House Rules

These rules override defaults. They are non-negotiable in Getty projects.
Object-system specifics live in `getty-perl-moo` / `getty-perl-moose`.

## Module loading

- **`use Module;` at the top.** Always. Every dependency loads at compile time.
- **`require` is forbidden as a "lazy optimization".** Never use it to shave startup. `require Foo;` inside a method body → hoist it to a top-level `use`.
- **`require` only for true runtime plugin loading** — the class comes from config/DB at runtime (`Module::Runtime::use_module($class_from_db)`). Known at write-time → `use` it.
- **`require` + `->new` in a controller action** is a red flag. Hoist to `use`.

## strict and warnings

Both are active in **every** file, without exception. How they get there differs:

- **Scripts, tests, plain modules:** `use strict; use warnings;` explicitly.
- **Classes:** `Moo`, `Moose`, `Catalyst` and anything derived from them enable both on import — do not repeat them there.

The rule is "always on", never "leave them out". Omitting them from a class is correct only because the object system already did it.

## Object system

- **One object system per distribution.** Pick Moo or Moose and use it everywhere; mixing is for boundaries a framework forces (e.g. RapidApp), not a choice.
- **`is => 'ro'` is the default.** `rw` is the exception and needs a reason.
- **`lazy_build => 1` + `sub _build_foo`** over `default => sub { ... }` for anything non-trivial.
- **`weak_ref => 1`** on attributes holding a reference back to a parent/owner — standard for nested object graphs, prevents circular refs.
- **`namespace::autoclean`** on every class file. Classes extending DBIx::Class (`MooseX::NonMoose`) use **`MooseX::MarkAsMethods autoclean => 1`** instead.
- **`no Moose;` + `__PACKAGE__->meta->make_immutable;`** at the bottom of every Moose class.
- **Types:** the tendency is to type what arrives from outside — Moose's own constraints where Moose is already there, `Types::Standard` where it is not. Not every distribution needs a type system, and none needs one for every field: `getty-perl-typing`.

## Singletons

- **`->instance`** for `MooseX::Singleton` / `MooX::Singleton` classes. Never `->new` on a singleton.
- **`->new`** for everything else.

## Subroutines

- **`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 valid invocant: `$self` simply *is* `$class` when nothing is instantiated. Going through it costs nothing even for a never-instantiated class, and it is the one thing that lets a subclass override the behaviour — a literal `Some::Package->helper(...)` nails the call to that exact package, and no override or mock can reach it.

- **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 `Thing->do(...)` keep working unchanged.

## 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 point. Choose deliberately.
- **`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 for. It names an unreleased version for no reason and breaks the build for everyone off this machine.
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 one thing: the dependency is released first. Until it is, `cpanm` cannot resolve the pin, so nothing may go out ahead of it.
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.

**Only the FIRST `our $VERSION` in a file gets rewritten.** RewriteVersion::Transitional
and BumpVersionAfterRelease both stop after the first match, so a file holding two
packages leaves the second one frozen at whatever version it was written with —
while MetaProvides::Update happily reports the real release version. The result is
a distribution whose META and whose code disagree, silently, for as many releases
as it takes someone to notice.

So: **one package per file.** If you find several `package` statements in one file,
split them out before releasing.

**Executables belong in `bin/`, never `script/`.** The bundle sets no `ExecDir`, so
Dist::Zilla's default of `bin` applies: files under `script/` are not installed as
executables, and a Perl executable there has no `$VERSION` rewrite. A distribution
with a `script/` directory should have it renamed to `bin/` — otherwise none of
the executable handling takes effect.

## Forbidden

❌ `require Foo` inside a method to "speed up startup" · ❌ a Getty repo's `$VERSION` as a cpanfile requirement · ❌ `'0'` or `'>= x'` as a version argument · ❌ `default => sub {...}` for a non-trivial attribute default · ❌ 4-space indent · ❌ `File::Spec` in new code · ❌ `JSON::PP::true` / `JSON::PP::false` as barewords · ❌ `Data::Dumper` in shipped code (use `DDP` / `Data::Printer` for debug, strip before commit) · ❌ `die` where `croak` belongs

## When in doubt

Grep hand-written Getty code for how the pattern is used there — the reference is an older repo with no AI commits in its history. Newer repos may show an agent's guess rather than the house rule.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…