/** * 确定性步骤的**声明式配方**(ADR-014 / spec 2026-08-29 §5.1)。 * * 配方是**数据不是代码**:装进来仍然只是数据,可静态审、可回放、可在治理台上摆出 * 「这一步系统会做什么」。执行第三方代码则相反 —— ADR-004 的立场是「不执行包代码即知包为何物」。 * * **只声明真实现了的那几式。** 表达力有上限是刻意的:上限之外的不要硬塞, * 退回模型,或者把那一段判断从这一步里切出去。 */ import { z } from 'zod'; /** 表达式 —— 递归,`z.lazy` 是唯一写法(zod 的自引用) */ export type AutoExpr = | string | number | boolean /** 入参 / 基座读数 / `let` 里算好的值 —— 三者同一个命名空间,`let` 优先 */ | { readonly in: string } /** 本行的列 */ | { readonly col: string } /** 外层那一行的列(两层嵌套时) */ | { readonly outer: string } /** 拼接,一律按字符串 */ | { readonly cat: readonly AutoExpr[] } /** 只留数字(铸号:`case:openedAt` → `20260829215103`) */ | { readonly fmt: AutoExpr; readonly as: 'digits' } /** 查另一张表的一列;查不到给 `else`(缺省空串) */ | { readonly lookup: string; readonly by: Readonly>; readonly take: string; readonly else?: AutoExpr | undefined; } /** S-FEEL 条件(可嵌套)—— 已算出的同行字段进它的作用域 */ | { readonly when: string; readonly then: AutoExpr; readonly else: AutoExpr }; export const autoExprSchema: z.ZodType = z.lazy(() => z.union([ z.string(), z.number(), z.boolean(), z.object({ in: z.string().min(1) }).strict(), z.object({ col: z.string().min(1) }).strict(), z.object({ outer: z.string().min(1) }).strict(), z.object({ cat: z.array(autoExprSchema).min(1) }).strict(), z.object({ fmt: autoExprSchema, as: z.literal('digits') }).strict(), z .object({ lookup: z.string().min(1), by: z.record(z.string(), autoExprSchema), take: z.string().min(1), else: autoExprSchema.optional(), }) .strict(), z.object({ when: z.string().min(1), then: autoExprSchema, else: autoExprSchema }).strict(), ]), ); /** * 数据源 —— 这一笔要对着几行办。 * * 地址只有四种(spec §5.1):`域.对象`(**任务视图 = 正式表 + 本单草稿叠加**,默认该用的那一种)· * `mine:域.对象`(只有本单草稿写下的行)· `in:入参数组` · `result:xxx`(本拍 analyze 的出参)。 * **没有「纯正式表」那一种** —— 要区分「本单写的」与「别人放行的」,`mine:` 就够了。 */ export type AutoSource = | { readonly once: true } | { readonly rows: readonly Readonly>[] } | { readonly each: string; readonly where?: Readonly> | undefined; /** 第二层,**到此为止** —— 三层以上就别 auto 了 */ readonly then?: | { readonly each: string; readonly where?: Readonly> | undefined } | undefined; }; export const autoSourceSchema: z.ZodType = z.union([ z.object({ once: z.literal(true) }).strict(), z.object({ rows: z.array(z.record(z.string(), autoExprSchema)).min(1) }).strict(), z .object({ each: z.string().min(1), where: z.record(z.string(), autoExprSchema).optional(), then: z .object({ each: z.string().min(1), where: z.record(z.string(), autoExprSchema).optional() }) .strict() .optional(), }) .strict(), ]); /** 五个动词 —— `when` 为假就跳过这一笔 */ export type AutoAction = | { readonly kind: 'project'; readonly when?: string | undefined; /** 台账地址「域.对象」—— 必须在 `declarations.ledger` 里(静态审) */ readonly into: string; /** 这一笔投影出来的行**就是这张表在本单草稿里的全部**,多出来的撤掉。只动草稿层 */ readonly prune?: boolean | undefined; readonly from: AutoSource; readonly fields: Readonly>; } | { readonly kind: 'analyze'; readonly when?: string | undefined; /** 活动声明过的模型名 */ readonly model: string; /** 出参落进拍内作用域 `result:`,供后面的 project 用 `each`/`lookup` 消费 */ readonly into: string; /** 给模型的 system 段 —— 说清判什么、四态各是什么意思 */ readonly instruction: string; /** 入参由声明组装:一行一条,`fields` 决定每条摆哪几个键 */ readonly input: { readonly from: AutoSource; readonly fields: Readonly> }; /** 出参 JSON Schema —— 顶层必须是 object */ readonly output: Readonly>; } | { readonly kind: 'card'; readonly when?: string | undefined; readonly as: 'execution' | 'decide'; readonly variant?: ('data' | 'review') | undefined; readonly title: AutoExpr; /** 决策卡问的那句话 */ readonly ask?: AutoExpr | undefined; /** 作业指导书 —— 一条一句,静态文本 */ readonly guide?: readonly string[] | undefined; readonly standard?: AutoExpr | undefined; /** 调用必附上下文 —— **基座拒发 basis 为空的工单**,别指望兜底(它摆的是内部家具) */ readonly basis?: readonly { readonly k: string; readonly v: AutoExpr }[] | undefined; /** 这张卡的提交闸盯哪几张表 */ readonly gateLedgers?: readonly string[] | undefined; /** 决策卡的选项;`variant:'review'` 时允许只有一个 */ readonly options?: | readonly { readonly value: string; readonly label: string; readonly detail?: string | undefined }[] | undefined; } | { readonly kind: 'submit'; readonly when?: string | undefined; readonly rationale: AutoExpr } | { readonly kind: 'say'; readonly when?: string | undefined; readonly text: AutoExpr }; export const autoActionSchema: z.ZodType = z.discriminatedUnion('kind', [ z .object({ kind: z.literal('project'), when: z.string().min(1).optional(), into: z.string().regex(/^[a-z_]+\.[a-z_]+$/, '台账地址写成「域.对象」'), prune: z.boolean().optional(), from: autoSourceSchema, fields: z.record(z.string(), autoExprSchema), }) .strict(), z .object({ kind: z.literal('analyze'), when: z.string().min(1).optional(), model: z.string().min(1), into: z.string().min(1), instruction: z.string().min(1), input: z.object({ from: autoSourceSchema, fields: z.record(z.string(), autoExprSchema) }).strict(), output: z.record(z.string(), z.unknown()), }) .strict(), z .object({ kind: z.literal('card'), when: z.string().min(1).optional(), as: z.enum(['execution', 'decide']), variant: z.enum(['data', 'review']).optional(), title: autoExprSchema, ask: autoExprSchema.optional(), guide: z.array(z.string().min(1)).optional(), standard: autoExprSchema.optional(), basis: z.array(z.object({ k: z.string().min(1), v: autoExprSchema }).strict()).optional(), gateLedgers: z.array(z.string().min(1)).optional(), options: z .array(z.object({ value: z.string().min(1), label: z.string().min(1), detail: z.string().optional() }).strict()) .optional(), }) .strict(), z.object({ kind: z.literal('submit'), when: z.string().min(1).optional(), rationale: autoExprSchema }).strict(), z.object({ kind: z.literal('say'), when: z.string().min(1).optional(), text: autoExprSchema }).strict(), ]); export type AutoRecipe = { /** 配方级局部值:先算一遍,后面所有表达式都能 `{in:'名'}` 引用 —— 整单只有一个案号,靠它 */ readonly let?: Readonly> | undefined; /** 按序执行 */ readonly do: readonly AutoAction[]; }; export const autoRecipeSchema: z.ZodType = z .object({ let: z.record(z.string(), autoExprSchema).optional(), do: z.array(autoActionSchema).min(1), }) .strict();