Check snapshots
A check target runs a development tool that reports on the shape of your application — routes, scheduled jobs, database schema, test results, static analysis, code style — and snapshots the normalized result. Drift then shows up as a semantic diff naming exactly what changed:
~ $.data.GET|HEAD /users: {"middleware":["web","auth"],…} → undefined
~ $.data.users.nickname: undefined → "varchar"
~ $.data.Tests\Unit\BillingTest.refunds are idempotent: {"status":"passed"} → {"status":"failed",…}Every parser normalizes its tool's output into a keyed record — entries keyed by identity (route, table, test name) rather than array position — so a removed route diffs as one named entry, not an index shift. Run-varying noise (timings, next-run dates, absolute paths, line numbers) is masked at capture time, and tools that emit different output shapes in different environments (several do, depending on who runs them) normalize to one snapshot, so a baseline captured in CI compares clean against a capture from anywhere else.
Configure a target
{ "kind": "check", "name": "routes", "tool": "laravel-routes" }Options:
tool(required) — which parser to use (see the table below).command— override the tool's default command.output— read an existing report file instead of capturing stdout. Set bothcommandandoutputto run a command and then read the file it writes (test runners work this way by default).cwd— directory to run in, relative to the project root.timeoutMs— command timeout (default: the lifecycle wait timeout).
Tools whose non-zero exit means "findings exist" (test runners, static analysis) don't fail the capture — the findings are the snapshot. The capture fails only when the tool produces no usable output.
Tools
tool | Default command | Snapshots |
|---|---|---|
laravel-routes | php artisan route:list --json | route → name, action, middleware |
laravel-about | php artisan about --json | environment, cache, driver sections |
laravel-schedule | php artisan schedule:list --json | cron entry → command, timezone, mutex |
laravel-schema | php artisan schema:dump (reads the dump file) | table → columns, indexes, constraints; migration names |
pest / phpunit | vendor/bin/pest --log-junit … | suite → test → status (+ failure message) |
phpstan | vendor/bin/phpstan analyse --error-format=json --no-progress | file → message → identifier, count |
pint | vendor/bin/pint --test --format=json -v | file → the style rules it violates |
Per-tool notes:
laravel-routes— keyed"GET|HEAD /users". Middleware changes and removed routes are the headline risky changes. Source file is kept; its line number is dropped (it churns on unrelated edits).laravel-about— version fields are kept on purpose (that drift is signal); absolute project paths are replaced with<project>so snapshots are machine-portable. Theviewscache flag is dropped — running the test suite compiles Blade views as a side effect, which would flake this target.laravel-schedule— needs Laravel ≥ 12 for--json.next_due_datefields are dropped (they vary with the wall clock); a cron expression change reads as remove + add, deliberately loud.laravel-schema— the default readsdatabase/schema/sqlite-schema.sql; on another connection setoutputto its dump path. Migration names are snapshotted, ids/batches are not.pest/phpunit— one JUnit normalizer covers both runners. Timing and assertion counts are dropped; failure messages keep the assertion text with project paths relativized.phpstan— Larastan included (same binary and output). Keyed by message per file with a count — the same identity PHPStan's own baseline uses — so line-number churn never dirties a snapshot.pint— snapshots each drifting file's fixer names (the style rules that would rewrite it), not diff text, so the baseline is identical whatever environment captured it.
Zero-config for Laravel
dungbeetle init detects a Laravel app via composer.json and scaffolds check targets automatically: routes, about, schedule, and schema always; tests (Pest preferred, PHPUnit fallback), style (Pint), and static-analysis (PHPStan/Larastan) when the tool is installed. dungbeetle doctor then verifies each wrapped binary exists — a missing vendor/bin/pint fails with "run composer install?" before any capture runs.
See the Laravel example for the full walkthrough.
Ingesting reports from CI
Every tool also works in ingest mode — point output at a report your CI already produces and no command runs:
{ "kind": "check", "name": "tests", "tool": "phpunit", "output": "reports/junit.xml" }