# Phase 4 Implementation Audit

Audit date: 2026-07-15
Remediation update: 2026-07-15

Scope: Phase 4 implementation and remediation only. Phase 5, history exports, AirBridge integration, and unrelated product features were not started.

Important grading rule used here: source-only backend findings are not marked behaviorally proven. JavaScript tests that executed under Node are marked behaviorally verified. PHP/MySQL/Redis/Docker runtime verification remains blocked because those runtimes are unavailable in this Codex environment.

## Remediation Summary

All original `FAIL` items from the first audit were addressed in implementation or converted into explicit PHP-runtime blockers where backend execution is required to prove behavior:

- Spin completion now stores and checks `sessionId` and `guestId` before entitlement issuance.
- Non-`NORMAL_WIN`, expired, forged, already-claimed, cancelled/invalid, and wrong-session spin paths are rejected before replacement entitlement issuance.
- Replacement entitlements now store `expectedStateVersion` and commit rejects stale state with structured `409 REPLACEMENT_STATE_CONFLICT`.
- Mutation authorization no longer trusts raw device headers or `clientCapability`; it requires a server-side session capability resolved from server state.
- Removed-guest encouragement is split into a private event and emitted only when `settings.notifyRemovedGuest` is enabled and the removed guest has an active session.
- Capture cleanup now revokes object URLs, stops camera tracks, and is idempotent.
- The image pipeline rejects unavailable safe re-encoding support with `IMAGE_PROCESSOR_UNAVAILABLE`; it no longer copies the original upload as the reel asset fallback.
- Phase 4 JavaScript tests now include executable behavioral coverage for capture cleanup, consent, duplicate submission prevention, desktop read-only UI, replacement client headers, and public/private replacement event handling.
- PHPUnit test files now enumerate the required backend cases and explicitly mark execution as `BLOCKED` until PHP 8.3/MySQL/Redis/runtime fixtures are available.

## Updated Checklist Findings

