yau-plant-assistant/cube/model/equipment.yml
Claude 3d19b186da Key the whole seed on the CI Server item name
45 of the 65 rows in db/seed/tags.csv had a PS_* name as their PRIMARY
KEY, and historian_items.csv, alarm_bits.csv, the fixtures, two Cube
models and the exam all referenced them. They are gone. The tag seed is
now keyed on the CI Server item for everything the historian carries -
AID.WRPS.STN.LEVEL - and on the instrument tag for the 20 field devices
that never reach SCADA.

gen_historian_items.py reads db/seed/scada-source/ by default, so it runs
for anyone with a clone: --wrps is now --source. The tag match is the item
name itself, an identity lookup, and the TAG_FOR_ITEM special case is
deleted - all 49 item names are unique, which the old point names were
not. scada_point in the output is replaced by ci_station, ci_point and
poll_group; 001_schema.sql and deploy.sh's upsert follow.

Regenerated, and it comes out the same shape it went in: 49 items, 45
answerable, 4 deliberately excluded, three groups at 5 s, 30 s and on
change. Every historian_items.tag_id and alarm_bits.tag_id resolves to a
tags.csv row. No duplicate keys.

THE ONE NAME THAT WAS AMBIGUOUS, AND NEARLY COST US

PS_STN_HIGH_LEVEL_ALARM named two different things in the old delivery:
the high level alarm STATUS BIT on coil 10, and the alarm SETPOINT on
holding register 1032. Building the rename map from that file kept
whichever came last, so the status bit was silently renamed onto the
setpoint. check_mapping() refused to write and named the item that no
longer resolved - which is the only reason this is a paragraph in a commit
message rather than a defect.

Had it gone through, alarm bit 0 - wet well high level - would have
pointed at the setpoint. "How many high level alarms last week" would have
counted setpoint changes and returned a small, plausible, confident,
wrong number. That check exists because the same ambiguity caused the
first Phase 5 finding in August. It has now bitten twice.

NOT YET VERIFIED: pytest api/tests could not be run here - this machine
has neither fastapi nor psycopg. The tests are unchanged and reference no
PS_ name, but they have not been run. pg-ai on lin001 still holds the old
keys and must be reloaded, Cube pre-aggregations rebuilt, and the Phase 1
gate re-run. eval/testset.jsonl changed, so the 78-case exam - never yet
run - should be run after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 19:00:38 +10:00

306 lines
9.4 KiB
YAML

