# Session 2026-05-10 — Verification Procedure

This document lists every artifact created in the autonomous-implementation
session of 2026-05-10 and the exact commands to verify each one. It is
intended to be run end-to-end by a human (the sole operator) on a machine
with PHP, Composer, and Node available — none of which were available in
the sandbox where the artifacts were created.

**Important honesty caveat:** None of the PHP code in this session was
executed. The author wrote it by following existing in-repo patterns
(e.g. `tests/Contracts/InertiaPageExistenceTest.php` for contract tests;
`tests/Unit/Services/KeywordOpportunityServiceTest.php` for the scenario
unit test) but cannot guarantee correctness. Treat every assertion in
this document as "should" not "does". Run the verification.

---

## Step 0 — Refresh autoloader

Composer needs to discover the new PHPStan rule class:

```bash
composer dump-autoload
```

Expected: completes without error.

---

## Step 1 — Verify PHPStan still runs at level 5 (no regressions)

```bash
vendor/bin/phpstan analyse --memory-limit=1G
```

Expected: same outcome as before this session. The added include of
`phpstan-custom-rules.neon` should not introduce any new errors against
`app/`, `routes/`, `config/` because no existing job class declares a
`$retries` property (verified via `grep -rn 'protected.*\$retries' app/`).

If PHPStan reports new errors in the `rankwiz.jobTriesProperty` family,
they are **real** — investigate the offending class.

---

## Step 2 — Verify the custom JobTriesPropertyRule fires correctly

The rule has three fixture files:

```bash
vendor/bin/phpstan analyse --memory-limit=1G \
    tests/PhpStan/Fixtures/JobTriesPropertyRule/
```

Expected:
- `IncorrectJob.php` → 1 error with identifier `rankwiz.jobTriesProperty`
- `CorrectJob.php` → 0 errors related to this rule
- `NotAJob.php` → 0 errors related to this rule

Note: the fixtures are not in PHPStan's default `paths:` config, so they
are only analyzed when the path is passed explicitly. They do not slow
down ordinary CI runs.

---

## Step 3 — Verify the new Pest contract tests

Run the four new contract tests in isolation first:

```bash
./vendor/bin/pest tests/Contracts/BannedMiddlewareTest.php
./vendor/bin/pest tests/Contracts/ServiceHasTestTest.php
./vendor/bin/pest tests/Contracts/ZiggyIsInSyncTest.php
./vendor/bin/pest tests/Contracts/MigrationsAreCleanTest.php
```

Expected outcomes per test:

### `BannedMiddlewareTest`
**Should pass.** `bootstrap/app.php` already deliberately omits
`AddLinkHeadersForPreloadedAssets` (verified via grep — only an explanatory
comment exists, and the test strips comments and string literals before checking).

### `ServiceHasTestTest`
**Likely fails on first run.** With 157 services in `app/Services/`, the test
will list every one without a referencing test file. The PR description
in the test class explains how to handle this:

1. Read the failure list.
2. For services you accept as currently-untested, add their short class
   names to `EXEMPT_FROM_BASELINE` with a one-line rationale each.
3. For services that should have tests, write the missing tests.

After bootstrapping the exemption list, the test enforces "no NEW services
without tests" — exactly the AI-discipline ratchet we want.

### `ZiggyIsInSyncTest`
**Should pass** if the committed `resources/js/ziggy.js` matches the
output of `php artisan ziggy:generate` against current routes. If it
fails, run `php artisan ziggy:generate` and commit the updated file.

The test's `markTestSkipped` paths handle the cases where Ziggy is not
installed or the file is missing — neither should apply in this codebase.

### `MigrationsAreCleanTest`
**Should pass** if existing migrations and seeders are clean. The four
sub-tests run `migrate:fresh`, `db:seed`, count migrations, and validate
each migration declares an `up()` method. If any fail, the failure
message will pinpoint the offending migration or seeder.

---

## Step 4 — Verify the scenario engine MVP

```bash
./vendor/bin/pest tests/Unit/Scenarios/SiteWithStrikingDistanceQueriesTest.php
```

Expected: all six tests pass.

The scenario test verifies:
- A user, a site, and the user-owns-site relationship are correctly seeded.
- The seeded GSC metrics satisfy the striking-distance thresholds defined
  in `config/keyword_opportunities.php`.
- The requested count of striking-distance queries is honored.
- The scenario throws `InvalidArgumentException` when constructor args
  produce a state that would NOT trigger the detector (position/impressions/
  clicks below threshold). This is a deliberate fail-fast design — far
  better than a downstream test mysteriously detecting nothing.

