# M01 — Monorepo Skeleton & Toolchain

> Fresh Claude Code agent prompt. You are starting from an empty repo (or a repo with only `SPEC.md` and an empty `PROGRESS.md`).
> Estimated effort: medium (mostly boilerplate; the goal is solid foundations).

## Mission

Create the monorepo layout, set up the toolchain (composer, npm, PHPUnit, PHPStan, php-cs-fixer), and produce buildable Docker images for both the `api` and `ui` containers. Both containers start under compose, return placeholder healthchecks, and `migrate` runs an empty Phinx migration set and exits 0. **No business logic in this milestone.**

## Before you start

1. Read `SPEC.md` end-to-end once. Even if it feels long, do it — every later milestone assumes you understand the architecture.
2. Then re-read these sections carefully: §1 (Project Goals), §2 (Tech Stack), §3 (Architecture), §10 (Docker), §11 (Project Structure), §14 (Coding Conventions).
3. Verify the working tree:
   ```bash
   ls -la                  # should see SPEC.md and PROGRESS.md
   git status              # clean
   ```
4. Confirm tooling: `docker --version`, `docker compose version`, `php --version` (8.3+), `composer --version`, `node --version` (20+), `npm --version`.

## Tasks

Execute in this order. Commit nothing until acceptance passes.

### 1. Repo skeleton

Create the directory structure exactly as in SPEC.md §11. Empty placeholders are fine where files come later (e.g. `api/src/Domain/.gitkeep`).

### 2. Root files

- `.env.example` — every env var from SPEC.md §9, grouped into "Shared", "API container", "UI container" sections, with comments.
- `.gitignore` — sensible defaults: `vendor/`, `node_modules/`, `public/assets/`, `.env`, `.phpunit.cache/`, `.phpunit.result.cache`, `data/`, IDE files.
- `docker-compose.yml` — exactly as in SPEC.md §10.
- `compose.scheduler.yml` — exactly as in SPEC.md §10. You don't need to make it work end-to-end yet; you only need it to be valid YAML.
- `README.md` — minimal: project name, one-paragraph description, "see SPEC.md and milestones/ for details".

### 3. `api/` subproject

