jessegall/code-commandments

An architecture compiler for PHP — single-concern discipline prophets that judge a codebase against a configurable style and architecture spec

Maintainers

Package info

github.com/jessegall/code-commandments

pkg:composer/jessegall/code-commandments

Statistics

Installs: 1 240

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 7

v4.29.0 2026-06-29 21:21 UTC

This package is auto-updated.

Last update: 2026-06-29 21:22:40 UTC


README

A compiler for architecture.

code-commandments judges a PHP codebase against a set of architectural disciplines and reports each violation — a "sin" — as a file:line that points at the skill which teaches the fix. It's built for driving an AI coding agent: the agent reads the skill, fixes at the source, and re-runs until clean.

It pairs two layers:

  • Skills — the teaching layer, one per architectural subject (absence, value-objects, spatie-data, exceptions, enums-with-behaviour, laravel-idioms, role-vocabulary, concurrent-state, documentation, fix-at-the-source). The source of truth for what good looks like.
  • Sin Detectors — thin finders over a fluent AST engine. Each finds one sin and names the skill that fixes it; it carries no fix logic.

Every detector is proven against a self-checking fixture where #[Sinful] attributes are the test spec, and must fire on ≥3 genuinely-different scenarios while leaving a righteous look-alike untouched.

Install

composer require --dev jessegall/code-commandments

Usage

# scan a codebase — sins grouped by the skill that fixes them
vendor/bin/commandments judge src

# scope to one skill (group) or one detector
vendor/bin/commandments judge src --skill=exceptions
vendor/bin/commandments judge src --detector=SwallowCatch

# scope to what you changed: only your branch's files vs main, or just the working tree
vendor/bin/commandments judge src --branch        # new/changed on this branch vs main (--branch=BASE to override)
vendor/bin/commandments judge src --changes       # uncommitted working-tree changes only (alias: --git)

# detectors run across 8 workers by default (capped at CPU cores); --parallel=1 disables
vendor/bin/commandments judge src --parallel=4

# skip paths; list everything
vendor/bin/commandments judge src --exclude=Generated,vendor
vendor/bin/commandments judge --list

Exit code is non-zero when sins are found. Files marked @code-commandments-generated are skipped automatically.

Detectors

50 detectors across 14 skills.

absence

Detector What it flags
DeNulledFinderDetector A ?T finder whose result TRAVELS and is de-nulled at every stop — checked (finder()?->…, === null, ?? default) at two or more call sites.
NullableCallbackDetector A nullable callback (?callable $cb = null) that the body null-normalises before calling — if ($cb !== null) { $cb(…); }, ($cb ?? fn () => …)(…).
NullableCollectionReturnDetector A method declared to return ?array / array | null — a collection modelled as "the list, or null", forcing every caller to guard before iterating.
OptionAsNullableDetector An Option worn as a nullable — ?Option / Option | null, or unwrapOr(null) collapsing it straight back to a null.

concurrent-state

Detector What it flags
ConcurrentSubclassDetector A class that extends Concurrent.

documentation

Detector What it flags
ArchaeologyCommentDetector A comment that narrates the code's past — // previously..., // changed from..., // now it returns....
BloatedDocblockDetector A class whose docblock runs to multiple paragraphs.
CeremonyDocblockDetector A docblock that only restates the typed signature — @param Type $x with no description on an already-typed parameter, plus maybe a bare @return Type.

enums-with-behaviour

Detector What it flags
ConstClassEnumDetector A class that is nothing but scalar constants — a closed set of values hand- rolled as const STATUS_PENDING = 'pending' instead of a native backed enum.
EnumCaseOrChainDetector $x === Status::Pending || $x === Status::Paid — a hand-rolled membership test against two-or-more cases of the same backed enum.
EnumValueMatchDetector A match/switch over a backed enum's ->value at a call site — the enum unwrapped to a scalar so it can be dispatched on out here.
InArrayMirrorsEnumDetector in_array($x, ['a', 'b', …]) whose literals ARE an existing backed enum's case values — testing membership of a set the type already seals.
MatchDefaultReturnsNullDetector A match whose default arm returns null/false/[] instead of throwing.
StringMatchMirrorsEnumDetector A match/switch whose arm conditions are string/int literals that ARE an existing backed enum's case values — dispatching on the loose strings instead of the type that already seals them.

exceptions

Detector What it flags
GenericExceptionDetector Throwing a generic SPL/base exception (throw new \RuntimeException(...)) instead of a named domain exception.
MessageAtThrowDetector throw new X("…message…") — the failure described with a prose string at the throw site instead of a named factory carrying domain VALUES (throw OrderNotFound::forId($id)).
SwallowCatchDetector A catch that swallows the failure into absence — an empty body, or whose only effect is return null/false/[].
WrappingWithoutCauseDetector Throwing a new exception inside a catch without passing the caught one on as its cause (previous) — the original failure and its stack trace are dropped, so the wrapped error lies about where it came from.

fix-at-the-source

Detector What it flags
DuplicateFunctionDetector Two-or-more functions/methods with an identical AST — the same code copy-pasted, down to a formatting-blind structural hash (spacing, newlines, and comments are ignored; only real code differences count).
ManufacturedFakeFillDetector Filling an argument with a manufactured fake on absence — name: $row['name'] ?? '', (int) ($row['id'] ?? 0).
NearDuplicateFunctionDetector Two-or-more functions/methods with the same SHAPE but not identical text — the same control-flow skeleton differing only in variable names or literal values (a type-2 clone).