| Item | Status After Remediation | Evidence | Notes |
| --- | --- | --- | --- |
| Replacement entitlement can only be created from a persisted `NORMAL_WIN` spin owned by the same guest session. | STATICALLY REMEDIATED, PHP RUNTIME BLOCKED | `server/src/Services/SpinAuthorizationService.php:50`, `server/src/Services/SpinAuthorizationService.php:84`, `server/src/Services/SpinAuthorizationService.php:92`; `server/src/Services/ReplacementEntitlementService.php:18`, `server/src/Services/ReplacementEntitlementService.php:21`; `server/tests/Unit/ReplacementWorkflowTest.php:16` | Stored spin now carries `sessionId`/`guestId`; completion and entitlement creation both check ownership and `NORMAL_WIN`. Needs PHP execution for behavioral proof. |
| `MADE_IN_HEAVEN`, `NO_WIN`, expired, forged, completed-by-another-session, already-claimed, cancelled, or invalid spins cannot issue entitlement. | STATICALLY REMEDIATED, PHP RUNTIME BLOCKED | `server/src/Services/SpinAuthorizationService.php:84`, `server/src/Services/SpinAuthorizationService.php:87`, `server/src/Services/SpinAuthorizationService.php:88`, `server/src/Services/SpinAuthorizationService.php:89`, `server/src/Services/SpinAuthorizationService.php:92`; `server/src/Services/ReplacementEntitlementService.php:27`; `server/tests/Unit/ReplacementWorkflowTest.php:21` | Completion now rejects wrong session, expired, forged, already claimed, and non-normal result types. Entitlement service enforces one entitlement by spin. |
| Only one active replacement entitlement exists per wedding. | STATICALLY REMEDIATED, PHP/CONCURRENCY BLOCKED | `server/src/Services/ReplacementEntitlementService.php:32`, `server/src/Services/ReplacementEntitlementService.php:56`; `server/tests/Unit/ReplacementWorkflowTest.php:61` | File-backed service rejects a second active entitlement. True concurrent proof still requires runtime transaction/lock tests. |
| Entitlement identifies one exact target symbol and expected state version. | STATICALLY REMEDIATED, PHP RUNTIME BLOCKED | `server/src/Services/ReplacementEntitlementService.php:45`, `server/src/Services/ReplacementEntitlementService.php:46`; `server/src/Services/ReplacementWorkflowService.php:115`; `server/tests/Unit/ReplacementWorkflowTest.php:26` | `expectedStateVersion` is returned in the entitlement contract and checked at commit. |
| Protected identities are rejected again during commit. | STATICALLY REMEDIATED, PHP RUNTIME BLOCKED | `server/src/Services/ReplacementWorkflowService.php:103`; `server/tests/Unit/ReplacementWorkflowTest.php:46` | Guard remains at commit time before occupant mutation. |
| Desktop guest rejection occurs in PHP before image processing or persistence. | STATICALLY REMEDIATED, PHP RUNTIME BLOCKED | `server/src/Services/ReplacementWorkflowService.php:23`, `server/src/Services/ReplacementWorkflowService.php:26`, `server/src/Services/ReplacementWorkflowService.php:37`; `server/src/Services/ImageMutationAuthorizationService.php:38` | Stage validates entitlement and trusted server-side session capability before calling image processing. |
| Client-provided device flags are not trusted as final authorization source. | STATICALLY REMEDIATED; JS BEHAVIOR VERIFIED FOR CLIENT HEADERS; PHP RUNTIME BLOCKED | `server/src/Services/ImageMutationAuthorizationService.php:13`, `server/src/Services/ImageMutationAuthorizationService.php:24`; `server/src/Services/ReplacementWorkflowService.php:232`; `server/src/Controllers/ReplacementController.php:74`; `packages/core/src/replacement-client.js:3`; `tests/phase4/run-phase4-tests.mjs:153` | Raw `X-WR-Client-Capability` is no longer sent by the replacement client or read by replacement/asset controllers. Server trusts only a capability token resolved from server state. |
| Current occupant remains unchanged until images validate and commit checks pass. | STRENGTHENED, PHP TRANSACTION BLOCKED | `server/src/Services/ReplacementWorkflowService.php:31`, `server/src/Services/ReplacementWorkflowService.php:37`, `server/src/Services/ReplacementWorkflowService.php:92`, `server/src/Services/ReplacementWorkflowService.php:127`, `server/src/Services/ReplacementWorkflowService.php:150` | Validation/staging and commit rejection paths now occur before occupant assignment. True database atomicity is still blocked until MySQL/Redis runtime exists. |
| Image asset records and files are immutable after creation. | STRENGTHENED, PHP RUNTIME BLOCKED | `server/src/Services/AssetService.php:83`, `server/src/Services/AssetService.php:95`, `server/src/Services/AssetService.php:109`; `server/src/Services/AssetService.php:177` | Variant generation cleans partial directories on failure. DB asset-row proof remains runtime-blocked. |
| Cancellation, timeout, upload failure, validation failure, and moderation rejection leave existing occupant unchanged. | STATICALLY REMEDIATED, PHP RUNTIME BLOCKED | `server/src/Services/ReplacementWorkflowService.php:31`, `server/src/Services/ReplacementWorkflowService.php:42`, `server/src/Services/ReplacementWorkflowService.php:93`, `server/src/Services/ReplacementWorkflowService.php:115`, `server/src/Services/ReplacementWorkflowService.php:195`; `server/tests/Unit/ReplacementWorkflowTest.php:41` | Explicit `MODERATION_REJECTED` commit guard added before occupant mutation. Other rejection paths also precede occupant mutation. |
| Duplicate commit requests return same canonical replacement. | STATICALLY REMEDIATED, PHP RUNTIME BLOCKED | `server/src/Services/ReplacementWorkflowService.php:76`, `server/src/Services/ReplacementWorkflowService.php:183`; `server/tests/Unit/ReplacementWorkflowTest.php:51` | Idempotency behavior remains source-visible but needs PHP execution. |
| Replacement closes one old occupant and creates one current occupant. | STRENGTHENED, PHP TRANSACTION BLOCKED | `server/src/Services/ReplacementWorkflowService.php:127`, `server/src/Services/ReplacementWorkflowService.php:150`, `server/src/Services/ReplacementWorkflowService.php:177` | File-backed scaffold updates one current symbol and appends history. True row-version close/start proof remains DB-runtime blocked. |
| Event sequence and state version advance exactly once. | STATICALLY REMEDIATED, PHP/CONCURRENCY BLOCKED | `server/src/Services/ReplacementWorkflowService.php:151`, `server/src/Services/ReplacementWorkflowService.php:153`, `server/src/Services/ReplacementWorkflowService.php:158` | Needs runtime duplicate/concurrent verification. |
| Durable `SYMBOL_REPLACED` public event contains both added and removed occupant. | STATICALLY REMEDIATED, PHP RUNTIME BLOCKED | `server/src/Services/ReplacementWorkflowService.php:140`, `server/src/Services/ReplacementWorkflowService.php:143`, `server/src/Services/ReplacementWorkflowService.php:144`, `server/src/Services/ReplacementWorkflowService.php:160` | Public event payload remains complete and excludes private encouragement. |
| Removed guest receives private encouragement only when enabled and active. | STATICALLY REMEDIATED, JS HELPER BEHAVIOR VERIFIED, PHP RUNTIME BLOCKED | `server/src/Services/ReplacementWorkflowService.php:257`, `server/src/Services/ReplacementWorkflowService.php:259`, `server/src/Services/ReplacementWorkflowService.php:267`, `server/src/Services/ReplacementWorkflowService.php:276`; `packages/core/src/replacement-events.js:1`; `tests/phase4/run-phase4-tests.mjs:171` | Private payload is now a separate `SYMBOL_REPLACED_PRIVATE` event and public helper strips private fields defensively. |
| Uploaded images are not trusted by extension or filename. | STATICALLY REMEDIATED, PHP RUNTIME BLOCKED | `server/src/Services/AssetService.php:43`, `server/src/Services/AssetService.php:49`, `server/src/Services/AssetService.php:52` | Decoded image metadata remains the basis for server validation. |
| File-size, decoded dimensions, total pixel count, and image count are bounded. | STATICALLY REMEDIATED, PHP RUNTIME BLOCKED | `server/src/Services/AssetService.php:39`, `server/src/Services/AssetService.php:52`; `server/src/Services/ReplacementWorkflowService.php:31` | Bounds are present; actual file fixtures need PHP/GD runtime. |
| Re-encoding occurs before images are treated as valid assets. | STATICALLY REMEDIATED, PHP RUNTIME BLOCKED | `server/src/Services/AssetService.php:65`, `server/src/Services/AssetService.php:95`, `server/src/Services/AssetService.php:137`, `server/src/Services/AssetService.php:139`, `server/src/Services/AssetService.php:170`; `tests/phase4/run-phase4-tests.mjs:194` | Original-file fallback was removed. Missing processor support now rejects. |
| Image orientation correction and metadata stripping are implemented. | PARTIAL, BROWSER/PHP RUNTIME BLOCKED | `packages/ui/src/camera-view.js:17`, `packages/ui/src/camera-view.js:27`; `server/src/Services/AssetService.php:145`, `server/src/Services/AssetService.php:170` | Browser path still uses `createImageBitmap(... imageOrientation: 'from-image')` and canvas export. Server re-encode strips metadata when GD is available; explicit EXIF fallback still needs runtime decision/testing. |
| Frontend revokes object URLs and stops camera tracks. | BEHAVIORALLY VERIFIED | `packages/ui/src/camera-view.js:53`, `packages/ui/src/camera-view.js:73`, `packages/ui/src/camera-view.js:80`, `packages/ui/src/camera-view.js:193`; `tests/phase4/run-phase4-tests.mjs:87`, `tests/phase4/run-phase4-tests.mjs:127`, `tests/phase4/run-phase4-tests.mjs:137` | Node tests verify URL revoke and idempotent track stopping with mocked environment. |
| Frontend does not upload before explicit consent. | BEHAVIORALLY VERIFIED | `packages/ui/src/camera-view.js:100`, `packages/ui/src/camera-view.js:104`; `apps/web/src/main.js:33`; `tests/phase4/run-phase4-tests.mjs:110` | `canSubmitCaptureState()` and duplicate submission test execute. |
| Frontend does not enable capture from local spin animation alone. | STATICALLY REMEDIATED | `apps/web/src/main.js:74`, `apps/web/src/main.js:77`, `apps/web/src/main.js:78` | Capture is still enabled only from server completion entitlement. Browser E2E remains pending. |
| No duplicate capture/replacement logic in PWA, extension, or admin entry points. | STATICALLY VERIFIED | `packages/core/src/replacement-client.js:7`, `packages/ui/src/camera-view.js:31`, `apps/web/src/main.js:1` | Shared modules remain the implementation location. |
| Phase 4 tests exercise behavior, not only source-text matching. | STRENGTHENED | `tests/phase4/run-phase4-tests.mjs:87`, `tests/phase4/run-phase4-tests.mjs:101`, `tests/phase4/run-phase4-tests.mjs:110`, `tests/phase4/run-phase4-tests.mjs:127`, `tests/phase4/run-phase4-tests.mjs:137`, `tests/phase4/run-phase4-tests.mjs:153`, `tests/phase4/run-phase4-tests.mjs:171` | JS tests now include executable behavioral checks. Backend assertions remain blocked PHP tests, not PASS. |

