Archived
feat(w05): S2 配方 —— AI 唯一一次出场,行由字典决定不由它决定
明细按类型字典 each 出来,AI 的出参只是拿参数名去 lookup 的一张查表: 它漏一条,那一行的四态就是空,required 立刻变成 _missing > 0,闸与放行门当场拦住。 从前是 from each(AI 出参) —— 少给两条就少两行,而每一行看着都合规。 值一律 lookup 台账,一个字不让它重述;推荐等级、风险、摘要、理由全由规则与模板拼。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -1,10 +1,19 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { planStepSchema } from '@contracts';
|
||||
import { planStepSchema, type PlanStep } from '@contracts';
|
||||
import { ASSESSMENT_STEPS, ASSESSMENT_PHASES, CMMN_XML, NARRATIVE } from '../src/case-model.ts';
|
||||
import { manifest } from '../src/manifest.ts';
|
||||
|
||||
const ITEMS = ['S1_inputs', 'S2_assess', 'S3_review', 'T_release'];
|
||||
const stepOf = (item: string) => ASSESSMENT_STEPS.find((s) => s.item === item);
|
||||
/*
|
||||
* **返回类型显式标 `PlanStep`**(2026-08-29,Task 5 逼出来的修正,与「文件里没有的名字先核一遍」同一课)——
|
||||
* `ASSESSMENT_STEPS` 不是元组,`.find()` 的返回类型是数组**元素类型**(四步 `auto.do` 里所有动作形状的并集),
|
||||
* 与查的是哪个 `item` 无关。S2 从占位 `say` 换成真配方之前,并集里只有 S1 自己的四种 `project` 形状,
|
||||
* S1 的断言侥幸压得住;换上真配方后并集里混进了 S2 的三种 `project` 形状(字段名完全不同),
|
||||
* S1 原有的 `fields?.['found_pn_ref']` 这类按属性名取值当场过不了 `tsc`(TS7053)。
|
||||
* 标成契约类型 `PlanStep` 之后 `.auto.do[i]` 落回 `AutoAction`(`fields: Readonly<Record<string, AutoExpr>>`
|
||||
* 本就有索引签名),按哪个字符串查步骤才真正决定读到的形状 —— 这也是测试原本就该核的东西。
|
||||
*/
|
||||
const stepOf = (item: string): PlanStep | undefined => ASSESSMENT_STEPS.find((s) => s.item === item);
|
||||
|
||||
describe('四步案例模型', () => {
|
||||
it('恰好四步,次序即业务次序', () => {
|
||||
@@ -189,3 +198,84 @@ describe('S1 配方', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('S2 配方 —— AI 只出场一次', () => {
|
||||
const recipe = stepOf('S2_assess')?.auto;
|
||||
const acts = recipe?.do ?? [];
|
||||
|
||||
it('五笔:分析 → 明细 → 关系 → 验证记录 → 交代;analyze 恰好一笔', () => {
|
||||
expect(acts.map((a) => a.kind)).toEqual(['analyze', 'project', 'project', 'project', 'say']);
|
||||
expect(acts.filter((a) => a.kind === 'analyze')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('**行由字典决定,不由 AI 决定** —— 明细 each 的是 type_param,不是 result:items', () => {
|
||||
const items = acts[1];
|
||||
expect(items?.kind === 'project' && items.from).toMatchObject({ each: 'parts.type_param' });
|
||||
const fields = items?.kind === 'project' ? items.fields : {};
|
||||
/* AI 的出参只是一张查表:漏一条,那一格就是空 —— required 立刻把它变成 _missing > 0 */
|
||||
expect(fields['difference_status']).toMatchObject({ lookup: 'result:items', take: 'difference_status' });
|
||||
expect(fields['impact_statement']).toMatchObject({ lookup: 'result:items' });
|
||||
expect(fields['evidence_note']).toMatchObject({ lookup: 'result:items' });
|
||||
});
|
||||
|
||||
it('**值不取 AI 出参,一律 lookup 台账**', () => {
|
||||
const fields = acts[1]?.kind === 'project' ? acts[1].fields : {};
|
||||
for (const col of ['original_value', 'candidate_value']) {
|
||||
expect(fields[col]).toMatchObject({ lookup: 'parts.parameter' });
|
||||
}
|
||||
/* 约束类型来自字典这一行本身,不是 AI 说的 */
|
||||
expect(fields['constraint_type']).toEqual({ col: 'constraint_type' });
|
||||
});
|
||||
|
||||
it('judgement_class 是合成列:约束类型 × 四态,规则写在配方里', () => {
|
||||
const fields = acts[1]?.kind === 'project' ? acts[1].fields : {};
|
||||
const jc = JSON.stringify(fields['judgement_class']);
|
||||
expect(jc).toContain('hard_mismatch');
|
||||
expect(jc).toContain('soft_mismatch');
|
||||
/* 顺序要紧:先认四态,最后才按约束类型分硬软 */
|
||||
expect(jc.indexOf('"match"')).toBeLessThan(jc.indexOf('hard_mismatch'));
|
||||
});
|
||||
|
||||
it('推荐等级读 judgement_class 的 tally 计数 —— 不读 AI 出参、不读自报列', () => {
|
||||
const rel = acts[2]?.kind === 'project' ? acts[2].fields : {};
|
||||
const level = JSON.stringify(rel['recommendation_level']);
|
||||
expect(level).toContain('substitution_difference_item_judgement_class_hard_mismatch > 0');
|
||||
expect(level).toContain('not_recommended');
|
||||
/* 硬约束不符必须直接落到「不推荐」,排在最前 */
|
||||
expect(level.indexOf('hard_mismatch')).toBeLessThan(level.indexOf('unknown'));
|
||||
});
|
||||
|
||||
it('P-06 一单固定一条,状态 not_started —— 本活动只写「还得去验什么」,不虚构实测结果', () => {
|
||||
const val = acts[3];
|
||||
expect(val?.kind === 'project' && val.into).toBe('validation.record');
|
||||
expect(val?.kind === 'project' && val.from).toEqual({ once: true });
|
||||
const fields = val?.kind === 'project' ? val.fields : {};
|
||||
expect(fields['validation_status']).toBe('not_started');
|
||||
expect(fields['validation_type']).toBe('parametric');
|
||||
});
|
||||
|
||||
it('提示词:三条硬规矩都写进去了,且不引任何真实料号与参数名', () => {
|
||||
const analyze = acts[0];
|
||||
const prompt = analyze?.kind === 'analyze' ? analyze.instruction : '';
|
||||
expect(prompt).toContain('不要复述');
|
||||
expect(prompt).toContain('unknown');
|
||||
expect(prompt).toContain('硬约束');
|
||||
for (const forbidden of ['LM2596S', 'XL2596S', 'AMS1117', '使能阈值', '输入电压范围']) {
|
||||
expect(prompt).not.toContain(forbidden);
|
||||
}
|
||||
/* 出参 schema 顶层是对象,数组字段名与 into 对得上 */
|
||||
const out = analyze?.kind === 'analyze' ? analyze.output : {};
|
||||
expect(out['type']).toBe('object');
|
||||
expect(analyze?.kind === 'analyze' && analyze.into).toBe('items');
|
||||
});
|
||||
|
||||
it('AI 的入参**每一条都带说明** —— 参数名本身说不清「它为什么要紧」', () => {
|
||||
const analyze = acts[0];
|
||||
const fields = analyze?.kind === 'analyze' ? analyze.input.fields : {};
|
||||
expect(fields['param_desc']).toEqual({ col: 'param_desc' });
|
||||
expect(fields['constraint_type']).toEqual({ col: 'constraint_type' });
|
||||
for (const col of ['original_value', 'candidate_value']) {
|
||||
expect(fields[col]).toMatchObject({ lookup: 'parts.parameter' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user