Laravel Necromancer

Unearth what AI needs to know about your Laravel app.

Scan your app, then generate a Markdown context file with every route, model, and job cited to its file and line — so Claude Code, Cursor, or Copilot stop guessing and start citing your actual codebase.

$ php artisan necromancer:scan
Necromancer manifest written to necromancer.json

$ php artisan necromancer:generate
Full context written to NECROMANCER.md
Compact context written to CLAUDE.md
NECROMANCER.md
## Routes (14)

| Name | Method | URI | Controller | Middleware | Source |
|---|---|---|---|---|---|
| orders.index | GET | /orders | OrderController@index | auth, verified | app/Http/Controllers/OrderController.php:12 |

## Models (1)

| Name | Table | Fillable | Casts | Relationships | Source |
|---|---|---|---|---|---|
| Order | orders | customer_id, total, status | total → decimal:2 | belongsTo Customer, hasMany OrderLine | app/Models/Order.php:1 |

## Policies (1)

| Class | Model | Methods | Source |
|---|---|---|---|
| OrderPolicy | Order | viewAny, view, create, update, delete | app/Policies/OrderPolicy.php:1 |

What Necromancer Collects

The manifest covers 18 artifact types across the full Laravel application structure. Every artifact carries a source field with file, line, and hash for precise citations and stale detection.

01
routes
Name, method, URI, controller, action, middleware, authorization, route metadata (domain, flow, risk, external services, ADR)
02
models
Table, fillable, casts, relationships, scopes, observers, policy, factory
03
jobs
Queue, connection, tries, timeout, backoff, max_exceptions
04
events
Listeners, broadcastable channels
05
listeners
Handled events, queued status
06
commands
Signature, description, aliases
07
form_requests
Rules, stop_on_first_failure, error_bag
08
policies
Model, policy methods
09
enums
Backing type, cases
10
tests
File, type (unit/feature), subject class, test methods
11
observers
Model, lifecycle hooks, queued status
12
scheduled_tasks
Command, cron expression, human-readable schedule, flags
13
middleware
Alias, class, scope (global/group/alias), group name
14
livewire_components
View, public properties with types, action methods, listened events
15
gates
Ability, kind (closure/class/before_hook/after_hook), parameters
16
mailables
Subject, queued status, queue name, view/markdown template
17
validation_rules
Implicit flag, docblock description
18
service_providers
Deferred flag, source location
One command, the whole shape

Every artifact, every relationship, one graph.

php artisan necromancer:graph projects the manifest into an interactive, force-directed graph — colored by kind, connected by the same relationships your AI agent reads out of NECROMANCER.md. No build step, no server: graph.html opens straight from disk.

routes models jobs policies commands form_requests + 12 more kinds, each colored the same way every time
Necromancer Artifact Graph viewer: a force-directed, kind-colored graph of an application's routes, models, jobs, policies, and their relationships, with a sidebar legend/filter and edge-type key
91 nodes · 17 kinds · 72 edges — from one unmodified Laravel app. necromancer:graph reference ↓

Commands

Necromancer follows a scan-first workflow — every other command reads the manifest produced by necromancer:scan. Grouped below by what you're trying to do, not execution order.

Understand your app

necromancer:scan
--only=TYPE,TYPE --diff --fail-on-drift --output=PATH

Bootstraps the application and writes the necromancer.json manifest. Re-run whenever your application changes. Supports partial scans and CI drift detection.

php artisan necromancer:scan
php artisan necromancer:scan --only=routes,models
php artisan necromancer:scan --diff --fail-on-drift    # CI gate
necromancer:map
--type=TYPE

Terminal-based pretty-printer of the manifest inventory. Read-only; never writes to disk. Shows a stale-manifest warning if source files are newer than the manifest.

php artisan necromancer:map
php artisan necromancer:map --type=routes
php artisan necromancer:map --type=models
necromancer:audit
--format=text|json|markdown --output=PATH --fail-on=SEVERITY

