/** * 配方数据源展开 —— **纯函数,零 IO**。 * * 嵌套**两层封顶**:两层(某几颗料 × 该类型的参数字段)是已知业务的上限; * 超出就别 auto —— 退回模型,或者把那一段切出去。 */ import type { AutoExpr, AutoSource } from '@contracts'; import { evalExpr, rowsAt, type ExprCtx, type Row } from './auto-expr.ts'; export type ExpandedRow = { readonly row: Row; readonly outer?: Row | undefined }; /** where 是等值过滤;比值按字符串(台账里数字与文本混着存) */ function matches( row: Row, where: Readonly> | undefined, ctx: ExprCtx, outer?: Row | undefined, ): boolean { if (where === undefined) return true; return Object.entries(where).every(([column, valueExpr]) => { const want = evalExpr(valueExpr, { ...ctx, row, ...(outer === undefined ? {} : { outer }) }); const got = row[column]; return (got === null || got === undefined ? '' : String(got)) === (want === null || want === undefined ? '' : String(want)); }); } export function expandSource(source: AutoSource, ctx: ExprCtx): readonly ExpandedRow[] { if ('once' in source) return [{ row: {} }]; if ('rows' in source) { return source.rows.map((literal) => { const row: Record = {}; for (const [name, expr] of Object.entries(literal)) row[name] = evalExpr(expr, { ...ctx, row }); return { row }; }); } const outers = rowsAt(ctx.scope, source.each).filter((row) => matches(row, source.where, ctx)); if (source.then === undefined) return outers.map((row) => ({ row })); const inner = source.then; const out: ExpandedRow[] = []; for (const outer of outers) { for (const row of rowsAt(ctx.scope, inner.each)) { if (matches(row, inner.where, ctx, outer)) out.push({ row, outer }); } } return out; }