If `config/keyword_opportunities.php` thresholds are tightened beyond what
the scenario's defaults satisfy, the threshold-check tests fail loudly,
flagging the drift before downstream journey tests start producing
false negatives.

---

## Step 5 — Run the full PHP test suite

```bash
./vendor/bin/pest --parallel --processes=4
```

Expected: same outcome as before this session, plus:
- 4 new tests in `tests/Contracts/` (one of which — `ServiceHasTestTest` —
  may fail until `EXEMPT_FROM_BASELINE` is bootstrapped)
- 6 new tests in `tests/Unit/Scenarios/`

If any pre-existing test newly fails, the most likely cause is one of:
- The new `phpstan-custom-rules.neon` include broke something (unlikely;
  PHPStan is invoked separately from Pest)
- The composer autoloader was not refreshed (Step 0)
- A genuine code defect that this session's changes did not cause

---

## Step 6 — Run the full pre-flight gate

```bash
./vendor/bin/pest --parallel --processes=4 \
  && npm run build \
  && npm run lint \
  && npx tsc --noEmit \
  && vendor/bin/phpstan analyse --memory-limit=1G \
  && composer audit \
  && npm audit --audit-level=critical
```

Expected: same outcome as before this session, except `Pest` reports
the new tests.

---

## Files created / modified in this session

```
docs/qa-strategy/decision-log.md                    [new]
docs/qa-strategy/external-services.md               [new]
docs/qa-strategy/SESSION-2026-05-10-VERIFICATION.md [new]
phpstan-custom-rules.neon                           [new]
phpstan.neon                                        [modified — added include]
phpstan.no-baseline.neon                            [modified — added include]
app/Support/PhpStan/Rules/JobTriesPropertyRule.php  [new]
tests/PhpStan/Fixtures/JobTriesPropertyRule/CorrectJob.php    [new]
tests/PhpStan/Fixtures/JobTriesPropertyRule/IncorrectJob.php  [new]
tests/PhpStan/Fixtures/JobTriesPropertyRule/NotAJob.php       [new]
tests/Contracts/BannedMiddlewareTest.php            [new]
tests/Contracts/ServiceHasTestTest.php              [new]
tests/Contracts/ZiggyIsInSyncTest.php               [new]
tests/Contracts/MigrationsAreCleanTest.php          [new]
tests/Scenarios/Contracts/Scenario.php              [new]
tests/Scenarios/Contracts/SeededState.php           [new]
tests/Scenarios/SiteWithStrikingDistanceQueries.php [new]
tests/Unit/Scenarios/SiteWithStrikingDistanceQueriesTest.php [new]
```

## What was deferred (and why)

These items from the M0–M2 milestones were intentionally skipped this
session and remain open work:

| Item | Reason | Where to resume |
|---|---|---|
| Push PHPStan to level 9 | Would cascade thousands of new errors against a 5761-line baseline; cannot be verified safely without PHP available | dedicated session under user supervision |
| LazyLoadingViolationRule (Gotcha #1) | AST analysis non-trivial; high false-positive risk; runtime strict-mode already catches in dev/test | follow-up session |
| WpApiClientReturnTypeRule (Gotcha #6) | PHPStan with proper return-type annotation already mitigates at level 5+ | add a Pest contract test asserting `WpApiClient::getContent()` returns array |
| InertiaPageExistsRule (Gotcha #10) | Already covered by existing `tests/Contracts/InertiaPageExistenceTest.php` | not needed |
| Re-enabling `.husky/pre-commit` | Was deliberately disabled; reason unknown; re-enabling would risk breaking commit workflow | discuss with user |
| Additional scenarios (cannibal cluster, plan limit, etc.) | M2's stated MVP is one scenario; more belongs in M5 per `implementation-milestones.md` | M5 |
| `bin/setup-test-env.sh` (M8) | Out of scope for MVP — meaningful only after M3+M4 deliver containerized test env | M8 |

## What this session did NOT change

Out of caution, the following were NOT modified, even though the milestone
plan eventually calls for it:

- `.husky/pre-commit` — left as-is (silently disabled state preserved)
- `.github/workflows/ci.yml` — not modified; the user can wire the new
  contract tests into CI by ensuring the Pest run picks them up (it
  already does via the `Contracts` testsuite definition in `phpunit.xml`)
- `composer.json` — no new dependencies added
- `phpstan-baseline.neon` — not regenerated (the new rule produces no new
  errors against current `app/` code, verified by grep)
- Any production code in `app/` — only `app/Support/PhpStan/Rules/` was
  added, which is invoked only by static analysis

This is conservative on purpose. A larger blast radius needs explicit
sign-off.
