Archived
人手改的那一手真正落得下去;界面不撒谎那几处补齐
手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的: 件上就地改 · 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名, 于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 —— 而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。 · **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。 删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)—— 「没回传就是删了」会把一次不完整的回传变成一次删表。 · **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。 · 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。 判据与门 · 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来, 人填好的目标价它照样判「客户未提供」的毛病由此消掉。 · 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。 · 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开, 于是一单没打标就进了放行门。三个数全由基座现数。 · 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。 **指向两扇打不开的门,比不指还糟。** 界面不撒谎 · 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。 · 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、 往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。 · 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。 · 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。 · 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。 A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。 五道门禁全绿(971 passed)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
This commit is contained in:
@@ -180,19 +180,38 @@ describe('publishEnabled —— 引擎推进到 Enabled 为止', () => {
|
||||
describe('navigate —— 导航即交集,断点即人环', () => {
|
||||
it('有交集即择优推进', () => {
|
||||
const nav = navigate(
|
||||
{ entries: [{ id: 'a', name: 'A', type: 'humanTask', requiresManualStart: true, discretionary: false }], judgements: [], gaps: ['缺报价'], activeStages: [] },
|
||||
{ entries: [{ id: 'a', name: 'A', type: 'humanTask', requiresManualStart: true, discretionary: false }], inProgress: [], judgements: [], gaps: ['缺报价'], activeStages: [] },
|
||||
new Set(['a']),
|
||||
);
|
||||
expect(nav.kind).toBe('proceed');
|
||||
});
|
||||
|
||||
it('交集为空即断点 —— 调人或立监听的判定时机', () => {
|
||||
const nav = navigate({ entries: [], judgements: [], gaps: ['缺客户信用'], activeStages: [] }, new Set(['a']));
|
||||
it('交集为空**且手上没开着的活**才是断点 —— 调人或立监听的判定时机', () => {
|
||||
const nav = navigate({ entries: [], inProgress: [], judgements: [], gaps: ['缺客户信用'], activeStages: [] }, new Set(['a']));
|
||||
expect(nav).toEqual({ kind: 'breakpoint', gaps: ['缺客户信用'] });
|
||||
});
|
||||
|
||||
/*
|
||||
* 断点的定义是「无路可走」。手上还有开着的活就不算 —— 从前只看 Enabled 集,
|
||||
* 局面推进开以后手上全是 Active,每拍都判成断点,而断点的处置是「调人或立监听」:
|
||||
* 环于是把没干完的活丢下,去等一个不会来的事件。
|
||||
*/
|
||||
it('Enabled 为空但有进行中的项 → 接着干,不是断点', () => {
|
||||
const nav = navigate(
|
||||
{
|
||||
entries: [],
|
||||
inProgress: [{ id: 'w', name: '正在查历史价', type: 'humanTask', requiresManualStart: false, discretionary: false }],
|
||||
judgements: [],
|
||||
gaps: ['缺分类表'],
|
||||
activeStages: [],
|
||||
},
|
||||
new Set(['nothing']),
|
||||
);
|
||||
expect(nav).toEqual({ kind: 'proceed', candidates: [{ id: 'w', name: '正在查历史价', type: 'humanTask', requiresManualStart: false, discretionary: false }] });
|
||||
});
|
||||
|
||||
it('无缺口即收敛', () => {
|
||||
expect(navigate({ entries: [], judgements: [], gaps: [], activeStages: [] }, new Set()).kind).toBe('converged');
|
||||
expect(navigate({ entries: [], inProgress: [], judgements: [], gaps: [], activeStages: [] }, new Set()).kind).toBe('converged');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 收步判据要连着**此刻的读数**一起摆给模型 —— 「差在哪」不能靠猜。
|
||||
*
|
||||
* 这条是拿一次熔断换来的: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)',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 阶段进度 —— 进度条的刻度。判据是「这一阶段的计划项**全收完**才算过」。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { CasePhase } from '@contracts';
|
||||
import { CLOSING_PHASE, phaseProgress } from '../internal/domain/phase.ts';
|
||||
import type { ItemRuntime, ItemState } from '../internal/domain/lifecycle.ts';
|
||||
|
||||
/** A1 的三段:定位客户 / 归一受理 / 判定去向;放行那一格由进度条自己加 */
|
||||
const PHASES: CasePhase[] = [
|
||||
{ name: '定位客户', items: ['S1_customer'] },
|
||||
{ name: '归一受理', items: ['S2_normalize'] },
|
||||
{ name: '判定去向', items: ['S4_history', 'S5_classify', 'S6_route'] },
|
||||
];
|
||||
|
||||
const items = (states: Record<string, ItemState>): ItemRuntime[] =>
|
||||
Object.entries(states).map(([id, state]) => ({ id, state, instance: 1 }));
|
||||
|
||||
const ALL = ['S1_customer', 'S2_normalize', 'S4_history', 'S5_classify', 'S6_route', 'T_release'];
|
||||
const at = (done: readonly string[]): ItemRuntime[] =>
|
||||
items(Object.fromEntries(ALL.map((id) => [id, done.includes(id) ? 'Completed' : 'Available'])));
|
||||
|
||||
describe('阶段进度 —— 三段激活态 + 一格收口 = x/4', () => {
|
||||
it('一格都没收:停在第一阶段', () => {
|
||||
expect(phaseProgress(PHASES, at([]), false)).toEqual({ index: 1, total: 4, name: '定位客户' });
|
||||
});
|
||||
|
||||
it('逐段推进:收完一段才进下一段', () => {
|
||||
expect(phaseProgress(PHASES, at(['S1_customer']), false)).toMatchObject({ index: 2, name: '归一受理' });
|
||||
expect(phaseProgress(PHASES, at(['S1_customer', 'S2_normalize']), false)).toMatchObject({
|
||||
index: 3,
|
||||
name: '判定去向',
|
||||
});
|
||||
});
|
||||
|
||||
it('一段里的项没收齐就不算过 —— 三项收两项仍停在这一段', () => {
|
||||
const done = ['S1_customer', 'S2_normalize', 'S4_history', 'S5_classify'];
|
||||
expect(phaseProgress(PHASES, at(done), false)).toMatchObject({ index: 3, name: '判定去向' });
|
||||
});
|
||||
|
||||
it('三段都过、还没收口:进最后一格(等放行),但不是终态', () => {
|
||||
const done = ['S1_customer', 'S2_normalize', 'S4_history', 'S5_classify', 'S6_route'];
|
||||
expect(phaseProgress(PHASES, at(done), false)).toEqual({ index: 4, total: 4, name: CLOSING_PHASE });
|
||||
});
|
||||
|
||||
it('终态直接给最后一格 —— 不去数计划项', () => {
|
||||
// 放行完成那一瞬 T_release 还没收:去数项的话会显示「还差一格」,而单已经收口了
|
||||
expect(phaseProgress(PHASES, at([]), true)).toEqual({ index: 4, total: 4, name: CLOSING_PHASE });
|
||||
});
|
||||
|
||||
it('被跳过的裁量项(Terminated)算过 —— 它不会再收,等它等到天亮', () => {
|
||||
const mixed = items({
|
||||
S1_customer: 'Completed',
|
||||
S2_normalize: 'Completed',
|
||||
S4_history: 'Completed',
|
||||
S5_classify: 'Terminated',
|
||||
S6_route: 'Completed',
|
||||
T_release: 'Available',
|
||||
});
|
||||
expect(phaseProgress(PHASES, mixed, false)).toMatchObject({ index: 4, name: CLOSING_PHASE });
|
||||
});
|
||||
|
||||
it('没声明阶段就不给刻度 —— 界面照此不画进度条,不编一个出来', () => {
|
||||
expect(phaseProgress(undefined, at([]), false)).toBeUndefined();
|
||||
expect(phaseProgress([], at([]), false)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user