/** * 收步判据要连着**此刻的读数**一起摆给模型 —— 「差在哪」不能靠猜。 * * 这条是拿一次熔断换来的:A1 的 S2 收步判据是 * `missing_key_total + missing_minor_total = 0`,而草稿里 missing_key_total 是 1。 * 模型看不见这个数,以为缺口已经补齐了,就把同一批行再写一遍「确认一下」—— * 撞上空转检查被拒;拒了它还是不知道差在哪,再写一遍……连拒五拍熔断。 * 把读数摆出来,这一步就从死胡同变成一句话能办的事。 */ import { describe, expect, it } from 'vitest'; import { readingsOf } from '../internal/domain/lifecycle.ts'; describe('收步判据的读数', () => { it('把判据读的那几个值摆出来', () => { const out = readingsOf('missing_key_total + missing_minor_total = 0', { missing_key_total: 1, missing_minor_total: 0, }); expect(out).toContain('missing_key_total=1'); expect(out).toContain('missing_minor_total=0'); }); it('只摆判据读的,不摆整个任务视图', () => { const out = readingsOf('line_total > 0', { line_total: 3, subject: '询价', pn: 'LM358DR' }); expect(out).toContain('line_total=3'); expect(out).not.toContain('subject'); expect(out).not.toContain('pn'); }); it('没写过的名字如实说没写过 —— 它正是这一步推不动的原因', () => { expect(readingsOf('benchmark_total > 0', {})).toContain('benchmark_total=(没写过)'); }); it('保留字不当台账名去查', () => { const out = readingsOf('need_sourcing = true and line_total > 0', { need_sourcing: false, line_total: 2 }); expect(out).toContain('need_sourcing=false'); expect(out).not.toContain('true='); expect(out).not.toContain('and='); }); it('字符串字面量里的词不当台账名', () => { expect(readingsOf('disposition = "照原价报"', { disposition: '通过' })).toBe('(现在:disposition=通过)'); }); it('没有名字可摆时给空串 —— 不往提示词里塞空括号', () => { expect(readingsOf('1 = 1', {})).toBe(''); }); }); /** * 判据名归一要削掉**引擎自己贴的等级标记**。 * * 提交被拒时门印的是「…没达标 —— 某判据(L3)」,紧接着让模型「照判据名原样写」; * 模型照做,把 `(L3)` 一起抄进 criterion —— 裁定的键与清单里的判据名对不上, * 裁了等于没裁,再提交再被拒,连拒五拍熔断(实测 A2 死于此)。 */ import { criterionKey } from '../internal/domain/lifecycle.ts'; describe('判据名归一', () => { it('模型抄进等级标记也要认得出是同一条', () => { expect(criterionKey('每项数据来源可溯(以原厂官网规格书为准)(L3)')).toBe( criterionKey('每项数据来源可溯(以原厂官网规格书为准)'), ); }); it('五个等级都削', () => { for (const level of ['L1', 'L2', 'L3', 'L4', 'L5']) { expect(criterionKey(`某判据(${level})`)).toBe(criterionKey('某判据')); } }); it('全角括号的等级标记也削 —— 模型爱把半角敲成全角', () => { expect(criterionKey('某判据(L3)')).toBe(criterionKey('某判据')); }); it('只削末尾那一个,不碰判据名里本来就有的括号', () => { expect(criterionKey('吃不准降 B(不许臆断 A)')).toBe('吃不准降b(不许臆断a)'); }); it('照旧折全角、去空白 —— 老毛病不许回来', () => { expect(criterionKey('吃不准降B,不许臆断A')).toBe(criterionKey('吃不准降 B,不许臆断 A')); }); }); /** * 同一句交代不进两遍对话区。 * * 实测试跑 2e091d25:「有两颗料客户没写清楚……你看是我起草一封信去问,还是你直接找对方?」 * 一字不差连着吐了三拍(先 start 那一项、又 start 了一次、才发出决策卡)—— * 人在对话区看到同一个问题被问三遍,像卡带。 * * 判定放在这儿单测:去空白后逐字比、只比紧邻的上一句。 * 近义句不拦(那是两次不同的交代),隔了几拍说回同一句也不拦(局面绕回来了)。 */ function isRepeatSay(say: string, trail: ReadonlyArray<{ kind: string; title: string }>): boolean { const now = say.trim().replace(/\s+/g, ''); const last = [...trail].reverse().find((t) => t.kind === 'narration'); return last !== undefined && last.title.trim().replace(/\s+/g, '') === now; } describe('对话区不结巴', () => { const n = (title: string) => ({ kind: 'narration', title }); it('跟上一句一字不差 —— 压下', () => { expect(isRepeatSay('缺两颗料的信息,要我起草封信吗?', [n('缺两颗料的信息,要我起草封信吗?')])).toBe(true); }); it('只差空白也算同一句 —— 模型抄自己时空格常飘', () => { expect(isRepeatSay('缺 两颗料 的信息', [n('缺两颗料的信息')])).toBe(true); }); it('中间隔着别的痕,比的仍是**上一句叙事**', () => { const trail = [n('缺两颗料的信息'), { kind: 'action', title: '写草稿:x 1 行' }, { kind: 'engine-transition', title: 'manualStart:S2' }]; expect(isRepeatSay('缺两颗料的信息', trail)).toBe(true); }); it('近义但不同的句子 —— 放行,那是两次不同的交代', () => { expect(isRepeatSay('历史价都查到了,接下来打标。', [n('三颗料的历史价都查到了,接下来打标。')])).toBe(false); }); it('隔了一句别的又说回同一句 —— 放行,局面可能真绕回来了', () => { expect(isRepeatSay('缺两颗料的信息', [n('缺两颗料的信息'), n('客户回话了')])).toBe(false); }); it('一条叙事都还没有 —— 放行', () => { expect(isRepeatSay('第一句话', [{ kind: 'action', title: '写草稿' }])).toBe(false); }); }); /** * **行数对不上时,把是哪几行也摆出来**(2026-08-01,连跑三次熔断逼出来的)。 * * 行数类判据拒回时只给两个数,模型看得见「10 ≠ 9」,却不知道多的是哪一行 —— * 它只能猜:改数、重写、再提,三条路都不对,连拒五拍熔断。实测三种形态全是这一类: * · 记录写了 10 行而条目数报 9(同一颗料两个 PN 写法,或把客户说跳过的那条也写了); * · 分类表 0 行而记录 10 行(它说「打完标了」,其实一行没写); * · 替代请求 1 张而 C 类 0 条(给那颗料开了单,却没把它标成 C)。 * 三种一摆主键就都是一句话能办的事。 */ describe('行数判据拒回:连主键名单一起摆', () => { it('谓词读了 _rows,就把那张表本单写下的行摆出来', () => { const out = readingsOf('quote_intake_record_rows = line_total', { quote_intake_record_rows: 10, line_total: 9, quote_intake_record_keys: 'LM358DR、TL072CDR、NE555P', }); expect(out).toContain('quote_intake_record_rows=10'); expect(out).toContain('line_total=9'); expect(out).toContain('是这几行:LM358DR、TL072CDR、NE555P'); }); it('两张表都读了 → 两份名单都摆,差集自己对得出来', () => { const out = readingsOf('quote_intake_type_table_rows = quote_intake_record_rows', { quote_intake_type_table_rows: 2, quote_intake_record_rows: 3, quote_intake_type_table_keys: 'A、B', quote_intake_record_keys: 'A、B、C', }); expect(out).toContain('是这几行:A、B;'); expect(out).toContain('是这几行:A、B、C'); }); it('那张表一行没写就没有名单 —— 不摆一句空的「是这几行:」', () => { const out = readingsOf('quote_intake_alt_request_rows = type_c_total', { quote_intake_alt_request_rows: 0, type_c_total: 0, }); expect(out).toBe('(现在:quote_intake_alt_request_rows=0、type_c_total=0)'); }); it('不读 _rows 的判据照旧只摆读数 —— 名单不是每条判据都要的', () => { expect(readingsOf('missing_key_total = 0', { missing_key_total: 1, quote_intake_record_keys: 'X' })).toBe( '(现在:missing_key_total=1)', ); }); });