Pest plugin

Make debt growth fail in CI.

The plugin scans deterministic PHP debt signals, compares them to explicit budgets, and can ratchet against a committed baseline.

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();
});
What happens next:

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.