1/** 2 * @factory/database-engine/formula — the safe formula engine (TASK-0084). 3 * 4 * Public API (R7): 5 * - `parseFormula(src)` → AstNode | FormulaError 6 * - `validateFormula(src, propertyNames, propertyTypes?)` → 7 * {ast, returnType, dependencies, usesNow} | FormulaError (save-time entry; 8 * TASK-0085 calls this — an invalid formula is never stored) 9 * - `evaluateFormula(ast, ctx)` → {value, steps} | FormulaError (read-time) 10 * - `FormulaError {code: PARSE|TYPE|RUNTIME|UNKNOWN_FUNCTION}` 11 * 12 * ZERO eval / new Function (AGENTS.md rule 18) — the evaluator walks the AST 13 * only; the closed function table is the entire dispatch surface. The CI 14 * no-eval grep gate covers this directory (TASK-0090 runs it repo-wide). 15 */ 16 17import type { AstNode, EValue, FormulaContext, FormulaValue } from "./types.js"; 18import { FormulaError } from "./types.js"; 19import { parseFormula } from "./parser.js"; 20import { evaluateFormula } from "./evaluator.js"; 21import { inferType, extractDependencies, usesNow } from "./validate.js"; 22 23export * from "./types.js"; 24export { tokenize } from "./lexer.js"; 25export { parseAst, parseFormula } from "./parser.js"; 26export { evaluateFormula } from "./evaluator.js"; 27export { FUNCTIONS, normalizeProperty } from "./functions.js"; 28export { inferType, extractDependencies, usesNow } from "./validate.js"; 29 30export interface ValidatedFormula { 31 ast: AstNode; 32 returnType: import("./types.js").FormulaType; 33 dependencies: string[]; 34 usesNow: boolean; 35} 36 37/** 38 * Save-time validation: parse + static type inference + name resolution. 39 * `properties` = name → value (a sample record's values), `propertyTypes` = 40 * name → PropertyType. Returns the structured AST + returnType + 41 * dependencies + usesNow, or throws FormulaError (→ 400 at the API layer — 42 * never stored). 43 */ 44export function validateFormula( 45 src: string, 46 properties: Record<string, unknown>, 47 propertyTypes?: Record<string, string>, 48): ValidatedFormula { 49 const ast = parseFormula(src); 50 const ctx: FormulaContext = { now: 0, properties, propertyTypes }; 51 const returnType = inferType(ast, ctx); 52 return { ast, returnType, dependencies: extractDependencies(ast), usesNow: usesNow(ast) }; 53} 54 55/** 56 * Demote a typed EValue to its raw FormulaValue: strip the `t` annotation and 57 * round-trip the array branch (`EValue[]` → `FormulaValue[]`). Needed because 58 * `EValue.v`'s array branch is `EValue[]` (typed children) while the public 59 * raw return is `FormulaValue` (raw elements). 60 */ 61function demote(value: EValue): FormulaValue { 62 if (value.t === "array") return (value.v as EValue[]).map(demote); 63 // Non-array branches carry a primitive (number|string|boolean|null) — a 64 // subtype of FormulaValue. The cast is safe: the array branch is handled above. 65 return value.v as number | string | boolean | null; 66} 67 68/** Read-time evaluation. Returns the raw value (null | number | string | bool | array). */ 69export function evalFormulaValue(ast: AstNode, ctx: FormulaContext): FormulaValue { 70 const { value } = evaluateFormula(ast, ctx); 71 return demote(value); 72} 73