# Mettle — Architecture and Design Decisions

Decisions made when a requirement was ambiguous. Each entry references the
relevant requirement ID and explains the choice.

---

## DEC-001 — Web-based migration runner (Phase 0)

**Requirement:** The production server has no SSH, so Phinx cannot be run from
the command line on the server.

**Decision:** Expose a protected POST endpoint `/admin/maintenance/migrate` that
invokes Phinx programmatically via its PHP API. Access requires either an active
admin session or a one-time `MIGRATE_TOKEN` from `.env`. The token should be
rotated after each use.

**Alternative considered:** A standalone PHP script uploaded and run via cPanel's
"Run PHP Script" feature. Rejected because it requires manual steps and is harder
to audit.

---

## DEC-002 — .env file location (Phase 0)

**Requirement:** Configuration must live outside the web root.

**Decision:** `bootstrap.php` looks for `.env` first in `dirname(METTLE_ROOT)`
(one level above the project, outside `public_html/`) and falls back to
`METTLE_ROOT` for local development. This supports both the preferred layout
(domain root → `public/`) and the shared-hosting fallback layout
(`public_html/` fixed, app in `~/Mettle/`).

---

## DEC-003 — No Composer on the server (Phase 0)

**Requirement:** Composer is not available on the production server.

**Decision:** The `vendor/` directory is included in the deployment zip produced
by `composer run package`. The `.gitignore` excludes `vendor/` from version
control but the packaging script includes it. `composer install --no-dev
--optimize-autoloader` is run locally before packaging.

---

## DEC-004 — Tailwind CSS build (Phase 0)

**Requirement:** No Node.js on the server; Tailwind must be compiled locally.

**Decision:** Use the Tailwind CSS standalone CLI binary (no Node required) to
compile `resources/css/app.css` → `public/assets/css/app.css`. A placeholder
CSS file is committed so the app renders before the first build. The compiled
file is included in the deployment zip.

---

## DEC-005 — CSP nonce strategy (Phase 0)

**Requirement:** Content-Security-Policy with no inline scripts.

**Decision:** Generate a per-request nonce in `SecurityHeadersMiddleware`, store
it as a request attribute, and inject it into Twig globals via the container so
every `<script>` and `<style>` tag can use `nonce="{{ csp_nonce }}"`. This avoids
`unsafe-inline` while supporting htmx and Alpine.js.

---

## DEC-006 — Australia-first defaults (Phase 0)

**Requirement:** Section 12 open question — Australia only or international?

**Decision:** Default to Australia (timezone `Australia/Sydney`, metric units,
Australian English spelling, FSANZ food data). Imperial units are available as a
user preference. GDPR compliance deferred to a future phase if the audience
expands.

---

## DEC-007 — Fonts self-hosted (Phase 0)

**Requirement:** No CDN dependency in production; SIL OFL licence.

**Decision:** Use Cinzel (display/headings) and Inter (body) from Google Fonts,
downloaded and placed in `public/assets/fonts/`. Both are SIL OFL licensed.
Font files are included in the deployment zip.

---

## DEC-008 — Level curve actual progression (Phase 2)

**Requirement:** Section 7.2 states "roughly level 6–7 after 2 weeks" at ~150 XP/day.

**Finding:** The formula `floor(120 * (n-1)^1.6)` produces cumulative totals of:
- Level 5: ~2088 XP (2 weeks at 150 XP/day = 2100 XP → level 5)
- Level 6: ~3480 XP (~3.3 weeks)
- Level 10: ~10,800 XP (~10 weeks)

The spec's "level 6–7 after 2 weeks" estimate was optimistic. The actual curve is
slightly slower. **Decision:** keep the formula as specified (it is the source of
truth) and update the test expectations to match the formula. Record here so the
game designer can adjust `base` or `exponent` in `config/game.php` if desired.

---
