# SPRINT 2 — ARCHITECTURE REVIEW

| Field | Value |
|-------|-------|
| Document Type | Architecture Review |
| Subject | Inveetaire Sprint 2 — Authentication & Authorization |
| Reviewer | Independent AI Architecture Audit |
| Review Date | 2026-07-05 |
| Sprint Status | Implementation Complete — Pending Review Acceptance |
| Repository Commits | 50e9b19 (AUTH-001) · 499f8c2 (AUTH-002) · 94ea225 (AUTH-004) · AUTH-003 awaiting commit |
| Sprint 1 Baseline | 01dbd10 |
| Sprint 0 Baseline | e961697 |
| Version | 1.0 (Draft — for Human Approver review) |

---

## 1. Executive Summary

Sprint 2 delivered the complete Authentication & Authorization layer for the Inveetaire platform. All four primary tasks — AUTH-001 (Unified Login), AUTH-002 (Secure Logout), AUTH-004 (Middleware & Session Timeouts), and AUTH-003 (Password Reset) — have been implemented.

The Sprint 0 kernel is intact. Eleven kernel files remain byte-identical to the Sprint 0 baseline. The single approved kernel extension (`app/core/Session.php`) received three additive-only timeout helper methods with no modification to existing method signatures.

Two documentation conflicts were detected and resolved during implementation through the documented conflict governance protocol (AI.md §27–28). Both resolutions were approved by the Human Approver. Neither required an architectural redesign or kernel modification.

The primary Sprint 2 objective — to establish a secure boundary between anonymous public users and authenticated portal operators — has been achieved. Every portal route requiring authentication or role-specific access can now be protected using `AuthMiddleware` and `RoleMiddleware`. Session timeouts are enforceable. Password recovery is operational for eligible roles.

**Sprint 2 objectives: ACHIEVED.**

---

## 2. Architecture Assessment

### 2.1 Layer Separation

| Layer | Responsibility | Compliance |
|-------|---------------|------------|
| Controller (`AuthController`) | HTTP input handling, flash messages, redirect decisions | ✅ Thin — no SQL, no password logic, no session management |
| Service (`AuthService`) | Business logic, lockout, session binding, token lifecycle | ✅ Pure service — no HTTP globals, no direct PDO |
| Model (`AuthModel`) | Parameterized SQL — user lookup, password hash update | ✅ Data layer only — no logic, no session |
| Middleware (`AuthMiddleware`, `RoleMiddleware`) | Infrastructure gate — session check, path inference, timeout | ✅ Thin gate — no SQL, no business logic |
| Kernel (`Session.php`) | Session lifecycle, timeout helpers | ✅ Extension only — three additive static methods |

Layer separation is sound. No cross-layer contamination was observed. Business logic does not leak into the controller; controllers do not call the model directly.

**Verdict: PASS**

### 2.2 Service Layer

`AuthService` is the correct host for all auth business logic. It performs:
- Credential resolution and verification (`attemptLogin`)
- IP-based lockout (`isLockedOut`, `incrementAttempts`, `clearAttempts`)
- Session binding on login (`bindSession`)
- Session destruction on logout (`performLogout`)
- Timeout property accessors (`getAbsoluteTimeout`, `getInactivityTimeout`)
- Password reset token lifecycle (`requestPasswordReset`, `resetPassword`, `loadResetToken`)
- Token file I/O (`writeTokenFile`, `readTokenFile`, `deleteTokenFile`)
- Log simulation (`writeResetLinkLog`)

The service is large (>490 lines) but internally coherent. All methods are grouped by task with section separators. There is no God-class anti-pattern — each method group maps to a documented sprint task (AUTH-001, AUTH-002, AUTH-004, AUTH-003).

**Assessment**: The size of `AuthService` is expected at the end of Sprint 2 given it absorbs 4 full task deliveries. Refactoring into sub-services (e.g., `PasswordResetService`, `LockoutService`) is a future consideration but not a current requirement.

**Verdict: PASS**

### 2.3 Middleware Architecture

`AuthMiddleware` and `RoleMiddleware` both implement `MiddlewareInterface` with zero constructor arguments, as required by the frozen `MiddlewarePipeline` which uses `new $class()` for instantiation.

Execution order in portal route arrays is `[AuthMiddleware, RoleMiddleware]`. This is correct: authentication must be verified before role checking, since `RoleMiddleware` reads `role_snapshot` which is only valid in an authenticated session.

