diff --git a/api-report/packages/contracts/src/__tests__/auto-recipe.test.d.ts b/api-report/packages/contracts/src/__tests__/auto-recipe.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/api-report/packages/contracts/src/__tests__/auto-recipe.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/api-report/packages/contracts/src/api/governance.d.ts b/api-report/packages/contracts/src/api/governance.d.ts index d685558..4ccea05 100644 --- a/api-report/packages/contracts/src/api/governance.d.ts +++ b/api-report/packages/contracts/src/api/governance.d.ts @@ -2238,6 +2238,7 @@ export declare const installActivityRoute: { }>>; acceptanceChecklist: z.ZodOptional>; humanWrites: z.ZodOptional>; + auto: z.ZodOptional>>; }, z.core.$strip>>>; phases: z.ZodOptional>; + readonly take: string; + readonly else?: AutoExpr | undefined; +} +/** S-FEEL 条件(可嵌套)—— 已算出的同行字段进它的作用域 */ + | { + readonly when: string; + readonly then: AutoExpr; + readonly else: AutoExpr; +}; +export declare const autoExprSchema: z.ZodType; +/** + * 数据源 —— 这一笔要对着几行办。 + * + * 地址只有四种(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 declare const autoSourceSchema: z.ZodType; +/** 五个动词 —— `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 declare const autoActionSchema: z.ZodType; +export type AutoRecipe = { + /** 配方级局部值:先算一遍,后面所有表达式都能 `{in:'名'}` 引用 —— 整单只有一个案号,靠它 */ + readonly let?: Readonly> | undefined; + /** 按序执行 */ + readonly do: readonly AutoAction[]; +}; +export declare const autoRecipeSchema: z.ZodType; diff --git a/api-report/packages/contracts/src/index.d.ts b/api-report/packages/contracts/src/index.d.ts index 57b945d..857d0b2 100644 --- a/api-report/packages/contracts/src/index.d.ts +++ b/api-report/packages/contracts/src/index.d.ts @@ -15,4 +15,5 @@ export * from './manifest.ts'; export * from './org.ts'; export * from './mail.ts'; export * from './activity-ops.ts'; +export * from './auto-recipe.ts'; export * from './api/index.ts'; diff --git a/api-report/packages/contracts/src/manifest.d.ts b/api-report/packages/contracts/src/manifest.d.ts index 8a91f3f..19a17cd 100644 --- a/api-report/packages/contracts/src/manifest.d.ts +++ b/api-report/packages/contracts/src/manifest.d.ts @@ -208,6 +208,7 @@ export declare const planStepSchema: z.ZodObject<{ }>>; acceptanceChecklist: z.ZodOptional>; humanWrites: z.ZodOptional>; + auto: z.ZodOptional>>; }, z.core.$strip>; export type PlanStep = z.infer; /** @@ -274,6 +275,7 @@ export declare const loopModelSchema: z.ZodObject<{ }>>; acceptanceChecklist: z.ZodOptional>; humanWrites: z.ZodOptional>; + auto: z.ZodOptional>>; }, z.core.$strip>>>; phases: z.ZodOptional>; acceptanceChecklist: z.ZodOptional>; humanWrites: z.ZodOptional>; + auto: z.ZodOptional>>; }, z.core.$strip>>>; phases: z.ZodOptional>; acceptanceChecklist: z.ZodOptional>; humanWrites: z.ZodOptional>; + auto: z.ZodOptional>>; }, z.core.$strip>>>; phases: z.ZodOptional>; acceptanceChecklist: z.ZodOptional>; humanWrites: z.ZodOptional>; + auto: z.ZodOptional>>; }, z.core.$strip>>>; phases: z.ZodOptional { + it('收下一份用全了五个动词的配方', () => { + const parsed = autoRecipeSchema.safeParse({ + let: { evalRef: { cat: ['W05-', { fmt: { in: 'case:openedAt' }, as: 'digits' }] } }, + do: [ + { kind: 'project', into: 'sub_eval.part', prune: true, from: { rows: [{ pn: { in: 'originalInternalPn' } }] }, fields: { internal_pn: { col: 'pn' } } }, + { kind: 'analyze', model: 'deepseek-v4-flash', into: 'items', instruction: '逐条判四态', input: { from: { each: 'parts.type_param' }, fields: { name: { col: 'param_name' } } }, output: { type: 'object' } }, + { kind: 'card', as: 'execution', variant: 'data', title: '补数据', basis: [{ k: '原 PN', v: { in: 'originalInternalPn' } }], gateLedgers: ['parts.profile'] }, + { kind: 'submit', rationale: '结论已备好' }, + { kind: 'say', text: '这一步办完了' }, + ], + }); + expect(parsed.success).toBe(true); + }); + + it('不认识的动词当场拒 —— 没实现的形状不许装进来', () => { + const parsed = autoRecipeSchema.safeParse({ do: [{ kind: 'drop', into: 'a.b' }] }); + expect(parsed.success).toBe(false); + }); + + it('嵌套只许两层:第三层当场拒', () => { + const parsed = autoRecipeSchema.safeParse({ + do: [{ kind: 'project', into: 'a.b', from: { each: 'a.b', then: { each: 'c.d', then: { each: 'e.f' } } }, fields: {} }], + }); + expect(parsed.success).toBe(false); + }); + + it('prune 只许挂在 project 上', () => { + const parsed = autoRecipeSchema.safeParse({ do: [{ kind: 'say', text: 'x', prune: true }] }); + expect(parsed.success).toBe(false); + }); +}); diff --git a/packages/contracts/src/auto-recipe.ts b/packages/contracts/src/auto-recipe.ts new file mode 100644 index 0000000..5e77be2 --- /dev/null +++ b/packages/contracts/src/auto-recipe.ts @@ -0,0 +1,197 @@ +/** + * 确定性步骤的**声明式配方**(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(); diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 5b5b4ff..91cf9de 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -16,4 +16,5 @@ export * from './manifest.ts'; export * from './org.ts'; export * from './mail.ts'; export * from './activity-ops.ts'; +export * from './auto-recipe.ts'; export * from './api/index.ts'; diff --git a/packages/contracts/src/manifest.ts b/packages/contracts/src/manifest.ts index 76c9fec..16f3fa5 100644 --- a/packages/contracts/src/manifest.ts +++ b/packages/contracts/src/manifest.ts @@ -7,6 +7,7 @@ */ import { z } from 'zod'; import { activityIdSchema, skillIdSchema } from './id.ts'; +import { autoRecipeSchema } from './auto-recipe.ts'; import { jsonSchemaSchema, sevenPartContractSchema } from './contract7.ts'; import { listenerSpecSchema } from './envelope.ts'; import { objectDeclarationSchema } from './ledger.ts'; @@ -191,6 +192,14 @@ export const planStepSchema = z.object({ */ /* 可选:老活动一个字都不用改(`writes`/`reads` 用 default([]) 是历史,这里不跟 —— 加默认值会逼六个包全改) */ humanWrites: z.array(z.string()).optional(), + /** + * **这一步系统按声明自己做**(2026-08-29,ADR-014 / spec §5)。 + * + * 引擎在这一步的每一拍拍首跑配方:发了卡就收拍(零 token);没发卡且这一步没有 `guide` + * 就重求 `doneWhen` 收步;有 `guide` 的照常问模型(半 auto)。 + * **老活动一个字不用改** —— 不写就是不写,不是缺件。 + */ + auto: autoRecipeSchema.optional(), }); export type PlanStep = z.infer;