/** * @factory/database-engine/formula — types + FormulaError (TASK-0084). * * The AST is JSON-serializable (stored in `formulas.ast`) — the same * discriminated union the editor autocomplete and the Phase-12 "generate * formula" tool consume. Zero eval/new Function (AGENTS.md rule 18): the * evaluator walks this AST only. */ /** Runtime formula types (the inferred static type). */ export type FormulaType = "number" | "string" | "date" | "bool" | "null" | "array" | "unknown"; /** Runtime values (the RAW wire/AST representation): dates are ISO strings. */ export type FormulaValue = number | string | boolean | null | FormulaValue[]; /** * The payload a TYPED value carries. Its array branch is `EValue[]` — the * evaluator's array representation: the walker produces typed child EValues * (`ArrayLiteral` → `node.elements.map(e => walk(...))`), so an `EValue` with * `t: "array"` carries `EValue[]` at runtime. (That is why this is distinct * from `FormulaValue[]`, whose element type is the raw `FormulaValue` — the * two are disjoint, and typing `v` as `FormulaValue` made every * `v as EValue[]` cast a TS2352 "may be a mistake" error.) */ export type EValuePayload = number | string | boolean | null | EValue[]; /** A typed value as the evaluator carries it. */ export interface EValue { v: EValuePayload; t: FormulaType; } import type { FormulaAstNode } from "@factory/shared/dto"; /** The formula AST (shared type — see @factory/shared/dto/formula-ast.ts). */ export type AstNode = FormulaAstNode; /** * `FORMULA_TIMEOUT` (canonical error code — docs/api/authentication.md * §9.1, 422) is thrown when evaluation exceeds EITHER the operation budget * (`EVAL_OP_LIMIT`) or the wall-clock budget (`EVAL_TIMEOUT_MS`). It is * distinct from the other codes: it signals "too much work / too slow", not * a syntax/type problem, so a hostile expression is rejected as a typed * timeout rather than hanging the request. */ export type FormulaErrorCode = | "PARSE" | "TYPE" | "RUNTIME" | "UNKNOWN_FUNCTION" | "FORMULA_TIMEOUT"; export class FormulaError extends Error { readonly code: FormulaErrorCode; // `| undefined` (not just `?`): under `exactOptionalPropertyTypes` a `T?` // field does not accept a `T | undefined` assignment, so the optional // fields must be declared `T | undefined`. readonly pos?: number | undefined; readonly property?: string | undefined; constructor(code: FormulaErrorCode, message: string, pos?: number, property?: string) { super(message); this.code = code; this.pos = pos; this.property = property; this.name = "FormulaError"; } } /** Evaluation context — the ONLY host access the evaluator has (R5). */ export interface FormulaContext { /** Injectable "now" (epoch ms) — `now()` reads this. */ now: number; /** Property name → domain value (number | ISO date string | bool | string | string[] | null). */ properties: Record; /** Property name → PropertyType (used to normalize date-family props). */ propertyTypes?: Record | undefined; } /** * The two evaluation budgets (TASK-0134 security gate). Exceeding EITHER * throws `FormulaError("FORMULA_TIMEOUT")` — the API surface maps it to a * 422 with `details[0] = { reason: "formula_timeout" }` (canonical table, * docs/api/authentication.md §9.1): * - `EVAL_OP_LIMIT` (100k node-visits): bounds worst-case AST walks. A * `varargs` `and()/or()`/`concat()` can chain 100k+ cheap ops in a single * line, so this is the primary DoS guard (the old 10k step limit caught * deep trees but not wide ones). * - `EVAL_TIMEOUT_MS` (50 ms wall-clock): the wall-clock backstop — a hostile * expression that is *expensive per step* (large numeric arithmetic, deep * string ops) but under the op cap still gets cut off before it can hang * the request. Measured with `process.hrtime.bigint()`; on environments * without hrtime it degrades to `Date.now()` (still bounded, never hangs). */ export const EVAL_OP_LIMIT = 100_000; export const EVAL_TIMEOUT_MS = 50; // Back-compat alias (the old "steps" budget — now the op budget). Existing // tests / call sites reference MAX_STEPS; keep it in sync with EVAL_OP_LIMIT. export const MAX_STEPS = EVAL_OP_LIMIT; export const MAX_OUTPUT_BYTES = 1_000_000; // 1 MB export const MAX_NESTING = 16;