`AuthMiddleware` handles three cases:
1. No `auth_user_id` in session → redirect to `/login` (no flash)
2. Inactivity timeout exceeded → destroy session, flash, redirect
3. Absolute timeout exceeded → destroy session, flash, redirect

`RoleMiddleware` infers the required role from the URL path prefix (see §7.2 conflict review). This is an approved non-standard approach. The `PATH_ROLE_MAP` constant is clear and maintainable. The prefix-match logic (`$path === $prefix || strpos($path, $prefix . '/') === 0`) correctly prevents false positives (e.g., `/app/coupleExtra` would not match `/app/couple`).

**Verdict: PASS** (with documented deviation — see §7.2)

### 2.4 Authentication Flow

```
GET /login        → AuthController::showLogin()
                    → redirect authenticated users to portal
                    → render auth/login view

POST /login       → [CsrfMiddleware] → AuthController::processLogin()
                    → AuthService::attemptLogin()
                    → AuthModel::findActiveUserByEmail() OR findActiveUserByUsername()
                    → password_verify()
                    → [lockout check]
                    → Session::regenerate(true) + bindSession()
                    → redirect to role portal
```

The session binding on login stores: `auth_user_id`, `role_snapshot`, `workspace_id`, `login_at`, `last_active_at`. This is the complete set required by both `AuthMiddleware` (timeout checking) and `RoleMiddleware` (role validation).

Session fixation is prevented: `Session::regenerate(true)` is called before any session write in `bindSession()`.

**Verdict: PASS**

### 2.5 Session Lifecycle

| Event | Action |
|-------|--------|
| Login | `Session::regenerate(true)` + bind 5 keys |
| Each authenticated request | `AuthMiddleware` checks timeouts → `Session::touch()` updates `last_active_at` |
| Inactivity timeout | `Session::destroy()` → `session_start()` → `Session::regenerate(false)` → flash → redirect |
| Absolute timeout | Same as inactivity |
| Logout | `Session::destroy()` → `session_start()` → `Session::regenerate(false)` → flash success → redirect |
| Expired reset token access | No session change — token file deleted, redirect to `/forgot-password` |

The `session_start()` call after `Session::destroy()` is architecturally correct and necessary: PHP requires an active session to write `$_SESSION['flash']`. The implementation pattern (destroy → start → regenerate) is consistent between `AuthService::performLogout()` and `AuthMiddleware::destroyAndRedirect()`.

One minor note: `Session::regenerate(false)` is called with `deleteOld=false` after `destroy()`. This is correct: there is no old server session file at that point (destroy already removed it). This is documented in both implementation memories.

**Verdict: PASS**

### 2.6 Password Reset Lifecycle

```
GET  /forgot-password → show form
POST /forgot-password → [CsrfMiddleware]
                      → normalize email
                      → AuthModel::findActiveUserByEmailForReset()  (couple/admin only)
                      → random_bytes(32) → bin2hex() → 64-char hex token
                      → write storage/cache/reset_token_{token}.json
                      → write storage/logs/reset_links_{date}.log (simulated email)
                      → generic success message (always — no enumeration)

GET  /reset-password/{token} → AuthService::loadResetToken()
                              → validate format (ctype_xdigit, len=64)
                              → read token file
                              → check expiry
                              → if invalid/expired: delete file, flash error, redirect /forgot-password
                              → render reset form with $token

POST /reset-password/{token} → [CsrfMiddleware]
                              → validate format
                              → read token file
                              → check expiry → delete file if expired, redirect
                              → check password >= 8 chars
                              → hash_equals(password, confirm)
                              → password_hash(PASSWORD_BCRYPT)
                              → AuthModel::updatePasswordHash()
                              → DELETE token file (BEFORE returning success)
                              → Logger::info()
                              → flash success → redirect /login
```

The single-use token delete-before-return pattern eliminates replay attack window. The `ctype_xdigit()` + length-64 guard prevents path traversal via crafted token strings (the token is used as a filename). User enumeration is prevented by always returning the same generic message from `requestPasswordReset()`.

**Verdict: PASS**

### 2.7 Routing

All AUTH-003 routes use named URL parameters (`{token}`) which are a documented and tested capability of the Router. `$request->param('token')` correctly reads these values (verified by reading `Request.php`).

POST routes at every stage (`/login`, `/logout`, `/forgot-password`, `/reset-password/{token}`) are protected by `CsrfMiddleware`. No POST route is exposed without CSRF protection.

GET routes (`/login`, `/forgot-password`, `/reset-password/{token}`) are correctly public — no middleware is needed since the forms themselves do not modify state.

