docs(structure): govern .planning/ and register the ops runbook

- docs/STRUCTURE.md: add a .planning/ registry (vision/roadmap/history/breakdown
  roles), broaden the roadmap role, add enforcement items 8-10.
- tools/check_requirements.py: scan .planning/ too - every .planning/**.md must be
  registered, registered files must exist, requirement rows rejected there.
- .planning/: PROJECT.md, ROADMAP.md, MILESTONES.md, and milestones/v0.5/M3/
  {roadmap.md,task_breakdown.md}. M3 breakdown holds only the tasks; decisions live
  in the contracts.
- docs/DEPLOYMENT.md: add the ops runbook (registered as ops).
This commit is contained in:
2026-09-19 10:12:32 -07:00
parent 90e862cfb0
commit 6435199f08
8 changed files with 234 additions and 7 deletions
+111
View File
@@ -0,0 +1,111 @@
# Deployment
Role: ops. This is the canonical runbook for getting a build from a working tree to a live
dev/test host. `deploy/INVENTORY.md` is the only source for what runs where (hosts, ports,
container names). `docs/UAT-PRACTICE.md` is the human walk once the app is up; this doc is what
makes it up.
## 1. Topology
| Node | Role | Reach |
|---|---|---|
| Ty dev box (Windows) | code, git push to Gitea. No Docker CLI. | LAN + Tailscale |
| Gitea (`gitea.vps1.afterthedemo.com`) | source of truth for the repo. SSH port closed; HTTPS only. | Tailscale |
| `ubudual5060` (UAT host, LAN `192.168.1.202`, Tailscale `100.64.0.9`) | builds and runs Docker images for UAT. | LAN + Tailscale |
The dev box never builds images. The UAT host pulls source from Gitea and builds locally.
Do not install Docker on the dev box to work around this; the pull-build path is the plan.
## 2. Access (already provisioned; do not re-do)
- Dev box → UAT host: `ssh ubudual5060` (key `~/.ssh/ubudual5060`, wired in `~/.ssh/config`).
Unattended, passphrase-less.
- UAT host → Gitea: HTTPS with a read-only token named `5060-pull`, stored in
`~/.git-credentials` on the host. The host clones/pulls with the plain HTTPS URL;
no key material in the repo.
- Deploy checkout on the UAT host: `~/slab-deploy` (branch `main`).
The pre-existing `~/SlabUploader` on that host is a stale work clone — do not use it.
If access is broken, fix it here before touching compose. Do not commit credentials to the repo.
## 3. Build and deploy (M3)
Precondition: the M3 scaffold exists on the branch being deployed — `frontend/Dockerfile`,
`backend/Dockerfile`, and `deploy/.env` on the host. `main` currently
has `frontend/Dockerfile` only; `backend/` lands with M3 implementation.
Prompts live in SQLite (`docs/contracts/PROMPTS.md`); no prompt files ship with the image.
1. Push the branch to Gitea from the dev box.
2. On the UAT host:
```
cd ~/slab-deploy
git fetch origin && git checkout <branch> && git pull
cp deploy/.env.example deploy/.env && chmod 600 deploy/.env # first time only
docker compose -f deploy/docker-compose.yml --env-file deploy/.env build
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d
```
3. Verify with the smoke checks in §5.
### `.env` values
Generate on the UAT host, never in the repo:
- `SLAB_AES_KEY` — `openssl rand -base64 32`
- `WOO_BASE_URL` — the store, `https://` only. FastAPI refuses `http://` at startup.
- `APP_HOSTNAME` — from `deploy/INVENTORY.md`.
- `FRONTEND_PORT` / `FASTAPI_PORT` — see the port override below.
Woo Application Password and inference keys are not in `.env`; they are entered in the app's
Settings and stored AES-GCM encrypted in the app's SQLite.
### Port binding for the dev-test environment
`deploy/docker-compose.yml` binds `127.0.0.1` only (Caddy is the TLS edge in the full stack).
The dev-test environment is reached directly on a port from the project block
`10880`–`10889` (per `deploy/INVENTORY.md`) and **must be reachable from any machine on
`192.168.1.0/24`**, so the deploy must override the port bindings to `0.0.0.0`
(compose override file or `ports:` edit scoped to this deploy). Never bind `:10880`
(Ty's local Node/SvelteKit) or Ty's `:18080`.
M3 takes over port `10881` from the M2 container (`slabuploader-m2-overnight`),
per the replace-prior-container rule in `deploy/INVENTORY.md`.
Remove the M2 container when the M3 one is healthy.
## 4. Local loop (no UAT host needed)
- Frontend: `cd frontend && npm install && npm run dev` (Vite).
- Backend: `uvicorn` from `backend/` once the M3 scaffold lands; needs the same env vars as §3.
- Tests: frontend `npm test` (Playwright config in `frontend/`); backend tests land with M3.
Use the local loop for iteration; use §3 for anything Ty must see on a phone.
## 5. Smoke checks (after every deploy)
Run from the UAT host, then from a phone on the same network (LAN or Tailscale):
1. `curl -fsS http://127.0.0.1:<frontend-port>/api/health` on the host returns ok.
2. Phone browser opens the inventory URL (`deploy/INVENTORY.md`) and the app loads.
3. `docker compose -f deploy/docker-compose.yml --env-file deploy/.env ps` shows both services
up; `docker logs` for `fastapi` shows no startup refusal (WOO_BASE_URL is `https://`).
4. `test-woo` in app Settings reports ok (store reachable, taxonomy synced).
If a check fails, the deploy is not live. Roll back per §6 before debugging on the live port.
## 6. Rollback
1. `docker compose -f deploy/docker-compose.yml --env-file deploy/.env down` on the UAT host.
2. Check out the last known-good ref in `~/slab-deploy` and re-run §3 steps 2–3.
3. Re-run §5. Volumes `slab_db` and `slab_images` persist across rollback; do not remove them
unless the data itself is the problem.
## Where each rule lives
| Rule | Source |
|---|---|
| Hosts, ports, container names | `deploy/INVENTORY.md` |
| Compose shape, env contract | `deploy/docker-compose.yml`, `deploy/.env.example` |
| UAT walk once the app is up | `docs/UAT-PRACTICE.md` |
| M3 scope and exit | `docs/MILESTONES.md`, `docs/REQUIREMENTS.md` |
| Woo auth model | `AGENTS.md`, `docs/contracts/CONTENT-WOO.md` |
+23 -2
View File
@@ -1,7 +1,7 @@
# Structure contract
This file defines the shape of the `docs/` system and how requirements enter it.
`tools/check_requirements.py` enforces it.
This file defines the shape of the `docs/` system and the `.planning/` tree, and how requirements
enter it. `tools/check_requirements.py` enforces it.
If the repo and this file disagree, fix one deliberately. Do not let them drift.
@@ -21,6 +21,7 @@ Every Markdown file under `docs/` is registered here. `docs/archive/**` and `doc
| `docs/capabilities/CREATE.md` | capability |
| `docs/contracts/CONTENT-WOO.md` | contract |
| `docs/UAT-PRACTICE.md` | practice |
| `docs/DEPLOYMENT.md` | ops |
| `docs/capabilities/README.md` | index |
| `docs/contracts/README.md` | index |
| `docs/contracts/WOO-API.md` | contract |
@@ -30,6 +31,18 @@ Every Markdown file under `docs/` is registered here. `docs/archive/**` and `doc
| `docs/contracts/UX.md` | contract |
| `docs/contracts/TEST-SPEC.md` | contract |
Every Markdown file under `.planning/` is registered here. `.planning/` holds planning artifacts
only and is **not** a second requirements home: `docs/REQUIREMENTS.md` and `docs/MILESTONES.md`
remain the point of record, and `.planning/` files cite requirement IDs without defining them.
| Path | Role |
|---|---|
| `.planning/PROJECT.md` | vision |
| `.planning/ROADMAP.md` | roadmap |
| `.planning/MILESTONES.md` | history |
| `.planning/milestones/v0.5/M3/roadmap.md` | roadmap |
| `.planning/milestones/v0.5/M3/task_breakdown.md` | breakdown |
Roles:
- **index** — navigation only.
@@ -40,6 +53,11 @@ Roles:
- **capability** — behavior and acceptance for one capability; cites IDs, does not define them.
- **contract** — cross-cutting interface detail; cites IDs, does not define them.
- **practice** — human walkthrough.
- **ops** — runbook for hosts, builds, and release; cites inventory and contracts, defines no requirements.
- **vision** — overall project vision and current state; cites IDs, defines none.
- **roadmap** — version or milestone objectives; cites IDs, defines none.
- **breakdown** — ordered task list for a version or milestone; cites IDs, defines none.
- **history** — historical log of shipped milestones; cites IDs, defines none.
## ID namespaces
@@ -90,6 +108,9 @@ Nothing outside `REQUIREMENTS.md` may define a requirement. Capability and contr
5. `Test` references resolve: a path given for a `shipped` requirement exists; an ID given resolves to a defined requirement; `—` is allowed.
6. Requirement rows appear only in `REQUIREMENTS.md`.
7. Every ID listed in `MILESTONES.md` exists in `REQUIREMENTS.md`.
8. Every `.planning/` Markdown file is registered above.
9. Every registered `.planning/` file exists on disk.
10. Requirement rows appear nowhere outside `REQUIREMENTS.md`, including `.planning/`.
## Phase 2 migration (complete)