Scored AI-readability report (0–100). Findings grouped by severity: error (−10), warning (−5), suggestion (−1). Checks unnamed routes, missing model casts, dead events, unconfigured jobs, high-risk artifacts without an ADR, external-service artifacts without tests, and more.

ERRORS
  Unnamed routes (3)
   GET /orders           routes/web.php:12

WARNINGS
  Missing return types (7)
   OrderController::store()   app/Http/Controllers/OrderController.php:45
php artisan necromancer:audit
php artisan necromancer:audit --format=markdown --output=audit.md
php artisan necromancer:audit --fail-on=error      # exit 1 on errors (CI)
php artisan necromancer:audit --fail-on=warning    # exit 1 on warnings or errors
necromancer:doctor
--json --min-score=N --only=KEYS

Eight-dimension AI readability percentage dashboard with progress bars. Dimensions: Route Clarity, Model Expressiveness, Authorization Coverage, Validation Coverage, Async Clarity, Codebase Vocabulary, Test Presence, Artifact Annotation Coverage.

  Laravel Necromancer — AI Readability Score
  ──────────────────────────────────────────
  Score: 74%

  Route Clarity             ████████░░  82%  (12/15 named · 14/15 controller-backed)
  Model Expressiveness      ██████░░░░  61%  (3/5 casts · 4/5 fillable · 2/5 relationships)
  Authorization Coverage    ███████░░░  70%  (2/3 policies · 8/12 write routes with auth)
  Validation Coverage       ████████░░  80%  (8/10 write routes with FormRequest)
  Async Clarity             ████████░░  83%  (4/5 jobs configured · 4/4 events with listeners)
  Codebase Vocabulary       ██████░░░░  63%  (5/8 commands described · 1/1 backed enums)
  Test Presence             ████████░░  80%  (4/5 models · 3/3 jobs)
  Artifact Annotation Cov.  ████████░░  83%  (5/6 tagged with domain · 2/2 high-risk with ADR · 1/2 external-service artifacts tested · 4/4 flow-consistent)

  Tip: run necromancer:audit for a detailed findings list.
php artisan necromancer:doctor
php artisan necromancer:doctor --json
php artisan necromancer:doctor --min-score=80         # exit 1 when score < 80 (CI)
php artisan necromancer:doctor --only=route-clarity

Feed it to AI

necromancer:generate
--only=TYPE,TYPE --except=TYPE,TYPE --paths=PATH,PATH --output=PATH --force

Produces NECROMANCER.md — a full Markdown context file for AI coding agents. Renders Domain/Risk/External Services/ADR columns in the routes table when route metadata is declared. When Laravel Boost is installed, writes to .ai/guidelines/necromancer.md automatically. When a Knowledge Bundle exists (okf/ and/or okf-enriched/), a ## Knowledge Bundle section names its path, regenerate command, and live stats — unaffected by --only/--except/--paths, suppressible via okf.announce_in_context. Each line compares the bundle's own content_hash against the current manifest's and appends a "may be stale" caveat on mismatch — never a claim either way for a bundle exported before this field existed.

php artisan necromancer:generate
php artisan necromancer:generate --only=routes,models
php artisan necromancer:generate --except=listeners,validation_rules
php artisan necromancer:generate --paths=app/Models,app/Http/Controllers/Admin
php artisan necromancer:generate --output=.ai/context/app.md --force
necromancer:prompt
--top=N --no-ai --output=PATH

Builds a ready-to-paste AI prompt grounded in the most relevant manifest entries for your question. Keyword-searches the manifest, ranks artifacts by relevance, and outputs a prompt block with file:line citations.

php artisan necromancer:prompt "Where is tenant isolation enforced?"
php artisan necromancer:prompt "billing" --top=5
php artisan necromancer:prompt "auth" --no-ai --output=prompt.txt

Talk to your codebase

necromancer:ask requires laravel/ai
--provider= --model=

