gracjankubicki/laravel-architecture-kit
Laravel Architecture Kit
Laravel tooling for four explicit capabilities: generated architecture guidance, bounded project context for AI agents, AST-backed audit, and an optional guard for selected enforceable rules.
This package is installed as a runtime dependency because the committed architecture configuration references its enum classes while the application boots. It lets a project choose the architecture patterns it uses, then generates commit-ready Laravel Boost guidelines and skills so AI agents code closer to the project's conventions.
Capabilities and enforcement
| Capability | What it provides | Enforced by guard? |
|---|---|---|
| Guidance | Generated guidelines, skills, and MCP resources | No — prose is advice for agents and reviewers. |
| File rules | The rules that govern one path, each marked enforced or advisory | No — it tells the agent what will be checked before it writes. |
| Scaffolding | The files and skeletons a new element of an enabled architecture needs | No — the skeleton is built to pass the audit that follows. |
| Project context | Static dependencies, dependents, roles, violations, and inspect paths for one PHP symbol | No — it informs the change before coding. |
| Audit | Deterministic AST and filesystem findings | Yes, for implemented audit rules. |
| Guard | Doctor plus audit as a CI/hook gate | Yes, according to selected architectures and strict mode. |
Guidance is intentionally broader than the rules that can be verified deterministically. Add project-specific custom audit rules when a local policy, such as bilingual documentation, must be enforced.
Because that gap is invisible in a green guard, architecture-kit:file-rules reports it per file. Each architecture is marked enforced when a rule can detect a violation, or advisory when it ships guidance only. laravel-best-practices is advisory today, and it is part of the default selection, so most projects carry guidance no rule verifies:
php artisan architecture-kit:file-rules app/Actions/CreateInvoice.php
enforced governs actions actions, folder-purity
enforced shared form-requests form-request
advisory shared laravel-best-practices guidance only
governs marks the architecture that describes the file. shared marks an architecture whose rule also runs here without the file belonging to it, which matters when deciding which guideline to read.
Custom audit rules registered under rules in config/architectures.php are reported too, in a separate project list, because they fail the same gate as the built-in ones. Rules that run over the whole project rather than per file, such as layer-dependency and namespace-cycle, are reported as always active.
Scaffolding a new element
architecture-kit:make emits the folder, namespace, naming, and base-class conventions the project already declares, so an agent does not rebuild them from prose:
php artisan architecture-kit:make actions CreateInvoice
php artisan architecture-kit:make actions Billing/CreateInvoice
The interactive command writes the files and never overwrites an existing one. With --agent, and through the scaffold MCP tool, nothing is written: the plan and the skeleton are returned and the agent decides what to create. Skeletons are built to pass this package's own audit, and a test verifies that for every supported architecture.
A name must be a plain PHP identifier, optionally prefixed with /-separated sub-namespaces. A name that would escape the architecture folder is rejected, as is a name whose suffix marks a different kind of class, such as CreateInvoiceData inside app/Actions.
Route-aware read checks
With Thin Controllers and Actions enabled, the audit reads a fresh Laravel route collection and associates each controller method with its HTTP verbs. GET/HEAD may call a cohesive read Service when Services are enabled; when Query Objects are enabled, use a Query Object for reusable read composition. POST/PUT/PATCH/DELETE Service calls retain the Action advisory. Imports and unused injected parameters are not dependency findings. Constructor dependencies are attributed to the methods that call them.
The audit follows reachable project method bodies without executing endpoints. Recognized model, builder, relation and DB mutations, jobs, mail and notifications remain visible with a call chain and the operation's file/line. A write reached through GET or HEAD is not an Architecture Kit violation by itself. The audit reports placement advice in suggestions, while enabled rules continue to report their own findings. It examines called methods, not unrelated write methods in the same Service. A transaction alone is not a write; its callback is analysed.
The same bounded analysis recognizes standard Laravel, Inertia 3, and Fortify 1 calls. It distinguishes session reads from put(), forget(), flash(), and pull(). It follows the selected Gate policy method, API Resource transformation, Inertia prop callbacks, route-specific HandleInertiaRequests::share() data, and the Fortify action or view callback for the selected endpoint. A project override always takes precedence over the built-in framework model. Container registrations through bind, singleton, and scoped may infer the contract from a closure return type. Statically visible factory returns record concrete implementations and conditional alternatives without invoking the factory. Dynamic or ambiguous response bindings produce incomplete analysis when needed by Fortify dispatch.
Framework registration and route discovery stay static after Laravel boot. The audit does not dispatch a request, instantiate a controller for discovery, or invoke an application callback. A dynamic ability, middleware assignment, callback, Fortify binding, or pipeline produces an incomplete-analysis warning instead of crediting every possible target. These relationships show that a test can reach a symbol. They do not prove that the runtime executed it or that the test asserted its result. Inertia and Fortify architecture profiles are not required for this recognition.
analysis.notices records unresolved routes, calls, types, raw SQL, and bounded traversal. These notices identify incomplete analysis and do not fail --strict by themselves. Absence of a notice means no effect was detected in the supported static subset, not proof that runtime hooks, model events, macros, magic dispatch, vendor SDKs or every possible branch are harmless. Dynamic SQL is never treated as a proved read. W_THIN_CONTROLLER_READ_SERVICE remains an enforced warning when Query Objects are enabled.
Guard success means that no enforced rule blocks the change. Review architectural suggestions separately. Suggestions may propose an architecture that is not enabled; do not enable it or refactor outside the agreed scope without the user's decision. Incomplete analysis identifies unresolved code, not a violation or proof of correctness. Keep enforcing the project's selected architecture boundaries.
Route discovery boots Laravel in a fresh child PHP process, with a process-local route-cache override. It records active providers, raw route middleware, route groups, middleware aliases, exclusions, and supported package versions. It does not dispatch a request or instantiate controllers, and does not clear the application's route cache. Normal application bootstrap code still runs. This keeps a long-lived MCP server from using routes from its initial boot. The timeout is 15 seconds; boot failure becomes an explicit incomplete-analysis finding when relevant. Programmatic callers without a Laravel bootstrap can pass a fresh RouteMap::fromRoutes($router->getRoutes(), $context) as the optional routes argument to ApplicationAudit::run(). A map without framework context remains valid for older callers, but framework dispatch that depends on missing registrations stays incomplete.
Analysis is limited to 12 method levels, 128 method visits and 20,000 AST nodes per endpoint, 100 KB per source file and 1 MB of retained source per controller, with an additional PHP memory headroom check. A cycle or exceeded budget is incomplete analysis. Source lookup stays in the configured project graph; excluded/out-of-scope dependencies are unresolved. The graph cache also stores static test-invocation metadata. Package fingerprinting invalidates older entries; fresh route snapshots are not cached. In --changed, changed dependencies recheck their controller dependents; edits under routes/bootstrap/config/providers and deleted inputs recheck all controllers. Custom route-registration files outside those locations require a full audit.
Impact before a change
Ask for direct and indirect relationships before changing a class, file, or method:
php artisan architecture-kit:impact InvoiceCalculator --agent
php artisan architecture-kit:impact 'App\Services\InvoiceCalculator::calculate' --agent
php artisan architecture-kit:impact app/Services/InvoiceCalculator.php --limit=50 --depth=8
php artisan architecture-kit:impact --schema
The MCP tool impact accepts the same subject, limit, and depth. Short names return candidates when ambiguous. A path containing multiple classes also requires choosing a candidate. Method selection resolves inherited declarations and ordinary trait methods; trait adaptations or conflicting declarations remain unresolved.
For a method, the query follows method calls at every hop. A caller of CreateInvoiceAction::other() is not a caller of handle() merely because both methods belong to the same class. Each relationship contains a witness chain with its kind, source file, and line. The report separates resolved static targets, possible targets through interfaces or parent types, callable references, and class-only context. Resolved targets describe source relationships, not runtime execution. Class-only imports and injected types do not seed the method traversal. Weak class references are shown at one hop and do not propagate further. Override declarations are listed as places to inspect. The report does not issue a BREAKING verdict.
Receiver recognition supports static calls, $this, single named types in parameters and properties, promoted constructor properties, new, and recognized assignments. Branches and mutations invalidate uncertain local types. Union/intersection receivers, dynamic methods, container lookups, facades, macros, magic dispatch, callback bodies, and unsupported trait dispatch remain unresolved or outside the supported subset. A callable array or first-class callable is a reference, never evidence that it was invoked. Framework entrypoints may have no visible PHP caller; absence of callers does not establish that a method is unused. Runtime route, queue and table inventories are outside the supported scope. Immutable proposal continuation is opt-in as described below. The separate execution channel recognizes source declarations described below.
analysis.status distinguishes none, complete, incomplete, and limit for the supported static scope. complete does not prove complete runtime reachability. Errors distinguish ambiguity, a missing subject, an out-of-scope path, a missing method, and unresolved inherited dispatch. class_context is separately labelled, and test candidates explicitly say that they do not prove coverage or PASS. Ordinary class-based test references can include tests outside the audit graph, as in architecture-context; their basis is class_reference.
Impact and path analyze authorization from source. execution.authorization in impact contains checks, rule summaries, outgoing paths, consumers and uncertainty. A check carries the operation, boolean/response or exception behavior, result usage and recognized catches. Rules count distinct recognized source sites; partial counts are labeled as lower bounds. Use path for an individual caller-to-policy chain, or reach for continuation pages from the same report. Changed source metadata invalidates further pages.
For example, InvoiceController::update -> InvoiceAccess::ensureCanEdit -> Gate::forUser(...)->authorize('update', $invoice) -> InvoicePolicy::update preserves the exact method. A route ->can('update', 'invoice') or middleware('can:update,invoice') resolves its model argument from the handler parameter. Form Request authorize() is connected only to endpoints declaring that request parameter. Controller authorizeResource() uses the standard resource ability map; custom/dynamic maps remain unresolved. Custom middleware aliases and service methods follow their actual registrations and calls.
Policy selection considers static registrations, UsePolicy and source conventions before an ability callback. Ambiguous candidates are left for verification. Global before callbacks may stop policy dispatch. Policy before participates only for a callable ability method. After callbacks can run even after a non-null global before result, and replace only null results. Guest eligibility, activation, short-circuiting of multiple abilities and exceptions appear as path conditions. Inline allowIf and denyIf use their actual argument and bypass ordinary policies/hooks. These reports do not evaluate access for a user or declare vulnerabilities from absent checks. Blade authorization directives belong to rendering analysis.
CLI and MCP impact/path read static analysis configuration without executing project PHP. Dynamic configuration yields an explicit error instead of requiring the file. Vendor, generated sources and symlinks are boundaries. Discovery cannot prove runtime provider order or active middleware.
Impact also discovers HTTP declarations automatically in routes/, bootstrap/app.php, static project provider registrations, and literal extra sources. execution.routes shows verbs, URI, name, domain, middleware, registration sites, and the exact chain from the controller method or route callback to the selected symbol. For example, POST /orders -> OrderController::store -> CreateOrderAction::handle -> Calculator::calculate does not attach a route calling OrderController::show unless that method also reaches calculate. Controller arrays, strings, invokable controllers, closures, arrow functions, nested groups and standard resource routes are supported. only and except filter expanded resource actions. URL generation with route('name') is not a request invocation.
The execution section has its own none, complete, incomplete, or limit status. Discovered declarations do not prove that routes are active at runtime. Provider activation, conditional registration, dynamic paths, macros, unknown attributes, external handlers, and unsupported resource customization carry locations and reasons to inspect. Nullable metadata indicates unresolved values or an absent optional attribute; read the route reasons. Source-only analysis never boots the application or executes bootstrap files, providers, includes, route callbacks or route-list commands. Middleware execution and model binding are outside the method chain.
execution.flows adds contextual paths through jobs, events and Eloquent model events, including paths without an HTTP entry. For example, POST /orders -> Order::create -> OrderObserver::created -> SendInvoiceJob::handle -> InvoiceService::send carries the source location and condition for every transition. execution.routes retains the original HTTP call-channel witnesses; read flows for paths crossing framework dispatch. execution.flow_analysis provides separate flow totals, notices, freshness, limits and limitations. The combined execution status includes both channels without changing signature, delete or move verdicts.
Recognized dispatch forms include the Laravel dispatch helpers, Bus facade and typed dispatcher, the Dispatchable trait methods, pending dispatch options, withChain, Bus::chain, Bus::batch, nested groups and literal queued closures. Constructing a job, preparing a group, registering a callback or taking a callable reference does not prove dispatch. A chain item requires preceding items to complete; batch branches have no implied order. Callback edges describe before, progress, success, first failure and completion conditions, with cancellation and allowFailures kept explicit. A batch inside a chain affects the continuation condition.
Each execution edge separates mode from timing. Queue requests do not prove asynchronous execution because a connection can run synchronously. A job dispatched after the response runs through synchronous dispatch, whereas a batch dispatched after the response queues its jobs. Pending queue, connection and commit options are source metadata; worker state, middleware, uniqueness, runtime results and actual retries are not evaluated.
Console sources include class commands with handle() or a fallback __invoke(), inherited handlers, property names/signatures and aliases, and Artisan closure commands. Registration sources include the application builder, standard command directories, explicit class lists, service providers, the older console kernel, and literal custom paths and includes. Laravel 13 Signature/Aliases attributes keep their version condition. Artisan call and queue and command-to-command call, callSilent and callSilently create invocation edges. Closure commands bind $this to the command context. Similar methods on application classes remain ordinary calls. The Artisan facade itself has no standard callSilent method. Dynamic names, missing sources and conflicting registrations remain unresolved notices, including when no path reaches the subject.
Scheduler sources include Schedule::command, job and call, routes/console.php, withSchedule, the older kernel and custom registration files. Groups and pending attributes carry frequency, timezone, environment, mutex and background options as source metadata. The analyzer records these settings without evaluating whether a task is due. when filters run before skip rejects with short-circuit conditions. Their callbacks can reach the subject even when the task is skipped. Before, after/then and success/failure callbacks have separate conditional paths. A queued job's dispatch callback can succeed before its worker runs; its onSuccess path never goes through Job::handle. runInBackground on a callback/job event is invalid and produces a notice. Callback tasks require a name before withoutOverlapping or onOneServer; invalid declarations do not create task paths. Success and failure callbacks also require before callbacks and preceding after callbacks to complete without throwing. Shell tasks and dynamic callbacks have no fabricated PHP relation. Scheduler declarations inside callbacks without a recognized registration stay unresolved. None of these source witnesses proves that Artisan, the scheduler or a worker actually ran.
Event sources include provider $listen mappings, Event::listen, typed closures and queueable closures, subscribers with returned maps or imperative registrations, typed discovery methods, string events and wildcards, and event interfaces. Discovery respects source configuration, custom paths and disabled discovery. Public handle* and __invoke methods are candidates; provider activation, version-dependent discovery gates, propagation and conditional queueing remain explicit conditions. The analyzer does not reflect on or instantiate application classes.
Eloquent sources include observe, inherited ObservedBy attributes, $dispatchesEvents and model callbacks. Lifecycle transitions depend on existence, dirty data, successful operations and listener responses. Mass builder writes and quiet operations do not create lifecycle edges. withoutEvents propagates through the selected callback and helpers on that path; it does not remove explicit job dispatch or unrelated events. Deferred execution starts a separate event context. Custom model events and fallback string events keep their conditions.
Execution sources have independent freshness based on paths, modification times and sizes, including Composer configuration, custom listener roots, missing inputs and directory additions/deletions. Edits preserving both time and size remain outside this contract. Sources and callbacks are never executed. Discovery uses at most 20,000 directory entries, 10 MB of source, 100 KB per source and 20,000 AST visits per file with depth and memory checks. Link construction and traversal have explicit budgets; traversal retains at most 1,000 path states and visits 10,000 edges. Truncated totals are lower bounds. Inspect notices before interpreting an empty flow list.
HTTP source freshness is independent of the project graph cache. Its signature uses source paths, modification times and sizes, including missing inputs and directory additions/deletions; changes preserving both time and size are outside that contract. A mid-query change sets execution.fresh to false. HTTP discovery stops at 10,000 directory entries, 1,000 sources, 100 KB per file, 20 MB total source, 20,000 AST visits per file, 10,000 registrations and include/group depth 12, with memory headroom checks. Handler matching shares 10,000 query visits and 1,000 queued seeds across all routes. Same-line declarations and the same source included under distinct groups retain separate identities. When limited, totals can be lower bounds. This section is additive in ordinary, signature, delete and move queries and does not change their preflight verdicts or audit scope.
Answers are bounded by limit per section, default 20 and maximum 500, and by depth, default 4 and maximum 32. Method indexing is bounded to 50,000 dispatch edges and 1,000 candidate subtypes per call. Traversal allows at most 10,000 edge visits and 1,000 queued elements per direction. Impact extraction stops after 20,000 AST visits or 100 KB per file. Source size and PHP memory headroom are checked before reading or parsing impact sources; query indices also check headroom. Hierarchy lookup stops at depth 12 and reports the boundary class. Test candidates reuse application edges and stream test files individually, with limits of 10,000 directory entries, 20 MB of source, 1,000 intermediary classes and 501 results; skipped sources make the report incomplete or limited. Transient memory-limited file facts are reparsed on the next query rather than trusted as complete. Limits are explicit in the report. Increase the query limits or inspect boundary symbols to expand an answer; internal source and index limits require narrowing the analyzed scope or inspecting the sources. For immutable proposal continuation, start impact with --page=1 and follow the returned report ID and page. Analysis budgets remain bounded independently of display size.
Impact uses the shared graph loader and cache infrastructure with a separate fingerprint and an optional per-file fact channel. Audit and guard never treat method facts as class dependency edges. The report includes cache status, configured scope, and a stat-based snapshot identifier. Edited, added, deleted, or newly excluded sources invalidate the relevant facts according to the existing graph cache contract. A change detected during analysis marks the answer incomplete and asks for a fresh query.
Before editing, inspect the evidence and uncertain calls, select relevant tests, and ask before expanding the agreed change scope. Run the guard after editing. Resource sync adds these instructions to generated guidance.
Find paths between two endpoints
php artisan architecture-kit:path 'OrderController::store' 'PaymentService::charge' --agent
php artisan architecture-kit:path app/Http/Controllers/OrderController.php 'PaymentService' --limit=10 --depth=12
php artisan architecture-kit:path --schema
MCP path accepts from, to, limit and depth. Each endpoint can be a fully qualified class, unambiguous short class name, Class::method, or project PHP file. A file selects all its declarations and witnessed callbacks or framework entry points. A class selects its available methods for the execution channel. Inherited methods resolve to their declaring symbol. External endpoints require an observed external-call witness and an exact name. No application code runs and vendor sources are excluded.
dependencies.paths contains structural class dependencies. Method selectors project onto the owning class here, so a path does not prove that the selected methods call each other. Each edge carries its kind and strong/weak classification. Strength is not repair cost.
execution.paths contains ordinary method calls and supported HTTP, Artisan, scheduler, job, event and model transitions. Each path preserves call-site locations, certainty, conditions, execution mode and timing. Synthetic entry points have source locations; unread external declarations have a null location. External calls stop traversal and appear in external_boundaries unless the external symbol is the requested target.
Each channel returns found independently of status. Status is found, no_path, incomplete, limit or stale. no_path means no path in the analyzed graph, not runtime impossibility. Unknown calls and source notices remain visible even with no paths. Limits take precedence over stale status when freshness cannot be established within the budget; inspect fresh separately. Multiple simple paths are bounded by the requested depth, 1,000 queued states, 10,000 edge visits and PHP memory headroom. limit defaults to 20, permits 0 through 500, and caps paths, boundaries and notices per channel; depth defaults to 8 and permits 1 through 32. Limited totals are lower bounds. Increase limits or inspect boundary symbols; there are no continuation pages. Freshness checks source paths, mtime and size; edits preserving both stat values are outside the guarantee.
For E_LAYER_DEPENDENCY, pass the reported path and line to explain-finding or architecture-kit:explain. The additive occurrence.dependency.reported_edges identifies forbidden direct strong edges at that exact location. Several edges at one line remain separate, and a missing line or changed source returns unresolved evidence. context_paths supplies additional bounded context. Cutting one path does not prove that every violation disappears.
Evaluate a file move or class rename
Inspect uses before choosing a destination, or compare an explicit target:
php artisan architecture-kit:impact InvoiceCalculator --change=move --agent
php artisan architecture-kit:impact 'App\Services\InvoiceCalculator' --change=move --target-class='App\Billing\InvoiceCalculator' --target-path='app/Billing/InvoiceCalculator.php' --agent
php artisan architecture-kit:impact app/Services/InvoiceCalculator.php --change=move --target-path=app/Billing/InvoiceCalculator.php --agent
The MCP tool accepts change: "move", target_class, and target_path. Either target starts comparison. Without targets, the report only identifies uses to inspect. It never moves files, rewrites imports, or executes project code. Method moves are rejected.
A path target relocates every declaration in the file. A class target renames only the selected declaration. To rename a class in a file containing several declarations, select its FQCN rather than the file path. File-only moves preserve class names. Existing class/path collisions are reported; case-only changes require inspection. Targets must be valid PHP class names and project-relative PHP paths without traversal or external symlinks.
When the class name changes, resolved new, static calls, inheritance, interface and trait uses of the old name outside the subject are breaking. Types, class references, registrations, self references and dependencies affected by a namespace change require check. These verdicts assume the rest of the source stays unchanged. The graph does not inventory unused imports or text-based class registrations. Inspect manual includes, functions, constants, framework dispatch and callers outside the configured scope separately. safe_to_change is always false.
Autoload assessment reads the current composer.json without loading Composer code. It checks production/development PSR-4 prefixes, fallback prefixes, directory lists and exact casing. A target that contradicts a simple declared mapping is breaking. Class maps, manual files, authoritative maps, development-only loading, casing and incomplete configuration require check. Rebuild generated autoload maps after an actual change. The report includes the configuration hash and a final freshness check independent of graph cache. Concurrent configuration edits ask for a fresh query.
Move analysis allows 10,000 evidence visits, Composer files up to 1 MB, 1,000 mappings, 128 directories per mapping and 256 expected paths per assessment, with PHP memory headroom checks. Limits and truncation require further inspection; absence of breaking rows does not establish safety.
Installation
composer require gracjankubicki/laravel-architecture-kit
php artisan architecture-kit:install
Architecture Kit is a runtime dependency because the committed config/architectures.php references its Architecture enum while the application boots and caches configuration. Installing it only with --dev is unsupported and is blocked by install, doctor, and sync. Existing projects should follow UPGRADE.md before updating to v0.2.0.
The install command is interactive. It asks which architecture patterns the project uses, writes config/architectures.php, and generates:
.ai/guidelines/architecture-kit.md
.ai/skills/architecture-kit-{architecture}/SKILL.md
.ai/skills/architecture-kit-upgrade-{package}-{from}-to-{to}/SKILL.md
Before installing, inspect evidence-backed recommendations and the complete generated-file plan without changing the project:
php artisan architecture-kit:plan
php artisan architecture-kit:plan --agent
The planner recommends a pattern only when it finds explicit evidence such as an existing PHP file in the pattern's conventional folder, a relevant Composer dependency or a repo-local custom architecture guideline. Existing config/architectures.php selections remain authoritative. Missing evidence produces no recommendation, and the command never enables patterns or writes files.
It can also install agent integration without manual file editing:
.codex/config.toml
.mcp.json
.codex/hooks.json
.claude/settings.json
.architecture-kit/hooks/guard.sh
If Architecture Kit is newly added to a project that already uses Laravel Boost, the installer can run one-time third-party discovery:
php artisan boost:update --discover
Boost then syncs the generated .ai resources into agent files such as AGENTS.md, CLAUDE.md, and other configured AI instructions.
For a fresh Boost project, generate Architecture Kit resources and then run php artisan boost:install. For recurring Composer or Laravel AI profile updates, do not rediscover packages; run:
php artisan architecture-kit:sync --no-interaction
php artisan boost:update --no-interaction
Architecture Kit remains usable without Boost through .ai/**, CLI commands, and MCP resources.
The generated .ai/guidelines/architecture-kit.md file is a compact index, not the full rulebook. It lists enabled architectures, folders, hard rules, global rules, and the guard command. Agents should expand details only when needed through:
php artisan architecture-kit:guidelines actions --agent
The same full guidance is also available through the generated architecture-kit-{architecture} skills, the MCP tool architecture-rules, and the MCP resource architecture-kit://guideline.
Commands
php artisan architecture-kit:install
php artisan architecture-kit:install-agents
php artisan architecture-kit:install-agents --hooks
php artisan architecture-kit:mcp
php artisan architecture-kit:plan
php artisan architecture-kit:plan --agent
php artisan architecture-kit:upgrade-plan laravel/ai --to=0.11
php artisan architecture-kit:upgrade-plan laravel/ai --to=0.11 --agent
php artisan architecture-kit:doctor
php artisan architecture-kit:sync --no-interaction
php artisan architecture-kit:sync --dry-run --agent
php artisan architecture-kit:guidelines
php artisan architecture-kit:guidelines actions --agent
php artisan architecture-kit:file-rules app/Actions/CreateInvoice.php
php artisan architecture-kit:file-rules app/Actions/CreateInvoice.php --agent
php artisan architecture-kit:make actions CreateInvoice
php artisan architecture-kit:make actions CreateInvoice --agent
php artisan architecture-kit:context 'App\Actions\CreateInvoice' --agent
php artisan architecture-kit:context app/Actions/CreateInvoice.php
php artisan architecture-kit:guard --changed --strict
php artisan architecture-kit:guard --changed --base=origin/main --strict
php artisan architecture-kit:audit --changed --strict
php artisan architecture-kit:audit --changed --base=origin/main --strict
php artisan architecture-kit:audit --update-baseline
php artisan architecture-kit:audit --agent
php artisan architecture-kit:guard --agent
php artisan architecture-kit:doctor --agent
php artisan architecture-kit:explain E_THIN_CONTROLLER_MODEL_WRITE --agent
php artisan architecture-kit:cache-clear
architecture-kit:install is idempotent. Re-run it to change the selected architectures, the PHP runtime, or regenerate outdated .ai resources.
architecture-kit:plan is read-only. Before first install it recommends architectures from explicit project evidence; after installation it reports the configured selection. Both flows include requirement diagnostics and the predicted create, update, remove, and blocked resource changes. Use --agent for versioned JSON and --schema to inspect its contract.
architecture-kit:upgrade-plan is read-only. It requires a direct Composer package and an explicit target major.minor line, compares the declared constraint with locked and installed versions, then resolves a unique route through local atomic upgrade guides. The full route is visible, but only its first step is active. After completing and verifying that step, rerun the planner so it can derive the next step from the new project state. Use --agent for versioned JSON, --schema for its contract, or the MCP tool plan-upgrade from an AI agent.
architecture-kit:sync is the non-interactive recurring path. It reads the existing config, validates every enabled requirement and compatibility profile before writes, regenerates only marker-owned .ai/**, removes only stale marker-owned Architecture Kit skills, preserves unmanaged files, and never changes architecture selection. Use --dry-run for CI/agent preflight and --schema to inspect its JSON contract.
architecture-kit:install-agents bootstraps MCP and hook configuration for selected AI agents. It writes Codex MCP config to .codex/config.toml and Claude Code MCP config to .mcp.json, using the runtime from config/architectures.php as the initial wrapper command.
Agent integration files are developer-owned after creation. Re-running the command preserves existing Architecture Kit MCP entries that actually invoke architecture-kit:mcp, hook entries, .architecture-kit/hooks/guard.sh, and its README byte-for-byte. A valid existing config without the integration is merged safely; invalid JSON, an unrelated server under a reserved Architecture Kit key, or other incompatible configuration blocks installation instead of being overwritten. If the runtime or project-specific behavior changes later, edit these files directly.
In contrast, .ai/guidelines/** and .ai/skills/** are package-generated resources and may be regenerated by architecture-kit:install. .architecture-kit/install.json is internal package state.
architecture-kit:doctor is read-only. It reports missing, outdated, stale, or blocked generated resources and, when agents were installed, verifies that selected agent MCP and hook integrations still exist and remain parseable. Valid developer customizations are not treated as outdated.
architecture-kit:guidelines is read-only. Without an argument it lists known architectures with a one-line summary. With a slug it returns the full guideline for one architecture, even when that architecture is available but not enabled.
architecture-kit:context is read-only. It resolves one exact project FQCN or app-relative PHP path and returns a bounded static context: the subject role, direct dependencies and dependents with source evidence, current graph violations, files to inspect, and the next guard command. A path containing multiple symbols is rejected as ambiguous; use an exact FQCN. Use --agent for versioned JSON, --schema for its contract, or the MCP tool architecture-context.
architecture-kit:audit is read-only. It scans application code against the enabled file rules and the static project graph. Use --changed --strict before finishing AI-generated code so warnings and errors block the final handoff. In CI or after committing, pass --base=origin/main or another base ref to audit the committed diff.
Use architecture-kit:audit --update-baseline when adopting Architecture Kit in a legacy project. It writes the current findings to .architecture-kit/baseline.json; future audits suppress only the matching existing findings and still report new violations. Use --no-baseline to ignore the baseline for one run.
architecture-kit:guard is read-only. It combines doctor-equivalent generated-resource checks with the deterministic audit rules that are actually implemented. Guidance without a corresponding audit rule remains reviewer- and agent-enforced. Use --json for hooks and MCP tools.
Impact of a method signature change
Before editing a method declaration, inspect its immediate callers and contracts:
php artisan architecture-kit:impact 'InvoiceCalculator::calculate' --change=signature --agent
php artisan architecture-kit:impact 'InvoiceCalculator::calculate' --signature='calculate($invoice, $currency)' --agent
php artisan architecture-kit:impact 'InvoiceCalculator::calculate' --signature='public static function calculate($invoice, $currency = "PLN")' --agent
MCP impact accepts optional change: "signature" and signature with the same result. Supplying signature implies the change mode. A proposal is one PHP method declaration without a body; a shorthand such as calculate($invoice) preserves the current visibility and static modifier. An explicit public function ... supplies those modifiers. The selected method name must match. Use fully qualified class types. Abstract and final flags are preserved, and proposals cannot change those flags or introduce attributes. The parser reads declarations and default expressions without evaluating them. Invalid proposals return E_IMPACT_SIGNATURE_INVALID; a concrete proposal without a selected method returns E_IMPACT_SIGNATURE_SUBJECT. Without a proposal, class/file subjects return a signature_overview of methods to select explicitly.
The optional signature section separates breaking, check, and compatible rows. Each row has a source path, line and reason. breaking means a supported static incompatibility if that caller reaches the selected declaration. Examples include a missing required argument, an unknown named argument, a non-reference value passed to a new reference parameter, inaccessible visibility, or a static invocation that requires an instance. Compatible argument counts alone do not prove compatible behaviour. Extra positional arguments accepted by PHP and removed parameters need a semantic check.
Only immediate callers are compared; indirect BFS users remain in the ordinary relationship sections. Possible subtype calls, callable references, unpacked arguments, trait caller scope, magic dispatch after access/static changes and unresolved dispatch require inspection. Interface and parent requirements, descendant overrides and ordinary trait declarations are included. A selector for an inherited method changes the actual declaration shown by the report, rather than adding an override in the selected child. Concrete constructors are exempt from ordinary signature compatibility; interface or abstract constructor requirements still apply. new calls are checked, while Laravel autowiring and container registrations need inspection.
Parameter and return types are recorded with resolved names. The comparison proves only limited declaration rules, including identical types and known missing-type restrictions; other variance and runtime-value compatibility remain check. Changes in defaults, parameter names, references, promotion, variadic handling and return types can require inspection of the method body. Dynamic framework dispatch and callbacks remain outside the static proof.
safe_to_change is always false: no breaking rows is not a safety guarantee or a test result. status distinguishes inspection, proved incompatibility, uncertain results, no proved incompatibility, and limits. Per-section totals and truncation are explicit; limit=0 retains totals. Signature comparisons have a 10,000-visit and PHP memory budget in addition to ordinary impact limits. The cache has a separate updated impact fingerprint and validates the added declaration and call-site facts. Existing impact calls without either option retain the previous response shape and do not add the signature section.
Project Architecture Graph
Architecture Kit parses every non-excluded PHP file in the audited scope into one deterministic graph. The scope is app/ unless the project widens it. It records project classes, interfaces, traits and enums plus evidenced dependencies such as constructor and method types, inheritance, implementations, instantiation, static calls, class constants, enum cases and traits. Imports alone are not dependencies. Role classification follows architecture path segments in both top-level and domain-first layouts.
The graph distinguishes strong executable/type dependencies from weak context-only references. SomeClass::class and Eloquent relationship targets are visible to context, but do not independently trigger layer errors or namespace-cycle warnings. The graph is static: dynamic service-container bindings, runtime reflection and dependencies assembled from strings cannot be guaranteed.
Three graph-aware findings are available:
E_PORT_BYPASSwhen application code depends on a concrete infrastructure adapter that implements an available port. Provider bindings remain valid composition-root code.E_LAYER_DEPENDENCYfor a statically confirmed dependency from an inner domain/application/port role to an outer application, HTTP adapter or infrastructure role.W_NAMESPACE_CYCLEfor a deterministic strongly connected component between project namespaces.
audit.exclude, inline ignores and the baseline apply to graph findings as they do to file findings. Changed-only audit still builds the full graph so an edited edge can reveal a cycle through unchanged files; reporting remains focused on findings anchored in changed source files.
Before a change: what breaks and what to run
architecture-kit:context answers what a change to one symbol would break, not just what it is connected to. Each relationship carries an impact level derived from the edge the graph recorded:
| Level | Edge | What a change does |
|---|---|---|
breaking |
extends, implements, trait |
The dependent stops loading |
signature |
parameter, property, return |
The type system of the dependent catches it |
usage |
new, static, instanceof, catch, attribute, class-constant |
The call site breaks |
context |
weak references such as ::class |
Readable, but nothing breaks |
Relationships are ordered by that level, so --limit cuts the least dangerous first. Sorting used to be alphabetical, which meant a subclass could fall out of the answer while a passing type reference stayed in it.
The same answer names the tests that cover the symbol, including those reaching it through another class, and next carries them as a ready run_tests: hint. That lets an agent run a narrow relevant set before the full suite instead of learning the effect from a red run. Coverage is resolved without building a project graph: files are filtered by the class's short name and only the matches are parsed, which keeps the cost in seconds even on a large application.
After a finding: what is wrong here
architecture-kit:explain takes the reported path and line, not only the code:
php artisan architecture-kit:explain E_THIN_CONTROLLER_MODEL_WRITE \
--path=app/Http/Controllers/InvoiceController.php --line=14 --agent
The answer then names the symbol at fault and, where the rule states a destination, carries a proposal describing what to move and where. Without a path the output is unchanged, so existing callers keep what they had.
A proposal appears only where the destination follows from the rule itself. Guessing it for the remaining codes would send an agent somewhere wrong with confidence, which costs more than saying nothing. Nothing here edits a file: the proposal is text to apply or reject.
Audit scope beyond app/
Business logic closed inside a route file used to be invisible: the audit only read app/, so a green gate could mean nothing more than where the file was saved. A project can widen the scope:
// config/architectures.php
'audit' => [
'paths' => ['routes'],
],
A route closure that validates a request, writes to a model, opens a transaction, or dispatches a job is then reported as route-logic, using the same signals the package already applies to controllers. Files outside the scope stay unread, so a project that does not change its configuration gets exactly the audit it had before upgrading.
Missing tests
The missing-test rule reports architecture elements without a statically identified relationship to a test:
// config/architectures.php
'audit' => [
'missing_test' => 'warn', // 'off' (default), 'warn', 'error'
],
The rule combines existing transitive class references with Laravel HTTP, Artisan and factory analysis. It applies across architectures and supports classless Pest tests.
HTTP calls use a fresh route map to select the handler by verb and address, including named and resource routes and unambiguous symbolic IDs. Analysis follows the selected method and its calls. It also follows the selected Gate policy, API Resource transformation, Inertia prop callback, route middleware shared data, and Fortify action or view callback. Testing update does not automatically link destroy, another Fortify action, or an unused injected dependency. Route discovery boots Laravel in an isolated process; the audit never executes the endpoint or deletes the application's route cache.
For dynamic HTTP IDs from json('id') or getKey(), an integer cast or explicit global \rawurlencode() / \urlencode() establishes a single symbolic path segment. For example, $id = (int) $response->json('id'); $this->deleteJson('/projects/'.$id); can select a unique DELETE handler. Raw unknown strings, dynamic prefixes or hosts, partial symbolic segments, competing routes and unresolved constraints remain incomplete. In namespaced tests, qualify or explicitly import encoding functions to avoid an unresolved local function shadow.
Literal Artisan::call('invoices:send'), $this->artisan('invoices:send') and the Pest Laravel artisan helper select a registered class command and follow its actual handler, including inherited handlers, $signature and #[Signature]. Other commands and unused methods receive no dispatch credit. Dynamic selectors, conflicting or conditional registrations and callback commands remain incomplete. Registration lookup reads bounded source files without booting the application.
Model::factory() resolves supported Laravel declarations and naming conventions. A HasFactory<T> annotation confirms a mapping but cannot override runtime selection. A factory is linked as test setup, not proof that its states are tested. Needed factory declarations and their parents or traits are read automatically as auxiliary sources through conventional paths and static Composer PSR-4 mappings. They are not added to the audit graph and receive no audit findings unless their directories are explicitly included in audit.paths. Missing, unsafe, ambiguous, oversized or unparseable auxiliary sources remain incomplete. Auxiliary sources are read again on each analysis, including when test facts come from graph cache; only tests is added automatically to the audit scope.
W_MISSING_TEST_ANALYSIS_INCOMPLETE identifies an unresolved address, handler, factory mapping, or analysis limit at its source. It is a warning at both enabled levels and blocks guard --strict. Existing inline and baseline suppression apply. Uncertainty neither credits a class nor hides unrelated missing relationships.
These are static relationships, not runtime coverage or assertion-quality checks. Inspect existing behavioural tests before adding another. A meaningful endpoint test can suffice; do not add artificial SomeClass::class references or a separate test solely because of a class's role. Graph cache modes preserve the same analysis results and refresh the route map on each audit that needs HTTP dispatch.
Interfaces and traits are not reported, because neither is tested on its own. An enum is reported only when it declares methods: a plain set of cases has nothing to assert beyond the language itself, while a method on an enum is behaviour like any other.
Enabling it also brings tests/ into the audited scope. That is not optional: without test files in the graph the rule would see no test for anything and report every class, including well covered ones. Rules written for application code never fire inside test files.
The rule is off unless a project asks for it. Measured on an application with 6305 files under app/ and 5153 test files, enabling it by default would have produced roughly 700 findings on the first run after an upgrade.
The project graph is built once
Building the graph means parsing every PHP file in scope, which is 87% of the cost and takes about 19.5s on an application with 11566 files. guard --changed paid it on every run: its rules only check the files you touched, but a changed edge can reveal a cycle through a file you did not, so the graph still had to cover the whole project.
The graph is now kept between runs, per file, so a run parses only what moved:
// config/architectures.php
'audit' => [
'cache' => false, // turn it off
'cache' => 'storage/graphs', // or put it somewhere else
],
Measured on that application, with app/ in scope and nothing edited between runs:
| Without the cache | With it | |
|---|---|---|
guard --changed |
6.44s | 0.47s |
Full audit |
6.57s | 6.63s |
context on one symbol |
19.67s | 0.72s |
A full audit gains nothing, and that is not an oversight: its rules need the syntax tree of every file they check, so those files are parsed either way. The cache removes parsing that nothing else needed, which is what --changed and context are made of.
Staleness is the risk, not speed: a graph that describes code the project no longer has would report findings for deleted code and stay silent about new code, with nothing in the output to say so. An entry is discarded when the enabled architectures, custom rules or audited scope change, and when the package itself changes, which is decided by hashing its sources rather than by a version constant somebody has to remember to bump. A file is parsed again when its modification time or size moves, and the run is driven by the files the project actually has, so a deleted file leaves the graph and a contribution missing from the entry is rebuilt rather than assumed. The stored file carries a hash of its own contents, because a damaged entry can still be well formed: one stripped of its symbols looks exactly like a file that declares none, and no structural check can tell those apart.
File state is stat rather than a content hash: hashing the same project costs 1.58s against 0.02s, on every run including the ones that hit the cache, and it would eat most of what the cache saves. The gap that leaves is narrow and named: content changed while both timestamp and size stayed identical, which rsync -a, an archive unpacked with its timestamps, and touch -r can do but Git, editors and agents cannot. php artisan architecture-kit:cache-clear is the answer to it, and to any entry you no longer trust. A corrupt or unreadable entry is not an error: the command rebuilds and moves on.
A rebuild answers correctly, which is why a rejected entry would otherwise be invisible while costing a full build on every run. When an entry is unreadable, or too large to restore inside the remaining memory budget, audit, guard and context all say so: --agent output carries a cache field and human output prints a note. An ordinary first run says nothing, because there is nothing to report.
The default location is storage/framework/cache/architecture-kit, which Laravel already ignores: the skeleton's storage/framework/cache/.gitignore excludes everything under it except data/. The entry is sizeable, 33.8 MB for 11629 symbols and 198858 edges, so it is not somewhere a project should commit by accident. Restoring it peaks at about six times that in memory and writing it adds more, so the cache checks the remaining budget first and steps aside rather than pushing the process past memory_limit. After moving or disabling the cache the old directory is no longer in the configuration, so cache-clear takes --path= for that case.
Recommended agent flow:
php artisan architecture-kit:context 'App\Actions\CreateInvoice' --agent
# inspect returned files and implement the approved change
php artisan architecture-kit:guard --changed --strict --agent
For example, when context classifies App\Actions\FetchDocument as application and returns App\Documents\Ports\DocumentGateway as a strong, allowed port dependency, the agent should inspect the Action and contract, preserve port injection, and avoid loading or depending on the concrete HTTP adapter unless the approved change reaches that boundary. If the Action points directly to the adapter, context reports allowed: false together with E_PORT_BYPASS and E_LAYER_DEPENDENCY.
This flow proves that the CLI/MCP payload carries the boundary, relevant files and next guard needed for the maintenance decision. It is not an independent benchmark across LLM providers or a guarantee about dependencies assembled dynamically by Laravel's runtime container.
Agent Output
Human-facing command output stays descriptive. Existing --json output stays compatible for hooks and integrations. AI agents can use --agent for compact, single-line JSON:
php artisan architecture-kit:audit --agent --limit=20
Example:
{"v":1,"ok":false,"cmd":"audit","scope":"changed","err":1,"warn":0,"sup":{"inline":0,"baseline":0},"trunc":false,"find":[{"r":"thin-controller","s":"err","p":"app/Http/Controllers/InvoiceController.php","l":27,"m":"E_THIN_CONTROLLER_MODEL_WRITE","n":1}],"next":["fix_findings","rerun:audit --agent"]}
By default, --agent returns finding codes instead of full messages to reduce token usage. Use --full when the agent needs full text, or ask for one code:
php artisan architecture-kit:explain E_THIN_CONTROLLER_MODEL_WRITE --agent
--limit=0 returns only the summary. When findings or doctor issues are truncated, the payload contains trunc, total, and shown.
Agents can inspect the contract without running the audit:
php artisan architecture-kit:audit --agent --schema
php artisan architecture-kit:plan --schema
php artisan architecture-kit:upgrade-plan --schema
php artisan architecture-kit:context --schema
For cheap on-demand rule expansion:
php artisan architecture-kit:guidelines --agent
php artisan architecture-kit:guidelines actions --agent
php artisan architecture-kit:guidelines --schema
To install only hook integration through the merge-aware agent installer, use architecture-kit:install-agents --hooks:
.architecture-kit/hooks/guard.sh
.codex/hooks.json
.claude/settings.json
The generated hooks run:
php artisan architecture-kit:guard --changed --strict --json
Existing valid agent config is merged and unrelated MCP servers or hooks are preserved. Invalid JSON/TOML, or incompatible unmanaged architecture-kit entries, block installation with a clear error.
Docker, Sail, and Custom PHP Runtimes
config/architectures.php stores how the project runs PHP. When runtime is omitted, Architecture Kit uses local PHP as the default runtime.
return [
'enabled' => [
Architecture::Actions,
],
'runtime' => [
'driver' => 'docker', // local | sail | docker | custom
'service' => 'app',
'php' => 'php',
'command' => null,
],
];
For Docker Compose and Sail, generated hooks and MCP configs use raw non-TTY compose commands:
docker compose exec -T app php artisan architecture-kit:guard --changed --strict --json
docker compose exec -T app php artisan architecture-kit:mcp
Sail is treated as detection convenience only. Architecture Kit reads APP_SERVICE from .env and defaults to laravel.test, but still generates docker compose exec -T ... so hooks and MCP stdio stay deterministic.
For --changed audits inside Docker or Sail, the PHP runtime must have:
gitinstalled in the container,- the repository mounted with
.git, - the same project files available where artisan runs.
If those requirements are missing, architecture-kit:doctor reports runtime warnings. Hooks are fail-closed for every runtime: if the runtime is unavailable and no Architecture Kit JSON payload is returned, the agent turn is blocked with a runtime message.
Mixed teams can commit a small wrapper and use driver: custom:
#!/usr/bin/env bash
set -euo pipefail
if command -v docker >/dev/null 2>&1 && [ -f compose.yaml ]; then
exec docker compose exec -T app php "$@"
fi
exec php "$@"
'runtime' => [
'driver' => 'custom',
'service' => null,
'php' => 'php',
'command' => ['bin/php-runner'],
],
Architectures
The architecture catalog:
| Pattern | Default placement | Focus |
|---|---|---|
| Thin Controllers | app/Http/Controllers |
Controllers as HTTP adapters only |
| Form Requests | app/Http/Requests |
Request validation and authorization |
| Actions | app/Actions |
Application use cases with a single public handle() |
| Services | app/Services |
Aggregated application APIs over several Actions |
| Query Objects | app/Queries |
Encapsulated read use cases |
| Custom Eloquent Builders | app/Models/Builders |
Reusable model-level query scopes |
| Data Objects | app/Data |
Typed immutable input and result carriers |
| Value Objects | app/ValueObjects |
Validated immutable domain values |
| Enums | domain-first | Closed sets with exhaustive match |
| API Resources | app/Http/Resources |
Read output shaping |
| Eloquent Lifecycle | app/Observers, app/Lifecycle |
Model lifecycle boundaries: thin observers, handlers, after-commit events |
| Saloon | app/Http/Integrations |
Application-owned direct HTTP through Saloon; official SDK transport stays inside provider adapters |
| Ports And Adapters | near the owning boundary | Explicit outbound seams for providers and infrastructure |
| Modern PHP 8.5 | cross-cutting | Strict modern PHP runtime contract |
| Laravel AI | app/Ai |
Typed laravel/ai agents, tools, and prompts |
| Inertia | presentation layer | Explicit Inertia 3 page props and one-way application boundaries |
| Fortify | authentication extension points | Fortify 1 actions, response contracts, and authentication safeguards |
| Laravel Best Practices | cross-cutting | Laravel-native defaults composed with the other enabled patterns |
Some patterns have hard requirements, validated by architecture-kit:install and architecture-kit:doctor:
Modern PHP 8.5is a strict runtime contract. The consuming project must require PHP 8.5 or newer incomposer.json; otherwise the configuration is reported as invalid.Saloonrequiressaloonphp/saloon^4.0,saloonphp/laravel-plugin, andsaloonphp/rate-limit-plugin. The install command offers tocomposer requirethe missing packages. Constraints that still allow Saloon 3 are reported as invalid because Saloon 4 fixes security issues in v3.Laravel AIrequireslaravel/aidirectly in root runtimerequire, an installed version consistent withcomposer.lock, and a declared constraint fully contained in the verified support ranges.Inertiarequiresinertiajs/inertia-laraveldirectly in root runtimerequire, with the declared, locked, and installed versions on the supported>=3.0.0 <4.0.0line. Architecture Kit reports the required Composer command but never installs or updates Inertia.Fortifyrequireslaravel/fortifydirectly in root runtimerequire, with the declared, locked, and installed versions on the supported>=1.0.0 <2.0.0line. Architecture Kit reports the required Composer command but never installs or updates Fortify and never changesconfig/fortify.phpor authentication features.
On the first install, Architecture Kit preselects Inertia when it detects a supported Inertia 3 installation. The architecture prompt remains authoritative, so you can remove the suggestion before any configuration is written. Later installs preserve the configured selection instead of enabling the profile from dependency detection.
The inertia audit rule enforces two checks when the profile is enabled. Actions and Query Objects cannot depend on recognized Inertia\* symbols, and Inertia page or shared props cannot receive a whole unfiltered request. Explicit validated(), safe(), only(...), and keyed input(...) or collect(...) access pass the second check. An unresolved request transformation produces a warning. The remaining profile rules, including prop completeness, frontend types, authorization, partial reloads, and deferred error handling, are guidance backed by application tests rather than deterministic audit checks.
The route-aware Inertia 3 semantics described above remain active without the Inertia architecture profile. They support endpoint and missing-test analysis; they do not opt the project into Inertia-specific guidance or findings.
The Fortify profile keeps the package's native extension contracts instead of forcing every class into the generic Action shape. Registered user actions must implement their matching Fortify contract and expose the public create(), reset(), or update() method. Bound Fortify responses must implement the selected response contract and expose public toResponse(). These verified classes are accepted in app/Actions and app/Http/Responses; the profile does not disable checks for either whole folder. A confirmed mismatch is an error, while a dynamic target or unavailable source reports W_FORTIFY_ANALYSIS_INCOMPLETE and fails strict mode.
Generated Fortify guidance keeps simple operations in the native classes and delegates broader reusable workflows to ordinary application Actions. It covers static provider bindings, Blade or Inertia response callbacks, authenticateUsing, authentication pipelines, throttling, session preparation, guards, password reset, email verification, two-factor authentication, and passkeys when the application has enabled them. It does not add Features::*, bypass stateful web authentication, require Form Requests for Fortify's array inputs, or make Fortify depend on the optional Inertia profile.
Laravel AI compatibility:
| Architecture Kit profile | Supported laravel/ai versions |
Structured response | Provider options |
|---|---|---|---|
laravel-ai@0.8 |
>=0.8.0 <0.9.0 |
toArray() or ArrayAccess |
Laravel AI 0.8 contracts |
laravel-ai@0.9 |
>=0.9.0 <0.10.0 |
toArray() or ArrayAccess |
withProviderOptions() where applicable |
laravel-ai@0.10 |
>=0.10.0 <0.11.0 |
toArray() or ArrayAccess |
withProviderOptions() plus approval resumption |
laravel-ai@0.11 |
>=0.11.0 <0.12.0 |
toArray() or ArrayAccess |
failover, stream errors, and real queued fake jobs |
Constraints such as ^0.8, ^0.9, ^0.10, ^0.11, ^0.8 || ^0.9 || ^0.10 || ^0.11, and >=0.8 <0.12 are supported. Constraints that also permit 0.12, 1.x, or development branches fail closed. Architecture Kit never guesses that the newest known profile is compatible with an unknown Laravel AI release.
Only the profile selected from the actually installed Laravel AI version is generated at .ai/skills/architecture-kit-laravel-ai/SKILL.md. Architecture Kit owns the application architecture overlay; exact SDK features remain in the official ai-sdk-development skill shipped by the installed laravel/ai package.
When the profile is enabled, the audit reports direct SDK agent calls from controllers, form requests, API resources, and models. It recognizes prompt, stream, queue, and the broadcast methods through new, make, typed parameters and properties, local assignments, aliases, and bounded fluent chains. It also checks Files::put, putFromPath, and putFromStorage outside the AI boundary.
The audit first proves that a symbol implements the Laravel AI Agent or Tool contract or uses Promptable. A class name ending in Agent, an unrelated prompt() method, or a project-owned Tool interface is not enough. If the SDK relationship is known but a dynamic method or chain cannot be resolved, the audit reports W_LARAVEL_AI_ANALYSIS_INCOMPLETE, which blocks strict mode. The analysis reads source files without executing application PHP. It does not resolve arbitrary container bindings, runtime reflection, dynamic class names, or unknown method return types.
Versioned package upgrade guides
Architecture Kit can ship atomic, evidence-first upgrade guides as generated AI skills. These are instructions for an agent working in the consuming repository, not deterministic codemods: the agent must inspect the real dependency state, classify each breaking change by applicability, follow the repository's Target State and plan gates, make only approved changes, run the project's verification, and return a requirement-evidence handoff.
The Laravel AI architecture currently generates:
.ai/skills/architecture-kit-upgrade-laravel-ai-0-8-to-0-9/SKILL.md
.ai/skills/architecture-kit-upgrade-laravel-ai-0-9-to-0-10/SKILL.md
.ai/skills/architecture-kit-upgrade-laravel-ai-0-10-to-0-11/SKILL.md
Each guide represents one atomic version edge. A request to upgrade 0.8 -> 0.9 -> 0.10 -> 0.11 must complete and verify each guide before loading the next one. The guides distinguish mandatory, conditional, informational, and blocked work so an agent does not apply an application migration or contract change without evidence that the project uses it.
Before loading a guide, resolve the route from the real project state:
php artisan architecture-kit:upgrade-plan laravel/ai --to=0.11
The planner accepts only a direct dependency with a valid constraint, matching locked and installed stable versions, an enabled guide architecture, and a current marker-owned skill for the active edge. Missing state returns blocked, a route gap returns unsupported, and multiple complete routes return ambiguous; none of these outcomes changes project files. Target versions are explicit and local—Architecture Kit does not query for or infer the latest release.
Future package transitions follow resources/upgrades/{package}/{from}-to-{to}/SKILL.md. The source skill declares its package, architecture and version edge in frontmatter. Install, plan, sync and doctor then reuse the same marker-owned resource lifecycle as architecture skills; no new mutation command or remote recipe execution is introduced.
On the first install (before config/architectures.php exists), Services is preselected when the project already has an app/Services folder. Laravel AI, Inertia, and Fortify are preselected only when their valid supported runtime dependencies are detected.
Architecture Kit does not add Composer post-update hooks. After dependency updates, run the explicit sync commands shown above so validation failures remain visible and cannot silently rewrite project files.
The install command also warns about weak pattern combinations, for example Thin Controllers, Eloquent Lifecycle, or Saloon enabled without Actions as the application boundary.
The project source of truth is config/architectures.php:
<?php
use GracjanKubicki\ArchitectureKit\Architecture;
return [
'enabled' => [
Architecture::ThinControllers,
Architecture::FormRequests,
Architecture::Actions,
Architecture::DataObjects,
Architecture::ApiResources,
],
];
Optional audit configuration lives in the same file:
return [
'enabled' => [
Architecture::Actions,
'billing-workflows',
],
'audit' => [
'exclude' => ['app/Legacy/*'],
],
'rules' => [
App\Architecture\Rules\NoForbiddenBillingState::class,
],
];
Custom audit rules must return valid AuditFinding objects. Severity is exactly error or warn; the rule is a non-empty kebab-case slug; path and message are non-empty; and line is at least 1. Occurrence, when provided, is at least 1. A custom code is optional, but when present it must use the E_* or W_* uppercase underscore format. Invalid findings throw immediately and fail audit/guard instead of being silently ignored. Projects upgrading custom rules must correct their finding construction before enabling the stricter package version.
The public Ports And Adapters rule requires a real boundary reason in PHPDoc, but does not prescribe its language. A project that requires bilingual Port documentation can enforce it with its own AuditRule scoped to the architecture:
'rules' => [
'ports-and-adapters' => [
App\Architecture\Rules\RequireBilingualPortDocumentation::class,
],
],
Suppression
Inline suppression is for reviewed false positives only. Always name the rule and include a reason:
// @architecture-kit-ignore thin-controller -- legacy endpoint accepted in PR #123
$invoice->update($payload);
The directive can also be placed in a multi-line docblock. It applies to findings inside the comment and to the line immediately after the comment. Repeat the directive in one comment to suppress several rules:
/**
* @architecture-kit-ignore thin-controller
* @architecture-kit-ignore actions
*/
File-level suppression is also rule-specific:
// @architecture-kit-ignore-file query-objects -- generated legacy query object
Unknown suppression rules are reported as invalid-suppression warnings and do not hide the original finding.
Known inline rules that do not match any finding are also reported as
invalid-suppression warnings with an Unused message, so stale suppressions
are visible instead of being silently ignored.
Baseline files use schema version 2, which fingerprints severity as well as rule, path, and message. A legacy version 1 baseline is rejected because it cannot distinguish a warning from a later error; recreate it deliberately with php artisan architecture-kit:audit --update-baseline.
Optional Project-Owned Composer Update Check
Architecture Kit does not install Composer hooks. If the project deliberately owns one, it may use this read-only hook to detect stale generated resources after package updates:
{
"scripts": {
"post-update-cmd": [
"@php artisan architecture-kit:doctor --agent"
]
}
}
Do not run architecture-kit:install from Composer hooks. It is interactive; run it manually when doctor reports outdated resources.
Custom Architectures
Project-owned architectures live under:
.architecture-kit/architectures/{slug}/guideline.md
.architecture-kit/architectures/{slug}/summary.md
.architecture-kit/architectures/{slug}/SKILL.md
guideline.md is required. summary.md is optional; when it is missing, Architecture Kit uses the first non-empty guideline line as the compact index summary. SKILL.md is optional; when it is missing, Architecture Kit generates a skill from the guideline. Custom architecture slugs must be kebab-case and can be enabled as strings in config/architectures.php.
This is a guidance contract for agents. It generates .ai/guidelines, .ai/skills, and MCP rule output, but Architecture Kit does not infer deterministic AST checks from prose in guideline.md.
return [
'enabled' => [
Architecture::Actions,
'billing-workflows',
],
];
Custom Audit Rules
Custom rules are PHP classes registered in config/architectures.php under rules. Each rule must implement GracjanKubicki\ArchitectureKit\Audit\AuditRule. Custom rule findings participate in inline suppression, baseline suppression, audit, guard, hooks, and MCP output.
Prefer architecture-scoped rules when the rule enforces one architecture:
return [
'enabled' => [
Architecture::Actions,
'billing-workflows',
],
'rules' => [
'billing-workflows' => [
App\Architecture\BillingWorkflows\Rules\NoInvoiceStateChangeOutsideBillingAction::class,
App\Architecture\BillingWorkflows\Rules\NoInvoiceTransitionInController::class,
],
],
];
Scoped rules run only when their architecture is enabled. The rule class does not need to check in_array('billing-workflows', $enabled, true) just to bind itself to that architecture.
Flat rules remain supported for global project checks that are not owned by one architecture. Programmatic callers should use ArchitectureConfig::customRuleSet() for all custom audit rule access; it exposes globalRules(), scopedRules(), rulesFor($enabled), and knownRuleClasses().
return [
'enabled' => [
Architecture::Actions,
],
'rules' => [
App\Architecture\Rules\NoForbiddenProjectPattern::class,
],
];
Laravel Boost Integration
This package ships a small package-level Boost guideline at:
resources/boost/guidelines/core.blade.php
That guideline only points agents to the generated project-specific file:
.ai/guidelines/architecture-kit.md
The compact architecture index, detailed architecture rules, and skills are generated into the consuming project after architecture-kit:install.
Generated Architecture Kit guidance includes a Package-First Architecture Rule. AI agents must search existing Laravel features, maintained Laravel ecosystem packages, and maintained third-party PHP packages before writing custom infrastructure. Custom code is allowed only when no suitable maintained package fits the project constraints or can safely provide the required behavior.
Generated guidance also includes a Testability Architecture Rule. AI agents must keep dependencies explicit and must not replace app(SomeClass::class) with private static factories that call new SomeClass(). Use constructor or method injection, or move behavior behind an enabled architecture boundary.
Laravel MCP Integration
Architecture Kit requires laravel/mcp and registers a local MCP server named:
architecture-kit
Agent config is generated by:
php artisan architecture-kit:install
php artisan architecture-kit:install-agents
For the local runtime, Codex receives:
[mcp_servers.architecture-kit]
command = "php"
args = ["artisan", "architecture-kit:mcp"]
required = true
For Docker and Sail runtimes, command becomes docker and args include compose exec -T {service} php artisan architecture-kit:mcp. Claude Code receives the same server under .mcp.json / mcpServers.
The MCP server exposes read-only tools for enabled architectures, generated rules, project architecture context, package upgrade plans, doctor state, changed-file audit, guard state, and finding explanations. It does not regenerate files, install hooks, run migrations, write code, or mutate application data.
Source-only application graph
The source-only application graph is exposed through architecture-search and architecture-graph. Search returns candidate IDs and source locations; graph queries inspect direct relationships, directed paths, or possible change impact. Read the static graph reference for scope, parameters, examples, completeness, continuation, and cache behavior. Completeness describes the inspected inputs and supported source contracts; dynamic or unsupported source shapes remain explicit diagnostics.
The core catalog uses these source contracts. A relationship is a possible transition with a source location; it does not prove runtime execution.
| Family | Source information | Limits |
|---|---|---|
| Models, pivots, builders and scopes | Inherited contracts, table and connection selectors, relation targets, selected scopes and terminal reads or writes | Dynamic selectors and source overrides remain unresolved. Query preparation alone is not a write. |
| Casts, accessors and mutators | Source cast maps, effective inherited methods, attribute callbacks and serialization candidates | Runtime cast changes and loaded model data are unknown. Attribute writes do not imply database persistence. |
| Factories, seeders and migrations | Model selectors, conditional factory lifecycle, related factories, seeder calls and migration direction | make and create have separate effects. A migration declares schema operations, not the current database schema. |
| Actions, services, queries, DTOs, value objects, ports, adapters and gateways | Directory conventions with evidence, source type references and resolved calls | Placement is a role hint. It does not invoke methods or impose an application layout. Saloon connectors and requests additionally require their package contracts. |
| Views, Blade components, composers and creators | Literal view references, source component registration, selected render and composer methods, Blade dependencies with original lines | Blade is not rendered or compiled. Dynamic names and invalid PHP fragments produce diagnostics. |
| Mail, notifications and broadcasting | Preparation, send or queue sites, selected channel bodies, broadcast resources and subscription authorization | Queue delivery, runtime channel selection and subscription acceptance remain conditions. Preparing a message does not send it. |
| Cache, storage, configuration and external services | Store-qualified keys, locks, disks, configuration selectors and sanitized source endpoints | Dynamic identifiers remain explicit. Payloads, credentials, URL queries and fragments are omitted. |
| PHPUnit and Pest | Test declarations, HTTP route candidates, source lifecycle and attribute hooks, data providers, named datasets and global Pest hook targets | Links recommend candidate tests. They do not execute tests, retain dataset values or report PASS. Dynamic or unsupported registration shapes remain explicit. |
For example, pest()->beforeEach(fn () => prepareBilling())->in('Feature') in tests/Pest.php links the hook to source tests under tests/Feature. Later in() calls replace the selected targets. Later hooks of the same kind replace earlier callbacks. pest() in Pest.php defaults to its directory; uses() defaults to its source file. These links retain test_executed=false. A generator hook does not create a path to its deferred body. The same boundary applies to Inertia prop and shared callbacks: resolving a callback that returns a generator does not prove consumption of its body. Such callbacks produce an explicit diagnostic instead of a semantic path to their deferred calls.
Development
composer install
composer test
composer lint
bash tests/Smoke/workbench-commands.sh
Inspect removal before editing
Use delete to simulate removal while leaving the rest of the project unchanged:
php artisan architecture-kit:impact 'InvoiceCalculator::calculate' --change=delete --agent
php artisan architecture-kit:impact 'InvoiceCalculator' --change=delete --agent
php artisan architecture-kit:impact 'app/Services/InvoiceCalculator.php' --change=delete --agent
MCP impact accepts the same subject with change: "delete". Do not supply a proposed signature in this mode. No source is deleted or executed. A file selector removes all indexed declarations in that file, even when it contains several classes. An inherited method selector removes its actual declaration, shown in delete.removed.
delete.breaking contains proved static incompatibilities in remaining calls and declarations. A surviving parent or trait method can replace a removed override; the report checks its arguments, access and required contracts, and asks you to inspect the changed body. Magic dispatch and implicit constructors remain checks. Types, imports, injections and registrations alone do not prove a PHP error. Calls inside removed code are omitted from the removal verdicts.
Inspect delete.check, limitations and truncation before deciding. File-level functions, includes, aliases and framework dispatch are not fully modelled. A report without breaking rows does not prove that removal is safe. Test candidates do not prove coverage or PASS. Run the relevant checks after making an authorized change.
Find uses of a database table
Start from a table when you need its source reads, writes or schema declarations:
php artisan architecture-kit:impact --table=orders --agent
php artisan architecture-kit:impact --table=orders --connection=named:crm --operation=write --agent
php artisan architecture-kit:impact --table=orders --table-match=contains --limit=100 --depth=16
For MCP impact, omit subject and provide table: "orders". Optional filters are table_match: "exact" | "contains", connection: "default" | "dynamic" | "named:connection", and operation: "read" | "write" | "schema" | "schema-read". A table query cannot be combined with a symbol or a change proposal. Invalid filters return an error before analysis. Existing symbol calls remain unchanged.
exact compares the full source table name, including any schema qualifier, case-sensitively. orders does not match archived_orders or public.orders; query public.orders explicitly. contains is a literal substring, so % and * are not wildcards. Results include all connections unless filtered. A named connection called default is queried as named:default, separately from the unresolved configured default connection. A dynamic connection never matches a named connection filter. These source labels do not identify physical databases.
table_report.tables lists distinct table/connection pairs. usages contains DATA operations with source path/line, conditions, migration up/down, and separate query preparation witnesses. Each usage keeps a local operation via and bounded paths from callers or recognized HTTP, command, schedule, job and event contexts. Paths retain queue modes, timing and conditions. A usage needs no discovered entrypoint. A callback declaration can appear with an explicit invocation condition; its presence does not prove an invocation. Same-table users never become execution paths through that table. Paths do not establish runtime execution, statement order or data dependence.
unresolved describes global source boundaries, not guessed uses of the searched table. Known matches remain available beside these notices. status: "none" means no matches were detected in the completed source query. Empty incomplete or limit results do not prove absence. totals counts detected tables, usages, paths and unresolved notices before display limits; source or traversal limits make these lower bounds. limit applies per section and per usage's paths; path_total and paths_truncated explain hidden paths. depth bounds caller traversal; a preserved local DATA witness can be longer. Increase limits or inspect boundary symbols rather than treating a partial report as exhaustive.
Sources, safety guards and operation semantics are shared with DATA. The query builds or restores the existing project graph once and extracts DATA once, without a separate table cache, executing PHP/SQL/migrations, reading .env or expanding the architectural audit scope. fresh, source_signature and the impact snapshot include the discovered DATA and HTTP inputs. Freshness uses paths, mtime and size; equal-stat edits remain outside the guarantee.
Database effects in impact reports
architecture-kit:impact and MCP impact include a data section for class, method and file subjects. data.outgoing lists source effects reachable from the subject. data.consumers lists effects in methods that use it. Each consumer keeps two separate witnesses: subject_via reaches the selected element, and via reaches the database operation from that consumer. A consumer effect does not prove that its query depends on the selected return value or runs after it. preparation_via records a continuous custom query construction witness separately from the operation path; preparation_paths retains additional construction witnesses. Two processes using the same table never become an execution path through that table.
For example, inspecting CreateOrder::handle can show orders with a write effect from Order::create. Inspecting PriceCalculator::calculate can show a caller Checkout::handle, its call to the calculator, and the separate path to its orders write. A method needs no discovered HTTP, command or job entrypoint to expose its own effects.
The source recognizer covers Eloquent, declared relations and pivot tables, local scopes and custom builders, Query Builder joins and subqueries, literal SQL, and Schema calls. Query or relation preparation alone is not a read. Eager relations attach to model-fetching terminals; aggregate-only count/exists operations do not hydrate ordinary eager relations. Relation count/existence subqueries remain reads. lazy relation access carries a not-already-loaded condition. Quiet operations and mass writes remain DATA effects even when they emit no model lifecycle event. Relation updates read a joining pivot; pivot operations such as sync can read and write it. Runtime relation touching, polymorphic targets, macros and custom pivot hooks remain explicit inspection boundaries.
An effect contains table, connection, kind, operation, source path/line, source conditions and path witnesses. Kinds include read, write, schema and schema-read. A connection is a separate {kind: named|default|dynamic, name} field, not part of the qualified SQL table name. crm plus public.users describes a declared connection and table; it does not identify the physical database. Model table conventions use Laravel plural snake names; literal $table/$connection and supported source overrides take precedence. Dynamic overrides remain unresolved. Secrets and .env are not read. Composer files and includes are read as source only when they have a PHP extension; other inputs remain unresolved.
Migration files under database/migrations are discovered automatically without extending the architectural audit scope. Both named and anonymous migrations expose declared up/down effects. For example:
php artisan architecture-kit:impact database/migrations/2026_01_01_create_orders.php --agent
The report does not inspect migration history or claim that a migration was applied. Existing ambiguous class/file selectors still return candidates; this addition does not guess a declaration. Existing delete, signature and move reports retain their preflight contracts alongside DATA.
Recognized effects coexist with data.unresolved. A known orders query with a dynamic join retains the orders read and marks the remaining table set incomplete. Literal SQL is lexically recognized with comments, quoted identifiers, joins, subqueries and CTE aliases. Unsupported syntax, procedures, raw query fragments and dynamic SQL are inspection boundaries, not guessed table lists. The recognizer is not a SQL validator and makes no exhaustive dialect claim.
status, totals, truncated, fresh, source_signature and limitations describe the DATA channel independently. limit=0 hides rows but retains totals; source/traversal limits make totals lower bounds. Empty limited or unresolved results do not prove absence of effects. Freshness includes source discovery, models, migration files and Composer source declarations, using paths, mtime and size. New DATA inputs participate in the impact snapshot when DATA sources exist. Equal-stat edits remain outside the freshness guarantee. Sources are reread only while their discovered stat and safe path remain valid; symlinks and vendor are excluded. No analyzed PHP, SQL or migration is executed for DATA recognition. Name-based table search is a separate feature.
Find a starting point
Use architecture-kit:search or the read-only MCP search tool when you know only part of a name, path, route or command:
php artisan architecture-kit:search invoice --agent
php artisan architecture-kit:search /invoices --kind=route
php artisan architecture-kit:search --kind=job --limit=50
php artisan architecture-kit:search --schema
MCP accepts query, kind and limit. Matching is a case-insensitive literal substring, not a regular expression or fuzzy match. Exact full names, short class names, method names, paths, route names/URLs and command names sort first. Other results use stable name/kind/path/line ordering. Distinct source declarations remain separate; the tool never chooses a candidate automatically.
Kinds are class, interface, trait, enum, method, file, route, command, job, controller, model, action, query, service, listener, event, policy, request, resource and test. PHP kinds come from declarations. Application kinds currently use the corresponding conventional directory segments; tests also recognize tests/. Job candidates require an inherited queue contract, Dispatchable trait or a resolved supported dispatch witness. A Jobs directory alone adds only a placement note. Route and command candidates are source declarations, not proof of runtime registration or execution.
Choose a candidate explicitly. Pass its selector to impact or path. selector_scope=symbol selects a class/method; file includes the whole file and may include other route or command callbacks. Duplicate class names use the broader file selector so an ambiguous name is not silently resolved. Read notes before following a selector. Tables remain available through impact with table.
Empty query requires kind. limit is 0..500 and only limits displayed candidates/notices. total, ambiguous and truncated remain available at limit zero. status distinguishes found/no matches from incomplete analysis, analysis limits and stale inputs. total_is_lower_bound means analysis stopped early; an empty candidate list then does not prove absence. Known candidates can coexist with unresolved notices.
Search shares the existing source readers and impact graph cache under a separate source-only fingerprint. It parses config/architectures.php as a returned array without requiring it or loading project classes. Static audit.paths, audit.exclude, audit.cache and audit.missing_test are supported. Dynamic audit settings, executable configuration statements, unsafe paths and symlink configuration are rejected explicitly. An absent configuration uses default analysis settings; existing tools retain their loading behavior. Additional PHP sources follow existing safe Laravel/Composer registration discovery. .env, vendor, node_modules, Git internals and symlink sources are not read. Freshness checks paths, mtime and size, including config; equal-stat edits are outside the guarantee. Search does not bootstrap or invoke the analyzed application itself; an Artisan or MCP host may already have booted Laravel before calling the tool.
Unique reach and continuation reports
Use architecture-kit:reach or MCP reach to count recognized users and dependencies of a class, Class::method, or a PHP file. Files include their classes and declared methods. Existing impact calls and change proposals retain their contracts.
php artisan architecture-kit:reach 'App\Actions\SendInvoice::handle' --agent --limit=1
php artisan architecture-kit:reach --report=REPORT_ID --page=2 --limit=1 --agent
php artisan architecture-kit:reach app/Actions/SendInvoice.php --agent --depth=8
php artisan architecture-kit:reach --schema
For example, when A calls B and C, both call D, and D calls A, outgoing resolved code counts are 2 direct and 1 exclusively indirect. D counts once and the cycle does not add A. limit=1 displays one row per section and retains these totals. limit=0 displays no rows and retains totals; use a positive limit with the returned report ID to read evidence. BFS keeps one shortest witness per symbol and certainty; additional paths do not inflate code counts. References remain leaves, and resolved/possible sets may overlap, so do not sum them as an overall unique total.
reach.counts separates code symbols per direction/certainty, incoming execution entry declarations per declared/possible certainty, and unique DATA source operations per direction. Multiple DATA witnesses do not multiply an operation; different offsets on the same line remain distinct. Shared tables never connect execution paths. An execution declaration does not prove dispatch, registration, worker success or actual execution. layer_crossings preserves source witnesses and known roles, with unknown for unread or unclassified symbols. Crossings retain recognized edge witnesses independently of unique-symbol deduplication, including alternate diamond edges and cycle-closing edges. They are informational and is_violation=false is not an audit verdict.
reach.pagination.report_id identifies a saved immutable analysis. Each section uses the same page offset and limit. pages, next_page and truncated describe displayed lists; changing page size changes offsets. CLI uses --report, MCP uses report_id. Continuation omits subject; a new analysis starts at page 1. Missing, invalid, corrupt, unsafe, stale or out-of-range reports return explicit errors. Continuation reads report bytes and source metadata without reparsing PHP. Reports are stored under storage/framework/cache/architecture-kit/reach; removing this directory makes old IDs unavailable.
reach.status, fresh, total_is_lower_bound, analysis_budget, source notices, scope and requested depth describe analysis boundaries separately from display limits. Internal per-channel row limits are 1000, code traversal budgets are 10000 visits and 1000 queued symbols, and execution/DATA retain their documented source, path and memory limits. A limit means incomplete analysis and lower bounds. Inspect boundary symbols, increase depth within 32, narrow configured scope or increase memory and start a new report. Zero means no recognized links in this scope, not absence of runtime users. Test candidates are not coverage or PASS. Class/file queries also retain unresolved calls in their declared methods and reached code symbols.
Settings use the static source-only reader described for search, without requiring project configuration. The graph/cache and existing source readers are reused without expanding audit scope or executing analyzed code. Freshness checks project PHP/composer metadata, configuration and file additions/removals using paths, mtime and size; equal-stat edits are outside the guarantee. This conservative inventory reads names/stats, not extra source contents, and excludes vendor, node_modules, Git internals, .env, symlinks and report storage. Tests preserved by the graph when missing-test is enabled remain freshness inputs even when an exclusion matches them. It has a 20000-entry and memory budget. Report persistence/readback has bounded size and memory; inability to preserve a report returns an explicit error, never a complete zero result. Artisan/MCP hosts may already have booted Laravel before calling the analyzer.
Declare roles and modules for existing directories
An application can keep its current layout and declare what each location contains in config/architectures.php:
'audit' => [
'classification' => [
'roles' => [
['path' => 'app/Billing/UseCases', 'role' => 'application', 'kind' => 'action'],
['namespace' => 'App\\Billing\\Readers', 'role' => 'application', 'kind' => 'query'],
['pattern' => 'app/*/Readers', 'role' => 'application', 'kind' => 'query'],
],
'modules' => [
['name' => 'Billing', 'path' => 'app/Billing'],
['name' => 'Refunds', 'path' => 'app/Billing/Refunds'],
],
'unknown_role' => 'off', // off, warn or error
],
],
app/Billing/UseCases/Pay.php is an application action in Billing. A class under app/Billing/Refunds belongs to Refunds and retains Billing as its parent. app/Models/User.php stays a shared model without a module unless explicitly assigned. Existing models inside modules are supported.
Selectors are directory prefixes, namespace prefixes, or directory globs. * matches one complete directory segment; ** matches zero or more. A literal selector wins over globs. The most specific literal prefix wins in its selector family. Conflicting matching globs, path and namespace declarations, or module owners fail explicitly. Use parent to declare a module parent when directory nesting does not express it. Missing parents and cycles fail.
Supported roles are domain, application, adapter, infrastructure, composition, port, unknown and test. Supported application kinds are action, query, controller, model, service, job, listener, event, policy, request, resource, data, value-object, enum, exception, builder, port, provider and test. PHP declaration kinds such as class, interface, trait and enum remain separate. Tests keep their test classification. A classless file remains unknown or test rather than receiving an artificial layer.
Declarations override directory conventions, without enabling a profile or extending audit.paths. Actions and Queries receive their enabled rules wherever declared. Unknown means an unrecognized architectural layer, not a missing action/query label. unknown_role defaults to off. Audit, context, search, impact, reach and file rules expose classification provenance. Module relations distinguish intra-module, inter-module and unassigned links with a source path and line. These structural witnesses are not module dependency policy or runtime proof. Output limits can truncate the classification rows.
With classification declarations, configuration must be a static returned array. Literal data, package Architecture/MissingTestLevel enum cases and class-name strings are supported. Dynamic calls and application constants fail without execution. Legacy configuration without declarations keeps its existing behavior. Graph cache keys include the mapping fingerprint. When role/module declarations or unknown-layer reporting are active, audit does not boot the application to discover endpoints. Pass an explicit RouteMap to the programmatic audit for endpoint read analysis; otherwise the report marks that channel incomplete.
An agent can propose organization while working on related code. For example, it may suggest placing existing payment actions in app/Billing/Actions and invoice readers in app/Billing/Queries. The proposal must list files, reasons, uncertainties, reference and registration changes, and test consequences. It respects the current layout, stays separate from accepted declarations and requires approval before execution. Models remain in app/Models by default. Module scaffolding is not provided.
Package public API comparison
Compare a package Git revision with working files, including uncommitted changes, or with another revision. The command reads production Composer autoload declarations and PHP, Artisan, published config/migrations, event identities/payloads and declared MCP input/output schemas. It requires a package checkout with Git history; a vendor installation is insufficient.
php artisan architecture-kit:public-api v1.2.3 --agent --current-version=1.2.3
php artisan architecture-kit:public-api v1.2.3 --to=v1.3.0 --agent
php artisan architecture-kit:public-api HEAD --public-path=src/Contracts --limit=20 --agent
php artisan architecture-kit:public-api v0.6.2 --current-version=0.6.2 --zero-policy=breaking-minor --agent
php artisan architecture-kit:public-api --schema
MCP public-api accepts from, to (default working), public_paths, current_version, zero_policy and limit. Both interfaces use the same report. public_paths restricts the production surface and does not make private or @internal declarations public.
Each change includes its element, before/after declaration and source, reasons and verdict: breaking, compatible, check or internal. Inherited members are reported on the public class that exposes them, even when their base class is internal. Internal types in public signatures expose only their type identity; their other members remain internal. Changes to implementation, configuration values and published migrations require inspection. Declarations do not prove behavioural compatibility or existing installation safety.
sources identifies both revisions and content fingerprints. Read analysis.status, fresh, total_is_lower_bound, notice_total and notices; dynamic declarations, parse failures, external inheritance and unsupported autoload forms make analysis incomplete. Working-source changes require a new report. limit only sizes displayed changes and notices; totals and version evidence use the recognized inventory. Source budgets are 3000 files, 16 MiB total, 1 MiB per file, 10000 API entries, inheritance depth 32 and 15 seconds per Git process. Memory budgets can stop parsing earlier. Symlinks, environment files and vendor sources are unread.
For a stable x.y.z, proved incompatibilities establish a minimum major component and recognized extensions a minimum minor component. A 0.x project must choose breaking-minor or breaking-major; compatible extensions establish a minimum minor under either policy. Without a policy, no component is guessed. Unchanged declarations never prove patch. semver.release_ready is always false: inspect check rows, run consumer tests and review the release. The report never checks out a revision, runs studied code/migrations, changes the version or publishes a package. Ordinary application audit remains unchanged.
Declare and migrate toward target architecture
Create .architecture-kit/target.json to describe the architecture you want for a selected area. The target is separate from config/architectures.php, which continues to govern the current audit. Its default mode is informational.
{
"id": "billing",
"version": "1",
"paths": ["app"],
"classification": {
"roles": [{"path": "app/Billing/Actions", "role": "application", "kind": "action"}],
"modules": [{"path": "app/Billing", "name": "Billing"}]
},
"placements": [
{"kind": "action", "paths": ["app/Billing/Actions"]},
{"kind": "model", "paths": ["app/Models"]}
],
"dependencies": [
{"from": {"role": "application"}, "allow": [{"role": "domain"}, {"role": "port"}, {"role": "application"}]}
],
"mode": "info",
"only": "all"
}
For example, app/Actions/CreateInvoice.php can remain where it is while the report identifies app/Billing/Actions as its destination. Invoice can stay in app/Models. Classification uses the same directory, namespace and whole-segment pattern declarations as current project classification. It does not enable architecture profiles. Placements select a kind, optionally a module, and may constrain the role. The module-specific placement takes precedence. Conflicting selectors or placement roles produce a configuration error. Dependency policies apply together: every matching policy must allow a dependency. An empty allow list forbids all recognized project dependencies from that selector.
php artisan architecture-kit:target --agent
php artisan architecture-kit:target 'App\Actions\CreateInvoice::handle' --agent
php artisan architecture-kit:target app/Billing/Actions/NewInvoice.php --agent
php artisan architecture-kit:target --schema
php artisan architecture-kit:guard --target --agent
MCP architecture-target accepts subject and limit. MCP guard accepts target: true to add this separate gate. CLI and MCP return the same source report. file-rules adds the target report when a declaration exists, including guidance for a future PHP file. A future namespace is not guessed. Without a target file these paths preserve their existing behavior.
Each element has current, expected, source path and line, issues, and a status of conformant, migration, requires_check or outside_scope. Known dependencies come before callers in migration_order. Cycles require a joint migration decision. The order is a proposal and never moves or edits code. Missing, external, dynamic or unrecognized dependencies remain explicit uncertainty. Existing PHP files without classes remain file elements requiring inspection; unparseable existing sources are not future files. Source uncertainty notices accompany their affected elements with file-level scope. Blocking fails on incomplete structure or dependency analysis, stale inputs or covered elements requiring inspection. Informational and warning modes report that uncertainty without blocking.
Current audit findings remain in audit with their rule, source and suppression of inline, baseline or null. They also appear next to related elements, with file-level witness scope. audit_divergence names the current rule requiring a separate decision, even when a target element is conformant. This does not prove that every finding corresponds to the selected class in a file with several classes. Suppression remains unresolved and never fixes target issues. Custom audit rules, runtime route registration and runtime-dependent test reachability are not executed; current-audit limits are separate from target compliance.
Choose mode: "warn" to report warning counts, or mode: "block" to fail the target command and opt-in target guard. Choose only: "new" after accepting a reference. The ordinary guard never adds the target implicitly, and target warnings do not inherit ordinary --strict behavior.
The report returns reference_candidate as data with accepted: false. Review the complete candidate, save that object manually as a project-relative JSON file, then set accepted: true and a nonempty accepted_by identifying the human approval. Set reference in the target to that path. No package command or MCP tool writes or accepts a reference. The ordinary audit baseline cannot serve as this reference.
Only a fresh candidate with complete structure and dependency analysis can be accepted. Separate runtime limits of the current audit do not make the target itself incomplete. References bind the target id, version, declarations, target paths and analysis configuration. Changing placement, classification, audit scope or excludes invalidates comparison; changing only mode, only or the reference location does not. New-only rejects a missing, unaccepted, incomplete or incompatible reference. Removed/unobserved sources and outside-scope elements are not fixed issues. Partial analysis and uncertain elements cannot contribute resolved issues. A conformant transition claims code improvement only when the source declaration or path also changed. Changing scope or relaxing the target cannot claim code improvement.
Analysis reads frozen bounded source bytes without booting project PHP or requiring Git. Current audit uses its own scope and excludes; target analysis covers the declared target area even if the ordinary audit excludes it. analysis reports freshness, scope, completeness, lower bounds and display truncation. limit is 0..500 displayed rows per channel and does not change totals or the full reference candidate. Source budgets are 10,000 listing entries, 3,000 files, 1 MB per file, 16 MB total and available memory. Dependency policy evaluation stops at 100,000 checks and reports a lower bound. Target JSON is limited to 100 KB; references to 1 MB. An oversized candidate is omitted with an explicit notice, never truncated into an acceptable reference. Inspect notices and rerun stale sources before acting.
Compare architecture across revisions
Use revision-diff to see how the application's architecture changed between a Git revision and working sources, including dirty and untracked files, or between two commits. It reads both states without checking out either version or booting their application code.
php artisan architecture-kit:revision-diff HEAD --agent
php artisan architecture-kit:revision-diff v1.0.0 --to=v1.1.0 --agent
php artisan architecture-kit:revision-diff HEAD --shared-config=before --agent
php artisan architecture-kit:revision-diff HEAD --pair='App\OldAction=App\NewAction' --agent
php artisan architecture-kit:revision-diff --schema
MCP revision-diff accepts from, to, shared_config, manual_pairs and limit. manual_pairs maps old symbol names to new names. Both interfaces use the same comparison; timing metrics vary per invocation. limit is 0..500 displayed rows per channel, including candidate pairs. Totals remain counts of the recognized differences before display truncation.
The report separates these differences:
| Channel | What it shows |
|---|---|
symbols |
Added, removed, moved or renamed classes, methods and PHP files, with role, application kind, module and source evidence. |
structure |
Code dependency witnesses and method call sites. Repeated calls remain distinct occurrences. |
http |
Recognized HTTP declarations and handlers, including registration witnesses and certainty. |
execution |
Recognized Artisan, scheduler, job, event and model lifecycle links, with mode, timing and conditions. |
data |
Recognized table effects, connections, read/write/schema operations and helper paths. |
transitions |
Layer and module crossings between known declarations. A crossing is not an architecture quality verdict. |
configuration |
Scope, excludes, mapping, rule settings and enabled profile changes. |
rule_sources |
Source changes of statically configured custom rules. Rule code is never instantiated or run. |
Each state normally uses its own config/architectures.php. The source-only decoder accepts literal arrays and package Architecture/MissingTestLevel enum cases. Dynamic calls, application constants and conditional or executable configuration remain unresolved. Missing configuration uses package defaults. An unreadable existing configuration is not replaced with today's configuration. --shared-config=before or after explicitly applies the selected state's settings to both sides; configuration_sources still identifies each state's own settings.
A path removed from scope is reported as scope_left, rather than deleted code. An element entering scope uses scope_entered. Incomplete analysis may show not_observed_before or not_observed_after; those labels do not prove an addition or deletion. Read analysis.channels and the before/after source identities, completeness and freshness. Unresolved execution calls retain their source witnesses in the execution channel and side-tagged notices; they make that channel incomplete.
Same symbol identities, unique declaration evidence and explicit manual pairs preserve relations across moves. Similar or ambiguous declarations remain candidates. Source lines, offsets and generated callback coordinates are ignored for comparison identity and retained as witnesses. Changing certainty or conditions remains visible. A new comment does not create a relation; removing one of two identical calls still removes one occurrence.
metrics records elapsed time and parsed/reused file graph contributions. Compatible facts are reused in memory with content, path and configuration fingerprints. The comparison does not write the application's graph cache. Source, memory and candidate limits produce partial reports with known rows and notices. Working freshness checks paths, mtime and size and revalidates source content. Detected changes require rerunning the report; equal-stat edits remain outside the stat guarantee. Missing differences never prove identical runtime behaviour or SQL. External declarations and unrecognized dynamic registrations remain boundaries. Ordinary application audits retain their existing behaviour.
Immutable impact proposal pages
Start one saved analysis using --page=1 or MCP page: 1. The saved report fixes the subject, delete/signature/move proposal and depth. Continue with the returned report_id, page and display limit, without subject or proposal. Continuation reads metadata and the saved report rather than project PHP bodies. Changed PHP/Composer/configuration metadata invalidates old pages. Edits preserving both mtime and size remain outside the freshness guarantee.
php artisan architecture-kit:impact 'PaymentService::charge' --change=delete --page=1 --limit=20 --agent
php artisan architecture-kit:impact --report=REPORT_ID --page=2 --limit=20 --agent
Each channel is independently paged. pagination.next supplies MCP continuation arguments; pagination.truncated concerns presentation. Analysis remains bounded to 1000 rows per channel, source/traversal budgets and memory headroom. Analysis limits make totals lower bounds. limit=0 returns summaries, including proposal check totals, and can later be expanded from the same report. Inspect normalized_intent to verify the saved subject and proposal.
execution.flows marks direction: incoming for source entries reaching the subject and direction: outgoing for operations reachable from its own methods. Outgoing traversal retains dispatch mode, timing, conditions and path-local model-event suppression. It does not expand other methods of a caller or connect processes through a shared table.
Related Packages
The rules, generators and checks that keep every Laravel project built the same...
Spatie's coding guidelines as AI skills for Laravel Boost and skills.sh
Distributes and maintains the aaix TALL stack AI agent rules and guidelines acro...
Swiss-army artisan CLI for Laravel — Scan, inspect, debug, and explore every asp...
Version History
| Version | Released | PHP | Laravel | License |
|---|---|---|---|---|
| v0.6.1 | ^8.3 | ^12.41.1| | MIT | |
| v0.6.0 | ^8.3 | ^12.41.1| | MIT | |
| v0.5.0 | ^8.3 | ^12.41.1| | MIT | |
| v0.4.1 | ^8.3 | ^12.41.1| | MIT | |
| v0.4.0 | ^8.3 | ^12.41.1| | MIT | |
| v0.3.0 | ^8.3 | ^12.41.1| | MIT | |
| v0.2.6 | ^8.3 | ^12.41.1| | MIT | |
| v0.2.5 | ^8.3 | ^12.41.1| | MIT | |
| v0.2.4 | ^8.3 | ^12.41.1| | MIT | |
| v0.2.3 | ^8.3 | ^12.41.1| | MIT | |
| v0.2.2 | ^8.3 | ^12.41.1| | MIT | |
| v0.2.1 | ^8.3 | ^12.41.1| | MIT | |
| v0.2.0 | ^8.3 | ^12.41.1| | MIT | |
| v0.1.10 | ^8.3 | ^12.41.1| | MIT | |
| v0.1.9 | ^8.3 | ^12.41.1| | MIT | |
| v0.1.8 | ^8.3 | ^12.41.1| | MIT | |
| v0.1.7 | ^8.3 | ^12.41.1| | MIT | |
| v0.1.6 | ^8.3 | ^12.41.1| | MIT | |
| v0.1.5 | ^8.3 | ^12.41.1| | MIT | |
| v0.1.4 | ^8.3 | ^12.41.1| | MIT | |
| v0.1.3 | ^8.2 | ^12.41.1| | MIT |