Archived
地址四种、取不到一律空数组(首拍很多表本来就没行,抛了等于开局交人); 比值一律按字符串(台账里数字与文本混着存);谓词算不出来走 else,不拖垮整拍。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
79 lines
3.5 KiB
TypeScript
79 lines
3.5 KiB
TypeScript
/**
|
|
* 配方表达式求值 —— **纯函数,零 IO**(编码规范:domain 层不碰端口)。
|
|
*
|
|
* S-FEEL 不在这里实现:`condition` 由调用方注入(与哨兵、`doneWhen` 同一个求值器)。
|
|
* **不发明第二套求值** 是 ADR-014 的一条硬约束。
|
|
*/
|
|
import type { AutoExpr } from '@contracts';
|
|
|
|
export type Row = Readonly<Record<string, unknown>>;
|
|
|
|
export type ExprCtx = {
|
|
/** 任务视图(正式 + 本单草稿叠加)+ 基座读数 + 起单入参,外加本拍的 `result:*` */
|
|
readonly scope: Readonly<Record<string, unknown>>;
|
|
/** 配方级 `let` 算好的值 —— 与 scope 同一命名空间,它优先 */
|
|
readonly lets: Readonly<Record<string, unknown>>;
|
|
readonly row?: Row | undefined;
|
|
readonly outer?: Row | undefined;
|
|
/** 同一行里**已经算出来**的字段 —— 「先 lookup 出依据,再按依据判 found/absent」靠它 */
|
|
readonly computed?: Row | undefined;
|
|
readonly condition: (expr: string, scope: Readonly<Record<string, unknown>>) => boolean;
|
|
};
|
|
|
|
/**
|
|
* 地址取行 —— 四种地址(spec §5.1),取不到一律空数组。
|
|
*
|
|
* **取不到不抛**:首拍很多表本来就一行都还没有,抛了等于每一单开局就交人。
|
|
*/
|
|
export function rowsAt(scope: Readonly<Record<string, unknown>>, 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);
|
|
}
|