Natural-language codebase Q&A. The manifest is injected verbatim into the AI's context — answers are grounded in your actual application, not the model's prior knowledge.

necromancer:infer requires laravel/ai
--locale= --temperature= --max-critic-rounds=N --dry-run --fresh

Generates Architecture Decision Records (ADRs) from the manifest. Evaluates nine architectural dimensions (async-processing, authorization, event-driven, api-design, data-modeling, command-scheduling, form-validation, external-services, architecture-pattern), with a critic agent reviewing and filtering the initial ADRs.

Review & measure

necromancer:diff
--base-manifest=PATH --review --format=markdown --output=PATH

Compares the current manifest against a branch or snapshot. Shows added, removed, and modified routes, models, jobs, events, listeners, policies, and other artifacts. Added or changed artifacts of any family tagged high/critical risk or declaring external services surface in a dedicated "Flagged Artifacts" section — no AI required. With --review, an AI agent summarises the architectural impact and surfaces risks, grounded in the same flagged-artifacts signal.

php artisan necromancer:diff main
php artisan necromancer:diff main --review --format=markdown
php artisan necromancer:diff --base-manifest=snapshots/before.json
necromancer:benchmark
--generate-suite --suite-output=PATH --no-judge --format=markdown --output=PATH

Measures how much Necromancer's generated context improves AI accuracy, hallucination rate, latency, and token cost. Runs a task suite in three conditions by default — no context, manual AGENTS.md, and Necromancer-generated NECROMANCER.md — and reports results side by side, including per-condition average latency (generation and, separately, judge). An opt-in fourth condition, necromancer-mcp (bare instructions plus live, tool-based manifest queries instead of a pre-assembled document), adds a "Necromancer (MCP) vs Necromancer (static)" comparison line when requested via --condition=. Use --generate-suite to scaffold a grounded task suite from the current manifest before running the benchmark.

php artisan necromancer:benchmark
php artisan necromancer:benchmark --generate-suite   # scaffold tasks from manifest → config/benchmark-tasks.php
php artisan necromancer:benchmark --generate-suite --suite-output=custom/tasks.php
php artisan necromancer:benchmark --no-judge        # automated checks only
php artisan necromancer:benchmark --format=markdown --output=benchmark.md
php artisan necromancer:benchmark --condition=necromancer,necromancer-mcp   # static context vs. live tool-querying

Knowledge Bundle & Graph

necromancer:okf
--output=PATH --allow-stale --allow-partial

Projects the manifest into a deterministic Open Knowledge Format (OKF) bundle — one Markdown file per artifact under okf/artifacts/, with authoritative YAML front matter and a concise Architectural Context prose mirror. Resolvable cross-artifact relationships (a route's controller, a model's relationships/policy/observers, event/listener pairings) render as Markdown links; every shared domain/flow value gets a synthesized, linked concept; and declared local ADRs are copied into the bundle with provenance — a missing one fails the export.

---
title: "SendInvoiceEmail"
type: "artifact"
kind: "jobs"
necromancer:
  id: "jobs:App\\Jobs\\SendInvoiceEmail"
  facts:
    queue: "emails"
    tries: 3
  annotations:
    domain: "billing"
    risk: "high"
---

## Architectural Context

domain: billing · risk: high
php artisan necromancer:okf
php artisan necromancer:okf --allow-stale --allow-partial
php artisan necromancer:okf --output=dist/okf
necromancer:okf-enrich requires laravel/ai
--provider= --model= --refresh --output=PATH

Writes an AI-enriched sibling bundle alongside the deterministic one — the okf/ bundle from necromancer:okf is never touched. Enrichment can only add a description field and an "AI-Enriched Summary" section — it structurally cannot change facts, annotations, Artifact IDs, or links, because the AI is never given access to those fields. Prompts exclude raw framework metadata, source paths/hashes, configuration, and ADR body content. Each concept caches independently, and a generated okf-enriched/README.md documents all of this, with bundle.json carrying its own content_hash.