**Verdict: PASS**

### 2.8 Security Boundaries

| Boundary | Mechanism | Status |
|----------|-----------|--------|
| Unauthenticated → portal | `AuthMiddleware` → 302 /login | ✅ |
| Wrong role → portal | `RoleMiddleware` → 403 Forbidden | ✅ |
| POST without CSRF | `CsrfMiddleware` → 403 Forbidden | ✅ |
| Inactive session → portal | `AuthMiddleware` → 302 /login + flash | ✅ |
| Expired absolute session | `AuthMiddleware` → 302 /login + flash | ✅ |
| Crew reset attempt | `findActiveUserByEmailForReset()` role filter | ✅ |
| Expired reset token | `resetPassword()` expiry check + file delete | ✅ |
| Path traversal via token | `ctype_xdigit()` + len=64 guard | ✅ |

### 2.9 Kernel Integrity

| Kernel File | Modified? | Justification |
|------------|-----------|---------------|
| `Bootstrap.php` | No | ✅ Frozen |
| `Router.php` | No | ✅ Frozen |
| `Database.php` | No | ✅ Frozen |
| `BaseController.php` | No | ✅ Frozen |
| `BaseModel.php` | No | ✅ Frozen |
| `BaseService.php` | No | ✅ Frozen |
| `ErrorHandler.php` | No | ✅ Frozen |
| `Logger.php` | No | ✅ Frozen |
| `MiddlewarePipeline.php` | No | ✅ Frozen |
| `Request.php` | No | ✅ Frozen |
| `Csrf.php` | No | ✅ Frozen |
| `Session.php` | Yes — additive only | ✅ Approved extension point (AUTH-004, SPRINT2_PLANNING §8) |

**Kernel Integrity: INTACT. All 11 freeze-listed kernel files are unmodified.**

---

## 3. Security Assessment

### 3.1 Password Handling

| Check | Status | Detail |
|-------|--------|--------|
| `password_verify()` | ✅ | `AuthService::attemptLogin()` — timing-safe credential check |
| `password_hash(PASSWORD_BCRYPT)` | ✅ | `AuthService::resetPassword()` — bcrypt hash before DB write |
| Plain-text password never logged | ✅ | Only `user_id`, `role`, `ip` logged on all paths |
| Plain-text password never stored | ✅ | Only bcrypt hash written to `users.password_hash` |

### 3.2 Token Security

| Check | Status | Detail |
|-------|--------|--------|
| `random_bytes(32)` | ✅ | CSPRNG — cryptographically secure token source |
| `bin2hex()` encoding | ✅ | 64-char hex string — safe for filenames and URLs |
| `ctype_xdigit()` + length-64 guard | ✅ | Prevents path traversal — only hex chars reach `file_get_contents` |
| Token not stored in log | ✅ | Only reset link URL (`/reset-password/{token}`) written to log |
| Token deleted before returning success | ✅ | `deleteTokenFile()` called as last step before `Logger::info()` and return |
| `hash_equals()` for password confirmation | ✅ | Constant-time string comparison in `resetPassword()` |
| Single-use enforcement | ✅ | Token file deleted on both success and expiry |

> **Note on `hash_equals()` usage**: The implementation correctly uses `hash_equals()` for password confirmation comparison (preventing timing-oracle attacks on the confirmation check). It also applies `hash_equals()` was the approved mechanism. The token validation relies on file existence + format check rather than a direct string comparison — this is architecturally correct since the 64-char hex token IS the filename, making it a possession-based proof with no string-comparison timing vector.

### 3.3 CSRF Enforcement

| Route | Protection | Status |
|-------|-----------|--------|
| POST /login | CsrfMiddleware | ✅ |
| POST /logout | CsrfMiddleware | ✅ |
| POST /forgot-password | CsrfMiddleware | ✅ |
| POST /reset-password/{token} | CsrfMiddleware | ✅ |
| GET routes | None required | ✅ (read-only) |

AF-001 from Sprint 1 is **fully resolved** — `CsrfMiddleware` is now active on all four POST routes.

### 3.4 Session Security

