"This module owns the MODEL. The frontend keeps the PROSE."
The split, drawn precisely
Moving a computation out of a frontend raises an immediate question: how much goes with it? The lazy answer is everything, and it produces an engine that returns pre-formatted display strings and a frontend that is a dumb pipe. That is a real architecture and it is usually the wrong one, because it puts editorial decisions behind a deploy.
The split here is stated in one line, and it is worth adopting verbatim.
The engine owns the model: which series feed each indicator, which direction each one reads, what weight each category carries, what reference scale applies before a series has earned a percentile. Everything that is arithmetic or is an input to arithmetic.
The frontend keeps the prose: each card's context paragraph, its historical anchors, its labels. Everything that is writing.
Why this line and not another
Because the two halves have completely different change profiles, and different people change them.
Model changes are consequential and must be auditable. Adding an indicator, reweighting a category, altering a reference range — each of those changes the number, and someone will eventually need to explain why the figure moved on a day when the market did not. That demands version control, review, a stored trace, and a source string.
Prose changes are frequent and cheap. Rewording a paragraph, adding a historical anchor, clarifying a label — none of them change any number, and treating them with the same ceremony as a model change means either the ceremony gets skipped or the prose never improves.
What crosses the boundary
The engine's response carries everything the prose needs to be written about: the composite, the regime phrase, per-indicator scores with the path each took, the as-of date, the oldest input, the narrowest window, the list of reference-scored members, and the formula as a string.
That last one looks like the elegant part, and it is worth being exact about how far it actually goes. Shipping a formula string beside the number means the frontend could explain the model without knowing the weights. But the string is assembled by hand in the same module and hard-codes 40 / 30 / 30 separately from the CATEGORIES list that the arithmetic actually reads. So it is a copy after all — a very short-range one, two hundred lines from its original, but a copy.
And the copies have already drifted, which makes this the live example rather than the cautionary tale. Count them honestly. The weights exist in three places: the CATEGORIES list the arithmetic reads, the hand-built formula string beside the number, and — in the very sentence form the next callout offers as the mistake nobody made — the dashboard's own methodology footer, spelling out "Valuation 40% · Risk pricing 30% · Liquidity 30%" in prose. Two of the three are copies, and the page has drifted in two further ways beside them: it says "Weighted mean of 7 indicators" in two separate places while the engine scores eight, and its footer still describes the long-window gauge as something that will join the model later, when it has been a scoring member for a while.
Note what the same screen gets right, because it is the whole argument in one contrast: the per-category weights rendered beside that footer come from the payload, and they cannot drift. Same page, same numbers, two techniques — and every wrong statement on it is on the hard-coded side.