Why the calculator parses its own expressions
The calculator used to evaluate input with Function(...). A regex guarded the string, so in practice only arithmetic and a fixed set of function names reached it. That made it look safe. It was still dynamic code execution: under a CSP script-src without 'unsafe-eval', the browser throws rather than run it. We saw real violation reports from /tools/calculator, which made this one of the blockers for moving CSP from Report-Only to enforce.
The replacement is a recursive-descent parser in components/tools/calculator/eval.ts. Input is tokenized, then parsed through a chain of levels: expression (+ -), term (* / %), power (^, right-associative), unary (+ -), postfix (!), and finally a primary — a number, a parenthesized expression, a function or a constant. Implicit multiplication is accepted, so 2pi, 3(4+1) and 2sqrt(9) all parse.
The security property is not "the string is checked carefully". It is that the grammar has nowhere to put anything dangerous. Identifiers must match a whitelist of function and constant names. There are no commas, so no multi-argument calls; no strings, property access, semicolons or square brackets. Invalid input throws EvalError, and so does a non-finite result such as division by zero.
Two checks are about drift rather than arithmetic. A test reads eval.ts and Calculator.tsx, strips comments, and fails if eval(, new Function( or Function( reappears. The behavior tests keep the old contract: precedence, unary minus, nested parentheses, .5 + .5, and full-width operators pasted from elsewhere.
Floating-point noise is handled at display time, not in the parser. The evaluator returns the double; the result formatter shows 0.1 + 0.2 as 0.3.
This removes one blocker, not all of them. CSP stays Report-Only until the remaining preconditions pass scripts/check-csp-enforce-ready.sh.