| Check | Status | Detail |
|-------|--------|--------|
| Session regeneration on login | ✅ | `Session::regenerate(true)` before any session write |
| Session destruction on logout | ✅ | `Session::destroy()` → start → regenerate(false) |
| Session destruction on timeout | ✅ | Same pattern in `AuthMiddleware::destroyAndRedirect()` |
| No Back-button replay after logout | ✅ | No-cache headers (Cache-Control, Pragma, Expires) sent in `AuthController::logout()` |
| Anonymous session after destruction | ✅ | `session_start()` + `regenerate(false)` opens clean session for flash |
| `last_active_at` updated per request | ✅ | `Session::touch()` called by `AuthMiddleware` after timeout checks pass |
| `login_at` set once on login | ✅ | `AuthService::bindSession()` — never updated |

### 3.5 Output Escaping

All three auth views (`login.php`, `forgot_password.php`, `reset_password.php`) exclusively use `escape_html()` for dynamic output. No raw `echo` of user-controlled data. Flash messages, page titles, and the `$token` variable in `reset_password.php` are all wrapped in `escape_html()`.

### 3.6 Flash Handling

Flash messages are written to `$_SESSION['flash']` by `BaseController::flash()` or directly to `$_SESSION['flash']['error']` in `AuthMiddleware` (where no BaseController context is available). They are consumed and cleared by `BaseController::pullFlash()` on the next render. No stale flash leakage.

### 3.7 Role Authorization

`RoleMiddleware` reads `role_snapshot` (set at login, from DB `roles.name`) and compares against the path-inferred required role. The snapshot is immutable during the session lifetime (only written at login via `bindSession()`), so privilege escalation via session manipulation is not possible for the `role_snapshot` key.

### 3.8 Timeout Enforcement

| Timeout Type | Limit | Mechanism |
|-------------|-------|-----------|
| Inactivity (all roles) | 2h (7200s) | `Session::isInactivityExpired()` in `AuthMiddleware` |
| Absolute (super_admin) | 8h (28800s) | `Session::isAbsoluteExpired()` + `ROLE_TIMEOUT_KEY` map |
| Absolute (crew) | 12h (43200s) | Same |
| Absolute (couple) | 24h (86400s) | Same |

Timeout config is read from `config/session.php` via the `config()` helper. No hardcoded timeout values in middleware (except `DEFAULT_ABSOLUTE_TIMEOUT = 28800` as a safe fallback for unrecognized roles).

### 3.9 Logging

| Event | Level | Status |
|-------|-------|--------|
| Successful login | `Logger::info` | ✅ |
| Failed login | `Logger::warning` | ✅ |
| IP lockout triggered | `Logger::warning` | ✅ |
| Successful logout | `Logger::info` | ✅ |
| Inactivity timeout | `Logger::warning` | ✅ |
| Absolute timeout | `Logger::warning` | ✅ |
| Role mismatch | `Logger::warning` | ✅ |
| Malformed/invalid reset token | `Logger::warning` | ✅ |
| Expired reset token | `Logger::warning` | ✅ |
| Successful password reset | `Logger::info` | ✅ |
| DB errors (reset flow) | `Logger::error` | ✅ |
| Token file write failure | `Logger::error` | ✅ |

### 3.10 Identified Weaknesses

| ID | Weakness | Severity | Assessment |
|----|---------|---------|------------|
| W-001 | No rate-limiting on `POST /forgot-password`. A malicious actor can submit hundreds of reset requests per minute, creating thousands of token files in `storage/cache/`. | Low | Storage exhaustion risk at volume. Not a concern at current scale. Token files are 1-use and cleaned on use/expiry. Mitigation: rate-limit by IP in a future sprint. |
| W-002 | Reset token file names are not encrypted in storage. The `/reset-password/{token}` URL structure is deducible from the filename `reset_token_{token}.json`. | Informational | The token is cryptographically secure (random_bytes). The filename IS the token — no additional information is leaked. `.htaccess` must block web access to `storage/cache/`. |
| W-003 | `last_active_at` safe-pass behavior: if `last_active_at` is absent from session, `isInactivityExpired()` returns `false`. | Low | AUTH-001 always sets `last_active_at` on login. A session without it would be anonymous (no `auth_user_id`), caught by the pre-check. |
| W-004 | No token cleanup job. Expired but unused tokens accumulate in `storage/cache/` until the same email submits a new request (overwrites) or the file is manually deleted. | Low | Not a security risk — expired tokens are checked and rejected. A periodic cleanup task (`cron`) is recommended for pre-launch. |

---

## 4. Technical Debt Register

### TD-001 — MiddlewareInterface Missing
**Status**: ✅ RESOLVED (Sprint 1, CORE-001)