necromancer:graph
--output=PATH --allow-stale --allow-partial

Projects the manifest into a deterministic Artifact Graph — see it pictured above. Structural edges mirror the OKF bundle's relationship taxonomy (route→controller, model→relationships/policy/observers, event→listeners); grouping edges connect an artifact to its declared domain/flow; reference edges connect it to a declared local ADR. The sidebar doubles as a color-coded legend and kind filter. graph.html is self-contained (no CDN dependencies) and opens straight from disk — no server needed. Entirely independent of necromancer:okf; refuses a stale or partial manifest by default; writes atomically.

php artisan necromancer:graph
php artisan necromancer:graph --allow-stale --allow-partial
php artisan necromancer:graph --output=dist/graph
open necromancer-graph/graph.html   # opens directly, no server needed

Getting Started

Three steps from zero to AI-readable codebase.

1 Install
composer require --dev robertogallea/laravel-necromancer

The service provider is auto-discovered — no manual registration needed. Optionally publish the configuration:

php artisan vendor:publish --tag=necromancer-config
2 Scan

Build the manifest. Re-run whenever your application changes.

php artisan necromancer:scan
3 Audit & Generate

Check AI-readability and generate the Markdown context file.

php artisan necromancer:audit
php artisan necromancer:generate
PHP ≥ 8.3
Laravel 13.x

CI Integration

Add these steps to your pipeline to enforce manifest freshness and AI-readability quality gates.

- name: Check manifest is up to date
  run: php artisan necromancer:scan --diff --fail-on-drift

- name: Fail on AI-readability errors
  run: php artisan necromancer:audit --fail-on=error

- name: Enforce minimum AI readability score
  run: php artisan necromancer:doctor --min-score=80

MCP Tools

When laravel/mcp is installed, Necromancer exposes the manifest as read-only tools via a laravel-necromancer MCP server handle — auto-configured in .mcp.json on first run.

query_routes

List routes, optionally filtered by method or name/URI pattern.

query_models

List Eloquent models, optionally filtered by class name.

query_artifacts

List artifacts of any type, optionally filtered by JSON substring. Use when you know the artifact type.

search_artifacts

Full-text search across all artifact types. Use when you need to search across types.

{
  "mcpServers": {
    "laravel-necromancer": {
      "command": "php",
      "args": ["artisan", "mcp:start", "necromancer"]
    }
  }
}

PHP Attribute Support

Necromancer reads PHP 8 attributes as primary data sources alongside class properties. Codebases using the attribute-based API introduced in Laravel 11+ are fully supported.

Subsystem Attributes Manifest fields
Routing #[Authorize] authorization
Eloquent #[ObservedBy] #[UsePolicy] #[ScopedBy] #[UseFactory] #[UseEloquentBuilder] #[Scope] observers, policy, global_scopes, factory, custom_builder, scopes
Queue #[Queue] #[Connection] #[Tries] #[Timeout] #[Backoff] #[MaxExceptions] all job queue config fields
Console #[Aliases] aliases
FormRequest #[StopOnFirstFailure] #[ErrorBag] stop_on_first_failure, error_bag
Universal annotations #[Necromancer] annotations on every class-backed artifact, controllers (class default + action refinement), and middleware classes
Exact-ID mappings config('necromancer.annotations') annotations on closures, test files, gates, scheduled tasks, and registration-specific middleware — keyed by exact canonical Artifact ID, no wildcards

Route Metadata

On Laravel 13.17+, Necromancer reads route metadata set via the framework's native Route::metadata() API, under a reserved necromancer namespace — a compact, declared-by-the-developer semantic signal that takes priority over anything Necromancer infers from naming. Entirely opt-in: apps that don't set route metadata, or that run an older Laravel version, are unaffected.

The withNecromancer() route macro declares it with named arguments — every field optional, and dropped when left unset:

