Archived
feat(loop-engine): analyze 动词 —— 系统组装入参、声明出参 schema、系统把结果落表
AI 在这里只是一个纯函数:不给工具、不给记忆、不多轮。 行数不由它决定 —— 出参是拿主键去 lookup 的一张查表,它漏一条那一格就是空, required 立刻把它变成 _missing > 0,提交闸与放行门当场拦住。 出参不成形就地重问一次,再不成抛出去交人。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -6,8 +6,10 @@ import { createAutoRunner } from '../internal/usecase/auto-runner.ts';
|
||||
const TASK = taskIdSchema.parse('11111111-1111-4111-8111-111111111111');
|
||||
const OWNER = staffIdSchema.parse('22222222-2222-4222-8222-222222222222');
|
||||
|
||||
type AnalyzeStub = { run: (i: { payload: readonly unknown[]; model: string; instruction: string }) => Promise<{ object: Record<string, unknown> }> };
|
||||
|
||||
/** 假端口:一个内存草稿层 + 一本台账声明。**不是 mock 框架,是手写的依赖注入替身。** */
|
||||
function harness(seed: Record<string, unknown> = {}) {
|
||||
function harness(seed: Record<string, unknown> = {}, override: { analyze?: AnalyzeStub } = {}) {
|
||||
const drafts = new Map<string, { domain: string; object: string; key: Record<string, string>; patch: Record<string, unknown> }>();
|
||||
const notes: string[] = [];
|
||||
const view = (): Record<string, unknown> => {
|
||||
@@ -42,7 +44,7 @@ function harness(seed: Record<string, unknown> = {}) {
|
||||
unanswered: () => Promise.resolve([]),
|
||||
},
|
||||
memory: { append: (m: { title: string }) => { notes.push(m.title); return Promise.resolve(); } },
|
||||
analyze: { run: () => Promise.reject(new Error('本用例不该调 analyze')) },
|
||||
analyze: override.analyze ?? { run: () => Promise.reject(new Error('本用例不该调 analyze')) },
|
||||
sfeel: {
|
||||
condition: (expr: string, scope: Readonly<Record<string, unknown>>) => {
|
||||
if (expr === 'never') return false;
|
||||
@@ -147,3 +149,100 @@ describe('配方执行器', () => {
|
||||
expect(out.failedAt?.message).toContain('nope.nope');
|
||||
});
|
||||
});
|
||||
|
||||
describe('analyze 动词', () => {
|
||||
it('入参由声明组装,出参落进 result: 供后一笔 each 消费', async () => {
|
||||
let seen: readonly unknown[] = [];
|
||||
const { runner, drafts, ctx } = harness(
|
||||
{
|
||||
'parts.type_param': [
|
||||
{ type_id: 1, param_name: '输入电压范围', constraint_type: 'hard' },
|
||||
{ type_id: 1, param_name: '输出电流', constraint_type: 'soft' },
|
||||
],
|
||||
typeId: 1,
|
||||
},
|
||||
{
|
||||
analyze: {
|
||||
run: (i) => {
|
||||
seen = i.payload;
|
||||
return Promise.resolve({
|
||||
object: {
|
||||
items: [
|
||||
{ param_name: '输入电压范围', difference_status: 'match' },
|
||||
{ param_name: '输出电流', difference_status: 'unknown' },
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
await runner.run(
|
||||
{
|
||||
do: [
|
||||
{
|
||||
kind: 'analyze',
|
||||
model: 'deepseek-v4-flash',
|
||||
into: 'items',
|
||||
instruction: '逐条判四态',
|
||||
input: {
|
||||
from: { each: 'parts.type_param', where: { type_id: { in: 'typeId' } } },
|
||||
fields: { param_name: { col: 'param_name' }, constraint_type: { col: 'constraint_type' } },
|
||||
},
|
||||
output: { type: 'object' },
|
||||
},
|
||||
{
|
||||
kind: 'project',
|
||||
into: 'parts.parameter',
|
||||
from: { each: 'parts.type_param', where: { type_id: { in: 'typeId' } } },
|
||||
fields: {
|
||||
internal_pn: 'XL2596S-5.0',
|
||||
param_name: { col: 'param_name' },
|
||||
/* **行由字典决定,AI 出参只是查表** —— 它漏一条,那一格就是空 */
|
||||
param_value: { lookup: 'result:items', by: { param_name: { col: 'param_name' } }, take: 'difference_status' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
expect(seen).toEqual([
|
||||
{ param_name: '输入电压范围', constraint_type: 'hard' },
|
||||
{ param_name: '输出电流', constraint_type: 'soft' },
|
||||
]);
|
||||
const values = [...drafts.values()].filter((d) => d.object === 'parameter').map((d) => d.patch['param_value']);
|
||||
expect(values.sort()).toEqual(['match', 'unknown']);
|
||||
});
|
||||
|
||||
it('AI 漏了一条:那一行的格子是空的 —— 行数不由它决定', async () => {
|
||||
const { runner, drafts, ctx } = harness(
|
||||
{ 'parts.type_param': [{ type_id: 1, param_name: 'A' }, { type_id: 1, param_name: 'B' }], typeId: 1 },
|
||||
{ analyze: { run: () => Promise.resolve({ object: { items: [{ param_name: 'A', difference_status: 'match' }] } }) } },
|
||||
);
|
||||
await runner.run(
|
||||
{
|
||||
do: [
|
||||
{ kind: 'analyze', model: 'm', into: 'items', instruction: 'x', input: { from: { each: 'parts.type_param' }, fields: { param_name: { col: 'param_name' } } }, output: { type: 'object' } },
|
||||
{ kind: 'project', into: 'parts.parameter', from: { each: 'parts.type_param' }, fields: { internal_pn: 'P', param_name: { col: 'param_name' }, param_value: { lookup: 'result:items', by: { param_name: { col: 'param_name' } }, take: 'difference_status' } } },
|
||||
],
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
const rows = [...drafts.values()].filter((d) => d.object === 'parameter');
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows.find((r) => r.key['param_name'] === 'B')?.patch['param_value']).toBe('');
|
||||
});
|
||||
|
||||
it('有卡在等人交回时不调 AI —— 只算、只写', async () => {
|
||||
let called = 0;
|
||||
const { runner, ctx } = harness(
|
||||
{ 'parts.type_param': [{ type_id: 1, param_name: 'A' }] },
|
||||
{ analyze: { run: () => { called += 1; return Promise.resolve({ object: { items: [] } }); } } },
|
||||
);
|
||||
await runner.run(
|
||||
{ do: [{ kind: 'analyze', model: 'm', into: 'items', instruction: 'x', input: { from: { each: 'parts.type_param' }, fields: { p: { col: 'param_name' } } }, output: { type: 'object' } }] },
|
||||
{ ...ctx, quiet: true },
|
||||
);
|
||||
expect(called).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user