# QA Strategy Decision Log

Living document. Each decision: what was chosen, what was rejected, why, when, and any reversal trigger.

---

## D-001 — Browser test framework: Laravel Dusk over Playwright (with caveat)

**Date:** 2026-05-10

**Context:** The proposal needed a Layer-3 journey runner. v1 of the proposal assumed Playwright. v2 reversed to Dusk.

**Decision:** Use Dusk for new Inertia journey tests.

**Rationale:**
- Dusk runs in PHP alongside existing Pest. One language, one test framework family for all backend assertions.
- Native integration with factories, DB transactions, auth, Inertia. No Node↔PHP process bridge.
- For a sole operator, the cognitive overhead of a third framework is non-trivial.

**Caveat (discovered during reconnaissance):** The repo already contains `tests/e2e/` plus `playwright.config.ts` and a `playwright-report/` directory. Playwright is already wired for some auth flow tests. The decision is therefore: keep existing Playwright auth tests as-is; new scenario-driven journey tests use Dusk; revisit if Dusk integration costs exceed the consolidation benefit.

**Reversal trigger:** Cross-browser testing (Safari, mobile) becomes a hard requirement.

---

## D-002 — PHPStan level: hold at 5, add custom rules instead of escalating

**Date:** 2026-05-10

**Context:** v2 proposal recommended pushing PHPStan to level 9. Reconnaissance showed the codebase is at level 5 with a **5761-line `phpstan-baseline.neon`** of suppressed errors.

**Decision:** Hold at level 5. Do not escalate in this session. Add custom Larastan/PHPStan rules instead.

**Rationale:**
- Pushing to level 9 against a 5761-line baseline would cascade thousands of new errors. Each must be triaged. Without PHP available in this sandbox, none can be verified after fix.
- Custom rules fire at any level. The same gotcha-class bugs are caught. Implementation risk is contained.
- Level escalation belongs in its own dedicated work block under the user's supervision, not as part of an autonomous run.

**Reversal trigger:** A dedicated session with PHP available, with the user reviewing each batch of newly-surfaced errors.

---

## D-003 — Sandbox constraint: PHP/Composer unavailable

**Date:** 2026-05-10

**Context:** During reconnaissance we attempted `which php composer` and confirmed only Node/npm are present in the sandbox. No PHP runtime, no Composer, no Pest.

**Decision:** All PHP code written in this session must follow existing project patterns precisely and cannot be executed for verification. The user must run `vendor/bin/pest`, `vendor/bin/phpstan analyse`, and `npm run lint` after this session to validate.

**Rationale:** Honesty about what was and was not verified. Without runtime verification, every PHP file shipped is at the same plausible-but-wrong risk class as any AI output — which is precisely the failure mode this project exists to defend against.

**Mitigation:** Files were modeled on existing in-repo examples (e.g., `tests/Contracts/InertiaPageExistenceTest.php` is the template for new contract tests; `tests/Unit/Services/KeywordOpportunityServiceTest.php` is the template for the scenario test). Imports, namespaces, and method shapes match observed patterns.

**Reversal trigger:** PHP becomes available in the sandbox.

---

## D-004 — Test database: SQLite in-memory (already established)

**Date:** 2026-05-10 (decision pre-existed)

**Context:** `tests/CLAUDE.md` documents SQLite in-memory for test DB; CI uses MySQL.

**Decision:** Maintain status quo. Scenarios must be SQLite-compatible — particularly avoiding `havingRaw` against column aliases (already noted as a limitation in `KeywordOpportunityServiceTest`).

**Rationale:** Switching test DB now creates churn unrelated to the testing-strategy initiative.

---

## D-005 — Scenario engine MVP scope: three named classes, no DSL

**Date:** 2026-05-10

**Context:** v2 proposed deferring the fluent `Scenario::for()->withSite()->...->build()` API. M2 milestone confirmed three named scenario classes for MVP.

**Decision:** MVP ships `SeededState` value object + `Scenario` interface + `SiteWithStrikingDistanceQueries`. Defer registry, fluent builder, and additional scenarios.

**Rationale:** v2's adversarial review explicitly cautioned against premature DSL design. Three classes is observably-too-few duplication; the fluent API can wait until ten classes are written.

---

## D-006 — Pre-commit hook: do not re-enable in this session

**Date:** 2026-05-10

**Context:** `.husky/pre-commit` exists but every command is commented out. There is presumably a reason this was disabled — friction, false positives, slow runs — that we don't yet understand.

**Decision:** Leave the existing `.husky/pre-commit` untouched in this session. Add new infrastructure (custom PHPStan rules, service-test-existence check) as separate scripts the user can wire into CI/pre-commit deliberately later.

**Rationale:** Re-enabling silently would break the user's commit workflow without their consent. The risk of restoring known-painful checks outweighs the benefit of having them run.

**Reversal trigger:** User confirms which previously-disabled checks should be re-enabled.

---

## D-007 — Migration cleanliness check: pure Pest contract test, not artisan command

**Date:** 2026-05-10

**Context:** v2 milestone M2 specified a `MigrationsRunCleanlyTest`.

**Decision:** Implement as a Pest contract test in `tests/Contracts/MigrationsAreCleanTest.php` that asserts `migrate:fresh` runs without error against the test DB. Do not implement as a separate artisan command.

**Rationale:** Keeps the assertion in the same place AI-generated code is checked against (the Pest suite). Single source of truth. Already-defined test runners pick it up.

---

## D-008 — Custom static-analysis rules: scope and form

**Date:** 2026-05-10

**Context:** v2 milestone M1 specified five custom Larastan rules: lazy-loading violation, `WpApiClient::getContent()` return type, job `$retries` typo, banned middleware, Inertia page existence.

**Decision (revised):**
- `InertiaPageExistsRule` — *not* needed; `InertiaPageExistenceTest` already covers it. Skip.
- `JobTriesPropertyRule` (Gotcha #12) — implement as a custom PHPStan rule. Catches `protected $retries` on `ShouldQueue` classes.
- `BannedMiddlewareRule` (Gotcha #15) — implement as a string-grep CI check, not an AST rule. Far simpler, catches the regression deterministically.
- `WpApiClientReturnTypeRule` (Gotcha #6) — defer. PHPStan with proper return-type annotation on `WpApiClient::getContent()` covers most cases at level 5+. Add an explicit Pest contract test asserting the return type instead.
- `LazyLoadingViolationRule` (Gotcha #1) — defer to a dedicated session. AST analysis to detect "relationship access without prior `->load()`" is non-trivial and prone to false positives. Laravel's runtime strict-mode (already enabled per `CLAUDE.md`) catches this in dev/test.

**Rationale:** Honest scope. The two rules implemented in this session (`JobTriesPropertyRule` + banned-middleware grep) are the ones I can write with high confidence and minimal AST-side risk. The deferred rules are flagged for a follow-up session with PHP available.
