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>
4.3 KiB
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
- No instructions for safety-critical actions. Procedural questions get document identity, revision, effective date and verbatim prerequisites — never synthesised steps.
- 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.
- 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
proxynetwork, 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 autheliafrom a Caddyfile block — it silently makes a service public. ~/authelia/configuration.ymlis 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-aiandcubeare pinned — do not add them to Watchtower's update list.openplc-runtimeis 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 its10.0.0.17binding to0.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 onimh. - No secrets in Git, compose files, logs or error messages.
0600env 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.
imhis pending — inspect it, or ask. Fixtures are marked as fixtures. - Store UTC. Convert to
SITE_TIMEZONEexactly 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 throughai-docs-worker, which has no HTTP surface. - When something fails, add the failing case to
eval/testset.jsonlbefore 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.