Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .perlcriticrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Perl::Critic configuration for Langertha
#
# Scope: this file enforces NAMING CONVENTIONS as machine-checkable rules so
# that contributors (human and AI agents) get consistent identifiers without
# relying on review-by-eye. It deliberately runs only the naming-related
# policies; broader style/complexity policies are left out so the gate stays
# focused and low-noise. It is tuned to pass clean on the current tree, so it
# can be wired into CI as an enforcement gate.
#
# Conventions enforced (the de-facto standard already used across lib/):
# * Packages StartsWithUpper (CamelCase) Langertha::Engine::OpenAIBase
# * Subroutines snake_case from_openai, chat_with_tools_f
# (leading "_" for private/builder subs allowed:
# _build_api_key, _build__json)
# * Lexical variables snake_case my $tool_choice
# * Globals / constants snake_case OR ALL_CAPS our $VERSION, our %ROLE_TO_CAPS
#
# Run: perlcritic lib/ bin/
# (.perlcriticrc is the default profile lookup path, so no --profile needed)

# Only report violations from the policies explicitly enabled below, and run
# them regardless of their stock severity.
severity = 1
only = 1
verbose = 8

#-----------------------------------------------------------------------------
# Capitalization: enforce the project's identifier casing.
# Each option takes a built-in scheme tag (e.g. :starts_with_upper) or a
# literal regexp the identifier must match. The snake_case regexp below allows
# leading underscores (private/builder subs) and multi-underscore separators
# (Moose builders for private attributes, e.g. _build__json -> attr _json).
#-----------------------------------------------------------------------------
[NamingConventions::Capitalization]
severity = 5

# Packages start with an upper-case letter (CamelCase namespaces).
# package_exemptions matches each "::"-separated component independently and is
# auto-anchored with \A...\z, so "vLLM" exempts the Langertha::Engine::vLLM
# namespace whose capitalization mirrors the upstream project's own brand.
packages = :starts_with_upper
package_exemptions = vLLM

# Subroutines: snake_case, optional leading underscore(s) for private helpers
# and Moose builders. subroutine_exemptions REPLACES the policy's built-in list,
# so we re-list the Moose/Perl lifecycle + tie/overload methods and add
# FOREIGNBUILDARGS (MooseX::NonMoose) which Moose dispatches on by exact name.
subroutines = _*[a-z][a-z0-9]*(?:_+[a-z0-9]+)*
subroutine_exemptions = AUTOLOAD BUILD BUILDARGS FOREIGNBUILDARGS CLEAR CLOSE DELETE DEMOLISH DESTROY EXISTS EXTEND FETCH FETCHSIZE FIRSTKEY GETC NEXTKEY POP PRINT PRINTF PUSH READ READLINE SCALAR SHIFT SPLICE STORE STORESIZE TIEARRAY TIEHANDLE TIEHASH TIESCALAR UNSHIFT UNTIE WRITE

# Local lexicals: snake_case (optional leading underscore allowed).
local_lexical_variables = _*[a-z][a-z0-9]*(?:_+[a-z0-9]+)*

# Package globals & constants: snake_case OR ALL_CAPS
# (covers our $VERSION, our %ROLE_TO_CAPS, and ordinary package globals).
global_variables = [a-z][a-z0-9]*(?:_[a-z0-9]+)*|[A-Z][A-Z0-9_]*

#-----------------------------------------------------------------------------
# Reject deliberately confusing identifier names. The forbid list is the PBP
# default minus "last", "record", "set", and "left", which appear here with
# unambiguous, well-scoped meanings (e.g. $last = most recent result).
#-----------------------------------------------------------------------------
[NamingConventions::ProhibitAmbiguousNames]
severity = 5
forbid = abstract bases close contract no right second
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ Test framework: `Test2::Bundle::More`. `dzil release` is forbidden without expli
- **MCP**: `Net::Async::MCP` (client), `MCP::Server` (tool definitions, `inputSchema` camelCase).
- **POD**: `@Author::GETTY` PodWeaver. `# ABSTRACT:` required on every `.pm`; inline `=attr`,
`=method`, `=seealso`. Use the `pod-writer` agent for documentation.
- **Naming**: `CamelCase` packages, `snake_case` subs/methods/variables (leading `_` for
private/builder subs), `ALL_CAPS` for constants/package globals (`$VERSION`, `%ROLE_TO_CAPS`).
Enforced by `.perlcriticrc` (NamingConventions policies) — run `perlcritic lib/ bin/`; the
tree passes clean. See README "Coding conventions".

## Architecture

Expand Down
10 changes: 10 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ Revision history for Langertha

{{$NEXT}}