### TD-002 — Content Security Policy (CSP) Absent
**Status**: 🔄 DEFERRED
**Severity**: Medium
**Description**: No `Content-Security-Policy` header is set. Sprint 2 adds user-controlled content (flash messages, role names) to authenticated views, slightly increasing the urgency.
**Recommended Sprint**: Sprint 3 pre-checklist (unchanged from Sprint 1 recommendation)

### TD-003 — Log File Rotation Absent
**Status**: 🔄 DEFERRED
**Severity**: Low
**Description**: Log files (`storage/logs/`) grow indefinitely. Sprint 2 adds reset link logs (`reset_links_{date}.log`) as an additional log stream.
**Recommended Sprint**: Pre-launch (unchanged)

### TD-004 — TailwindCSS CDN in Production Layouts
**Status**: ✅ RESOLVED (Sprint 1, CORE-003 + CORE-004)

### TD-005 — Google Fonts Loaded Externally
**Status**: 🔄 DEFERRED
**Severity**: Low
**Recommended Sprint**: Sprint 3+

### TD-006 — Build Configuration Not Yet Versioned
**Status**: ✅ RESOLVED (Sprint 2 pre-checklist — `tailwind.config.js` committed per SPRINT2_PLANNING §2)
**Evidence**: SPRINT2_PLANNING §12 lists TD-006 as resolved in Sprint 2.

### TD-007 — Build Tooling Not Committed
**Status**: ✅ RESOLVED (Sprint 2 pre-checklist — `build/README.md` committed per SPRINT2_PLANNING §2)
**Evidence**: SPRINT2_PLANNING §12 lists TD-007 as resolved in Sprint 2.

### TD-008 — RoleMiddleware Role Injection Not Parameterized (NEW — Sprint 2)
**Status**: 🔄 DEFERRED
**Severity**: Low
**Description**: `RoleMiddleware` infers the required role from the URL path prefix rather than receiving the role as a constructor argument or middleware parameter. This is an approved deviation from the non-normative `RoleMiddleware:couple` notation in the `Router.php` docblock. The current path-prefix approach is correct for all Portal paths (`/app/couple`, `/app/crew`, `/app/admin`). A future sprint adding portal paths with different prefix structures would require either an additional entry in `PATH_ROLE_MAP` or a formal parameterized middleware implementation via DRP.
**Resolution**: Parameterized middleware support requires modifying `MiddlewarePipeline::wrap()` to parse a colon-suffix (`ClassName:param`) and pass the parameter to a constructor or `configure()` method. This requires a Design Revision Pack (modifying frozen kernel).
**Recommended Sprint**: Sprint 5 or 6 (when portal complexity warrants it). Alternatively, track as a pre-Sprint 3 DRP.

### TD-009 — No Rate Limiting on Password Reset Requests (NEW — Sprint 2)
**Status**: 🔄 DEFERRED
**Severity**: Low
**Description**: `POST /forgot-password` has no per-IP or per-email rate limit. Repeated reset requests generate token files in `storage/cache/` and log entries. Storage exhaustion is possible under sustained attack.
**Resolution**: Apply IP-based throttle (same lockout pattern as AUTH-001) or enforce a minimum interval between reset requests per email.
**Recommended Sprint**: Sprint 3 pre-checklist or security hardening pass.

### TD-010 — Expired Reset Token Files Not Purged (NEW — Sprint 2)
**Status**: 🔄 DEFERRED
**Severity**: Low
**Description**: Reset token files expire after 1 hour but are only deleted on use or on expiry detection during a reset attempt. Tokens for emails that never followed the link remain in `storage/cache/` indefinitely. No automated cleanup exists.
**Resolution**: Add a scheduled cleanup task that deletes `reset_token_*.json` files where `expires_at < time()`. This can be triggered on each `POST /forgot-password` submission as a housekeeping step.
**Recommended Sprint**: Sprint 3 (low-friction addition to `requestPasswordReset()`).

---

## 5. Architecture Findings

### AF-001 — CSRF Middleware Not Yet Active on Any Route
**Status**: ✅ RESOLVED (Sprint 2 — AUTH-001 through AUTH-003 wired all POST routes)

