# Artisan-Migrate — Requirements

> Produced by the Reversa **Writer** (phase: generation) · doc_level: `complete`
> Generated on 2026-09-21

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

## Overview

`artisan-migrate` is a maintenance endpoint that runs the application's database migrations over HTTP. It is an **inline closure route** (`GET /artisan`, no controller, no route name) registered inside the admin route group. When hit, it calls `Artisan::call('migrate', ['--force' => true])` and unconditionally echoes the raw string `"Migrating completed<br>"`. 🟢 (`routes/web.php:31-37`)

It is a deployment/operations convenience: it lets an operator apply pending schema migrations from a browser, without shell/CLI access to the host — `--force` suppresses the production confirmation prompt that `php artisan migrate` would otherwise raise on a non-interactive/production environment. 🟡 (`routes/web.php:32-34`)

This route was **not** documented by the Scout or the Archaeologist; it was found during plan reconstruction (2026-09-19) and added as a unit because it is a privileged, side-effecting endpoint that runs schema changes behind only the admin authentication guard. 🟢 (`.reversa/state.json` `redator_progress.units` note; `routes/web.php:31`)

## Responsibilities

- Expose `GET /artisan` inside the admin group (`['web','admin']` middleware, empty admin prefix). 🟢 (`routes/web.php:23-31`, `config/admin.php:25-29`)
- Invoke `Artisan::call('migrate', ['--force' => true])` — run every pending migration in `database/migrations/` non-interactively. 🟢 (`routes/web.php:32-34`)
- Return the literal HTML text `"Migrating completed<br>"` to the browser after the call returns. 🟢 (`routes/web.php:36`)

## Business Rules

- **Idempotent-by-migration, not by request.** `migrate` only applies migrations not already recorded in the `migrations` table, so re-hitting the URL after a successful run applies nothing further; the message is printed regardless. 🟢 (Laravel migrator semantics; `routes/web.php:32-36`)
- **`--force` bypasses the production guard.** In a non-interactive/production context `php artisan migrate` aborts unless confirmed; `--force => true` runs it unconditionally, which is required for a web-triggered call (no TTY). 🟢 (`routes/web.php:33`)
- **Exit code is discarded.** The return value is captured into `$exitCode` but the only consumer (`dd($exitCode)`) is commented out; the response is always `"Migrating completed<br>"` even when the migrator reports a non-zero exit code. A thrown exception (e.g. a broken migration) is not caught here and surfaces as an HTTP `500`. Reviewed with the project owner 2026-09-21 — accepted as-is, no change scheduled. 🟢 (`routes/web.php:32,35,36`)
- **Authentication only, no authorization.** The endpoint sits behind `['web','admin']`, which authenticates but does not enforce any permission/role — every authenticated admin can trigger a production migration. 🟡 (`config/admin.php:29`; ADR-0009)
- **Side-effecting `GET`.** A schema-mutating operation is exposed over an HTTP verb that clients treat as safe/cacheable/prefetchable. Reviewed with the project owner 2026-09-21 — accepted as a known risk, kept as legacy behaviour. 🟢 (`routes/web.php:31`)

## Functional Requirements

| ID | Requirement | Priority | Acceptance criterion |
|----|-------------|----------|----------------------|
| RF-01 | Run pending migrations at `GET /artisan` | Must | An authenticated admin request applies all pending migrations (rows appear in the `migrations` table) and the response body is `Migrating completed<br>`. 🟢 |
| RF-02 | Run migrations non-interactively (`--force`) | Must | The call does not block on a production confirmation prompt; it executes to completion. 🟢 |
| RF-03 | Return a completion message | Should | After `Artisan::call` returns, the response body is the literal `Migrating completed<br>` (HTTP `200`). 🟢 |
| RF-04 | Require an authenticated admin session | Must | An anonymous request → `302` to `auth/login`; it does not run migrations. 🟢 (`routes/web.php:20,23-29`) |
| RF-05 | Surface a broken migration | Could | A migration that throws propagates as an HTTP `500` (no try/catch here). 🟡 |

## Non-Functional Requirements

| Type | Inferred requirement | Evidence in code | Confidence |
|------|----------------------|------------------|------------|
| Security | Admin authentication required (admin route group middleware `['web','admin']`) | `routes/web.php:23-31`, `config/admin.php:29` | 🟢 |
| Security | No authorization/role gate beyond authentication — any admin may run migrations | `config/admin.php:29` (absence of permission middleware); ADR-0009 | 🟡 |
| Security | Side-effecting operation exposed over `GET` (browser/proxy prefetch, no CSRF barrier since it is a read verb). Accepted as-is 2026-09-21. | `routes/web.php:31` | 🟢 |
| Availability | Long-running/failed migrations block the request thread and can leave the schema partially migrated with no rollback here | `routes/web.php:32-36` (no transaction/try-catch wrapper) | 🟡 |
| Observability | None — no log/metric/audit of who triggered the migration or its result; the exit code is discarded (`dd` commented out). Accepted as-is 2026-09-21. | `routes/web.php:32,35` | 🟢 |
| Maintainability | Closure route — cannot be serialized by `route:cache`, and is not unit-testable as an isolated controller action | `routes/web.php:31-37` | 🟡 |

> Inferred from code. Validate with the operations team.

## Acceptance Criteria

```gherkin
Given an authenticated administrator
When he makes GET /artisan
Then all pending migrations are applied and the response body is "Migrating completed<br>" with HTTP 200

Given there are no pending migrations
When GET /artisan is called again
Then nothing is applied (idempotent migrator) and the response is still "Migrating completed<br>"

Given a request with no authenticated admin session
When GET /artisan is called
Then it receives HTTP 302 redirecting to auth/login and no migration runs

Given a pending migration that throws an exception
When GET /artisan is called
Then the exception propagates as HTTP 500 (no try/catch) and the completion message does not confirm the real outcome
```

## Priority (MoSCoW)

| Requirement | MoSCoW | Justification |
|-------------|--------|---------------|
| Run pending migrations (RF-01) | Must | The endpoint's sole purpose |
| Non-interactive `--force` (RF-02) | Must | Without it the web-triggered call would abort in production |
| Admin authentication (RF-04) | Must | Enforced by the route group; the only access control present |
| Completion message (RF-03) | Should | User feedback, but does not reflect the true outcome |
| Surface a broken migration (RF-05) | Could | No explicit handling; relies on the framework's default `500` |
| Audit / authorization of who ran it (gap) | Won't (as built) | Absent in the legacy; flagged as a 🔴 gap, not current behaviour |

> Priority inferred from the endpoint being a single-purpose maintenance trigger behind the admin auth guard.

## Code Traceability

| File | Function / Class | Coverage |
|------|------------------|----------|
| `routes/web.php:31-37` | `GET /artisan` inline closure — `Artisan::call('migrate', ['--force'=>true])` + `echo` | 🟢 |
| `routes/web.php:23-30` | Admin route group (`prefix`/`namespace`/`middleware` from `config/admin.php`) | 🟢 |
| `config/admin.php:24-30` | `route.prefix=''`, `route.namespace='App\Http\Controllers'`, `route.middleware=['web','admin']` | 🟢 |
| `routes/web.php:20` | `Admin::registerAuthRoutes()` — the `auth/login` gate hit when unauthenticated | 🟢 |
| `database/migrations/` | Migration set applied by the call | 🟡 |
