Operators cannot add a document today: ingestion is CLI-only, needs a host
login and a TTY for confirm_header(), /datadisk/ai-docs is mounted read-only,
ai-api has no identity, and nothing in the stack has a role that can write
doc_chunks. This designs the way in, the way out, and control over what is in
the retrieval pool. Design and schema only - no router, worker or UI code yet.
Documents in (16.1-16.9, db/004):
upload -> pre-scan -> review -> approve -> published, with the header
confirmation moved from a terminal prompt to a review screen and recorded
rather than discarded. A CHECK constraint refuses an approved row without a
confirmed number, revision and effective date, so an API bug cannot skip it.
Three roles: agent_ro unchanged, uploads_rw writes the queue only, ingest_rw
writes doc_chunks and has no HTTP surface.
Documents out (16.10-16.11, db/005):
--supersede needs a revision to keep, so a cancelled procedure cannot be
withdrawn at all. Adds withdraw (immediate, reversible, audited), restore
(refused while another revision is live) and purge (off by default). A
column grant plus a trigger let the web-facing role make a document less
citable and never more.
The pool (16.13-16.15, db/006):
pool_enabled, orthogonal to superseded: one is a claim about the document,
the other about the corpus. Retrieval requires both, so re-enabling a
withdrawn document does not make it citable. Named profiles and a
per-request override let a demo trim the corpus without mutating state on a
shared live host, and every reduced-pool answer carries a banner with the
document count, following the used_fixture_data precedent.
Two existing defects found and documented while designing this:
- ai-ingest takes PGUSER=agent_ro from api.env, a SELECT-only role, so the
Phase 3 command in the README cannot write doc_chunks (16.1).
- ingest_file() always inserts superseded = FALSE, so `--all` re-ingests a
superseded revision as live. --supersede survives only until the next bulk
run (16.10).
One commit rather than three: the upload, withdrawal and pool designs
interleave in the same spec, README and compose files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66 lines
4.3 KiB
Markdown
66 lines
4.3 KiB
Markdown
# 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.
|
||
- `~/authelia/configuration.yml` is **root-owned**: `sudo`, back up as `.bak-<purpose>-<date>`.
|
||
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.
|
||
- 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. Source of truth for tags: `db/seed/tags.csv`, derived from
|
||
`WRPS/04-plc/register-map.csv` and `WRPS/05-scada/modbus/scada-points.csv`.
|