Incremental architectural analysis for Laravel projects. Zero dependencies.
ACE scans your Laravel codebase, measures architectural consistency across five dimensions, infers dominant patterns, evaluates a stack-aware security baseline, and exposes everything through a CLI and an MCP server for LLM integration.
AchCoverage: 74% (+3) | Security: 82% | Pattern: service-layer | Confidence: 100%
As Laravel projects grow, architectural decisions drift. Controllers that started with services start calling models directly. Validation moves from FormRequests to inline. New team members follow the most recent file they find, not the agreed pattern.
ACE makes this drift visible and measurable — without requiring any configuration, AST parsers, or external dependencies.
- Measures what matters: layering, validation, testability, consistency, and authorization — weighted and composable
- Infers patterns: detects your dominant patterns automatically instead of forcing conventions
- Tracks decisions: formalize architectural decisions and rules that your team agreed on
- Stack-aware security: evaluates Laravel, Filament, Livewire, Sanctum, Spatie Permission, and Horizon controls
- Runtime dependency audit: surfaces Composer/NPM vulnerabilities directly in the report (with severity and fix hints)
- Actionability Index: each inconsistency gets a priority score (
P1..P5) based on severity, recurrence, hotspot concentration, and test signal - LLM-native: MCP server lets Claude, Codex, Cursor, Copilot, or any MCP-compatible LLM query your architecture in real-time
- Incremental: SHA1 cache means re-scans only analyze changed files
- Zero dependencies: pure Node.js stdlib, runs everywhere Node 18+ runs
# Install globally
npm install -g ace-engine
# Or run directly with npx
npx ace-engine scan --scope=all
# Initialize ACE in your Laravel project
cd /path/to/laravel-project
ace init
ace scan --scope=all
ace statusnpm install -g ace-enginenpm install --save-dev ace-enginegit clone https://github.com/hassekf/ace-engine.git
cd ace
node ./bin/ace.js helpACE computes an AchCoverage score (0–100%) from five weighted dimensions:
| Dimension | Weight | What it measures |
|---|---|---|
| Layering | 30% | Are controllers delegating to services consistently? Or using direct model calls consistently? ACE adapts to your chosen pattern. |
| Validation | 18% | FormRequest adoption vs inline validation. Penalizes $request->all() usage. |
| Testability | 18% | Test presence + quality signals (assertion density, edge-case coverage, mock pressure, test files without asserts). |
| Consistency | 19% | Linear severity-weighted penalty normalized by scanned files (stable in large codebases). |
| Authorization | 15% | Authorization signals, model↔policy presence, and auth hygiene in state-changing route surfaces. |
Weights are configurable in .ace/config.json:
{
"coverage": {
"weights": {
"layering": 0.30,
"validation": 0.18,
"testability": 0.18,
"consistency": 0.19,
"authorization": 0.15
}
}
}report.language defines the default dashboard locale (en-US by default, optional pt-BR).
--lang on ace scan/ace report overrides it for that execution.
ace scan --scope=all # Full scan
ace scan --scope=changed # Incremental (only changed files)
ace scan --files=app/Http/Controllers/UserController.php
ace scan --scope=all --lang=en-US # Force report language for this scan
ace status # Current state summary
ace status --json # Machine-readable output
ace report # Generate HTML dashboard
ace report --lang=pt-BR # Regenerate report in Portuguese (pt-BR)
ace watch --interval=2200 # Watch for changes and re-scanThe report includes:
- in-page language selector (
en-US/pt-BR) - trend correlations
- hotspot concentration summary (violation clustering by file)
- actionability summary for inconsistencies (
P1..P5+ average score) - pattern drift waves (aggregated drift alerts)
- evidence accordions per security control
- clickable KPI cards (jump/filter by relevant panel)
- security split cards (
Security CodevsPipeline Maturity) - dependency audit panel (Composer/NPM vulnerabilities)
- pre-generated localized files for both languages
ace init # Scaffold .ace/ layout, gitignore, LLM onboarding (all targets)
ace init --llms=claude,codex # Only generate artifacts for specific LLMs
ace init --select-llms # Interactive LLM selection in terminal
ace init --force # Overwrite existing files
ace config:init # Create .ace/config.json with defaults
ace config:show # Print current configSupported --llms targets: claude, cursor, copilot, codex (or all).
When targeting Claude or Codex, ace init also creates a local skill:
.claude/skills/ace-architectural-guardian/SKILL.md.codex/skills/ace-architectural-guardian/SKILL.md
These skills instruct the LLM to consult ACE before and after structural changes.
Rules are formalized team agreements about how code should be structured:
ace rule:add --title="Controller via Service" \
--constraints="No direct model call,Use Service" \
--applies_to="controller"
ace rule:update --id="controller-via-service-v1" --status=deprecated \
--note="Replaced by UseCase pattern"Decisions record what pattern is preferred for a given concern:
ace decision:add --key="controller.data_access" \
--preferred="service-layer" \
--rationale="Team consensus from sprint 12"
ace decision:update --id="controller-data-access-service-layer-v1" \
--status=approved
ace decision:list --jsonSuppress specific violations for legacy code or ongoing refactors:
ace waiver:add --type="pattern-drift:*" \
--file="app/Legacy/*" \
--reason="Refactor in progress" \
--until=2026-12-31
ace waiver:list --json
ace waiver:update --id="waiver-id" --status=inactiveWaivers support wildcards for type, file, severity, and contains.
ACE infers patterns automatically, but you can also define custom ones:
ace pattern:list --json
ace pattern:upsert --json='{
"key": "controller.data_access",
"detector": {
"type": "split_ratio",
"totalMetric": "controllers",
"sideAMetric": "controllersUsingService",
"sideBMetric": "controllersWithDirectModel",
"labels": { "sideA": "service-layer", "sideB": "direct-model" },
"orientation": "high_is_good"
}
}'
ace pattern:disable --key="controller.validation"
ace pattern:enable --key="controller.validation"
ace pattern:remove --key="custom.pattern"Accelerate setup for existing Laravel projects:
ace bootstrap:laravel --dry-run --json # Preview proposals
ace bootstrap:laravel # Apply proposalsBootstrap will:
- Run an initial full scan
- Ensure useful Laravel patterns in the registry
- Propose architectural decisions based on inference + confidence + adoption
- Optionally apply proposals (skip with
--dry-run)
Detect your Laravel stack and get scoped recommendations:
ace modules:list --json
ace modules:list --enabled-onlyDetected modules: Laravel Core, Filament, Livewire, Sanctum, Spatie Permission, Horizon.
Export a comprehensive context bundle for LLM consumption:
ace learning:bundle --json
ace learning:bundle --files=app/Http/Controllers/PaymentController.php --jsonIncludes: coverage, security highlights, pattern registry, decisions, rules, hotspots, representative files, and guidance prompts.
When --files is provided, ACE narrows the bundle to that scope plus directly related files.
ACE includes a stdio-based MCP server compatible with Claude Code, Codex, Cursor, GitHub Copilot, and any MCP-compatible client.
ace mcp # Compact profile (15 tools)
ace mcp --profile=full # Alias to the same consolidated 15-tool API
ace mcp --root=/path/to/project # Analyze a different projectAfter running ace init, MCP configuration snippets are generated in .ace/integration/.
Claude Code (.claude/settings.json or project-level):
{
"mcpServers": {
"ace": {
"command": "npx",
"args": ["ace-engine", "mcp", "--root=/path/to/project"]
}
}
}Cursor (.cursor/mcp.json):
{
"mcpServers": {
"ace": {
"command": "npx",
"args": ["ace-engine", "mcp", "--root=/path/to/project"]
}
}
}| Tool | Description |
|---|---|
ace.get_status |
Full project status |
ace.get_coverage |
AchCoverage with dimensions |
ace.get_trend |
Temporal analysis for coverage, security, and violations |
ace.get_project_model |
Pattern model and metrics |
ace.get_security |
Security baseline evaluation |
ace.report_inconsistencies |
Violations with actionability ranking (P1..P5), optional priority filter, and actionability/severity sorting |
ace.scan_scope |
Trigger scan (all, changed, or specific files) |
ace.get_learning_bundle |
Context bundle for LLM reasoning |
ace.get_modules |
Detected stack modules with docs |
ace.manage_rules |
CRUD for architectural rules |
ace.manage_decisions |
CRUD for architectural decisions |
ace.manage_waivers |
CRUD for violation waivers |
ace.manage_patterns |
CRUD for pattern registry |
ace.manage_config |
Read/write project config |
ace.bootstrap_laravel |
Auto-setup for Laravel projects |
ace init remains available via CLI for project onboarding/scaffolding.
When ACE is connected to an LLM via MCP (or through the ace-architectural-guardian skill), it acts as a live architectural advisor during code generation and refactoring. The LLM doesn't just generate code — it checks your project's architecture before and after every structural change.
The ace-architectural-guardian skill (auto-created by ace init for Claude and Codex) instructs the LLM to follow this flow:
- Before generating code — consult
ace.get_statusandace.get_project_modelto understand the current architecture - Generate or refactor — write code aligned with the detected dominant pattern
- After the change — call
ace.scan_scopeon the modified files and report the delta - Surface issues — if inconsistencies appear, present options (no refactor / simple refactor / full refactor) with effort estimates
- Formalize only with consent — never auto-create rules or decisions; wait for explicit user approval
The LLM always ends with a status line:
AchCoverage: 74% (+2) | 2 new inconsistencies, 1 resolved
Test quality: 81%
Trend: improving | no regression alert
Security: 82% | Report: .ace/report.html
You ask the LLM: "Create a new OrderController with CRUD for orders"
Here's what happens behind the scenes:
┌─ LLM reads ACE state ─────────────────────────────────────────────┐
│ │
│ 1. ace.get_status │
│ → AchCoverage: 72%, Pattern: service-layer, Confidence: 100% │
│ │
│ 2. ace.get_project_model │
│ → 14/16 controllers use Service injection │
│ → Dominant: service-layer (87% adoption) │
│ → Active decision: controller.data_access → service-layer │
│ │
│ LLM understands: this project uses the service layer pattern. │
│ It should NOT create a controller with direct Model calls. │
│ │
├─ LLM generates code ──────────────────────────────────────────────┤
│ │
│ Creates: │
│ • app/Http/Controllers/OrderController.php (injects OrderService) │
│ • app/Services/OrderService.php │
│ • app/Http/Requests/StoreOrderRequest.php │
│ • app/Http/Requests/UpdateOrderRequest.php │
│ │
├─ LLM re-scans ────────────────────────────────────────────────────┤
│ │
│ 3. ace.scan_scope (files: OrderController.php, OrderService.php) │
│ → AchCoverage: 74% (+2) │
│ → 0 new inconsistencies │
│ → Pattern adoption: 15/17 controllers (88%) │
│ │
│ 4. ace.report_inconsistencies │
│ → No new violations. Coverage improved. │
│ │
└────────────────────────────────────────────────────────────────────┘
The LLM responds with the generated code plus the ACE summary:
Created
OrderControllerwithOrderServiceinjection, following the project's service-layer pattern (88% adoption).
AchCoverage: 74% (+2) | 0 new inconsistencies | Security: 82%
If the LLM had generated a controller with direct Order::all() calls instead, the re-scan would flag it:
ace.scan_scope → AchCoverage: 70% (-2)
ace.report_inconsistencies → pattern-drift:controller.data_access in OrderController.php
The LLM then presents options:
Inconsistency detected:
OrderControlleruses direct model calls, but this project follows the service-layer pattern (87% adoption).
- Keep as-is — accept temporary debt, add waiver
- Simple refactor — extract queries to
OrderService, keep controller thin (~10 min)- Full refactor — add repository layer + service + FormRequests (~30 min)
Which approach do you prefer?
When you and the LLM agree on a pattern, the LLM can formalize it:
You: "Let's make service-layer the official pattern"
LLM calls: ace.manage_decisions({
action: "create",
key: "controller.data_access",
preferred: "service-layer",
rationale: "Team consensus — 88% adoption, reduces controller complexity"
})
→ Decision created: controller-data-access-service-layer-v1
Future scans will flag controllers that don't follow this pattern.
Even without LLM integration, ACE works as a standalone CLI tool. Run ace scan --scope=all in CI or locally to get the same coverage, security, and consistency metrics.
ACE does not block CI by default. Installing/running the package only analyzes and reports.
If your team wants enforcement, enable it explicitly in your pipeline using ace scan --json.
Recommended: keep enforcement policy versioned in .ace/config.json so the whole team shares the same gate.
{
"enforcement": {
"enabled": false,
"failOnRegression": true,
"thresholds": {
"minCoverage": 40,
"maxRegressionDrop": 5,
"maxSecurityFailures": 0
}
}
}Example (optional gate in CI):
ace scan --scope=all --json > /tmp/ace-scan.json
node -e '
const fs = require("node:fs");
const scan = JSON.parse(fs.readFileSync("/tmp/ace-scan.json", "utf8"));
const cfg = JSON.parse(fs.readFileSync(".ace/config.json", "utf8"));
const enforcement = cfg.enforcement || {};
if (!enforcement.enabled) {
console.log("ACE enforcement disabled by config.");
process.exit(0);
}
const t = enforcement.thresholds || {};
const failures = [];
if ((scan.achCoverage ?? 0) < (t.minCoverage ?? 0)) failures.push(`AchCoverage below ${t.minCoverage}`);
if ((enforcement.failOnRegression ?? true) && (scan.regressionAlert?.triggered ?? false)) {
const drop = Number(scan.regressionAlert?.drop ?? 0);
const maxDrop = Number(t.maxRegressionDrop ?? 5);
if (drop >= maxDrop) failures.push(`Coverage regression drop ${drop} >= ${maxDrop}`);
}
if ((scan.securityFailures ?? 0) > (t.maxSecurityFailures ?? 0)) failures.push(`Security failures > ${t.maxSecurityFailures}`);
if (failures.length) {
console.error("ACE optional gate failed:");
failures.forEach((f) => console.error(" - " + f));
process.exit(1);
}
console.log("ACE optional gate passed.");
'This keeps the default developer experience non-blocking while still allowing strict projects to enforce quality/security in CI.
ACE evaluates a security baseline tailored to your detected stack. Controls are only included when the relevant technology is detected.
- CSRF protection on state-changing routes
- Authentication on state-changing routes
- Rate limiting (throttle middleware)
- Upload validation
- Webhook signature verification
- Raw SQL injection risk (distinguishes safe vs unsafe usage)
- Policy/authorization coverage
- Route-level auth coverage
- Runtime Composer dependency audit (when
composer.json/composer.lockexists)
- Filament:
canAccessPanel(), page authorization, widget authorization - Livewire:
#[Locked]property usage on public properties - Sanctum: API guard configuration
- Spatie Permission: permission/role enforcement
- Horizon: dashboard protection
- NPM/Node stack: runtime NPM audit (only when
package.jsonexists)
Each control reports pass, warning, fail, or unknown with a weighted score.
ACE classifies PHP files into kinds for targeted analysis:
| Kind | Detection |
|---|---|
controller |
app/Http/Controllers/ |
service |
app/Services/, app/Actions/, app/UseCases/ |
job |
app/Jobs/ |
listener |
app/Listeners/ |
middleware |
app/Http/Middleware/ |
helper |
app/Helpers/, app/Utils/ |
validator |
app/Validators/, app/Rules/, app/Domain/*/Validators/ |
value-object |
app/ValueObjects/ |
channel |
app/Channels/ |
mail |
app/Mail/ or classes extending Mailable |
logging |
app/Logging/ |
form-component |
app/Forms/, app/Tables/ |
scope |
app/Scopes/ or scope classes/interfaces |
kernel |
app/Http/Kernel.php, app/Console/Kernel.php |
websocket |
app/Websocket/ |
filament-support |
app/Filament/ files outside Resources/Pages/Widgets |
broadcasting |
app/Broadcasting/ |
queue-support |
app/Queue/ |
provider |
app/Providers/ or classes extending ServiceProvider |
event |
app/Events/ (and */Events/) |
observer |
app/Observers/ or classes ending with Observer |
notification |
app/Notifications/ or classes extending Notification |
trait |
app/Traits/ |
contract |
app/Contracts/ or App\\Contracts interfaces |
http-resource |
app/Http/Resources/ or classes extending JsonResource/ResourceCollection |
model |
app/Models/ |
policy |
app/Policies/ |
dto |
app/DTOs/, app/Dtos/, app/Data/ |
enum |
app/Enums/ |
command |
app/Console/Commands/ |
request |
app/Http/Requests/ |
filament-resource |
app/Filament/*/Resources/ |
filament-page |
app/Filament/*/Pages/ |
filament-widget |
app/Filament/*/Widgets/ |
livewire-component |
app/Livewire/ or app/Http/Livewire/ |
route-file |
routes/*.php |
ACE uses heuristic-based analysis (regex + brace/parenthesis matching) to detect:
- Service layer adoption (constructor injection of Service/Action/UseCase)
- FormRequest vs inline validation
- Direct model calls in controllers
- Fat controllers and long methods
$request->all()usage- Raw SQL with safe/unsafe distinction (bindings-aware; request input in bindings is treated as safe)
->get()without constraints (chain-aware, including multiline chains and bounded query variables)- N+1 query risks (lazy loading in loops)
- Critical financial writes without
DB::transaction() - Queue hygiene in jobs (
$tries,$timeout,failed(), and uniqueness for critical jobs) - Heavy listeners without queue hints
- Direct model access inside middleware
- Helpers with direct model access and oversized utility files
- Validators without clear entrypoint (
validate/rules/passes) - Mutable Value Objects (public mutable state/setters)
- Mailables without queue and sensitive payload in mail/log channels
- Scopes without
apply()contract - Websocket components without clear auth/authz signals
- Large providers and contract imports without container bindings
- Domain/database logic inside events and observers
- Notifications without queue and potential sensitive payloads
- API Resource relation access without
whenLoaded/relationLoadedguards - Fat/high-coupling traits and traits with direct model access
- Contracts without explicit container binding (
bind,singleton,scoped) - Authorization signals in Filament Pages/Widgets
- Livewire locked properties
- Test file existence per controller/service/model/job/middleware
- Test quality signals across
tests/(assertions, mocks, edge-case coverage, data providers, files with zero asserts)
ACE stores all artifacts in .ace/. Running ace init configures .gitignore automatically.
Versionable (keep in git — shared with the team):
.ace/config.json— project configuration (thresholds, weights, ignore paths, waivers, enforcement policy).ace/pattern-registry.json— dynamic pattern registry.ace/rules.json— formalized architectural rules.ace/decisions.json— versioned architectural decisions
Local/ephemeral (auto-gitignored):
.ace/ace.json— live state (coverage, security, violations, file index).ace/report.html— HTML dashboard.ace/report.en-US.html— English report (used by the in-report language selector).ace/report.pt-BR.html— Portuguese report (used by the in-report language selector).ace/history/*.json— per-scan snapshots.ace/integration/*— MCP configuration snippets
LLM skills (created by ace init outside .ace/):
.claude/skills/ace-architectural-guardian/SKILL.md— Claude Code skill.codex/skills/ace-architectural-guardian/SKILL.md— Codex skill
.ace/config.json supports:
{
"analysis": {
"ignorePaths": ["app/Legacy", "app/Generated"],
"regressionThreshold": 5,
"driftWaveThreshold": 3,
"trendWindow": 8,
"trendStableBand": 1.5,
"thresholds": {
"fatControllerLines": 220,
"largeControllerMethodLines": 80,
"fatServiceLines": 260,
"fatModelLines": 320,
"fatModelMethods": 15,
"fatCommandLines": 260,
"fatHelperLines": 220,
"fatValidatorLines": 220,
"fatFormComponentLines": 260,
"fatProviderLines": 280,
"fatEventLines": 140,
"fatObserverLines": 180,
"fatNotificationLines": 180,
"fatTraitLines": 180,
"fatTraitMethods": 10,
"highTraitImports": 8,
"fatFilamentResourceLines": 320,
"fatFilamentResourceMethods": 12
}
},
"coverage": {
"weights": {
"layering": 0.30,
"validation": 0.18,
"testability": 0.18,
"consistency": 0.19,
"authorization": 0.15
}
},
"security": {
"audits": {
"composer": true,
"npm": true,
"timeoutMs": 15000,
"maxEntries": 120
}
},
"enforcement": {
"enabled": false,
"failOnRegression": true,
"thresholds": {
"minCoverage": 0,
"maxRegressionDrop": 5,
"maxSecurityFailures": 0
}
},
"report": {
"language": "en-US",
"tableRowLimit": 200,
"suggestionLimit": 40,
"hotspotLimit": 12,
"historyLimit": 24
},
"waivers": []
}Every command accepts --root=/path/to/project to operate on a different directory. Many commands accept --json for machine-readable output.
ace help
ace scan [--scope=changed|all|path1,path2] [--files=a.php,b.php] [--lang=pt-BR|en-US] [--json]
ace watch [--interval=2200]
ace status [--json]
ace report [--lang=pt-BR|en-US]
ace mcp [--profile=compact|full]
ace init [--force] [--json] [--llms=all|claude,cursor,copilot,codex] [--select-llms]
ace config:init [--force] [--json]
ace config:show [--json]
ace rule:add --title="..." [--description="..."] [--applies_to=a,b] [--constraints=x,y]
ace rule:update --id="..." --status=active|deprecated|inactive|rejected [--note="..."]
ace decision:add --key="..." --preferred="..." [--rationale="..."]
ace decision:update --id="..." --status=active|approved|superseded|deprecated|rejected|expired|inactive [--note="..."]
ace decision:list [--key=...] [--json]
ace waiver:add [--type=...] [--file=...] [--severity=...] [--contains=...] --reason="..." [--until=YYYY-MM-DD]
ace waiver:update --id="..." --status=active|inactive|expired [--reason="..."] [--until=YYYY-MM-DD]
ace waiver:list [--status=active|inactive|expired] [--json]
ace learning:bundle [--files=a.php,b.php] [--max_files=20] [--json]
ace pattern:list [--json]
ace pattern:upsert --json='...'
ace pattern:disable --key="..."
ace pattern:enable --key="..."
ace pattern:remove --key="..."
ace modules:list [--enabled-only] [--json]
ace bootstrap:laravel [--dry-run] [--scope=all|changed] [--min_confidence=55] [--min_adoption=55]
Machine-readable outputs include a schemaVersion field for forward compatibility:
ace scan --jsonace status --jsonace learning:bundle --json- MCP tools
ace.get_statusandace.get_project_model
If you integrate ACE with scripts/CI bots, validate schemaVersion before parsing.
- Discovery — finds all
.phpfiles, respecting ignore paths - Cache check — compares SHA1 hash + analyzer version; unchanged files skip analysis
- Analysis — extracts a broad metric set per file using heuristic-based PHP analysis
- Pattern inference — detects dominant patterns using configurable detectors (
split_ratio,single_ratio) - Waiver application — matches and applies active waivers, auto-expires when due
- Coverage — computes 5 dimensions + weighted overall score
- Security — evaluates stack-aware baseline controls
- Suggestions — generates actionable improvement suggestions
- State persistence — saves to
.ace/with governance files as dedicated sidecars
- Node.js 18+
- No external dependencies — uses only Node.js built-in modules
- Target projects should follow Laravel conventions (
app/Http/Controllers/,app/Models/, etc.)
git clone https://github.com/hassekf/ace-engine.git
cd ace
npm test # 52+ tests, fast feedback, no setup neededSee CONTRIBUTING.md for contribution workflow, MCP API policy, and release checklist.