Route::post('/billing/cancel', [SubscriptionController::class, 'cancel'])
    ->withNecromancer(
        domain: 'billing',
        flow: 'subscription-cancellation',
        capability: 'subscription.cancel',
        summary: 'Cancels an active subscription.',
        risk: 'high',
        externalServices: ['stripe'],
        adrs: ['docs/adr/004-subscription-cancellation.md'],
    );

The macro is registered on every routing surface, so a group — or every route a resource registers — can be tagged in one place. Routes inherit their group's fields, and a field declared on the route itself wins for that field:

Route::withNecromancer(domain: 'billing')->prefix('billing')->group(/* ... */);
Route::prefix('billing')->withNecromancer(domain: 'billing')->group(/* ... */);

Route::resource('posts', PostController::class)->withNecromancer(domain: 'blog');
Route::singleton('profile', ProfileController::class)->withNecromancer(domain: 'account');

It is a shorthand, never a parallel metadata system — the fields go straight into native route metadata under the configured namespace. The raw array form stays fully supported, and is what to use on Laravel < 13.17, where the macro throws because the framework has no route metadata to write to:

Route::post('/billing/cancel', [SubscriptionController::class, 'cancel'])
    ->metadata([
        'necromancer' => [
            'domain' => 'billing',
            'risk' => 'high',
            'external_services' => ['stripe'],
        ],
    ]);
necromancer:doctor
Artifact Annotation Coverage dimension: tagged with domain, high-risk artifacts with an ADR, external-service artifacts with tests, flow-consistent domain/risk — across every artifact family, not just routes.
necromancer:audit
Six checks: high-risk artifacts without an ADR, external-service artifacts without tests, narrative/overlong summaries, flows that disagree on domain or risk, non-canonical/near-duplicate identifiers, and missing local ADR files.
necromancer:generate & :ask
Conditional Domain/Risk/External Services/ADR table columns for routes, a compact Architectural Context column for every other artifact family, and a relevance boost for declared annotations when ranking evidence for a question.
necromancer:diff
A deterministic "Flagged Artifacts" section for any added/changed artifact of any family tagged high/critical risk or declaring external services — no AI required.

Privacy & Exclusions

  • Never reads .env or raw configuration values
  • Never collects or stores application secrets
  • Horizon, Telescope, and Debugbar routes excluded by default
  • Livewire's unnamed asset routes and Inertia's local DevTools routes excluded by default too
  • Exclusions apply to every downstream command (map, audit, generate) — excluded artifacts never appear in results
// config/necromancer.php
'exclude' => [
    'routes' => ['horizon.*', 'telescope.*', 'debugbar.*'],
    'models' => [],
    'tests'  => [],
],

Upgrading to 2.0

2.0 removes the 1.x-only compatibility surfaces that eased the transition to universal Artifact Annotations and Knowledge Bundles. Declaring annotations is unchanged — #[Necromancer], withNecromancer(), and exact-ID config mappings all work as before. What changes is what 2.0 stops reading, emitting, and accepting. Full details and a diff example are in the README migration guide.

Manifests older than schema v1 are rejected, not upgraded
Every command that reads necromancer.json now treats a pre-1.5 manifest exactly like a missing one. Run php artisan necromancer:scan once after upgrading.
route_metadata.necromancer no longer appears in the manifest
Resolved annotations for every artifact family, routes included, live in the universal annotations key. route_metadata.raw — native Route::getMetadata() output — is unaffected.
--only=route-metadata-coverage no longer matches
Use the canonical --only=artifact-annotation-coverage dimension key on necromancer:doctor instead.
Two scan diagnostic codes were renamed
AN_LEGACY_VALUE/AN_LEGACY_RISK are now AN_SCHEMA_INCOMPATIBLE_VALUE/AN_SCHEMA_INCOMPATIBLE_RISK. The check is unchanged, only the name.
The singular adr parameter was removed
withNecromancer() and RouteMetadataFactory::forMetadata() only accept the plural adrs array now — swap adr: '...' for adrs: ['...'].