guard-clauses-and-flow

Detector What it flags
DeepNestingDetector An if nested three-deep — a pyramid of conditions.
IfElseLadderDetector An if/elseif ladder of four-plus branches — a chain of conditions doing the job of a match, a method on the type, or polymorphic dispatch.
InlineThrowDetector A ?? throw buried inside a larger expression — fed into a call or dereferenced on the same line instead of guarded at the top.
LoopInvertedGuardDetector A loop whose entire body is wrapped in one if — the iteration's real work pushed a level deep behind a condition.
NestedTernaryDetector A nested / chained ternary — $a ? $b : ($c ? $d : $e) — folds a branching decision into one unreadable expression where the operator precedence is a trap.
RedundantElseDetector An else after an if branch that already exits (return/throw/continue/ break).

laravel-idioms

Detector What it flags
ConfigReadDetector Reading configuration with config(...) inside a class instead of injecting a typed config object.
ContainerReachDetector Reaching into the container with app() / resolve() from a class the container itself resolves — the dependency belongs in the constructor.
FacadeCallDetector A Laravel facade call — Cache::get(...), Log::info(...), Mail::raw(...).
MassUpdateAtCallSiteDetector A bare $model->update([...]) on an Eloquent model at a call site — an anonymous array of column writes with no name and no home.
ModelMutationAtCallSiteDetector Setting an Eloquent model's properties then calling ->save() at a call site — $order->status = 'paid'; $order->save();.
RawRequestInputDetector Raw, untyped request reads (->input()/->get()/->query()/->post()) on a request from outside the request class — use a typed accessor instead (->string(), ->integer(), …).
RequestAccessorRecastDetector Re-coercing a typed request accessor at a CALL SITE — $request->string('id')->toString() (or (string) $request->string('id')) in a handler/tool/service.

pass-the-object

Detector What it flags
ParamResolvedFromParamDetector A method that UNPACKS its target out of a container parameter — takes a container object AND a scalar key, resolves the key against the container (request(Workflow $workflow, string $nodeId) doing $workflow->graph->nodeById($nodeId)), and works on the resolved target while the container is only ever packaging.

role-vocabulary

Detector What it flags
NullableRegistryLookupDetector A class's own keyed store handing back null on a miss — return $this->items[$key] ?? null.

spatie-data

Detector What it flags
AllNullableDataDetector A Spatie Data class whose every promoted field is optional — nullable or defaulted.
DataMethodHintCollisionDetector A Spatie Data class with a @method docblock tag that names a method the class ACTUALLY declares — e.g.
ManualHydrationLoopDetector <Data>::from(...) called per item of a collection — inside a foreach/for/ while loop, or as an array_map callback (array_map(X::from(...), $rows), array_map(fn ($r) => X::from($r), $rows)).
NewDataObjectDetector Constructing a RICH Spatie Data object with new instead of ::from() — the raw new skips the work ::from() does: a cast, a name map, a nested-Data hydration, or a magic fromX() factory.
NonFinalDataDetector A Spatie Data class that is not declared final.

tell-dont-ask

Detector What it flags
FeatureEnvyDetector Exiled behaviour (feature envy) — a method that reaches THROUGH one other owned object's structure, iterating its collection, to do work that belongs ON that object ($node->edges(), not EdgeDetector::detect($node)).
KeyedLookupEnvyDetector Feature envy through an indirect lookup — a method that uses an owned object's identity as a KEY to fetch data about it through a collaborator, then reads a fact back ($this->registry->get($node->key)->reservedOutputNames).

type-honesty

Detector What it flags
MaskedInvariantDetector $this->scratch?->call() ?? false — defaulting a reach into the object's own TRANSIENT nullable state.
ScratchStateRestoreDetector A method that SAVES one of its own properties to a local and RESTORES it afterwards — $prev = $this->scope; … $this->scope = $prev;.

value-objects

Detector What it flags
ArrayBagDetector An array parameter read by a string-literal key ($bag['total']) — a structured bag that should be a typed value object.
ArrayReturnBagDetector Returning a multi-field, string-keyed array literal — a structured bag that should be a typed value object.
DataClumpDetector The same three-or-more value parameters (string $shopId, string $userId, string $channelId) threaded through two-or-more signatures in different classes.
PositionalTupleReturnDetector Returning a positional TUPLE — return [$node, $key, $inputs, $outputs] (also from a closure / arrow fn) — bundles several independent values as a keyless list the caller must destructure by position.
RawDecodedArrayReturnDetector Returning a freshly-decoded payload straight out of a boundary — the raw array from json_decode(...) crossing back into the app untyped.

Developing detectors

A detector is a few lines of fluent AST query. Before writing one, load the project skills (via Claude Code's Skill tool): writing-detectors, detector-engine, detector-fixtures. The cardinal rule is AST/semantic detection over name matching. See CLAUDE.md and the roadmap in SINS.md.

final class FacadeCallDetector implements Detector
{
    public function skill(): string { return 'laravel-idioms'; }

    public function find(Codebase $codebase): array
    {
        return $codebase
            ->whereStaticCall()
            ->where(fn (AstNode $n): bool => str_starts_with($n->staticCallClass() ?? '', self::FACADE_NS))
            ->get();
    }
}

License

MIT.