# CLAUDE.md — rules to keep front of mind Plant Operations Assistant for the **Waterloo Road Pump Station (WRPS)**, deployed onto an existing, live, shared Docker host. Full detail: `BUILD-AI-CONTAINERS.md` (the build spec) and `YAU_Linux_Host_Onboarding.md` (the host brief). **Where the two conflict, the host brief wins.** ## The three lines this system does not cross 1. **No instructions for safety-critical actions.** Procedural questions get document identity, revision, effective date and verbatim prerequisites — never synthesised steps. 2. **No recommended setpoints or operating parameters.** Advisory questions get evidence, ranges, outcomes, documented limits, then an explicit deferral. Never a number presented as the answer. 3. **No answers outside the evidence.** Zero rows means "no records found", never a plausible figure. These are **code paths, not prompt instructions.** A safety rule living only in a prompt is not implemented. The classifier runs first; each class has a Pydantic contract validated in Python after generation and before returning. Contract failure -> regenerate once -> error. Never return. When class is uncertain, choose the **more restrictive** class. Procedural beats Reference. Advisory beats Historical. Partly-advisory is advisory. ## Host rules — inherited, non-negotiable - Growing data goes on **`/datadisk`**, never `/`. Root is 62 GB and has hit 100% before. - **No published host ports for anything we build.** Join the external `proxy` network, reach it through Caddy. Some existing containers do publish (`caddy`, `wireguard`, `mosquitto`, `chirpstack-gateway-bridge`, `openplc-runtime`) — they carry non-HTTP protocols. We do not. - **Never omit `import authelia`** from a Caddyfile block — it silently makes a service public. One deliberate exception, applied 2026-08-28 at the customer's direction: `ai.yokogawa.tech` admits `remote_ip 10.0.0.21` (`cicore1`) unauthenticated and 403s everything else. It does not generalise — do not copy it, and do not widen it. `api.yokogawa.tech` stays gated. - `~/authelia/configuration.yml` is **root-owned**: `sudo`, back up as `.bak--`. Restarting Authelia **logs out every active user** — announce it first. - AD group membership must be **DIRECT**. Nested membership silently fails. - `pg-ai` and `cube` are pinned — **do not add them to Watchtower's update list**. - **`openplc-runtime` is live control for this demo.** Never restart, update or reconfigure it as a side effect of AI work. Do not copy its published-port pattern, and never change its `10.0.0.17` binding to `0.0.0.0` — that binding is what keeps unauthenticated Modbus off the internet. - Never install on, write to or restart `cicore1`. Never exceed read-only on `imh`. - **No secrets in Git, compose files, logs or error messages.** `0600` env files under `~/ai/`. ## Verification `docker ps` showing "Up" is **not** proof. `curl -sI` the public URL, expect `302` to the auth portal, and read the container logs. Prove each layer without the LLM first: prove Cube returns the right number by hand, prove retrieval finds the right procedure by hand, *then* wire the agent. ## Working conventions - Prefer additive changes. Snapshot config before editing. - **Do not invent schema.** `imh` is pending — inspect it, or ask. Fixtures are marked as fixtures. Before connecting it, work through `db/README-standin-historian.md` — the stand-in's shape is reasoned from the SCADA config, but **every SQL Server detail in it is a guess**, and two of the twelve listed assumptions fail silently rather than erroring. - Store UTC. Convert to `SITE_TIMEZONE` exactly once, in Cube. Never do timezone maths in a prompt. - Fix failures in the classifier, Cube or ingestion — **not by adding instructions to the prompt.** - **A document becomes citable only after a human confirms its number, revision and effective date** — `confirm_header()` at a terminal, or the Phase 9 review screen. Never add a path that ingests an unconfirmed header. The web-facing role may only make a document **less** citable (withdraw); publishing and restoring go through `ai-docs-worker`, which has no HTTP surface. - When something fails, add the failing case to `eval/testset.jsonl` *before* fixing it. - Small commits, one concern each. If a change alters an accepted phase, re-run that phase's gate. ## The plant Waterloo Road Pump Station: a three-pump wastewater station. Wet well `WW-101` (0–7000 mm, 120 m³/m), duty/assist/assist pumps `PU-301/302/303` on a common VSD speed reference, discharging through manifold `MAN-301` against 22 m static lift. Spill weir at 6000 mm, `LSHH-102` at 5500 mm. Control runs on `openplc-runtime`; Yokogawa CI Server on `cicore1` polls it over Modbus TCP and historises the result. **Four namespaces name the same measurement. Know which one you are holding.** | Layer | Example | Source of truth | |---|---|---| | Instrument tag | `LIT-101` | `WRPS/01-design-doc` | | PLC symbol + address | `%QW0` | `WRPS/04-plc/register-map.csv` | | CI Server point | `PS_STN_WET_WELL_LEVEL` | `WRPS/05-scada/modbus/scada-points.csv` | | **CI Server item** | **`AID.WRPS.STN.LEVEL`** | `WRPS/05-scada/modbus/wrps_item_df.qli` | **The historian is keyed on the item, and only the item.** Modbus carries register numbers, not names, so the point layer and the item layer are free to differ — and they do. Keying history on a point name is what produced all three Phase 5 findings. `db/seed/tags.csv` owns plant facts; `db/seed/historian_items.csv` owns the item-to-tag mapping and is **generated** by `scripts/gen_historian_items.py` — never hand-edited. - **Equipment is asserted in exactly one place: `tags.equipment_id`.** Nothing in the history carries an equipment column. CI Server's section tree stops at the station and the three pumps; it has no wet well, weir, manifold or switchboard. - **The historian retains seven days.** Zero rows outside that window means "the historian does not go back that far", never "nothing happened". `metrics.HISTORY_RETENTION_DAYS`. - **Never hardcode a sample interval.** Read `scan_interval_seconds` from the item. The analogue groups run at 5 s and 30 s; a measure that assumed 60 s was wrong by twelvefold.