- Enforce naming conventions via Perl::Critic. Added a focused .perlcriticrc
enabling the NamingConventions::Capitalization and ProhibitAmbiguousNames
policies (CamelCase packages, snake_case subs/vars with leading "_" for
private/builder subs, ALL_CAPS constants/globals), tuned to pass clean on
the current tree so it can serve as a CI gate. Documented the conventions
in README ("Coding conventions") and CLAUDE.md. Renamed the camelCase
$operationId/$opId locals in Langertha::Role::OpenAPI to snake_case
($operation_id) to match the surrounding style (behaviour-preserving; the
OpenAPI spec key and POD term "operationId" are unchanged).

- Tool wire-translation value-object symmetry (karr #1, #2; ADR 0001/0003).
Reconciled the two inbound tool-call entry points ADR 0001 flagged as
unsettled: Langertha::ToolCall->extract is now strictly the format-pinned
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,34 @@ my $models = $engine->list_models(force_refresh => 1); # Bypass cache

Results are cached for 1 hour (configurable via `models_cache_ttl`).

## Coding conventions

Langertha enforces consistent identifier naming so the codebase stays
predictable for both human contributors and AI agents. The conventions are the
de-facto standard already used throughout `lib/`:

| Identifier | Convention | Example |
|---|---|---|
| Packages / namespaces | `CamelCase` | `Langertha::Engine::OpenAIBase` |
| Subroutines & methods | `snake_case` | `from_openai`, `chat_with_tools_f` |
| Private / builder subs | leading `_` + `snake_case` | `_build_api_key`, `_build__json` |
| Lexical variables | `snake_case` | `my $tool_choice` |
| Package globals / constants | `snake_case` or `ALL_CAPS` | `our $VERSION`, `our %ROLE_TO_CAPS` |

These rules are machine-checkable via [Perl::Critic](https://metacpan.org/pod/Perl::Critic).
A focused [`.perlcriticrc`](.perlcriticrc) at the repo root enables the
`NamingConventions::Capitalization` and `NamingConventions::ProhibitAmbiguousNames`
policies (and nothing else, to keep the gate low-noise). The current tree passes
clean, so it can be wired into CI as an enforcement gate.

```bash
# One-time: install the linter
cpanm Perl::Critic

# Lint the shipped code (uses .perlcriticrc automatically)
perlcritic lib/ bin/
```

## Testing

```bash
Expand Down
22 changes: 11 additions & 11 deletions lib/Langertha/Role/OpenAPI.pm
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ sub _build_openapi_operations {
for my $method (keys %{$paths->{$path}}) {
next unless ref $paths->{$path}{$method} eq 'HASH';
my $op = $paths->{$path}{$method};
my $opId = $op->{operationId} or next;
my $operation_id = $op->{operationId} or next;
my $ct;
if ($op->{requestBody} && $op->{requestBody}{content}) {
$ct = 'application/json' if $op->{requestBody}{content}{'application/json'};
$ct //= 'multipart/form-data' if $op->{requestBody}{content}{'multipart/form-data'};
}
$operations{$opId} = {
$operations{$operation_id} = {
method => uc($method),
path => $path,
defined $ct ? (content_type => $ct) : (),
Expand Down Expand Up @@ -105,10 +105,10 @@ a limited compatibility mode.
=cut

sub can_operation {
my ( $self, $operationId ) = @_;
my ( $self, $operation_id ) = @_;
return 1 unless scalar @{$self->supported_operations} > 0;
my %so = map { $_, 1 } @{$self->supported_operations};
return $so{$operationId};
return $so{$operation_id};
}

=method can_operation
Expand All @@ -121,12 +121,12 @@ returns true when C<supported_operations> is empty (unrestricted mode).
=cut

sub get_operation {
my ( $self, $operationId ) = @_;
my ( $self, $operation_id ) = @_;
croak "".(ref $self)." runs in compatibility mode and is unable to perform this OpenAPI operation"
unless ($self->can_operation($operationId));
unless ($self->can_operation($operation_id));
my $ops = $self->openapi_operations;
my $op = $ops->{operations}{$operationId}
or croak "".(ref $self).": operationId '$operationId' not found in spec";
my $op = $ops->{operations}{$operation_id}
or croak "".(ref $self).": operationId '$operation_id' not found in spec";
my $url = $self->url || $ops->{server_url};
return ( $op->{method}, $url.$op->{path}, $op->{content_type} );
}
Expand All @@ -142,9 +142,9 @@ operation is not in C<supported_operations>.
=cut

sub generate_request {
my ( $self, $operationId, $response_call, %args ) = @_;
my ( $method, $url, $content_type ) = $self->get_operation($operationId);
$log->debugf("[%s] %s %s (%s)", ref $self, $method, $url, $operationId);
my ( $self, $operation_id, $response_call, %args ) = @_;
my ( $method, $url, $content_type ) = $self->get_operation($operation_id);
$log->debugf("[%s] %s %s (%s)", ref $self, $method, $url, $operation_id);
$args{content_type} = $content_type if defined $content_type;
return $self->generate_http_request( $method, $url, $response_call, %args );
}
Expand Down