/** * 配方表达式求值 —— **纯函数,零 IO**(编码规范:domain 层不碰端口)。 * * S-FEEL 不在这里实现:`condition` 由调用方注入(与哨兵、`doneWhen` 同一个求值器)。 * **不发明第二套求值** 是 ADR-014 的一条硬约束。 */ import type { AutoExpr } from '@contracts'; export type Row = Readonly>; export type ExprCtx = { /** 任务视图(正式 + 本单草稿叠加)+ 基座读数 + 起单入参,外加本拍的 `result:*` */ readonly scope: Readonly>; /** 配方级 `let` 算好的值 —— 与 scope 同一命名空间,它优先 */ readonly lets: Readonly>; readonly row?: Row | undefined; readonly outer?: Row | undefined; /** 同一行里**已经算出来**的字段 —— 「先 lookup 出依据,再按依据判 found/absent」靠它 */ readonly computed?: Row | undefined; readonly condition: (expr: string, scope: Readonly>) => boolean; }; /** * 地址取行 —— 四种地址(spec §5.1),取不到一律空数组。 * * **取不到不抛**:首拍很多表本来就一行都还没有,抛了等于每一单开局就交人。 */ export function rowsAt(scope: Readonly>, address: string): readonly Row[] { const raw = address.startsWith('in:') ? scope[address.slice('in:'.length)] : scope[address]; if (!Array.isArray(raw)) return []; return raw.map((item) => typeof item === 'object' && item !== null ? (item as Row) : ({ value: item } satisfies Row), ); } /** 一个值参与比较时的归一:台账里数字与文本混着存,一律按字符串比 */ function asKey(value: unknown): string { return value === null || value === undefined ? '' : String(value); } export function evalExpr(expr: AutoExpr, ctx: ExprCtx): unknown { if (typeof expr === 'string' || typeof expr === 'number' || typeof expr === 'boolean') return expr; if ('in' in expr) { const fromLet = ctx.lets[expr.in]; return fromLet !== undefined ? fromLet : (ctx.scope[expr.in] ?? ''); } if ('col' in expr) return ctx.row?.[expr.col] ?? ''; if ('outer' in expr) return ctx.outer?.[expr.outer] ?? ''; if ('cat' in expr) return expr.cat.map((part) => evalText(part, ctx)).join(''); if ('fmt' in expr) return evalText(expr.fmt, ctx).replace(/\D/g, ''); if ('lookup' in expr) { const pairs = Object.entries(expr.by).map(([column, valueExpr]) => [column, asKey(evalExpr(valueExpr, ctx))] as const); const hit = rowsAt(ctx.scope, expr.lookup).find((row) => pairs.every(([column, want]) => asKey(row[column]) === want)); if (hit === undefined) return expr.else === undefined ? '' : evalExpr(expr.else, ctx); return hit[expr.take] ?? (expr.else === undefined ? '' : evalExpr(expr.else, ctx)); } /* * 条件走注入的 S-FEEL。谓词写坏了**不该拖垮整拍** —— 与 `doneWhen` 同一条处置: * 当作「没算出来」走 else。真写错了,静态审在装包时就该拦下(Task 9)。 */ const scope = { ...ctx.scope, ...ctx.lets, ...(ctx.outer ?? {}), ...(ctx.row ?? {}), ...(ctx.computed ?? {}) }; let hit = false; try { hit = ctx.condition(expr.when, scope); } catch { hit = false; } return evalExpr(hit ? expr.then : expr.else, ctx); } /** 出字符串的那一路 —— 拼接、标题、留痕都要它 */ export function evalText(expr: AutoExpr, ctx: ExprCtx): string { const value = evalExpr(expr, ctx); return value === null || value === undefined ? '' : String(value); }