Alert rules as data
Course 1 gave your watchlist threshold rules that ran on render. This lesson makes them documents, which is what lets the next lesson move them somewhere they can actually fire.
The rule document
type Rule = {
id: string;
symbol: string;
field: "close" | "change_p" | "volume";
op: ">" | "<" | "crosses_above" | "crosses_below";
value: number;
enabled: boolean;
lastFiredAt: number | null;
lastValue: number | null;
};
Same move as filters in course 2 and panels in unit 1: data rather than code. One evaluator reads all of them, and adding a rule is a row rather than a deploy.
field and op are closed sets, validated on read. A rule is a thing your server evaluates and may act on, so it is privileged input and gets the allowlist treatment.
Two of those fields are the interesting ones
lastValue and lastFiredAt are what separate a rule from a comparison.
crosses_above needs lastValue. "Price is above 300" is true on every evaluation while it stays above; "price crossed above 300" is true once. The difference is having the previous observation, and without it you cannot express the thing people actually want.
lastFiredAt is what stops the flood. Next lesson's subject, and the field has to exist in the document from the start or you will be migrating under pressure.
Evaluation is a pure function
function evaluate(rule: Rule, quote: Quote): { fires: boolean; nextLastValue: number };
No I/O, no clock read, no notification. It takes a rule and an observation and returns a verdict plus the new state to store.
That signature is what makes the whole feature testable, and course 3's craft applies directly: fixtures for crossing up, crossing down, sitting exactly on the threshold, and a missing field. The exactly-equal case is the one generated code gets wrong, because > and >= both look reasonable and only one matches what you told the user.
Say what the rule watches, in words
Render every rule back as a sentence: "Alert when AAPL.US trades above 320.00 (checked against the last price, not the session close)". Not a row of dropdowns echoing the JSON.
A person reading their own rules a month later needs to know what they asked for. If the sentence is awkward to generate, the rule model is probably wrong — that is a useful design signal and it costs one function.
Try it now
Write the evaluator and its fixtures before any UI, including the exactly-on-the-threshold case. Then ask your assistant what your rule does when the price equals the value precisely, and see whether its answer matches your test.