# =============================================================================
# equipment.yml — alias resolution at equipment and tag level.
#
# SOURCE: pg-ai public.equipment and public.tags. These live in pg-ai whatever
# happens to imh, so this file does not change at Phase 4.
#
# WHY IT IS IN CUBE AT ALL: so that a question about "Pump 02" can be answered
# without the caller knowing that its data is on AID.WRPS.PU302.RUNNING,
# AID.WRPS.PU302.TRIPPED and AID.WRPS.PU302.RUN_HOURS. Without equipment-level joins every
# equipment question fails, because no historian point is called "Pump 02".
#
# Alias matching itself is done in the API (api/tools/equipment.py) against the
# GIN-indexed alias arrays, not here - Cube is the aggregation layer, not the
# entity resolver. What Cube provides is the join, so that once the API has
# resolved "Pump 02" to PU-302 the measures in the other cubes can be filtered
# by equipment rather than by a list of tags the model would have to hardcode.
# =============================================================================
cubes:
- name: equipment
sql: >
SELECT
equipment_id,
display_name,
equipment_type,
unit_name,
description,
array_to_string(aliases, ' | ') AS alias_list
FROM public.equipment
description: >
Plant equipment at the Waterloo Road Pump Station - the station itself,
the wet well, three pumps, the discharge manifold, the spill weir and the
switchboard.
dimensions:
- name: equipment_id
sql: equipment_id
type: string
primary_key: true
description: Canonical identifier - PU-302, WW-101, STN-001.
- name: display_name
sql: display_name
type: string
description: What to call it in an answer - "Pump 02", not "PU-302".
- name: equipment_type
sql: equipment_type
type: string
- name: unit_name
sql: unit_name
type: string
- name: alias_list
sql: alias_list
type: string
description: >
Pipe-separated aliases, for display and debugging. Resolution happens
in the API against the array column with a GIN index; this string is
for showing an operator why "pump2" was understood as PU-302.
- name: description
sql: description
type: string
measures:
- name: count
type: count
- name: tags
sql_table: public.tags
description: >
Historian points and field instruments, their units, ranges and
setpoints. Read the description column before interpreting any value -
it records the historian-versus-PLC unit conversion and, critically,
whether the tag is historised at all.
joins:
- name: equipment
sql: "{CUBE}.equipment_id = {equipment}.equipment_id"
relationship: many_to_one
dimensions:
- name: tag_id
sql: tag_id
type: string
primary_key: true
- name: equipment_id
sql: equipment_id
type: string
- name: display_name
sql: display_name
type: string
- name: signal_type
sql: signal_type
type: string
description: level, flow, pressure, vibration, status, state, speed, hours.
- name: engineering_unit
sql: engineering_unit
type: string
description: >
The unit the HISTORIAN stores, which is not always the unit the PLC
works in. Wet well level is historised as percent of the spill weir
crest (raw mm / 60). Always report the unit with the number.
- name: range_low
sql: range_low
type: number
- name: range_high
sql: range_high
type: number
- name: alarm_setpoint_hi
sql: alarm_setpoint_hi
type: number
description: >
The configured alarm setpoint at the time this reference data was
loaded. Setpoints are writable from SCADA - if a question compares
two periods, check AID.WRPS.SP.HIGH_ALARM for a change before
attributing a difference in alarm counts to the process.
- name: alarm_setpoint_lo
sql: alarm_setpoint_lo
type: number
- name: trip_setpoint
sql: trip_setpoint
type: number
- name: description
sql: description
type: string
measures:
- name: count
type: count
# ---------------------------------------------------------------------------
# historian_items — the CI Server item dictionary.
#
# WHY IT IS A CUBE AT ALL: so "what is AID.WRPS.STN.LEVEL" and "which item
# holds the wet well level" are both answerable, and so an engineer checking
# a number can walk from the answer back through item -> point -> PLC address
# without leaving the assistant. Four namespaces name the same measurement
# and this table is the only place all four appear together.
#
# It is also where an item's SAMPLE RATE and RETENTION come from, both taken
# from the live CI Server historisation groups rather than assumed.
# ---------------------------------------------------------------------------
- name: historian_items
sql_table: public.historian_items
description: >
CI Server items - what the historian is actually keyed on - and how each
maps to a tag, a SCADA point and a PLC register.
joins:
- name: tags
sql: "{CUBE}.tag_id = {tags}.tag_id"
relationship: many_to_one
dimensions:
- name: item_name
sql: item_name
type: string
primary_key: true
description: AID.WRPS.STN.LEVEL. The historian's own key.
- name: tag_id
sql: tag_id
type: string
description: >
NULL only for items deliberately excluded from answering - see
exclusion_reason. A NULL here with no reason is a build failure.
- name: exclusion_reason
sql: exclusion_reason
type: string
description: >
Why this item cannot be asked about. Present it verbatim rather than
reporting "no records found" - the two mean very different things.
- name: section
sql: section
type: string
description: >
CI Server's own grouping - STN, PU301, PU302, PU303, SP, SIM. It is
COARSER than plant equipment: there is no wet well, weir, manifold or
switchboard section. Use equipment for the plant view, not this.
- name: attribute
sql: attribute
type: string
- name: description
sql: description
type: string
- name: eng_unit
sql: eng_unit
type: string
description: The unit CI Server presents, which is not the PLC's unit.
- name: raw_to_eng
sql: raw_to_eng
type: string
description: >
How the raw register becomes the engineering value, as written in the
SCADA point list - "value / 60" for level. Quote this when explaining
why a level reads 70.3 and not 4217.
- name: his_group
sql: his_group
type: string
description: WRPS_ONE_SEC (5 s), WRPS_THIRTY_SEC (30 s), WRPS_EVENT (on change).
- name: scan_interval_seconds
sql: scan_interval_seconds
type: number
- name: life_time
sql: life_time
type: string
description: >
Retention on the item's history group - "1 weeks" on every WRPS
group. This is the answer to "why can it not tell me about last
month".
- name: ci_point
sql: ci_point
type: string
- name: iec_address
sql: iec_address
type: string
description: The PLC address, %QW0. Field inputs (%IW/%IX) are not historised.
measures:
- name: count
type: count
views:
- name: item_reference
description: >
The CI Server item dictionary joined to the tag and equipment each item
belongs to. This is the lookup behind "which item holds the wet well
level", "how often is it sampled" and "how far back does it go".
cubes:
- join_path: historian_items
includes:
- item_name
- tag_id
- exclusion_reason
- section
- attribute
- description
- eng_unit
- raw_to_eng
- his_group
- scan_interval_seconds
- life_time
- ci_point
- iec_address
- count
- join_path: historian_items.tags
prefix: true
includes:
- display_name
- signal_type
- engineering_unit
- join_path: historian_items.tags.equipment
prefix: true
includes:
- equipment_id
- display_name
- equipment_type
- name: equipment_reference
description: >
Equipment joined to its tags - the lookup behind "what does the PVHI
alarm on the wet well mean" and "which pump is PU-302".
cubes:
- join_path: tags
includes:
- tag_id
- display_name
- signal_type
- engineering_unit
- range_low
- range_high
- alarm_setpoint_hi
- alarm_setpoint_lo
- trip_setpoint
- description
- join_path: tags.equipment
prefix: true
includes:
- equipment_id
- display_name
- equipment_type
- alias_list