- `api/composer.json` — Slim 4, doctrine/dbal, robmorgan/phinx, monolog, php-di, vlucas/phpdotenv (dev), guzzlehttp/psr7. Dev: phpunit ^11, phpstan ^1.10, friendsofphp/php-cs-fixer ^3. Set `"type": "project"` and PSR-4 autoload mapping `App\\` → `src/`.
- `api/public/index.php` — Slim app bootstrap. Two routes for now: `GET /healthz` returning `{"status": "ok"}` (JSON), and a 404 fallback. Wire structured JSON logging via Monolog to stdout.
- `api/config/settings.php` — builds a config array from environment variables. Don't read `.env` in production; do read it in `development` via phpdotenv.
- `api/config/phinx.php` — Phinx config that reads the same env vars. Migrations dir `db/migrations`, seeds dir `db/seeds`. Both SQLite and MySQL adapters configured.
- `api/db/migrations/.gitkeep` and `api/db/seeds/.gitkeep`.
- `api/bin/console` — minimal Symfony Console app with one command: `db:migrate` that delegates to `vendor/bin/phinx migrate`. Make it executable.
- `api/docker/entrypoint.sh` — dispatcher script that switches on `$1` (`api` default → starts FrankenPHP serving `public/`; `migrate` → runs migrations and exits). Make it executable.
- `api/docker/Caddyfile` — FrankenPHP/Caddy config serving on `:8081`. Configure `/internal/*` location with the `remote_ip` matcher from SPEC.md §6 (you don't need any internal routes yet, just the protective Caddy match).
- `api/Dockerfile` — multi-stage as described in SPEC.md §10, `dunglas/frankenphp:1-php8.3-alpine` base. Install `pdo_sqlite`, `pdo_mysql`, `mbstring`, `intl`, `opcache`, `bcmath` extensions.
- `api/phpunit.xml` — testsuite includes `tests/Unit` and `tests/Integration`.
- `api/phpstan.neon` — level 8 on `src/`.
- `api/.php-cs-fixer.dist.php` — PSR-12 + strict types.
- `api/tests/Unit/SmokeTest.php` — one trivial assertion (`$this->assertTrue(true)`) so the suite runs.

### 4. `ui/` subproject

Same shape, but:
- `ui/composer.json` — Slim 4, twig/twig, slim/twig-view, guzzlehttp/guzzle, jumbojett/openid-connect-php, monolog, php-di, vlucas/phpdotenv (dev). No DBAL, no Phinx. Same dev deps as api.
- `ui/package.json` — `tailwindcss ^3`, `postcss`, `autoprefixer`, `alpinejs`, `htmx.org`. Build script: `tailwindcss -i resources/css/app.css -o public/assets/app.css --minify`.
- `ui/tailwind.config.js` — content paths covering `resources/views/**/*.twig` and `resources/js/**/*.js`. `darkMode: 'class'`.
- `ui/postcss.config.js` — autoprefixer + tailwindcss.
- `ui/resources/css/app.css` — Tailwind directives only.
- `ui/resources/js/app.js` — Alpine + htmx imports.
- `ui/resources/views/layout.twig` — minimal HTML skeleton with Tailwind classes, `<html class="dark:bg-slate-900">`, dark-mode toggle button (no JS yet, just the markup).
- `ui/resources/views/pages/hello.twig` — extends layout, says "IRDB UI — milestone 1".
- `ui/public/index.php` — Slim with Twig. One route `GET /` renders `pages/hello.twig`. One route `GET /healthz` returns `{"status":"ok","api_reachable":null,"last_api_check_at":null}`.
- `ui/docker/Caddyfile` — serves on `:8080`.
- `ui/docker/entrypoint.sh` — single mode (`ui`).
- `ui/Dockerfile` — multi-stage as in SPEC.md §10.
- `ui/phpunit.xml`, `ui/phpstan.neon`, `ui/.php-cs-fixer.dist.php`, `ui/tests/Unit/SmokeTest.php`.

### 5. `examples/` and `doc/` placeholders

- `examples/scheduler/host.crontab` — a comment-only stub for now (real content lands in M13).
- `doc/.gitkeep` — empty (docs land in M13).

### 6. CI (local)

Do **not** create any GitHub Actions workflow. CI runs locally on this server (which has Docker installed). Create `scripts/ci.sh` that an operator can invoke manually:

- Make it executable (`chmod +x scripts/ci.sh`) and start with `#!/usr/bin/env bash` and `set -euo pipefail`.
- Runs each stage in order, fails fast on the first non-zero exit, and prints a clear banner before each stage.
- Stages:
  1. `api/`: `composer install --no-interaction --prefer-dist`, then `composer stan`, `composer cs`, `composer test`.
  2. `ui/`: `composer install --no-interaction --prefer-dist`, then `composer stan`, `composer cs`, `composer test`.
  3. `ui/`: `npm ci` then `npm run build`; assert `ui/public/assets/app.css` exists.
  4. `docker compose build` from the repo root to verify both images build.
- DB driver matrix: accept an env var `DB_DRIVERS` (default `"sqlite mysql"`) and loop the api test stage once per driver, exporting `DB_DRIVER` so phpunit can pick it up. For `mysql`, skip gracefully with a warning if no local MySQL is reachable (this milestone has no DB-touching tests yet, so the loop is mostly scaffolding for later milestones).
- At the end, print a green "CI OK" line. On failure, the failing command's exit code propagates.

Also add a one-line invocation note in `README.md` under a "Local CI" heading: `./scripts/ci.sh`.

## Implementation notes

- **FrankenPHP entrypoints**: the entrypoint script must `exec` the FrankenPHP process for proper signal handling. `exec frankenphp run --config /etc/Caddyfile`.
- **Caddy `:8081` for api, `:8080` for ui** — match SPEC.md §10. Don't expose the api on 8080.
- **healthz format**: api returns `{"status":"ok"}` for now; full payload (`db`, `jobs`) lands in later milestones. Comment in the code where additional fields will go.
- **Service token bootstrap**: do not implement yet. SPEC says the api ensures `UI_SERVICE_TOKEN` exists on startup — that lands in M03 when `api_tokens` table exists.
- **Volume mount**: api's `entrypoint.sh` should `mkdir -p /data` before launching, in case the SQLite path is set but the dir wasn't created.
- **Composer scripts**: add convenience scripts in each subproject's composer.json: `test` (phpunit), `stan` (phpstan), `cs` (cs-fixer), `cs-fix` (cs-fixer fix). Lets the agent in later milestones run `composer test` instead of remembering paths.

## Out of scope (DO NOT)

- Do **not** create any database tables or migrations beyond an empty migrations dir. Tables come in M02.
- Do **not** implement any auth, tokens, RBAC, sessions. That's M03/M08.
- Do **not** install any deps not listed above without strong reason. Note any addition in PROGRESS.md.
- Do **not** write business logic. The api should respond only to `/healthz`. The ui should render only the hello page and `/healthz`.
- Do **not** wire OIDC, MaxMind, or any external service.
- Do **not** create files in `doc/` beyond `.gitkeep`.
- Do **not** touch `SPEC.md`. It is read-only for milestone agents.

## Acceptance

Run all of these. Every one must pass. If any fails, fix and re-run; do not commit until all green.

```bash
# 1. Static analysis and style
cd api && composer install && composer stan && composer cs && composer test && cd ..
cd ui  && composer install && composer stan && composer cs && composer test && cd ..

# 2. Frontend build
cd ui && npm ci && npm run build && test -f public/assets/app.css && cd ..

# 3. Compose build
docker compose build

# 4. Compose up — migrate exits 0, api and ui become healthy
cp .env.example .env
# Set required secrets in .env so containers start (use placeholder values for now):
#   APP_SECRET, UI_SECRET, UI_SERVICE_TOKEN, INTERNAL_JOB_TOKEN — any 32 hex chars each.
docker compose up -d
sleep 15

# Migrate must have exited 0:
test "$(docker compose ps -a --format '{{.Service}} {{.State}} {{.ExitCode}}' | grep migrate | awk '{print $3}')" = "0"

# Healthchecks (poll up to 60s):
for i in {1..30}; do
  curl -sf http://localhost:8081/healthz && curl -sf http://localhost:8080/healthz && break
  sleep 2
done
curl -sf http://localhost:8081/healthz | grep -q '"status":"ok"'
curl -sf http://localhost:8080/healthz | grep -q '"status":"ok"'

# Hello page renders:
curl -sf http://localhost:8080/ | grep -q "milestone 1"

docker compose down -v
```

## Handoff

1. Commit:
   ```
   feat(M01): monorepo skeleton, toolchain, docker compose builds clean

   - api/ and ui/ subprojects with composer + slim 4
   - frankenphp dockerfiles, multi-stage builds
   - phpunit, phpstan level 8, php-cs-fixer wired into composer scripts
   - tailwind build pipeline in ui/
   - empty phinx migrations directory
   - scripts/ci.sh runs the full test/lint matrix locally (no GitHub Actions)
   ```

2. Append to `PROGRESS.md`:
   ```markdown
   ## M01 — Monorepo skeleton (done)

   **Built:** repo layout per SPEC §11, both Dockerfiles, compose stack, toolchain.

   **Notes for next milestone:**
   - DB schema empty; M02 owns all tables and seeds.
   - `entrypoint.sh` for api supports `migrate` mode and calls `vendor/bin/phinx`.
   - Healthcheck payloads are stubs; later milestones extend them.
   - Service-token bootstrap deferred to M03 (needs `api_tokens` table first).

   **Deviations from SPEC:** none.
   **Added dependencies beyond SPEC §2:** none.
   ```

3. **Stop.** Do not start M02.
