diff --git a/CLAUDE.md b/CLAUDE.md index 8f462da..3c1110a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,120 +1,90 @@ -# CLAUDE.md — rules to keep front of mind +# WRPS Plant Assistant — an index to this repository, and rules that are not advisory -Plant Operations Assistant for the **Waterloo Road Pump Station (WRPS)**, deployed onto an -existing, live, shared Docker host. Full detail: `spec/BUILD-AI-CONTAINERS.md` (the build spec) and -`spec/YAU_Linux_Host_Onboarding.md` (the host brief). **Where the two conflict, the host brief wins.** +A proof-of-concept assistant that answers an operator's plain-English questions from plant +data and controlled documents. It runs on `lin001`, an existing **live, shared** Docker host +that is not ours alone. + +This file holds only what you must know *before* you act; everything else is a pointer. +Keep it that way: put detail next to the thing it describes, not here. + +## Where everything is + +| To understand | Read | +|---|---| +| What this is, and how to get oriented | `README.md` | +| Why it is built this way, and the accepted shortcuts (§14) | `spec/BUILD-AI-CONTAINERS.md` | +| The host, its history and its rules — **wins on any conflict** | `spec/YAU_Linux_Host_Onboarding.md` | +| How to deploy or rebuild it | `spec/REBUILD.md` | +| What is running right now | `status/current-state.html` | +| What is blocked, and on whom | `status/REQUESTS.md` | +| What is broken or unfinished, and ours to fix | `status/OPEN-ISSUES.md` | + +| To change | Go to | +|---|---| +| The safety contracts, one per answer class | `api/contracts.py` | +| Which class a question gets | `api/classifier.py` | +| The SQL allow-list and query caps | `api/guardrails.py` | +| The document library screens — its docstring records how the build diverges from the design | `api/documents.py` | +| How a document becomes searchable, and how it is found and cited | `ingest/ingest.py`, `api/tools/retrieval.py` | +| How a question becomes a number | `cube/model/` | +| Retention, windows and history measures | `api/tools/metrics.py` | +| Plant facts, tags and equipment | `db/seed/tags.csv` | +| The schema, roles and migrations | `db/*.sql` | +| How a container is defined — networks, volumes, env files | `compose/ai-compose.yml` | +| The operator's screen | `web/` | +| SCADA namespaces and the delivered CI Server config | `db/seed/scada-source/README.md` | +| The stand-in historian — some assumptions fail silently, not loudly | `db/README-standin-historian.md` | +| Who can reach what | `caddy/ai-routes.caddy`, `authelia/access-rules.md` | +| The eval set and its scorecard | `eval/` | ## The three lines this system does not cross -1. **No instructions for safety-critical actions.** Procedural questions get document identity, +1. **No instructions for safety-critical actions.** Procedural answers give 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. +2. **No recommended setpoints or operating parameters.** Advisory answers give evidence and + documented limits, then defer. Never a number presented as the answer. +3. **No answers outside the evidence.** Zero rows is "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. +**These are code paths, not prompt instructions.** A safety rule living only in a prompt is not +implemented. Fix failures in the classifier, Cube or ingestion — never by adding to the prompt. +When a class is uncertain, choose the **more restrictive** one. -When class is uncertain, choose the **more restrictive** class. Procedural beats Reference. -Advisory beats Historical. Partly-advisory is advisory. +## Do not break these — a live host, and a live PLC -## 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. +- **`openplc-runtime` is live control.** Never restart, update or reconfigure it as a side effect + of AI work, and never move its `10.0.0.17` binding to `0.0.0.0` — that binding is the only thing + keeping unauthenticated Modbus off the internet. +- **Never install on, write to or restart `cicore1`.** Never exceed read-only on `imh`. - **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`. + One deliberate exception exists for `ai.yokogawa.tech`; do not copy or widen it. +- **Restarting Authelia or Caddy logs out every active user.** Announce it first. +- **Growing data goes on `/datadisk`, never `/`.** Root is 62 GB and has hit 100% before. +- **Publish no host ports.** Join the `proxy` network and go through Caddy. - **No secrets in Git, compose files, logs or error messages.** `0600` env files under `~/ai/`. +- **`lin001` has 2 vCPU and 3 GiB. Do not build images on it if you can build elsewhere** + and ship the image. When you cannot, check the host is quiet first — `uptime`, and who + else is mid-demo — and say so before you start. `deploy.sh` builds on the host today, + which is why this is a judgement and not an absolute. -## Verification +## How to work here -`docker ps` showing "Up" is **not** proof. `curl -sI` the public URL and read the container -logs. **Expect `302` to the auth portal from `api`, `cube` and `lf` — but `403` from `ai`**, -which is Authelia-free and IP-matched (see the exception above). A `302` on `ai` means the -SCADA-only block was reverted; a `200` means the matcher is restricting nothing. `verify.sh` -treats the `403` as the PASS. 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). - **The design puts publishing and restoring behind `ai-docs-worker`, which has no HTTP surface. - That is not what is built.** `ai-docs-worker` was never deployed: `api/documents.py` serves - `POST /documents/review/{id}/approve` and `POST /documents/restore` directly on `ai-api`. The - boundary still exists, but it moved — it is enforced by `db/005`'s trigger and by which - database role each function opens its connection with, not by deployment. Read the docstring - at the top of `api/documents.py` before touching that path, and do not assume an HTTP request - cannot publish. -- When something fails, add the failing case to `eval/testset.jsonl` *before* fixing it. -- A defect you are **not** fixing now goes in `status/OPEN-ISSUES.md`, not in a comment. - Read its header first — it says what belongs there and what belongs in `REQUESTS.md` or - in §14 of the build spec, which is closed by decision and must not be re-raised. -- 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 | `STN_LEVEL`, on Modbus station `WRPS_PLC` | `WRPS/05-scada/modbus/wrps_modbus_point_df.qli` | -| **CI Server item** | **`AID.WRPS.STN.LEVEL`** | `WRPS/05-scada/modbus/wrps_item_df.qli` | - -The point layer is written `WRPS_PLC:STN_LEVEL` in the `.qli` export: the Modbus **station** -name, a colon, then the point name. There are 49 points and 49 items, one to one here, but -that is a property of this small station and not a rule. - -**`PS_*` names are not a namespace.** An earlier delivery of the point list carried -`PS_STN_WET_WELL_LEVEL` and its like where the item name belongs. They were a proposal -derived from the PLC register map that CI Server never adopted, and they were removed -from this repository on 2026-09-01: the tag seed, the alarm bits, the fixtures, the Cube -models and the exam are all keyed on the item now. **Do not reintroduce them.** The only -legitimate `PS_` names are the four Modbus poll groups — `PS_STATUS_BITS`, `PS_PUBLISHED`, -`PS_SETPOINTS`, `PS_SIM_CONTROL` — which are live configuration and appear only in a -`poll_group` column. The delivered CI Server configuration lives in -`db/seed/scada-source/`; read its README before touching any of this. - -**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. +- **`docker ps` showing "Up" is not proof.** Run `scripts/verify.sh` and read the container logs. +- **When an answer is wrong, prove Cube and retrieval by hand before touching the agent** — + otherwise a wrong answer has four possible causes. +- **Do not invent schema.** `imh` is still pending — inspect it, or ask. Fixtures are marked. +- **Do not hand-edit generated files.** `db/seed/historian_items.csv` comes from + `scripts/gen_historian_items.py`. +- **Store UTC. Convert to local exactly once, in Cube.** Never do timezone maths in a prompt. +- **A document is citable only after a human confirms its number, revision and effective date.** + Never add a path that ingests an unconfirmed header. +- **When something fails, pin it before you fix it.** A question answered wrongly goes in + `eval/testset.jsonl`; anything provable without a model goes in `api/tests/`. Deployment and + migration defects have no test home yet, so they go in `status/OPEN-ISSUES.md`. +- **A defect you are not fixing now goes in `status/OPEN-ISSUES.md`** — read its header first; + it says what belongs there and what belongs elsewhere. +- **Never quote a count in prose.** Point at the file. Container counts, eval-set sizes and + assumption counts have all gone stale in this repo; the file never does. +- **Small commits, one concern each.** +- **If a change alters an accepted phase, re-run that phase's gate.** +- **Snapshot before you edit anything on the host** — config, database or seed. + Name back-ups `.bak--`.