### AF-002 — Module-Local View Path Pattern Not Supported by Frozen Kernel (NEW — Sprint 2)
**Status**: 🔄 OUTSTANDING — Documented deviation, no action required at this time
**Description**: SPRINT2_PLANNING §5 and §7 specify auth views at `app/modules/Auth/views/`. However, `BaseController::render()` (frozen kernel) resolves all views from `app/views/`. This conflict was detected during AUTH-001 and resolved by placing views at `app/views/auth/` (approved by Human Approver, documented in `30_AUTH001_Conflict.md`). The deviation is consistent across all three Sprint 2 auth views (`login.php`, `forgot_password.php`, `reset_password.php`).
**Architectural Impact**: Low — the `app/views/auth/` location is clear and consistent. Future modules using module-local views would require either a DRP to modify `BaseController::render()` or a consistent decision to use the centralized `app/views/{module}/` pattern.
**Future Consideration**: If module-local view resolution is required for Sprint 3+ workspace or theme modules, a DRP should be prepared to add an override mechanism to `BaseController::render()`.

### AF-003 — RoleMiddleware Path-Prefix Inference (NEW — Sprint 2)
**Status**: 🔄 OUTSTANDING — Documented deviation, tracked as TD-008
**Description**: `RoleMiddleware` uses URL path prefix inference instead of constructor injection for role resolution. Full detail in TD-008.

---

## 6. Documentation Review

### 6.1 ADR v1.0 Compliance

| Decision | ADR Requirement | Implementation | Compliant |
|---------|----------------|----------------|-----------|
| No SQL outside Model | ADR §1 | All SQL in `AuthModel` | ✅ |
| Middleware implements `MiddlewareInterface` | ADR §3 | Both middleware classes implement it | ✅ |
| Session lifecycle in `Session.php` | ADR §5 | Timeout helpers in `Session.php`; bind/destroy in `AuthService` | ✅ |
| Token storage in filesystem (no schema change) | ADR §DB | `storage/cache/reset_token_*.json` | ✅ |

### 6.2 DBP v1.1 Compliance

| DBP Rule | Implementation | Status |
|---------|----------------|--------|
| Thin controllers (≤20 lines per method) | All 6 controller methods reviewed | ✅ |
| All queries parameterized | Both `AuthModel` queries use `?` placeholders | ✅ |
| Never SELECT * | All SELECTs name columns explicitly | ✅ |
| No business logic in controller | Confirmed — only delegation and redirect | ✅ |
| `escape_html()` on all view output | All three views confirmed | ✅ |

### 6.3 MIB v1.1 Compliance

All AUTH-001 through AUTH-004 acceptance criteria are satisfied by the implementation. The implementation sequence followed the documented dependency order (AUTH-001 → AUTH-002 → AUTH-004 → AUTH-003).

### 6.4 Documented Implementation Clarifications

These approved clarifications should be reflected in future documentation updates (not required before Sprint 3):

| Clarification | Document | Decision |
|--------------|---------|---------|
| Auth views placed at `app/views/auth/` instead of `app/modules/Auth/views/` | SPRINT2_PLANNING §5, §7 | Human Approver approved. `BaseController::render()` is frozen; module-local view paths are not supported without kernel modification. |
| `RoleMiddleware:couple` colon-suffix notation in `Router.php` docblock is non-normative | Router.php docblock, MiddlewarePipeline.php | Human Approver confirmed notation is illustrative only. Path-prefix inference approved. |

---

## 7. Conflict Review

### 7.1 AUTH-001 Conflict — Module View Path vs. Frozen Kernel

**Conflict**: `SPRINT2_PLANNING.md §5` specified `app/modules/Auth/views/login.php`. `BaseController::render()` (frozen) resolves views only from `app/views/`. The two documents were irreconcilable without a kernel modification or an approved deviation.

**Handling**: Conflict was detected, reported with full document references, and three resolution options were proposed (move view, modify kernel, override render). The Human Approver selected Option A (centralized view location). Implementation proceeded only after approval.

**Governance Assessment**: ✅ Correctly handled. The AI stopped at the conflict, did not guess, documented the options precisely, and waited for explicit instruction. The conflict protocol (AI.md §27–28) functioned as intended.

**DRP Required?** No. The deviation is a file-location clarification within existing architectural boundaries. No new architectural pattern was introduced. Suggest updating SPRINT2_PLANNING §5 and §7 to reflect `app/views/auth/` as the canonical view location for Auth module views.

### 7.2 AUTH-004 Conflict — RoleMiddleware Parameterization vs. Frozen MiddlewarePipeline

**Conflict**: `Router.php` docblock (line 111) showed a non-normative example using `'App\\Middleware\\RoleMiddleware:couple'` colon-suffix notation. `MiddlewarePipeline::wrap()` (frozen) uses `new $class()` with no constructor arguments. These two representations were irreconcilable without either modifying the frozen pipeline or ignoring the docblock example.

