/** * @factory/database-engine/formula — the safe formula engine (TASK-0084). * * Public API (R7): * - `parseFormula(src)` → AstNode | FormulaError * - `validateFormula(src, propertyNames, propertyTypes?)` → * {ast, returnType, dependencies, usesNow} | FormulaError (save-time entry; * TASK-0085 calls this — an invalid formula is never stored) * - `evaluateFormula(ast, ctx)` → {value, steps} | FormulaError (read-time) * - `FormulaError {code: PARSE|TYPE|RUNTIME|UNKNOWN_FUNCTION}` * * ZERO eval / new Function (AGENTS.md rule 18) — the evaluator walks the AST * only; the closed function table is the entire dispatch surface. The CI * no-eval grep gate covers this directory (TASK-0090 runs it repo-wide). */ import type { AstNode, EValue, FormulaContext, FormulaValue } from "./types.js"; import { FormulaError } from "./types.js"; import { parseFormula } from "./parser.js"; import { evaluateFormula } from "./evaluator.js"; import { inferType, extractDependencies, usesNow } from "./validate.js"; export * from "./types.js"; export { tokenize } from "./lexer.js"; export { parseAst, parseFormula } from "./parser.js"; export { evaluateFormula } from "./evaluator.js"; export { FUNCTIONS, normalizeProperty } from "./functions.js"; export { inferType, extractDependencies, usesNow } from "./validate.js"; export interface ValidatedFormula { ast: AstNode; returnType: import("./types.js").FormulaType; dependencies: string[]; usesNow: boolean; } /** * Save-time validation: parse + static type inference + name resolution. * `properties` = name → value (a sample record's values), `propertyTypes` = * name → PropertyType. Returns the structured AST + returnType + * dependencies + usesNow, or throws FormulaError (→ 400 at the API layer — * never stored). */ export function validateFormula( src: string, properties: Record, propertyTypes?: Record, ): ValidatedFormula { const ast = parseFormula(src); const ctx: FormulaContext = { now: 0, properties, propertyTypes }; const returnType = inferType(ast, ctx); return { ast, returnType, dependencies: extractDependencies(ast), usesNow: usesNow(ast) }; } /** * Demote a typed EValue to its raw FormulaValue: strip the `t` annotation and * round-trip the array branch (`EValue[]` → `FormulaValue[]`). Needed because * `EValue.v`'s array branch is `EValue[]` (typed children) while the public * raw return is `FormulaValue` (raw elements). */ function demote(value: EValue): FormulaValue { if (value.t === "array") return (value.v as EValue[]).map(demote); // Non-array branches carry a primitive (number|string|boolean|null) — a // subtype of FormulaValue. The cast is safe: the array branch is handled above. return value.v as number | string | boolean | null; } /** Read-time evaluation. Returns the raw value (null | number | string | bool | array). */ export function evalFormulaValue(ast: AstNode, ctx: FormulaContext): FormulaValue { const { value } = evaluateFormula(ast, ctx); return demote(value); }