Quickstart
Install the plugin, add one Pest test, and make technical-debt growth part of the build instead of a thing someone remembers during code review.
composer require --dev odinns/volven-pest-plugin
it('keeps debt inside the budget', function (): void {
debt()->budget()
->phpstanIgnores(max: 12)
->todoComments(max: 20)
->mixedTypes(max: 12)
->largeClasses(max: 5)
->mustNotGrow()
->requireReasons()
->assert();
});
Existing counts must stay inside the budgets. If mustNotGrow() is enabled, anything above the committed baseline fails with concrete examples.
Supported Signals
PHPStan suppressions
Counts @phpstan-ignore, @phpstan-ignore-line, @phpstan-ignore-next-line, and @phpstan-ignore-*.
TODO and FIXME
Tracks comments that often become stale local knowledge unless they are owned or removed.
mixed types
Finds unknown shapes crossing PHP boundaries without a visible contract or validation point.
Large classes
Flags class-like PHP files large enough to trap unrelated reasons to change.
Budget Examples
Strict new package
debt()->budget()
->phpstanIgnores(max: 0)
->todoComments(max: 0)
->mixedTypes(max: 0)
->largeClasses(max: 0)
->assert();
Legacy ratchet
debt()->budget()
->phpstanIgnores(max: 25)
->todoComments(max: 40)
->mixedTypes(max: 30)
->largeClasses(max: 8)
->mustNotGrow()
->assert();
Suppression discipline
debt()->budget()
->phpstanIgnores(max: 10)
->requireReasons()
->assert();
Baseline And Ratchet
Generate .volven/debt-baseline.json from PHP, commit it, then use mustNotGrow() in the budget test. Old debt stays visible. New debt fails.
debt()->budget()
->phpstanIgnores(max: 999)
->todoComments(max: 999)
->mixedTypes(max: 999)
->largeClasses(max: 999)
->writeBaseline();
The MVP baseline flow is plain PHP, not a rich CLI. That is deliberate for now. The feature is useful before it needs a command palace.
Reason Checks
requireReasons() fails PHPStan suppressions with fewer than three reason words. It does not apply to TODOs, mixed, or large classes.
/** @phpstan-ignore-next-line */ // fails
/** @phpstan-ignore-line legacy */ // fails
/** @phpstan-ignore-next-line vendor generic loses model type */ // passes
Failure Output
Failures are written like Vølven review findings: priority, type, future cost, evidence, smallest useful fix, and the case where ignoring it is valid.
P1: PHPStan suppressions grew past the baseline
type: code
language: php
future cost: old debt can be captured, but new debt should fail CI before it becomes normal
evidence: baseline 12, current 14
smallest useful fix: remove the new findings or regenerate the baseline after review
ignore if: the increase is deliberate strategic debt with an owner and repayment path
- src/Example.php:14 /** @phpstan-ignore-next-line vendor generic loses model type */
PHPStan Annotation Rules
Suppressions count. Type-shaping annotations do not. @phpstan-type, @phpstan-import-type, @phpstan-template, @phpstan-var, @phpstan-param, and @phpstan-return describe contracts, so Vølven leaves them alone.
CI Example
name: tests
on:
push:
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
tools: composer
- run: composer install --no-interaction --prefer-dist
- run: composer test
Tool Boundaries
Vølven is not Pint, PHPStan, Psalm, Rector, Pest arch(), or a codemod. Those tools already have jobs. Vølven tracks debt pressure: whether debt grows, whether suppressions have reasons, and whether countable maintenance cost is drifting the wrong way.