yau-plant-assistant/db/README-standin-historian.md
Claude 73e90cf710 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>
2026-09-01 19:00:57 +10:00

204 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# The stand-in historian — what it is, and how to remove it
`imh` (`yau-sls-poc-imh`) does not exist yet. Everything the assistant says about
plant history today comes from generated data in the `fixture` schema, built by
[`002_fixtures.sql`](002_fixtures.sql).
This file exists so that removing it later is a checklist rather than an
archaeology exercise. **Read it before connecting `imh`,** not after.
> [!IMPORTANT]
> **The real historian will not be identical to this.** The stand-in was built
> from the SCADA configuration in `WRPS/05-scada/modbus`, so the *item names*,
> *sample rates*, *retention* and *timestamp semantics* are taken from the
> machine rather than invented. **Everything about the SQL Server side — table
> names, column names, column types, how a value is stored, how quality is
> expressed — is a guess.** Section 4 lists every one of those guesses with a
> way to test it. Work through that table against the real thing before you
> trust a single number.
---
## 1. Why there is a stand-in at all
Phase 4 (`imh` access) is the only true blocker in the build. Cube, the
contracts, the agent and the UI can all be built and tested without it. The
stand-in exists so that work is not idle, and so that the *shape* of the data
contract is settled and reviewed before the real connection lands.
It is switched on by `USE_FIXTURES=true` in `~/ai/api.env`.
Every row carries `is_fixture = TRUE`, and the flag rides all the way to a
banner on the operator's screen. **Do not remove that flag as tidying-up.**
---
## 2. The seam — where fixture ends and contract begins
This is the single most important thing in this document.
```
FIXTURE SCAFFOLDING (delete at cutover)
┌──────────────────────────────────────────────────────────────┐
│ fixture.build_meta the build clock │
│ fixture.f_peak / f_level / f_pumps / f_inflow / │
│ fixture.f_discharge / f_duty / f_alarm_word │
│ fixture.injected_condition the scripted trips and faults │
│ fixture.item_history the generated samples │
└──────────────────────────────────────────────────────────────┘
════════════════════╪═══════════ THE SEAM ═══════════
┌──────────────────────────────────────────────────────────────┐
│ fixture.process_value_history VIEW │
│ fixture.alarm_history VIEW │
│ fixture.operation_history MATERIALIZED VIEW │
└──────────────────────────────────────────────────────────────┘
CONTRACT (repoint, do not delete)
Cube models read only these
```
**Above the seam is generation. Below it is contract.** The three views are the
only things any Cube model names. Cutover means giving those three views a new
source; it does not mean touching a Cube model, and if you find yourself
editing one, stop and ask why the view could not absorb the difference.
`public.historian_items`, `public.alarm_bits`, `public.tags` and
`public.equipment` are **reference data, not fixtures.** They live in `public`,
they are loaded by `deploy.sh` regardless of `USE_FIXTURES`, and they survive
cutover unchanged. That placement is deliberate: the item-to-tag mapping and
the alarm bit map are properties of the SCADA and PLC configuration, not of the
stand-in.
---
## 3. Inventory
### Delete at cutover
| Object | What it is |
|---|---|
| `fixture.build_meta` | Origin and horizon of the generated window |
| `fixture.f_peak`, `f_level`, `f_pumps`, `f_inflow`, `f_discharge`, `f_duty`, `f_alarm_word` | The prescribed plant |
| `fixture.injected_condition` | Scripted trips, seal leak, vibration, frozen transmitter, rejected setpoint |
| `fixture.item_history` | ~685,000 generated samples |
| The `DO $$ … $$` assertion block at the foot of `002_fixtures.sql` | Asserts counts that are facts about generated data only |
### Repoint, do not delete
| Object | Becomes |
|---|---|
| `fixture.process_value_history` | A view over the real `imh` analogue history |
| `fixture.alarm_history` | The same bit-decomposition, over the real alarm word — **or** a view over CI Server's `ALARM_HISTORY` if anyone ever configures item alarm limits |
| `fixture.operation_history` | The same derivation from `PUMPS_RUNNING`, over real data |
Consider renaming the schema from `fixture` to something honest (`historian`)
at that point, and updating `sql_table:` in the four Cube models. That is a
rename, not a redesign — do it in its own commit.
### Keep unchanged
`public.historian_items`, `public.alarm_bits`, `public.tags`,
`public.equipment`, `scripts/gen_historian_items.py`, `db/seed/*.csv`, and all
four Cube model files.
---
## 4. Assumptions that may not hold — test every one
The shape below was reasoned from the SCADA configuration. **The SQL Server
side was not available and none of it is confirmed.**
| # | Assumption | Where it is encoded | How to test it | If it is wrong |
|---|---|---|---|---|
| **A1** | `imh` exposes **one** item-keyed history table | `fixture.item_history` shape; all three views | List the tables. Look for one row per (item, time, value) | If history is split per group or per data type, the three views absorb it with a `UNION ALL`. No Cube change |
| **A2** | Item names appear **verbatim** as `AID.WRPS.STN.LEVEL` | Join `history.item_name = historian_items.item_name` | `SELECT DISTINCT` the name column and compare against `db/seed/historian_items.csv` | Case, separators (`.` vs `\`), a node prefix, or a numeric item id with a lookup table. Normalise **in the view**, never by editing the seed |
| **A3** | Timestamps are **UTC** | Every view; `alarms.yml` converts once | Confirmed from config (`TIME_ZONE "Date+time GMT"`, `CORRECT_DAYLIGHT=0`) but **verify against data**: take a known event and check it against wall-clock | SQL Server has no `timestamptz`. A `datetime2` holding UTC must be cast with `AT TIME ZONE 'UTC'` in the view, or every answer shifts by ten hours |
| **A4** | Values are in **engineering units**, gain already applied | All measures; `86.7` and `91.7` thresholds in `process_values.yml` | Read `AID.WRPS.STN.LEVEL` and check it is ~0100, not ~07000 | If raw registers, apply `historian_items.eng_gain` **in the view**. The thresholds in the Cube model assume percent |
| **A5** | Quality is text `GOOD` / `BAD` / `UNCERTAIN` | Every measure filters `quality = 'GOOD'` | Inspect the column | CI Server may use numeric OPC quality codes. Map to the three strings in the view. **A missing quality column is not "all good"** — decide explicitly and write down which |
| **A6** | The alarm word is retained as an item we can decompose | `fixture.alarm_history` | Check `AID.WRPS.STN.ALARM_WORD` has history | If it is not retained, derive alarms from the discrete items instead (`STN.HIGH_LEVEL`, `PU30x.TRIPPED`, …). `public.alarm_bits.tag_id` already names them |
| **A7** | Sampling is **regular** at the declared interval | `time_weighted_avg`, `seconds_above_*` sum `scan_interval_seconds` | Compare consecutive `sample_time` gaps against `historian_items.scan_interval_seconds`. **This is on the Phase 4 gate** | If deadband compression is ever enabled, replace `scan_interval_seconds` with a `LEAD` window in the view. Every duration measure is wrong until you do |
| **A8** | Retention is **7 days** | `metrics.HISTORY_RETENTION_DAYS`; pre-aggregation build ranges | `SELECT min(sample_time)` | Update the constant, and widen `build_range_start` in all three pre-aggregations. **Note the constant is also the query window** — every builder asks for a rolling week whatever the question names, and that is by design. Longer retention alone is inert until the window is made variable |
| **A9** | There is **no** operations concept — pump-downs must be derived | `fixture.operation_history` | Look for any batch/campaign table | If one exists, prefer it, and re-verify the 5-minute noise threshold and the 10-minute look-back against it |
| **A10** | The `32767` sentinel survives into engineering units | Every measure excludes `value <> 32767` | Check `TIME_TO_SPILL` for the sentinel | If a gain is applied to it, the sentinel is no longer 32767 and every average silently includes it. **This one fails quietly** |
| **A11** | The alarm word can be read **unsigned** | `fixture.alarm_history` normalises with `((v % 65536) + 65536) % 65536` | Check whether the word ever goes negative | SQL Server `SMALLINT` is signed, so bit 15 makes the whole word negative and a right-shift sign-extends — reporting every higher bit as active at once. The normalisation already handles it; **do not remove it** |
| **A12** | One row per item per timestamp (the view's primary key) | `process_values.id` = `item_name \|\| '@' \|\| sample_time` | Check for duplicates | Duplicate timestamps break Cube's primary key. Deduplicate in the view and find out why they exist |
**A10 and A5 are the two that fail silently.** A wrong sentinel or a
misinterpreted quality code does not raise an error; it produces a plausible
number. Test those two with data, not by reading a schema.
---
## 5. Removal procedure
1. **Do not delete anything yet.** Stand the real source up beside the fixtures
and work through section 4 with real data. Write the answers into
`BUILD-AI-CONTAINERS.md` §10.
2. **Rewrite the three views** against `imh`, absorbing every difference found
in step 1. The views change; the Cube models must not.
3. **Re-verify the Phase 5 gate by hand, without the LLM.** Prove Cube returns
the right number by querying it directly and checking against `imh` with
your own SQL. The Phase 5 gate exists for exactly this moment.
4. **Re-derive the pinned eval expectations.** `eval/testset.jsonl` case `H31`
pins a high level alarm count of **14**. That is a fact about generated data
and nothing else. Replace it with a real figure an engineer has verified, or
remove the pin.
5. **Set `USE_FIXTURES=false`** and repoint `CUBEJS_DB_*` at `imh`.
6. **Drop the scaffolding** listed in section 3, and delete the assertion block
from `002_fixtures.sql`.
7. **Run `scripts/verify.sh`.** The historian-item-mapping and no-equipment-
column checks are not fixture-specific and must still pass. The fixture
banner should disappear.
8. **Check the fixture banner is actually gone** from an answer on the operator
screen, not just from the database. It is driven by `used_fixture_data` off
`USE_FIXTURES`, so it should follow — confirm it rather than assume it.
---
## 6. What the assertions do and do not prove
`002_fixtures.sql` ends with a block that **fails the load** on any of:
- 685,440 analogue rows; 32 alarm activations; 14 high level; 2 spills;
15 priority-1; 72 pump-downs; 720 BAD level samples
- every historised, answerable item having history
- alarm-word **bit 0** agreeing with the independent discrete item
`AID.WRPS.STN.HIGH_LEVEL`, and **bit 3** with `AID.WRPS.STN.SPILL_ACTIVE`
- the operations derivation and the alarm derivation agreeing on spill count
- volume-remaining-to-spill reconciling with level through the plant geometry
at every sample
Those are real tests of **the derivation logic**, and the cross-checks in
particular compare two independent paths through the data. They are worth
keeping in mind when rewriting the views, because the same cross-checks can be
run against `imh` and should still hold.
**They prove nothing about the plant.** Every number above is a fact about
generated data. The moment `imh` is connected they are meaningless, and step 6
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 (`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,
an alarm filed against the wrong equipment.
The lesson worth carrying into cutover: **a stand-in that is shaped wrongly is
worse than no stand-in**, because it produces confident answers and the defects
present as unrelated. When something here disagrees with `imh`, the first
question is not "which value is right" but "are these two things even the same
thing".