Skip to content

v0.3.0

Latest

Choose a tag to compare

@github-actions github-actions released this 29 May 13:58
e3572b9

Release-as-a-whole: MAJOR — collapses three rule-level contractual widenings into a single Major bump per ADR-0021 §Versioning. Each rule's pre-cascade audit returned 0 violators across all 5 consumer territories (kendo, entreezuil, emmie, ublgenie, brick-inventory-orchestrator), so the Major represents the contract change, not empirical violation count. Consumers upgrading from ^0.2 to ^0.3 accept the broader rule contracts whether or not their existing code trips them. Phase A pin sweep (^0.1.x^0.2) closed pre-release — all four laggard consumers (kendo, entreezuil, emmie, BIO) bumped between 2026-05-06 and 2026-05-08 via independent dispatches; verified by 4-territory Medic wave 2026-05-13 (all no-op, all composer phpstan clean against EnforceAuditSnapshotOnRetryRule). Phase B pin sweep (^0.2^0.3) follows post-tag as a separate war-room dispatch.

Added

  • EnforceResourceDataValidatorOptInRule — flags classes extending App\Http\Resources\ResourceData that declare a non-empty EAGER_LOAD_COUNT or EAGER_LOAD_SUM constant but do not call validateRelationsLoaded() anywhere in their method bodies. Without the call, missing eager-load aggregates fail open as 0 / null instead of throwing — silently re-introducing the silent-zero bug closed by kendo PR #1079 (KD-0494). Doctrine: ADR-0009 §EAGER_LOAD validator opt-in. Identifier: enforceResourceDataValidatorOptIn.missingValidatorCall. Promoted from kendo PR #1084's Pest arch test (Armorer, merged 2026-05-07 at db20ea9cf) — the third instance of the "arch test detects misuse but not omission" enforcement shape, dispositioned for Phase-2 promotion under war-room enforcement queue #55 by the Commander on 2026-05-07. Inheritance is matched via PHPStan reflection (FQCN ancestor traversal) — short-name collisions in unrelated namespaces do NOT match. The base FQCN is parameterizable via the resourceDataBaseClass PHPStan parameter (default: App\Http\Resources\ResourceData); territories whose ResourceData lives elsewhere can override per consumer phpstan.neon. Compliant call shapes: self::validateRelationsLoaded($model), static::validateRelationsLoaded($model), $this->validateRelationsLoaded($model) (instance form accepted for liberal compatibility with the source-of-truth Pest matcher, even though the base method is protected static). Empty-array constants (EAGER_LOAD_COUNT = []) do NOT fire — they are no-ops. Versioning: Minor-at-rule-level, collapses into the bundled v0.3.0 Major per ADR-0021 §Versioning. Cross-territory cascade audit (2026-05-08): 0 violators across emmie, kendo, entreezuil, ublgenie, brick-inventory-orchestrator — campaign report at campaigns/phpstan-warroom-rules/2026-05-08-pre-cascade-audit-resource-data-validator-opt-in.md. Side observations: emmie uses App\Http\Resources\DTOResource (non-default base, rule non-applicable absent resourceDataBaseClass override); entreezuil has not adopted the ResourceData pattern (still on JsonResource despite ADR-0009 in CLAUDE.md, latent adoption debt); BIO operates dual-base (ResourceData + ComputedResourceData<TSource> per BIO sovereign ADR-0010). Sister extractions for the FormRequest toDto() omission shape (queue #55 instance #2) and the routes ->can() middleware omission shape (queue #55 instance #1) are deferred to separate dispatches.

Security

  • Pinned all GitHub Actions references in ci.yml and release.yml to commit SHAs with # v<MAJOR> comments for Dependabot tag-tracking. Closes Sapper M1 Finding #3 (supply-chain forward-compatibility before potential Packagist OIDC migration). Versioning: none (CI workflow change, no consumer-facing surface).

