yau-plant-assistant/CLAUDE.md
Claude 98083cd8d6 Design Phase 9 - operator document management
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>
2026-08-21 13:17:38 +10:00

4.3 KiB
Raw Blame History

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 dateconfirm_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 (07000 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.