1/** 2 * @factory/database-engine/formula — types + FormulaError (TASK-0084). 3 * 4 * The AST is JSON-serializable (stored in `formulas.ast`) — the same 5 * discriminated union the editor autocomplete and the Phase-12 "generate 6 * formula" tool consume. Zero eval/new Function (AGENTS.md rule 18): the 7 * evaluator walks this AST only. 8 */ 9 10/** Runtime formula types (the inferred static type). */ 11export type FormulaType = "number" | "string" | "date" | "bool" | "null" | "array" | "unknown"; 12 13/** Runtime values (the RAW wire/AST representation): dates are ISO strings. */ 14export type FormulaValue = number | string | boolean | null | FormulaValue[]; 15 16/** 17 * The payload a TYPED value carries. Its array branch is `EValue[]` — the 18 * evaluator's array representation: the walker produces typed child EValues 19 * (`ArrayLiteral` → `node.elements.map(e => walk(...))`), so an `EValue` with 20 * `t: "array"` carries `EValue[]` at runtime. (That is why this is distinct 21 * from `FormulaValue[]`, whose element type is the raw `FormulaValue` — the 22 * two are disjoint, and typing `v` as `FormulaValue` made every 23 * `v as EValue[]` cast a TS2352 "may be a mistake" error.) 24 */ 25export type EValuePayload = number | string | boolean | null | EValue[]; 26 27/** A typed value as the evaluator carries it. */ 28export interface EValue { 29 v: EValuePayload; 30 t: FormulaType; 31} 32 33import type { FormulaAstNode } from "@factory/shared/dto"; 34 35/** The formula AST (shared type — see @factory/shared/dto/formula-ast.ts). */ 36export type AstNode = FormulaAstNode; 37 38/** 39 * `FORMULA_TIMEOUT` (canonical error code — docs/api/authentication.md 40 * §9.1, 422) is thrown when evaluation exceeds EITHER the operation budget 41 * (`EVAL_OP_LIMIT`) or the wall-clock budget (`EVAL_TIMEOUT_MS`). It is 42 * distinct from the other codes: it signals "too much work / too slow", not 43 * a syntax/type problem, so a hostile expression is rejected as a typed 44 * timeout rather than hanging the request. 45 */ 46export type FormulaErrorCode = 47 | "PARSE" 48 | "TYPE" 49 | "RUNTIME" 50 | "UNKNOWN_FUNCTION" 51 | "FORMULA_TIMEOUT"; 52 53export class FormulaError extends Error { 54 readonly code: FormulaErrorCode; 55 // `| undefined` (not just `?`): under `exactOptionalPropertyTypes` a `T?` 56 // field does not accept a `T | undefined` assignment, so the optional 57 // fields must be declared `T | undefined`. 58 readonly pos?: number | undefined; 59 readonly property?: string | undefined; 60 61 constructor(code: FormulaErrorCode, message: string, pos?: number, property?: string) { 62 super(message); 63 this.code = code; 64 this.pos = pos; 65 this.property = property; 66 this.name = "FormulaError"; 67 } 68} 69 70/** Evaluation context — the ONLY host access the evaluator has (R5). */ 71export interface FormulaContext { 72 /** Injectable "now" (epoch ms) — `now()` reads this. */ 73 now: number; 74 /** Property name → domain value (number | ISO date string | bool | string | string[] | null). */ 75 properties: Record<string, unknown>; 76 /** Property name → PropertyType (used to normalize date-family props). */ 77 propertyTypes?: Record<string, string> | undefined; 78} 79 80/** 81 * The two evaluation budgets (TASK-0134 security gate). Exceeding EITHER 82 * throws `FormulaError("FORMULA_TIMEOUT")` — the API surface maps it to a 83 * 422 with `details[0] = { reason: "formula_timeout" }` (canonical table, 84 * docs/api/authentication.md §9.1): 85 * - `EVAL_OP_LIMIT` (100k node-visits): bounds worst-case AST walks. A 86 * `varargs` `and()/or()`/`concat()` can chain 100k+ cheap ops in a single 87 * line, so this is the primary DoS guard (the old 10k step limit caught 88 * deep trees but not wide ones). 89 * - `EVAL_TIMEOUT_MS` (50 ms wall-clock): the wall-clock backstop — a hostile 90 * expression that is *expensive per step* (large numeric arithmetic, deep 91 * string ops) but under the op cap still gets cut off before it can hang 92 * the request. Measured with `process.hrtime.bigint()`; on environments 93 * without hrtime it degrades to `Date.now()` (still bounded, never hangs). 94 */ 95export const EVAL_OP_LIMIT = 100_000; 96export const EVAL_TIMEOUT_MS = 50; 97// Back-compat alias (the old "steps" budget — now the op budget). Existing 98// tests / call sites reference MAX_STEPS; keep it in sync with EVAL_OP_LIMIT. 99export const MAX_STEPS = EVAL_OP_LIMIT; 100export const MAX_OUTPUT_BYTES = 1_000_000; // 1 MB 101export const MAX_NESTING = 16; 102