Say what PS_* was, everywhere it is still mentioned

Seven mentions remain across the repository and none of them is data.
Each is a sentence recording that these names were wrong, what it cost,
and that they must not come back: the namespace tables in CLAUDE.md and
the build spec, the generator's docstring, the fixtures rewrite note, the
Phase 5 narrative in REBUILD.md and current-state.html, and the delivered
configuration's own README.

The build spec's namespace table carried the same error CLAUDE.md did -
PS_STN_WET_WELL_LEVEL listed as the CI Server point layer, sourced from a
file that was a PLC-side proposal. Both now name the real point layer,
WRPS_PLC:STN_LEVEL from wrps_modbus_point_df.qli, and both point at
db/seed/scada-source/ rather than at a path on one person's laptop.

The name-collision entry in the build spec is marked closed, and records
that it bit a second time during the rename itself.

These notes explain; they do not enforce. What actually caught the
collision was check_mapping() refusing to write, and what stops a retired
name being queried is the guard in equipment.resolve(). The prose is here
so the next person does not have to re-derive the story - not as a
control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-09-01 19:00:57 +10:00
parent f3c8020ac1
commit 73e90cf710
5 changed files with 64 additions and 22 deletions

View file

@ -42,8 +42,11 @@ Advisory beats Historical. Partly-advisory is advisory.
## 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
`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
@ -57,9 +60,18 @@ the right number by hand, prove retrieval finds the right procedure by hand, *th
- 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.
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
@ -76,9 +88,23 @@ historises the result.
|---|---|---|
| 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 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

View file

@ -191,7 +191,7 @@ deletes them.
## 7. History, so this is not relearned
The stand-in was rebuilt on 2026-08-31. The version before it was keyed on CI
Server **point** names (`PS_STN_WET_WELL_LEVEL`) when the historian is keyed on
Server **point** names (`AID.WRPS.STN.LEVEL`) when the historian is keyed on
CI Server **item** names (`AID.WRPS.STN.LEVEL`) — two layers apart. That single
substitution produced all three open Phase 5 findings, and each looked like an
independent bug: a tag that would not join, alarm times in the wrong timezone,

View file

@ -507,8 +507,15 @@ and only the last is what CI Server historises:
|---|---|---|
| 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` |
| CI Server **point** | `WRPS_PLC:STN_LEVEL` — Modbus station, colon, point name | `db/seed/scada-source/wrps_modbus_point_df.qli` |
| CI Server **item** | `AID.WRPS.STN.LEVEL` | `db/seed/scada-source/wrps_item_df.qli` |
**`PS_*` is not one of these layers.** 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. They were removed from
this repository on 2026-09-01 and must not return. 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.
`db/002_fixtures.sql` was keyed on the third. The historian is keyed on the
fourth. Everything else followed from that.
@ -522,7 +529,7 @@ switchboard.
**(a) The wet well level tag does not join — FIXED.**
`AID.WRPS.STN.LEVEL` is a real item in the `WRPS_ONE_SEC` group. The correct
reading was the one already suspected: `PS_STN_WET_WELL_LEVEL` becomes a tag row
reading was the one already suspected: the published level becomes a tag row
in its own right, and `LIT-101` is marked NOT HISTORISED — it is a field input
on `%IW0` and never reaches SCADA. `LIT-101` was the only row in the seed
carrying two addresses (`%QW0,%IW0`), which is the instrument and the published
@ -614,11 +621,14 @@ it is the same derivation that will run against `imh`.
server was taken as authoritative here. Someone should decide which is
intended and re-run the WRPS generators — right now the repository does not
describe the running system.
- **A name collision in `scada-points.csv`:** `PS_STN_HIGH_LEVEL_ALARM` names
both the coil 10 status bit and the holding register 1032 setpoint. The point
name is not unique; the item layer is the first place the two are
distinguishable (`STN.HIGH_LEVEL` versus `SP.HIGH_ALARM`). Harmless now that
the item is the key, but it will confuse anyone reading the point list.
- **A name collision in the delivered point list:** one name was used for
both the coil 10 status bit and the holding register 1032 setpoint. The item
layer is the first place the two are distinguishable (`STN.HIGH_LEVEL` versus
`SP.HIGH_ALARM`). **Closed 2026-09-01** — the corrected delivery is keyed on
the item, so both are unique and the special case that told them apart by hand
is gone. It is recorded here because it bit twice: once in the original seed,
and once again during the rename, where a naive substitution mapped the status
bit onto the setpoint.
**Gate**
- [ ] A `SELECT` from a container on `lin001` returns rows

View file

@ -107,11 +107,14 @@ banner all the way to the operator's screen.
**The three Phase 5 findings are fixed** (2026-08-31), against the SCADA
configuration rather than against the fixtures. All three came from one
substitution: the stand-in historian was keyed on CI Server **point** names
(`PS_STN_WET_WELL_LEVEL`) when the historian is keyed on CI Server **item**
names (`AID.WRPS.STN.LEVEL`) — two layers apart, not one. Nothing is aliased
across that gap now: `public.historian_items` holds the mapping, generated from
`WRPS/05-scada/modbus` by [`scripts/gen_historian_items.py`](../scripts/gen_historian_items.py),
substitution: the stand-in historian was keyed on names delivered as
`PS_STN_WET_WELL_LEVEL` when the historian is keyed on CI Server **item** names
(`AID.WRPS.STN.LEVEL`). Those `PS_*` names turned out not to be CI Server names
at all — a corrected delivery on 2026-09-01 replaced them, and the seed is now
keyed on the item throughout. Nothing is aliased across that gap:
`public.historian_items` holds the mapping, generated from
[`db/seed/scada-source/`](../db/seed/scada-source/README.md) by
[`scripts/gen_historian_items.py`](../scripts/gen_historian_items.py),
and an item that resolves to neither a tag nor a written reason is a build
error rather than a silent "no records found".

View file

@ -718,9 +718,12 @@ footer { margin-top: 72px; padding-top: 22px; border-top: 1px solid var(--hair);
<p><b>Four different names describe the wet well level, and only the fourth is what the
historian stores.</b> The instrument on the drawing is <span class="mono">LIT-101</span>. The
PLC publishes it at <span class="mono">%QW0</span>. SCADA polls that register and calls the
point <span class="mono">PS_STN_WET_WELL_LEVEL</span>. And then CI Server files the history
under an <em>item</em> name, <span class="mono">AID.WRPS.STN.LEVEL</span> &mdash; which appears
nowhere in this project&rsquo;s repository, because nothing had ever needed it before.</p>
point <span class="mono">WRPS_PLC:STN_LEVEL</span> &mdash; the Modbus station name, then the
point name. And then CI Server files the history under an <em>item</em> name,
<span class="mono">AID.WRPS.STN.LEVEL</span> &mdash; which appeared nowhere in this
project&rsquo;s repository, because nothing had ever needed it before. Worse, the point list
handed over at the time carried invented names that CI Server never used; a corrected one
arrived on 1 September and the whole repository is keyed on the item now.</p>
<p><b>Modbus carries register numbers, not names.</b> That is why the point layer and the item
layer are free to drift apart, and why reconciling against the register map would have proved
only that the first three agreed with each other. The stand-in was built on the third name. The