Changed

  • Doctrine: corrected publish-channel framing in CLAUDE.md (L11 and the Release process section) and the release.yml header comment. Public packagist.org has no OIDC Trusted Publishing option today — OIDC is a Private Packagist–only feature (packagist/artifact-publish-github-action, GA February 2026). The package's actual publish channel is the standard https://packagist.org/api/github push-event webhook (dev-* aliases on branch push, versioned releases on tag push via release.yml). Migration to Private Packagist would change ally-side Composer consumption (private repo URL + token in composer.json) and is a commercial decision; tracking continues on Issue #11. Closes Sapper M1 Finding #2 (doctrine drift on publish channel) and resolves Issue #11 audit. Versioning: none (doctrine alignment, no consumer-visible behaviour).
  • Governance: added .github/CODEOWNERS routing all changes to @script-development/phpstan-warroom-admins. A separate rule-authors team is intentionally not split out today — the admins team and the rule-design reviewer set are identical at the current shop size; revisit if the contributor base grows or rule-design review becomes a distinct concern from operational repo administration. Pairs with branch-protection update enabling require_code_owner_reviews=true. Closes Sapper M1 Finding #5 (no CODEOWNERS file). Versioning: none (governance change, no consumer-visible surface).
  • LogRule (BREAKING): extended to cover the static-call shapes Model::destroy(...) and Model::forceDestroy(...) on Log-named classes. getNodeType() broadened from MethodCall::class to CallLike::class and processNode branches on MethodCall vs StaticCall. Both shapes emit the same logRule.logModification identifier so consumer phpstan.neon ignoreErrors entries cover the whole rule with one identifier (the previous rule's compliance teeth depended on delete/forceDelete instance shapes; on a non-soft-delete log model Model::destroy([1]) purges and Model::forceDestroy([1]) always purges — both slipped through). Versioning: Major-at-rule-level per ADR-0021 §Versioning; ships as v0.3.0. Cross-territory cascade audit (2026-05-13): 0 violators across kendo, entreezuil, emmie, ublgenie, brick-inventory-orchestrator — campaign report at campaigns/phpstan-warroom-rules/2026-05-13-pre-cascade-audit-log-rule-static-call.md. The static-call shape proved cleaner than v0.2.0's instance-call expansion (which surfaced 1 operational-log false positive in ublgenie); no consumer-side ignoreErrors migrations required. Resolves issue #4.
  • LogBuilderTruncateRule (BREAKING): new sibling rule to LogRule, sharing the logRule.logModification identifier so consumer phpstan.neon ignoreErrors entries cover the whole append-only doctrine with one entry. Flags Builder->truncate() calls where the fluent chain's most recent table() invocation targets a Log-named table (string-literal first argument containing 'log' / 'logs', case-insensitive substring match). Covers DB::table('logs')->truncate(), DB::connection('central')->table('logs')->truncate(), and $this->db->table('logs')->truncate() (instance-injected ConnectionInterface). Receiver detection is type-based (Illuminate\Database\Query\Builder OR Illuminate\Database\Eloquent\Builder subtype via ObjectType::isSuperTypeOf()) — mirrors EnforceAuditSnapshotOnRetryRule's ConnectionInterface pattern. The Eloquent\Builder receiver branch covers the rare-but-coherent $eloquentBuilder->table('logs')->truncate() shape; Eloquent chains that set the table via the Model's $table property (AuditLog::query()->truncate()) or via Eloquent's from() vocabulary (AuditLog::query()->from('logs')->truncate()) are an acceptable miss in the same family as variable table names — the table name does not appear as a table()-call string-literal in the chain. Doctrine: ADR-0001 §Append-only — truncate() is the bluntest delete and bypasses Eloquent events, observers, and audit triggers entirely. Out of scope: variable table names ($t = 'logs'; DB::table($t)->truncate()), Eloquent from('logs') chains, and Model-property-driven tables — all would need value-flow or model-graph inspection; acceptable misses, rely on reviewer + consumer-side phpstan.neon ignoreErrors. Versioning: Major-at-rule-level per ADR-0021 §Versioning; collapses into the bundled v0.3.0 Major alongside the LogRule static-call expansion. Cross-territory cascade audit (2026-05-13): 0 violators across kendo, entreezuil, emmie, ublgenie, brick-inventory-orchestrator — campaign report at campaigns/phpstan-warroom-rules/2026-05-13-pre-cascade-audit-log-builder-truncate.md. The truncate() shape proved genuinely uncommon across the fleet (~6 calls total across 2,500+ scanned PHP files; none against log-named tables); no consumer-side ignoreErrors migrations required. Resolves issue #8.
  • CI: added PHP 8.5 to the ci.yml and release.yml test matrices alongside 8.4 (['8.4']['8.4', '8.5']). PHP 8.5.0 was released 2025-11-20; the war-room dev environment already runs 8.5.5 locally, so PRs were getting ad-hoc 8.5 coverage during pre-push but no CI signal. Adding (rather than replacing) keeps 8.4 — the composer.json ^8.4 contractual minimum — covered. shivammathur/setup-php@v2 supports 8.5 since GA. Resolves issue #5.
  • CI: added line-coverage measurement and a threshold gate. ci.yml switches coverage: nonecoverage: pcov on both 8.4 and 8.5 matrix legs (PCOV is line-coverage-only and faster than Xdebug — debugger features aren't needed). New composer scripts: test:coverage (runs PHPUnit with --coverage-clover=build/logs/clover.xml --coverage-text) and coverage:check (runs bin/coverage-check.php, a standalone clover parser — no extra runtime dependency added to a static-analysis package for a single CI gate). Two new CI steps replace the Tests step: Tests with coverage and Coverage threshold gate. Clover XML is uploaded as a per-leg artifact (clover-php-${{ matrix.php }}, 14-day retention) so reviewers can inspect uncovered lines without spelunking through workflow logs. Initial threshold: 83% — the measured baseline is 83.92% (240/286 lines across src/), set 0.92 percentage points lower to absorb trivial fluctuation on equivalent-but-renamed code. Class coverage (0/6) and method coverage (39%) are intentionally unmeasured by the gate v1; per the issue's deliberation, line coverage is the right v1 signal and branch/method coverage is a follow-up after the line gate is bedded in. The 16-percentage-point gap to 100% audits as defensive guard clauses on unexpected node shapes (the kind of branch the issue itself flagged as "genuinely hard to fixture" — LogRule's static-call branch falls back when $node->class is Expr rather than Name); a follow-up issue will audit and ratchet the threshold upward to 90%+. Versioning: none (pure CI/test-infra, no consumer-visible behaviour). Resolves issue #9.
  • CI: added Infection mutation testing gate, layered on top of the line-coverage gate. New infection/infection ^0.32.7 dev dependency, infection.json5 config (@default mutator profile, src/ source scope, fixtures stay out via PHPUnit's existing <source> block, --testsuite=Rules), and two new composer scripts: mutation (local, --threads=max --show-mutations for inspecting escaped mutants) and mutation:ci (CI: --threads=4 --no-progress --logger-github --min-msi=75 --min-covered-msi=75 — GitHub annotations on escaped mutants surface inline in PR diffs). Two new CI steps after the coverage gate: Mutation testing and Upload mutation report (per-leg infection-php-${{ matrix.php }} artifact, 14-day retention). composer config allow-plugins.infection/extension-installer true was set to permit the framework-adapter installer plugin. Initial thresholds: 75% MSI and 75% Covered Code MSI — measured baseline is 78.5% MSI (241 killed / 307 mutants, 100% Mutation Code Coverage), set 3.5 percentage points lower to absorb mutator-shape fluctuation on equivalent code. Same shape as the line-coverage gate: lock in current state, audit gaps, ratchet upward. The 22% surviving-mutant population audits as a mix of (a) genuinely-equivalent mutants the issue itself anticipated — mb_striposstripos on PSR-4 ASCII-only class names in LogRule, defensive guard inversions (LogicalNot/IfNegation) on early returns that filter the same nodes by either condition — and (b) genuinely-uncovered branch logic that warrants new fixtures. A follow-up issue will audit each survivor, kill where realistic, @infection-ignore-for-mutator-annotate where equivalent, and ratchet thresholds to the issue's target of 80% MSI / 90% Covered Code MSI. Versioning: none (pure CI/test-infra, no consumer-visible behaviour; infection is require-dev only). Resolves issue #10.