Releases: script-development/phpstan-warroom-rules
v0.3.0
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 extendingApp\Http\Resources\ResourceDatathat declare a non-emptyEAGER_LOAD_COUNTorEAGER_LOAD_SUMconstant but do not callvalidateRelationsLoaded()anywhere in their method bodies. Without the call, missing eager-load aggregates fail open as0/nullinstead 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 atdb20ea9cf) — 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 theresourceDataBaseClassPHPStan parameter (default:App\Http\Resources\ResourceData); territories whoseResourceDatalives elsewhere can override per consumerphpstan.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 isprotected 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 atcampaigns/phpstan-warroom-rules/2026-05-08-pre-cascade-audit-resource-data-validator-opt-in.md. Side observations: emmie usesApp\Http\Resources\DTOResource(non-default base, rule non-applicable absentresourceDataBaseClassoverride); entreezuil has not adopted theResourceDatapattern (still onJsonResourcedespite ADR-0009 in CLAUDE.md, latent adoption debt); BIO operates dual-base (ResourceData+ComputedResourceData<TSource>per BIO sovereign ADR-0010). Sister extractions for the FormRequesttoDto()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.ymlandrelease.ymlto 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 therelease.ymlheader 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 standardhttps://packagist.org/api/githubpush-event webhook (dev-*aliases on branch push, versioned releases on tag push viarelease.yml). Migration to Private Packagist would change ally-side Composer consumption (private repo URL + token incomposer.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/CODEOWNERSrouting 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 enablingrequire_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 shapesModel::destroy(...)andModel::forceDestroy(...)on Log-named classes.getNodeType()broadened fromMethodCall::classtoCallLike::classandprocessNodebranches onMethodCallvsStaticCall. Both shapes emit the samelogRule.logModificationidentifier so consumerphpstan.neonignoreErrorsentries cover the whole rule with one identifier (the previous rule's compliance teeth depended ondelete/forceDeleteinstance shapes; on a non-soft-delete log modelModel::destroy([1])purges andModel::forceDestroy([1])always purges — both slipped through). Versioning: Major-at-rule-level per ADR-0021 §Versioning; ships asv0.3.0. Cross-territory cascade audit (2026-05-13): 0 violators across kendo, entreezuil, emmie, ublgenie, brick-inventory-orchestrator — campaign report atcampaigns/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-sideignoreErrorsmigrations required. Resolves issue #4.LogBuilderTruncateRule(BREAKING): new sibling rule toLogRule, sharing thelogRule.logModificationidentifier so consumerphpstan.neonignoreErrorsentries cover the whole append-only doctrine with one entry. FlagsBuilder->truncate()calls where the fluent chain's most recenttable()invocation targets a Log-named table (string-literal first argument containing'log'/'logs', case-insensitive substring match). CoversDB::table('logs')->truncate(),DB::connection('central')->table('logs')->truncate(), and$this->db->table('logs')->truncate()(instance-injectedConnectionInterface). Receiver detection is type-based (Illuminate\Database\Query\BuilderORIlluminate\Database\Eloquent\Buildersubtype viaObjectType::isSuperTypeOf()) — mirrorsEnforceAuditSnapshotOnRetryRule'sConnectionInterfacepattern. 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$tableproperty (AuditLog::query()->truncate()) or via Eloquent'sfrom()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 atable()-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()), Eloquentfrom('logs')chains, and Model-property-driven tables — all would need value-flow or model-graph inspection; acceptable misses, rely on reviewer + consumer-sidephpstan.neonignoreErrors. Versioning: Major-at-rule-level per ADR-0021 §Versioning; collapses into the bundled v0.3.0 Major alongside theLogRulestatic-call expansion. Cross-territory cascade audit (2026-05-13): 0 violators across kendo, entreezuil, emmie, ublgenie, brick-inventory-orchestrator — campaign report atcampaigns/phpstan-warroom-rules/2026-05-13-pre-cascade-audit-log-builder-truncate.md. Thetruncate()shape proved genuinely uncommon across the fleet (~6 calls total across 2,500+ scanned PHP files; none against log-named tables); no consumer-sideignoreErrorsmigrations required. Resolves issue #8.- CI: added PHP 8.5 to the
ci.ymlandrelease.ymltest 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 — thecomposer.json^8.4contractual minimum — covered.shivammathur/setup-php@v2supports 8.5 since GA. Resolves issue #5. - CI: added line-coverage measurement and a threshold gate.
ci.ymlswitchescoverage: none→coverage: pcovon 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) andcoverage:check(runsbin/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 theTestsstep: 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...
v0.2.0
Added
EnforceAuditSnapshotOnRetryRule— flagsApp\Actions\*classes whose constructor injects an entity audit logger and whose$connection->transaction(...)calls do not begin with an in-memory state reset ($model->refresh(), fresh fetch via->newQuery()->findOrFail(...)/->fresh(), or fresh instantiation vianew .../->newInstance()). Doctrine: ADR-0001 §Snapshot-on-Retry Safety. Identifier:enforceAuditSnapshotOnRetry.firstStatementMustResetState. Promoted from cross-territory Pest arch tests (emmie PR #187, entreezuil PR #139, ublgenie PR #166, kendo PR #1029). Receiver detection is type-based (Illuminate\Database\ConnectionInterfacesubtype) — replaces territory-specific property-name matching ($this->dbvs$this->connection). Escape hatch:// @audit-snapshot-retry-safety: <rationale>marker preceding the transaction call.
Changed
- PHP constraint: bumped
composer.jsonphpfrom^8.3to^8.4. The package's Pint config (mb_str_functions: true) normalizesltrim/trimcalls tomb_ltrim/mb_trim, which are PHP 8.4+ functions. The new rule introduced the firstmb_ltrim/mb_trimcallsites; aligning the constraint with the formatter's actual output. All consuming territories already run PHP 8.4 — no real-world impact. LogRule(BREAKING): extendedFORBIDDEN_METHODSfrom['delete', 'update']to['delete', 'forceDelete', 'forceDeleteQuietly', 'update']. On aSoftDeletes-bearing model->delete()is a no-op against the underlying row and->forceDelete()is the only call that actually purges; the rule's compliance teeth previously rested on the migration-time convention that audit-log models never adoptSoftDeletes. Static-call shapes (Model::destroy(),Model::forceDestroy(),DB::table('logs')->truncate()) remain out of scope —getNodeType()returnsMethodCall::class, and static-call coverage is tracked as issue #4. Origin: issue #1, surfaced by ally review on Back-to-code/ublgenie-app#163. Pre-cascade audit across emmie, kendo, entreezuil, ublgenie surfaced one new violation:ublgenie/app/Actions/DeleteBranch.php:56(InvoiceLog::query()->whereIn(...)->forceDelete()) — operational/processing log, not an audit log; migrates to consumer-sidephpstan.neonignoreErrorsper package convention. Versioning: per ADR-0021 §Versioning, this is a Major bump (new errors in code that previously passed); within 0.x this ships asv0.2.0.
v0.1.1
Changed
- Compatibility: widened
illuminate/*constraints from^11.0 || ^12.0to^11.0 || ^12.0 || ^13.0across the five required packages (database,contracts,cache,filesystem,log,mail). Surfaced during ADR-0021 cascade onto entreezuil (Laravel 13). No behavioral change — the package's PHPStan rules reason about class names that are stable across Laravel 11/12/13. Forward-looking: removes the constraint as a future cascade blocker.
v0.1.0
Added
EnforceActionTransactionsRule— flagsApp\Actions\*classes whoseexecute()performs ≥2 writes without->transaction(). Doctrine: ADR-0011.ForbidDatabaseManagerInActionsRule— flagsApp\Actions\*constructors that injectIlluminate\Database\DatabaseManager. Doctrine: ADR-0021 §Why ConnectionInterface.ForbidAbortHelperRule— flagsabort(),abort_if(),abort_unless()function calls. Doctrine: war-room §Explicit over implicit.LogRule— flagsupdate()/delete()calls on classes whose name contains"Log"or"logs". Doctrine: ADR-0001 §Append-only.ConnectionTransactionReturnTypeExtension— resolves$connection->transaction(fn () => $foo)to the closure's return type instead ofmixed.
Notes
- Rules ported from emmie's
backend/app/PHPStan/. The territory-specificTerminologyexception inLogRulewas dropped — per-territory false positives are now suppressed via consumerphpstan.neon ignoreErrors. - Test coverage is smoke-level for v0.1.0; full matrix for
EnforceActionTransactionsRule(non-DB property exclusions, nested closure transaction detection, full 18-method write list) lands in a follow-up. - Action namespace assumption: rules that scope to Actions match
App\Actions\*. Lift to a parameter when a non-conforming territory onboards.