**Handling**: Conflict was detected, reported, and four resolution options were proposed (path inference, colon parsing, per-role subclasses, kernel extension). The Human Approver confirmed the docblock example is non-normative and selected path-prefix inference. The resolution is formally documented in `RoleMiddleware.php` class docblock, `33_AUTH004_conflict.md`, and this review (AF-003/TD-008).

**Governance Assessment**: ✅ Correctly handled. The conflict protocol functioned correctly. The resolution is architecturally sound for current portal structure.

**DRP Required?** Not now. A DRP will be required if a future sprint needs middleware role injection for non-portal-prefixed paths. This is tracked as TD-008 with a recommended future sprint.

---

## 8. Sprint Acceptance

### ✅ ACCEPTED WITH OBSERVATIONS

**Reasoning:**

All four Sprint 2 deliverables are implemented, syntactically valid, and architecturally compliant:
- AUTH-001: Unified login, bcrypt, IP lockout, session binding, session fixation prevention
- AUTH-002: CSRF-protected POST-only logout, session destruction, no-cache headers
- AUTH-004: `AuthMiddleware` (authentication + inactivity + absolute timeout), `RoleMiddleware` (path-inferred role guard), `Session.php` timeout helpers
- AUTH-003: Token generation (`random_bytes`), filesystem persistence, 1-hour expiry, bcrypt update, single-use invalidation

Two approved deviations exist (module view paths, RoleMiddleware parameter injection). Both were disclosed, approved, and documented during implementation. Neither represents an architectural violation.

The Sprint 0 kernel is intact — 11 of 12 kernel files are unmodified. The single approved extension (`Session.php`) is additive-only.

**Observations:**
1. `AuthService` is 700+ lines post-Sprint 2. Consider a service extraction pass (e.g., `PasswordResetService`, `LockoutService`) if Sprint 3 adds more business logic to the Auth module.
2. Three new low-severity technical debt items registered (TD-008, TD-009, TD-010).
3. `storage/cache/` reset token accumulation should be addressed in Sprint 3 (TD-010).

---

## 9. Recommended Next Step

**Proceed to Sprint 3**

Both identified conflicts were correctly resolved under governance. The two approved deviations do not require a DRP — they are file-location and middleware-registration clarifications within existing architectural boundaries, not new architectural decisions that contradict approved documents.

The three new technical debt items (TD-008, TD-009, TD-010) are low-severity and do not block Sprint 3. TD-009 (reset rate-limiting) and TD-010 (token cleanup) are recommended for Sprint 3 pre-checklist consideration.

TD-002 (CSP) should be added to the Sprint 3 pre-checklist as previously recommended.

**Documentation update note**: Future documentation synchronization should update SPRINT2_PLANNING §5 and §7 to reflect the `app/views/auth/` view location and the non-normative nature of the `RoleMiddleware:couple` docblock example. This is a documentation maintenance task, not a prerequisite for Sprint 3.

---

## 10. Lessons Confirmed

| Lesson | Evidence |
|--------|---------|
| **Kernel freeze respected** | 11 of 12 freeze-listed files are byte-identical to Sprint 0 baseline. `Session.php` received additive-only changes under the approved extension clause. |
| **Conflict protocol effective** | Both AUTH-001 and AUTH-004 conflicts were detected, reported, and resolved without guessing or unauthorized kernel modification. The protocol added one prompt cycle per conflict but prevented two incorrect implementations. |
| **Prompt Library sufficient** | AUTH-001 through AUTH-004 were implemented within documented scope. No undocumented files were created. No undocumented patterns were introduced. |
| **Planning accuracy** | Estimated AI coding sessions: 5 (AUTH-001×2, AUTH-002×1, AUTH-004×1, AUTH-003×1). Actual sessions: 5 (with one revision session for AUTH-004 conflict). Planning was accurate. |
| **Documentation quality** | Two documentation ambiguities were detected (view paths, colon-suffix notation). Both were present in the planning documents before Sprint 2 began. These should be resolved via documentation sync before Sprint 3 planning. |
| **AI governance effectiveness** | The AI stopped at both conflicts without proceeding. It provided structured option analysis in each case. Human Approver decisions were recorded and implemented exactly as specified. The governance system prevented both a kernel violation (modifying `MiddlewarePipeline.php`) and an undocumented pattern introduction (per-role middleware subclasses). |
| **AuthService growth** | The auth service absorbed 4 sprint tasks in sequence. Future Sprint 3 tasks should evaluate whether Auth business logic should be split into multiple services before new Auth features are added. |

