Skip to content

Commit f6e3757

Browse files
committed
feat: enforce ADR-0029 closure-scope discipline (queue #86)
Adds EnforceAuditTransactionScopeRule enforcing the success-side of ADR-0029 (Audit Row Durability Contract): non-transactional state mutations (StatefulGuard / Session / Cache / Bus / Queue / Mail / Notification / Broadcast / Storage facades + contracts, mutation methods only) MUST NOT happen inside transaction(...) closures in App\Actions\* classes. Identifier: enforceAuditTransactionScope.nonTransactionalMutationInClosure. Doctrine: ADR-0029 §Decision rule 3. Seed: ISMS-0003 PR #7 commit f1d357b (2026-05-28). Failure-side (sentinel-return; throws inside closure) is enforcement queue #85 — separate Pest arch tests, not bundled. Algorithm: namespace-gate App\Actions\*; locate execute()'s top-level transaction() calls; for each, walk the closure body recursively including nested closures and nested transactions; flag every MethodCall|StaticCall whose receiver-type + method match the blocklist. Instance-call detection matches the constructor-property's declared FQCN against blocklist keys; static-facade detection uses Scope::resolveName(). Reads (Auth::user(), Session::get(), Cache::get()) are deliberately permitted. CHANGELOG [Unreleased] grows; no version bump in this PR.
1 parent e3572b9 commit f6e3757

22 files changed

Lines changed: 1274 additions & 0 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- `EnforceAuditTransactionScopeRule` — enforces ADR-0029 (Audit Row Durability Contract) §Decision rule 3. Flags non-transactional state mutations (`StatefulGuard` / `Session` / `Cache` / `Bus` / `Queue` / `Mailer` / `Notification` / `Broadcaster` / `Filesystem` and their `Illuminate\Support\Facades\*` counterparts, mutation methods only) inside `transaction(...)` closures in `App\Actions\*` classes. Identifier: `enforceAuditTransactionScope.nonTransactionalMutationInClosure`. Doctrine: ADR-0029 §Decision rule 3. Seed: ISMS-0003 PR #7 commit `f1d357b` (2026-05-28) — three Auth Actions (`AuthenticateWorkerAction`, `VerifyTwoFactorChallengeAction`, `LogoutWorkerAction`) mutated `StatefulGuard` + `Session` state inside the transaction closure before the audit row write; an audit-write failure would have rolled back the audit row while leaving the session/guard mutation intact (A.8.15 / A.5.33 violation). Reads (`Auth::user()`, `Session::get()`, `Cache::get()`, etc.) are deliberately permitted — only mutations carry the rollback-vs-side-effect asymmetry. Instance-call detection matches the constructor-property's declared FQCN against the blocklist keys; static-call detection resolves the facade name via `Scope::resolveName()`. Nested `transaction(...)` calls inside an outer closure are walked transitively — a nested mutation is still inside the outermost transaction's rollback scope; top-level transaction discovery deduplicates so each violation reports exactly once. Out of scope: manual transaction management (`DB::beginTransaction()` / `commit()`); non-`App\Actions\*` namespaces; the failure-side discipline (sentinel-return; throw-inside-closure detection) which lives as per-territory Pest arch tests under enforcement queue #85. **Versioning: per ADR-0021 §Versioning, candidate Major bump (the rule surfaces new errors in already-clean code wherever a consumer territory has an `App\Actions\*` class mutating non-transactional state inside a `transaction(...)` closure). Pre-cascade audit required across ISMS, kendo, emmie, entreezuil, ublgenie, brick-inventory before tagging — ISMS-0003 PR #7 commit `f1d357b` already closed ISMS's known violators; other consumer territories may carry undetected violators.**
12+
913
## [0.3.0] — 2026-05-13
1014

1115
**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.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Composer package distributing war-room-doctrine PHPStan rules across `script-dev
2626
| `LogRule` | ADR-0001 §Append-only | `logRule.logModification` (covers instance `update`/`delete`/`forceDelete`/`forceDeleteQuietly`; static `Model::destroy()` / `Model::forceDestroy()` ship with v0.3.0 per `[Unreleased]`) |
2727
| `LogBuilderTruncateRule` | ADR-0001 §Append-only | `logRule.logModification` (shared with `LogRule`; covers `Builder->truncate()` on Log-named tables — ships with v0.3.0 per `[Unreleased]`) |
2828
| `EnforceAuditSnapshotOnRetryRule` | ADR-0001 §Snapshot-on-Retry Safety | `enforceAuditSnapshotOnRetry.firstStatementMustResetState` |
29+
| `EnforceAuditTransactionScopeRule` | ADR-0029 | `enforceAuditTransactionScope.nonTransactionalMutationInClosure` |
2930
| `EnforceResourceDataValidatorOptInRule` | ADR-0009 §EAGER_LOAD validator opt-in | `enforceResourceDataValidatorOptIn.missingValidatorCall` |
3031
| `ConnectionTransactionReturnTypeExtension` | (type extension, no rule) ||
3132

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ includes:
4343
| `LogRule` | `logRule.logModification` | `update()` / `delete()` calls | If the receiver type's class name contains `"Log"` or `"logs"` (case-insensitive), error. |
4444
| `LogBuilderTruncateRule` | `logRule.logModification` | `Builder->truncate()` calls | If the fluent chain's most recent `table()` call targets a Log-named table (string-literal argument matching `"log"` / `"logs"`, case-insensitive), error. Sibling rule to `LogRule`; shares the `logRule.logModification` identifier so a single `ignoreErrors` entry covers both. Eloquent `from()` chains and Model-`$table`-property-driven tables are acceptable misses. Doctrine: ADR-0001 §Append-only. |
4545
| `EnforceAuditSnapshotOnRetryRule` | `enforceAuditSnapshotOnRetry.firstStatementMustResetState` | `App\Actions\*` whose constructor injects an entity audit logger | The first statement inside `$connection->transaction(...)` must reset the model's in-memory state (`$model->refresh()`, fresh fetch, or fresh instantiation). Doctrine: ADR-0001 §Snapshot-on-Retry Safety. |
46+
| `EnforceAuditTransactionScopeRule` | `enforceAuditTransactionScope.nonTransactionalMutationInClosure` | `App\Actions\*` whose `execute()` calls `transaction(...)` with a literal closure | Mutating `StatefulGuard` / `Session` / `Cache` / `Bus` / `Queue` / `Mailer` / `Notification` / `Broadcaster` / `Filesystem` state (or their `Illuminate\Support\Facades\*` counterparts) inside the closure is an error. Reads (`Auth::user()`, `Session::get()`, `Cache::get()`) are permitted. Doctrine: ADR-0029 (Audit Row Durability Contract) §Decision rule 3. |
4647
| `EnforceResourceDataValidatorOptInRule` | `enforceResourceDataValidatorOptIn.missingValidatorCall` | Classes extending `App\Http\Resources\ResourceData` | If the class declares a non-empty `EAGER_LOAD_COUNT` / `EAGER_LOAD_SUM` constant but never calls `validateRelationsLoaded()` in any method, error. |
4748

4849
### `EnforceActionTransactionsRule` — write-method list

extension.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ services:
2727
-
2828
class: ScriptDevelopment\PhpstanWarroomRules\Rules\EnforceAuditSnapshotOnRetryRule
2929
tags: [phpstan.rules.rule]
30+
-
31+
class: ScriptDevelopment\PhpstanWarroomRules\Rules\EnforceAuditTransactionScopeRule
32+
tags: [phpstan.rules.rule]
3033
-
3134
class: ScriptDevelopment\PhpstanWarroomRules\Rules\EnforceResourceDataValidatorOptInRule
3235
arguments:

0 commit comments

Comments
 (0)