Installs into .claude/skills of the current project.
Are you the author of Dhpk Polyfill Version Matrix Audit?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/hmj1026-dhpk-polyfill-version-matrix-audit-dhpk)
---
name: dhpk-polyfill-version-matrix-audit
description: 'Audit multi-major-version polyfill code for branch coverage and CI-matrix gaps. Use when: a library spans multiple dep majors (Monolog 2/3, Laravel 6/7+, PHPUnit 8-11) using version_compare / class_exists / method_exists guards, or after a "fix: polyfill ..." commit. Not for: single-version libraries, application code, version-detection-only helpers. Output: branch + matrix-gap audit report.'
metadata:
dhpk-invocation-class: implicit-eligible
---
# Polyfill version matrix audit
For libraries whose value proposition is "works on framework X versions A–Z."
The risk is silent: a polyfill branch fires only when a specific
PHP × framework × dep cell of the CI matrix runs that exact code path,
and most matrix cells share most test bodies. A regression in the rarely
visited branch can ship for months before someone files an issue.
> Mental model: every `if (version_compare(...))` / `if (class_exists(...))` is
> a fork in the execution tree. The CI matrix is the **set of starting
> points**. A branch with no starting point that reaches it is dead code
> — until a user enters from outside (production) and trips it.
---
## When to Use
- Editing any file under `src/` that contains a runtime version guard
(`version_compare`, `PHP_VERSION_ID`, `class_exists`, `method_exists`,
`interface_exists`, `is_a`, `Composer\InstalledVersions::*`)
- Reviewing a PR that touches a polyfill class
- Before tagging a minor/patch release
- After a "fix: polyfill ..." commit, audit the symmetric branch
(it likely needs the same fix or the same test)
## When NOT to Use
- Single-version libraries (composer `require.<dep>` pins one major)
- Application code (not a published package)
- Pure version-detection helpers with no behavioural branching
---
## Inputs
Given one of:
- a file path (e.g. `src/Logging/GoogleChatHandler.php`)
- a symbol (`cx definition --name GoogleChatHandler`)
- the keyword "all" — scan `src/` for any file with a version guard
Read:
- `composer.json` → `require` constraint for the polyfilled dep (the **declared matrix**)
- `.github/workflows/*.yml` → `strategy.matrix` (the **executed matrix**)
- `phpunit.xml` → `testsuites` (which cells exercise which dirs)
- `git log --follow <file>` → recent asymmetric edits
---
## Procedure
### Step 1 — Enumerate branches
For each version guard in the target file, classify it:
| Guard | Pattern | Branch meaning |
|---|---|---|
| `class_exists(\Foo\Bar::class)` | new-API present | uses upstream class directly |
| `method_exists($x, 'foo')` | new method added | calls new API |
| `version_compare($ver, '7.0', '>=')` | new major | uses new-major idiom |
| `is_a($x, NewInterface::class)` | interface widened | adapts to new contract |
| `Composer\InstalledVersions::satisfies(...)` | constraint match | branches by dep version |
Output an enumeration table:
```
Guard L42 | class_exists(\Monolog\Formatter\NormalizerFormatter::class) | Monolog 3
Fallback | (else) | Monolog 2
Guard L88 | version_compare(PHP_VERSION, '7.4', '>=') | PHP 7.4+
Fallback | (else) | PHP 7.3
```
### Step 2 — Map branches to matrix cells
For each branch, list which CI matrix cells **could** enter it. A cell can
enter branch B if and only if the cell's pinned dep versions satisfy the
guard. Use the declared composer constraint as the upper bound.
```
Branch "Monolog 3" ← cells where monolog/monolog resolves ^3.x
Branch "Monolog 2" ← cells where monolog/monolog resolves ^2.x
Branch "PHP 7.4+" ← cells with matrix.php ≥ 7.4
Branch "PHP 7.3" ← cells with matrix.php = 7.3
```
### Step 3 — Cross with test coverage
For each branch, find a test that demonstrably exercises it. Acceptable
evidence:
- An assertion specific to the branch's output (different message format,
different class name in output, etc.)
- A test method whose `@requires` annotation matches the branch
- A `setUp` that skips the test when the wrong dep version is installed
**Reject** as evidence: a test that calls into the file but asserts only
that no exception was thrown. That's a smoke test — both branches pass it
trivially.
### Step 4 — Output the gap table
```
Branch | Matrix cells that enter | Tests that prove it | Gap?
--------------------|-------------------------|---------------------|-----
Monolog 3 path | PHP 8.1/8.2 × any L | tests/MonologV3Test | ✓
Monolog 2 path | PHP 7.3/7.4/8.0 × any | (none) | ✗ ADD TEST
PHP 7.4+ path | matrix.php ≥ 7.4 | tests/Modern*Test | ✓
PHP 7.3 path | matrix.php = 7.3 | (none) | ✗ ADD TEST
```
### Step 5 — Asymmetric-edit detection
`git log --oneline --follow <file>` — for the last 10 commits, check
whether each one touched both branches or only one. If a commit modified
the new-major branch but not the fallback, flag it: the fallback is now
N commits behind in semantics, which usually means a bug.
> Common pattern in devkit's polyfill churn:
> `fix: polyfill laravel 6 class casts` patched the L6 fallback;
> the L7+ branch already worked. Symmetric audit would have asked
> "what does L7+ do that L6 didn't, and is the L6 branch now equivalent
> or a different shape entirely?"
---
## Output
A single audit report with three sections:
1. **Enumeration** — the table from Step 1
2. **Matrix gap** — the table from Step 4 with one row per branch, ✓ / ✗ in the Gap column
3. **Asymmetric edits** — bullet list of commits that touched one branch only, with a one-line "what to verify on the other branch" prompt each
If all branches are covered and no asymmetric edits exist, say so in one
line and exit — don't pad. The report's value is in the **gaps**, not in
the confirmations.
---
## Common traps
- **Stub fallback**: the else-branch returns `null` or throws "not supported on this version." That can be valid (`@deprecated since 2.0, use X`), but the audit must verify the stub matches the docs. A silent `return null;` that callers don't expect is a runtime NPE waiting to happen.
- **Polyfill drift**: the new-major branch evolves, the old-major branch is frozen. Over N releases, the two branches diverge in subtle ways (different default options, different exception types). The matrix audit shouldn't enforce identical behaviour, but should flag divergence so the doc can say so.
- **Hidden version coupling**: a branch guards on `class_exists(\Foo\Bar::class)` but the rest of the method body assumes another class from the same major version, without a second guard. If a transitive dep changes when `\Foo\Bar` exists vs doesn't, the assumption breaks. Audit each method body for **all** version-sensitive symbols, not just the guarded ones.
- **PHPUnit version skipping**: `@requires PHP 7.4` is honoured by PHPUnit 8+, but the marker syntax changed in 10. A test annotated for PHPUnit 9 syntax might silently always-run on PHPUnit 10 if the matrix has a cell pinning PHPUnit 10. The audit must read the actual PHPUnit version that runs the cell.
---
## Verification
- [ ] Every version guard in the target file appears in the Step 1 enumeration table
- [ ] Each branch is mapped to the matrix cells that can enter it (declared composer constraint as upper bound)
- [ ] Each branch has a test asserting branch-specific behaviour — smoke tests ("no exception thrown") rejected as evidence
- [ ] `git log --follow <file>` checked for asymmetric edits that touched one branch only
- [ ] Gap table marks every uncovered branch ✗ ADD TEST; a fully-covered run says so in one line, no padding
---
## Related skills
- `dhpk-composer-package-hygiene` — semver decisions when adding/removing a polyfill branch (raising the floor is a major bump)
- `dhpk-legacy-characterization-tests` — for the single-version case where the polyfill branch IS the legacy code