---

## 11. Out of Scope

The following items are intentionally deferred. Their absence is by design.

| Domain | Implementing Sprint |
|--------|-------------------|
| Workspace provisioning, creation, and management | Sprint 3 |
| Content Security Policy (CSP) header | Sprint 3 pre-checklist |
| Parameterized middleware support (`RoleMiddleware:param`) | Future sprint (DRP required) |
| Module-local view path resolution (`app/modules/{Module}/views/`) | Future sprint (DRP required if needed) |
| SMTP email delivery for password reset | Post-MVP / production deployment |
| MFA / two-factor authentication | Not in scope |
| Remember-me / persistent sessions | Not in scope |
| API authentication (JWT, token-based) | Not in scope |
| Crew password self-service reset | Not in scope (Couple portal — Sprint 3+) |
| Rate limiting on `/forgot-password` | Sprint 3 pre-checklist (TD-009) |
| Expired reset token cleanup job | Sprint 3 (TD-010) |
| Google Fonts self-hosting | Sprint 3+ (TD-005) |
| Log rotation | Pre-launch (TD-003) |

---

## 12. Architecture Scorecard

| Dimension | Score | Notes |
|-----------|-------|-------|
| Architecture | Excellent | Layer separation clean; service layer coherent; middleware correct; kernel intact |
| Security | Excellent | All required security primitives used correctly; no credentials logged; CSRF on all POST routes |
| Maintainability | Good | AuthService is large but well-structured; two documented deviations are clear and justified |
| Scalability | Appropriate | Middleware pattern extensible; timeout config externalized to `config/session.php` |
| Documentation | Excellent | All classes and methods fully docblocked; implementation memories complete; conflicts documented |
| Execution | Excellent | All four tasks delivered on scope; two conflicts handled correctly; no kernel violations |
| Technical Debt | Good | 3 new low-severity items; 2 Sprint 1 items resolved; all items tracked and scheduled |
| Overall | **Strong** | Sprint 2 objectives fully achieved; governance system validated under conflict conditions |

---

## 13. Framework Metrics — Sprint 2 State

| Metric | Sprint 1 | Sprint 2 | Change |
|--------|---------|---------|--------|
| Kernel Classes | 16 | 16 | 0 (Session.php extended, not new) |
| Auth Module Files | 0 | 4 (`AuthController`, `AuthService`, `AuthModel`, `routes.php`) | +4 |
| Auth View Files | 0 | 3 (`login.php`, `forgot_password.php`, `reset_password.php`) | +3 |
| Middleware Files | 1 | 3 (+ `AuthMiddleware`, `RoleMiddleware`) | +2 |
| Active POST Routes | 0 | 4 (all CSRF-protected) | +4 |
| Technical Debt Items | 4 (1M, 3L) | 5 (1M, 4L) | +1 net (2 resolved, 3 new) |
| Conflicts Encountered | 0 | 2 | +2 (both resolved) |
| DRPs Issued | 0 | 0 | 0 |
| External CDN Dependencies | 0 scripts, 1 CSS | 0 scripts, 1 CSS | 0 change |

---

## 14. Final Recommendation

> **Sprint 2 Closed — Proceed to Sprint 3**

All Sprint 2 tasks (AUTH-001, AUTH-002, AUTH-004, AUTH-003) are implemented, reviewed, and architecturally sound. The Sprint 0 kernel is intact and unmodified beyond the approved `Session.php` extension. Both documentation conflicts were correctly resolved under governance without kernel modification or undocumented pattern introduction.

Three new low-severity technical debt items are registered and scheduled. None blocks Sprint 3.

**Sprint 3 pre-checklist should include:**
1. TD-002 — Add Content Security Policy headers
2. TD-009 — Add per-IP rate limiting to `POST /forgot-password`
3. TD-010 — Add expired reset token cleanup to `requestPasswordReset()`
4. Documentation sync: update SPRINT2_PLANNING §5/§7 view path references and Router.php docblock to reflect approved clarifications

---

*Review conducted: 2026-07-05*
*Reviewed by: Independent AI Architecture Audit*
*Sprint 2 implementation commits: 50e9b19 (AUTH-001) · 499f8c2 (AUTH-002) · 94ea225 (AUTH-004) · AUTH-003 pending commit*
*Sprint 1 baseline: 01dbd10*
*Sprint 0 baseline: e961697*
*Version: 1.0 (Draft — awaiting Human Approver acceptance)*
