Release-as-a-whole: candidate MAJOR (pre-1.0 minor bump — ^0.4 → ^0.5) — ships two new rules (ForbidHttpExceptionInActionsRule, ForbidResourceWrappedInJsonResponseRule) from the Commander's review of emmie PR #481 (war-room enforcement queue #123 + #124). Both surface new errors in already-clean code wherever a consumer violates, so each consumer adopts on its own ^0.4 → ^0.5 bump PR (the ^0.{minor} caret means ^0.4 excludes 0.5.0 — tagging auto-adopts nobody). Blast radius (surveyed 2026-06-25 origin/development): ZERO violators on emmie + kendo for both rules (the #481 offender is on its branch, not merged) — the per-territory bump is expected clean save for un-merged branch work.
Added
ForbidHttpExceptionInActionsRule— flagsthrowstatements insideApp\Actions\*classes whose thrown expression's type is a subtype ofSymfony\Component\HttpKernel\Exception\HttpExceptionInterface(theHttpExceptionfamily —HttpExceptionitself plus every subclass:NotFoundHttpException,AccessDeniedHttpException,UnprocessableEntityHttpException, …). HTTP status concerns belong to the HTTP layer; an Action that throws a 422 has reached past its boundary into transport. A uniqueness rule belongs in the FormRequest; a domain failure throws a custom domain exception the renderer maps to a status. Identifier:forbidHttpExceptionInActions.httpExceptionInAction. Doctrine: war-room §Architectural Principles — Explicit over implicit (#1) + Form Request → DTO → Action pipeline (#3). Type-aware sibling ofForbidAbortHelperRule(which bans theabort()helper family whose own message recommendsthrow new HttpException— correct for controllers, wrong for Actions): this rule closes the matching gap on the directthrow new HttpException(...)form inside Actions, catching subclass throws, fully-qualified throws with nouseimport, and typed-value throws that an import-checking arch test would miss.Illuminate\Validation\ValidationExceptionis explicitly OUT of scope — Actions legitimately thrownew ValidationException($validator)for stateful / cross-field validation that cannot live in a static FormRequest; it is not a member of the SymfonyHttpExceptionfamily, so the type gate never fires on it. Action-namespace gate mirrorsForbidDatabaseManagerInActionsRule(App\Actionsprefix via$scope->getNamespace()+str_starts_with). Out of scope: non-App\Actions\*namespaces (controllers, FormRequests, exception renderers, middleware all legitimately raise HTTP-layer exceptions); theabort()helper family (covered byForbidAbortHelperRule). Seed: Commander review of emmie PR #481 —CreateLocationEmailActionthrewHttpException(422, …)for an "override already exists" uniqueness check (war-room enforcement queue #123). Blast radius (surveyed 2026-06-25origin/development): ZERO raw-HttpException-in-Action on emmie + kendo (the #481 offender is on its branch, not merged) — the rule lands green and red-flags #481 at CI once enabled. Versioning: per ADR-0021 §Versioning, candidate Major bump (the rule surfaces new errors in already-clean code wherever a consumer territory has an Action throwing an HTTP-layer exception). Pre-cascade audit required per consumer at Phase-B bump (^0.4 → ^0.5); the emmie + kendo survey shows zero current violators, so the cascade is expected clean fleet-wide save for any un-merged branch work.ForbidResourceWrappedInJsonResponseRule— flagsresponse()->json($payload, …)andnew JsonResponse($payload, …)insideApp\Http\Controllers\*classes only when$payload's resolved type is a subtype ofIlluminate\Http\Resources\Json\JsonResource. AJsonResourceis already aResponsable— Laravel serializes it to a JSON response on its own; wrapping one in an explicit JSON response double-wraps the payload and discards the resource's own response shaping. Return the resource directly instead:return XxxResource::fromModel($model);(HTTP 200). Identifier:forbidResourceWrappedInJsonResponse.resourceWrapped. Doctrine: war-room §Architectural Principles — Explicit over implicit (#1) + ADR-0009 (Unified ResourceData Pattern — resources own their own response serialization). Type-awareness is mandatory: a blanket string-ban onresponse()->json(...)would false-positive on the overwhelmingly common legitimate sites that wrap a plain array / DTO / scalar / message envelope, and onresponse()->json(null, 204)(a 2026-06-25 fleet survey sized ~24 emmie + ~43 kendoresponse()->json/JsonResponsesites, almost all legitimate non-Resource payloads — a blanket ban would be ~67 false positives). Two AST shapes inspected: (1)MethodCallnamedjsonwhose receiver is theresponse()helperFuncCall(AST-shape match — the helper'sResponseFactoryreturn type is unloaded in stub-only analysis environments, mirroring howEnforceCurrentUserAttributeRulematches theauth()helper); (2)New_ofIlluminate\Http\JsonResponse(FQCN via$scope->resolveName()). Named-envelope edge (decided: EXCLUDE): a resource (or resource collection) nested under a named array key — e.g. emmieRegistrationBroadcastController:28's['registrations' => …Resource::collect(...)]— is a deliberate response envelope, not a bare double-wrap; the first argument is anArray_whose type isarray<…>, not aJsonResourcesubtype, so the type gate naturally lets it through. Theresponse()->json($resource, 201)status-override form still fires (the wrap is the violation, not the status; resource-with-non-200 is an unrelated future investigation). Controller-namespace gate mirrorsForbidEloquentMutationInControllersRule. Out of scope: non-App\Http\Controllers\*namespaces; non-JsonResource payloads; resources nested in any envelope. Seed: Commander review of emmie PR #481 —EmailController::storereturnedresponse()->json(EmailResource::fromModel($email), 201)while siblingupdate()correctly returned the Resource directly (war-room enforcement queue #124). Versioning: per ADR-0021 §Versioning, candidate Major bump (the rule surfaces new errors in already-clean code wherever a controller wraps a resource in an explicit JSON response). Pre-cascade audit required per consumer at Phase-B bump (^0.4 → ^0.5); the 2026-06-25 survey found ~0 current violators on emmie + kendodevelopment(the #481 offender is on its branch).