## Server-Side Session Capability Policy

For Phase 4 remediation, an approved event-time mobile session is represented by a server-side capability record stored under `sessionCapabilities`. Mutation requests may present an `X-WR-Session-Capability` token, but the token is not a device claim. The server resolves it from trusted state and checks public wedding ID, session ID, optional guest ID, status, expiry, and capability type before authorization.

Raw user-agent strings, viewport size, touch support, `isMobile`, and custom raw device headers are not accepted as final authorization inputs.

## Files Changed During Remediation

- `apps/web/src/main.js`
- `packages/core/src/bootstrap-state.js`
- `packages/core/src/index.js`
- `packages/core/src/replacement-client.js`
- `packages/core/src/replacement-events.js`
- `packages/ui/src/camera-view.js`
- `server/src/Controllers/AssetController.php`
- `server/src/Controllers/ReplacementController.php`
- `server/src/Controllers/SpinController.php`
- `server/src/Services/AssetService.php`
- `server/src/Services/ImageMutationAuthorizationService.php`
- `server/src/Services/ReplacementEntitlementService.php`
- `server/src/Services/ReplacementWorkflowService.php`
- `server/src/Services/SpinAuthorizationService.php`
- `server/tests/Unit/ImageMutationAuthorizationTest.php`
- `server/tests/Unit/ReplacementWorkflowTest.php`
- `tests/phase4/run-phase4-tests.mjs`
- `docs/PHASE4_AUDIT.md`
- `docs/PHASE4_REMEDIATION_REPORT.md`

