# Occurrences

> Watched conditions of the thing under test, written as a JavaScript expression over the model's channels and judged during a run or always.

An **occurrence** registers that the thing under test did something noteworthy. Where an alarm
watches the plant and the equipment, an occurrence belongs to the procedure: its edges stay with
the run that observed them and also enter the persistent Occurrences timeline of the Events page.
Its condition is a JavaScript expression over the model's channels, debounced on both edges and
gated by the quality of what it reads. Occurrences live under Process > Models > model > Setup >
**Occurrences** and need the **Edit recipes & evaluations** permission.

## The Occurrences folder {#the-occurrences-folder}

Selecting the **Occurrences** row opens the folder page ("Watched conditions of the thing under
test. Every edge stays with its run when applicable and also enters the persistent Occurrences
history.") with one card per occurrence, by name; clicking a card opens it and the X removes it.
With none, the page says "No occurrences yet. Add one."

| Command | What it does | Greyed when (situation) | Not drawn when (role) |
| --- | --- | --- | --- |
| **Occurrence** (detail bar, folder selected; right-click **Add occurrence**) | Adds `Occurrence <n>`: severity Warning, armed during procedures, enabled, no message, an empty condition. Selects it. | Any unit holds a run, or the runtime is stopped. | Without Edit recipes & evaluations. |

## The occurrence's page {#the-occurrences-page}

The header carries the name, a pill with the severity (in the severity's own colour) and a
**Disabled** pill when the occurrence is off. Two kinds of notice can stand under it:

- A caution note when the saved condition does not validate (the draft is still saved; the note
  says what is wrong, see below).
- One **Evaluation unavailable** banner per unit on which the live condition could not be
  evaluated, as "`<unit>`: `<error>`". A unit's diagnostic is written once per unchanged error and
  cleared when the evaluation succeeds again.

### Identity & behavior {#identity--behavior}

| Field | What it is | Values / default | Effect |
| --- | --- | --- | --- |
| **Name** | The occurrence's name: the tree row, the event label in the run file, the Events timeline and the reports. | Free text; `Occurrence <n>` at creation. | Renames the row and what a procedure's checklist calls it. |
| **Severity** | The weight of an activation. The picker shows each option in the ink its pill will carry. | **Info**, **Warning** (default), **Critical**. | Colours the pill and the episode wherever it is listed. During a run, an activation of a Warning or Critical occurrence also requests an operating-system notification while the app is hidden or minimized; Info stays in the window. |
| **Armed** | When the condition is judged. | **During a procedure** (default): only while a run of a procedure that arms it is active, over the recorded rows. **Always**: also outside runs, by a background watcher reading the unit's bindings live. | An Always occurrence needs no procedure to arm it; one armed During a procedure is judged only by the procedures that tick it. |
| **On delay (ms)** | How long the condition must stay true before **Activated** is confirmed, at the resolution of the samples. | A number of milliseconds; 0 by default. The field takes no negative number: the entry is refused, the note says so under the field, and the previous value comes back. | Debounces the rising edge. |
| **Off delay (ms)** | How long the condition must stay false before **Cleared** is confirmed. | Same; 0 by default. | Debounces the falling edge. |
| **Enabled** | Whether the occurrence arms at all. | On by default. | A disabled occurrence never arms, in a run or outside one, and never blocks Start. |
| **Message** | The text recorded and announced with an activation. | Free text; empty by default. | Travels with every edge into the run file and the journal. |

### Condition {#condition}

"A single JavaScript expression returning Bool." The card holds a code editor, the list of the
references you may use ("Available channel references: Channels.Ch_1, Channels.Outlet_temperature,
…"), and the reminder "During a run the condition reads the recorded datalog columns; the Always
watcher reads the unit bindings live. Syntax, missing references and runtime evaluation failures
are shown here without treating them as false or signal loss."

A channel is referenced as `Channels.<identifier>`, where the identifier is the channel's name
sanitized: letters and digits survive, every other character becomes `_`, and a name starting
with a digit gains a `_` in front. `Channels` holds only the model's channels and nothing else, so
a channel named `Math`, `isNaN` or `true` is still a valid channel name and hides no JavaScript
global. A present, good `NaN` is data you can test with `isNaN(Channels.Pv)`. Examples:
`Channels.Outlet_temperature > 80`, `Channels.Door_open && Channels.Heater_on`.

The draft is validated as you type, against sample values of each channel's type, and the caution
note under the header says what fails:

| Note | Meaning |
| --- | --- |
| "Syntax error: …" | The expression does not parse. |
| "Channel reference 'Channels.X' does not exist in this model." | A reference names no channel. |
| "More than one channel maps to 'Channels.X'." | Two channel names sanitize to the same identifier; rename one on the Channels grid. |
| "The condition must return Bool, but returned `<type>`." | The expression yields something other than true or false. |
| "Evaluation error: …" | The expression throws. |

An invalid condition is still saved as a draft. It has consequences elsewhere: an enabled
occurrence that fails validation and is armed by a procedure (ticked on it, or armed Always)
blocks that procedure's Start, and the procedure's page shows "Occurrence '`<name>`' is invalid:
…"; an invalid Always occurrence does not start its outside-run watcher and leaves one visible
diagnostic here instead of repeating it every poll.

Each evaluation runs in a constrained engine (a 500 ms limit, a bounded number of statements and
a bounded memory), so a runaway expression fails its evaluation rather than the station.

## How an occurrence is judged {#how-an-occurrence-is-judged}

During a run the condition is judged over the rows the datalog records, so a channel the
procedure's datalog does not record is unavailable to it. A procedure that arms an occurrence over
such a channel, or that arms one and records nothing at all, is refused on its own page and at
Start: the occurrence would never judge, and the caution note names it. Outside runs, an Always occurrence is
judged by a watcher that reads the unit's bindings live. Either judge holds the last confirmed
state while any referenced channel is unavailable: only a missing value or a bad quality suspends
judging, and a present, good value (a `NaN` included) is evaluable data. The judge records four
transitions: **Activated** (after the on delay), **Cleared** (after the off delay),
**Unavailable** (a referenced channel lost its signal) and **Restored** (every referenced channel
is back; the condition is judged again and the applicable delay restarts).

During a run every transition is written to the run file first and then copied, with its run
context, into the durable journal on the [Events](events-occurrences) page; outside runs the
watcher writes to the journal alone. Starting a run discards the unit's outside-run judge, and
leaving the run creates a fresh one, so no activation carries over an unobserved boundary. Edits
to the name, message or severity affect only later edges; edits to the condition, the delays, the
armed mode or the enabled state start a fresh observation.

Episodes (activation to clear, with any signal-loss interval counted separately) are derived from
those edges wherever the app shows them: the Occurrences card of a panel, the Events page,
Histories and the reports. An episode's active time is exactly the interval between the two
persisted edges, so it excludes the on delay and includes the off delay: the clock starts when the
condition has already held for the on delay, and it stops only when the clear is written, after the
off delay has run. That is the number a report hands to a third party.

## Where an occurrence is used {#where-an-occurrence-is-used}

A procedure's page carries the card **Occurrences armed during this procedure**, with an "`<n>`
armed" pill and one checkbox per occurrence of the model, by name. Ticking one arms it during that
procedure's runs; an occurrence armed **Always** watches regardless and needs no tick. With no
occurrence in the model the card says "No occurrences in this model yet. Add some under the
model's Occurrences." See [Procedures](process-procedures).

Renaming a channel rewrites that channel's `Channels.<identifier>` references in every condition
of the model; it does not touch strings, comments or a property of the same name on another
object.

## Remove occurrence {#remove-occurrence}

**Remove occurrence** stands in the detail bar's danger cluster with the occurrence selected, on
its right-click menu, and as the X on the folder page's card. It asks first ("Delete occurrence",
"This removes the occurrence from the model.") and un-arms the occurrence from every procedure
that ticked it. It needs Edit recipes & evaluations and is greyed under the engineering lock and
the stopped runtime.

## What an occurrence does not do {#what-an-occurrence-does-not-do}

- It is not an alarm: it is not acknowledged, it has no priority and it does not watch tags. The
  plant's alarms are on the [Logic](logic-alarms) page.
- It fires nothing. An activation is recorded and announced; no action, label or report is
  attached to it.
- It cannot read anything but the model's channels: no tag, no variable, no other model.
- It is not judged per unit differently: one condition serves every unit, over that unit's own
  bindings or recorded rows.
