A shift log is continuously updated, which rules out both stores we already
have. Not doc_chunks: that store is built on doc_number + revision +
effective_date and nothing in it is citable until a human confirms the header,
so a record changing every shift would wear out the gate that makes procedure
citations trustworthy. Not the historian: we hold read-only on imh and may not
write to it at all, it is keyed on the CI Server item and a log entry has no
item, and its seven-day retention would expire the one source that could
outlive it.
So a table in pg-ai, read on the historical lane because that lane's contract
already fits it - a time window, rows, and "no records found" when there are
none. Entries are rows, NOT citations: a citation is a controlled document with
a confirmed revision and an operator's note is not one however true it is. The
Citation contract is untouched.
Routed on the question text rather than a classifier label. Adding a sixth
class to the five-way classifier - the most safety-relevant component in the
stack - to reach one demo source would be a poor trade. Case S01 pins the
trigger.
Demo entries are anchored to the start of today in site local time, so a
re-run always lands them inside the rolling seven days and the shift label
always matches the clock. The alarm fixtures are absolute and have gone stale;
this cannot. Future-dated entries are dropped, and the load asserts the window
and the count rather than trusting them.
Eval, 78 -> 80 cases:
S01, S02 the shift log, whole and filtered by equipment
N04 repointed at the environmental discharge licence. The old wording
asked about the shift log, which now exists, so it had stopped
testing "no such source" - a different answer from "no records
found", and the distinction is the point of the case.
H02 window changed to a relative one. It still fails, on a false
positive in _contains_quantity: "the last 3 days" reads as a
fabricated figure, so the correct zero-row answer is rejected.
Left for its own change.
80 cases: 97.5% overall, 100% classification, p95 5257 ms. One contract
violation (H02), so the Phase 8 gate is still not met.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
150 lines
8.1 KiB
SQL
150 lines
8.1 KiB
SQL
-- =============================================================================
|
|
-- 008 - public.shift_log, the operator shift log.
|
|
--
|
|
-- ############################################################
|
|
-- ## DEMO DATA. NOT A REAL SHIFT LOG. DO NOT QUOTE. ##
|
|
-- ############################################################
|
|
--
|
|
-- WHY THIS IS A TABLE AND NOT A DOCUMENT, AND NOT THE HISTORIAN
|
|
-- -------------------------------------------------------------
|
|
-- A shift log is continuously updated, which rules out both of the stores we
|
|
-- already have:
|
|
--
|
|
-- * NOT doc_chunks. That store is built on doc_number + revision +
|
|
-- effective_date, and nothing in it is citable until a human has confirmed
|
|
-- that header. A record that changes every shift would need re-confirming
|
|
-- every shift, and a confirmation gate asked that often stops being a gate.
|
|
-- It is the same mechanism that makes a procedure citation trustworthy, so
|
|
-- wearing it out here would cost us something real elsewhere.
|
|
--
|
|
-- * NOT the historian. Three reasons and the first is decisive: we hold
|
|
-- READ-ONLY on imh and may not write to it at all. Beyond that, the
|
|
-- historian is keyed on the CI Server item and only the item - a log entry
|
|
-- has no item, no register and no scan interval, and inventing one is what
|
|
-- produced all three Phase 5 findings. And its retention is seven days,
|
|
-- so the one source that could outlive the historian would expire with it.
|
|
--
|
|
-- So it is a third evidence source alongside the historian and the document
|
|
-- library: free text, at irregular times, by a named author.
|
|
--
|
|
-- WHAT THIS DELIBERATELY DOES NOT DO
|
|
-- ----------------------------------
|
|
-- Entries are NOT citations. Citation means a controlled document with a
|
|
-- confirmed revision, and an operator note is not that however true it is.
|
|
-- Shift-log evidence is returned as rows on the historical lane, labelled as
|
|
-- an operator record, and the Citation contract is untouched. Extending
|
|
-- Citation to cover a second kind of source is a real design decision and is
|
|
-- not being made here.
|
|
--
|
|
-- There is no retention limit on this table. The seven-day query window in
|
|
-- tools/shiftlog.py is there to match the historical lane's window, not
|
|
-- because anything here expires. When the query window becomes variable
|
|
-- (REQUESTS.md), this table can answer questions the historian cannot.
|
|
--
|
|
-- WRITE PATH: none. There is no UI and no API that inserts here. The seed
|
|
-- below is the whole content, and it is demo fiction. An operator writing a
|
|
-- real entry is Phase 9 work that has not been scoped.
|
|
-- =============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS shift_log (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
entry_time TIMESTAMPTZ NOT NULL, -- UTC. Converted to site time once, on the way out.
|
|
shift TEXT NOT NULL,
|
|
author TEXT NOT NULL,
|
|
equipment_id TEXT REFERENCES equipment (equipment_id),
|
|
entry_text TEXT NOT NULL,
|
|
-- Mirrors fixture.is_fixture and the DEMO- document numbering: an entry
|
|
-- nobody wrote must never be indistinguishable from one somebody did.
|
|
-- tools/shiftlog.py carries this out to used_fixture_data on the answer.
|
|
is_demo BOOLEAN NOT NULL DEFAULT FALSE,
|
|
CONSTRAINT shift_log_shift_ck CHECK (shift IN ('day', 'night'))
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS shift_log_entry_time ON shift_log (entry_time DESC);
|
|
CREATE INDEX IF NOT EXISTS shift_log_equipment ON shift_log (equipment_id);
|
|
|
|
-- -----------------------------------------------------------------------------
|
|
-- Demo entries.
|
|
--
|
|
-- Anchored to the START OF TODAY IN SITE LOCAL TIME, then placed at fixed
|
|
-- local times on fixed days back. Two properties matter and the first is not
|
|
-- obvious:
|
|
--
|
|
-- * The shift label has to match the clock. An earlier version offset every
|
|
-- entry by a whole number of hours from now(), which is relative and
|
|
-- therefore never goes stale - but it slid every entry's local hour as the
|
|
-- day wore on, and produced night-shift entries timestamped 10:00. Demo
|
|
-- data that contradicts itself on the face of it is worse than none.
|
|
--
|
|
-- * Relative, not absolute, so re-running always lands entries inside the
|
|
-- rolling seven days. The alarm fixtures are absolute and have gone stale;
|
|
-- this does not.
|
|
--
|
|
-- ONE anchor for the whole load, not a per-statement now(), for the reason
|
|
-- 002_fixtures.sql gives: a statement-by-statement now() drifts within a load.
|
|
--
|
|
-- The site timezone is named here because this seed places entries at local
|
|
-- clock times and there is no other way to do that. It must match
|
|
-- SITE_TIMEZONE in ~/ai/api.env. The conversion still happens exactly once,
|
|
-- and never in a prompt.
|
|
--
|
|
-- Entries in the future are dropped rather than inserted: a load run at 03:00
|
|
-- would otherwise write a day-shift entry that has not happened yet.
|
|
--
|
|
-- Re-runnable: demo rows are deleted and reinserted. Only rows marked is_demo
|
|
-- are touched, so a real entry could never be removed by a re-run.
|
|
-- -----------------------------------------------------------------------------
|
|
DELETE FROM shift_log WHERE is_demo;
|
|
|
|
INSERT INTO shift_log (entry_time, shift, author, equipment_id, entry_text, is_demo)
|
|
SELECT entry_time, shift, author, equipment_id, entry_text, TRUE
|
|
FROM (
|
|
SELECT ((a.today - days_back * interval '1 day' + local_time)
|
|
AT TIME ZONE 'Australia/Sydney') AS entry_time,
|
|
v.shift, v.author, v.equipment_id, v.entry_text
|
|
FROM (SELECT date_trunc('day', now() AT TIME ZONE 'Australia/Sydney') AS today) a,
|
|
(VALUES
|
|
(6, time '02:40', 'night', 'demo:J. Whitmore', 'PU-302',
|
|
'PU-302 tripped on overload at 02:20 and was reset from the panel at 03:05. '
|
|
'Ran up clean afterwards. Mechanical to look at the strainer next day shift.'),
|
|
(6, time '13:15', 'day', 'demo:A. Ngata', 'PU-302',
|
|
'Strainer on PU-302 cleared - heavy rag. No repeat of the overload trip through the shift.'),
|
|
(5, time '09:55', 'day', 'demo:A. Ngata', 'WW-101',
|
|
'Wet well level instrument LIT-101 dropped out briefly around 09:40. '
|
|
'Reading recovered on its own. Logged for instrument tech to check the cable gland.'),
|
|
(4, time '01:30', 'night', 'demo:J. Whitmore', 'PU-303',
|
|
'PU-303 seal leak alarm came in and returned. Small weep at the seal, not running to drain. '
|
|
'Left on duty rotation, flagged for maintenance.'),
|
|
(3, time '15:20', 'day', 'demo:R. Patel', 'PU-301',
|
|
'PU-301 vibration alarm during the afternoon peak. Settled once flow came off. '
|
|
'Nothing obvious on inspection.'),
|
|
(2, time '04:05', 'night', 'demo:J. Whitmore', NULL,
|
|
'Quiet shift. Station on auto throughout, no alarms, no operator intervention.'),
|
|
(1, time '11:40', 'day', 'demo:R. Patel', 'WW-101',
|
|
'Heavy rain from mid-morning. Wet well worked hard but stayed well under the weir. '
|
|
'All three pumps called at times.'),
|
|
(0, time '06:10', 'day', 'demo:A. Ngata', NULL,
|
|
'Handover: station on auto, no outstanding alarms. '
|
|
'PU-302 strainer still to be signed off by mechanical.')
|
|
) AS v(days_back, local_time, shift, author, equipment_id, entry_text)
|
|
) placed
|
|
WHERE entry_time <= now();
|
|
|
|
DO $$
|
|
DECLARE n INT; newest TIMESTAMPTZ; oldest TIMESTAMPTZ;
|
|
BEGIN
|
|
SELECT count(*), min(entry_time), max(entry_time)
|
|
INTO n, oldest, newest FROM shift_log WHERE is_demo;
|
|
RAISE NOTICE 'shift_log: % demo entries, % to %', n, oldest, newest;
|
|
-- Every entry must be inside the rolling seven days the historical lane
|
|
-- asks for, or the demo shows an empty log.
|
|
IF n < 6 THEN
|
|
RAISE EXCEPTION 'expected at least 6 demo shift log entries, loaded %', n;
|
|
END IF;
|
|
IF oldest <= now() - interval '7 days' THEN
|
|
RAISE EXCEPTION 'oldest demo entry % is outside the 7 day query window', oldest;
|
|
END IF;
|
|
IF newest > now() THEN
|
|
RAISE EXCEPTION 'demo entry % is in the future', newest;
|
|
END IF;
|
|
END $$;
|