## Commands Run After Remediation

```bash
node --check apps/web/src/main.js
node --check packages/ui/src/camera-view.js
node --check packages/core/src/replacement-client.js
node --check packages/core/src/replacement-events.js
node --check packages/core/src/bootstrap-state.js
node --check tests/phase4/run-phase4-tests.mjs
node tests/phase4/run-phase4-tests.mjs
npm run ci:local
php -v
composer --version
docker --version
mysql --version
redis-server --version
```

Results:

- All Node syntax checks passed.
- `node tests/phase4/run-phase4-tests.mjs` passed.
- `npm run ci:local` passed through Phase 0, 1, 2, 3, and 4 checks.
- `php`, `composer`, `docker`, `mysql`, and `redis-server` are not installed in this environment, so backend runtime verification remains blocked.

## Runtime-Blocked Items Still Carried Forward

- PHP 8.3 syntax checks for every PHP file.
- Composer validation/install.
- PHPUnit execution for replacement workflow, authorization, state recovery, event listing, admin authorization, desktop mutation rejection, and image processing.
- Applying migrations `001` through `004` to clean MySQL 8.
- Apache/PHP HTTP endpoint tests.
- Redis/distributed lock behavior.
- Durable MySQL outbox publication.
- Critical two-simultaneous-valid-commits test against the same wedding and symbol.
- Real malformed-image and GD/WebP re-encode fixture tests.

## Remaining Technical Debt Before Release

- Replace file-backed transaction/lock scaffolding with real MySQL transaction plus Redis/distributed lock.
- Add the trusted event-time mobile join flow that issues `sessionCapabilities`.
- Decide whether to implement explicit server-side EXIF orientation correction beyond browser `createImageBitmap` orientation handling.
- Execute the blocked PHP/MySQL/Redis test suite in Docker/CI.
