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:
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* **件上就地改** —— 人手改的那几格,是不是落得下去、按不按得下提交。
|
||||
*
|
||||
* 两件事在这儿测,都是用户实测逼出来的(2026-08-02,同一单):
|
||||
*
|
||||
* ① **按地址收,不按「当下那张卡的第几块」收**。
|
||||
* 载荷从前是 `{workOrderId, values:{'table:0': …}}`:列名由那张工单的 `blocks[0]`
|
||||
* 给出,于是「这份件能不能改」被绑在了「此刻开着的那张卡长什么样」上。
|
||||
* 实测:一单在放行门连拒五拍熔断,引擎发的是一张**没有 blocks 的熔断卡**;
|
||||
* 屏上摆着的正是受理记录那份件,闸在、编辑进得去、保存按得下 ——
|
||||
* 前端找不到匹配的块,**一格都没往下送**。而人要补的恰恰是把这一单
|
||||
* 卡在那儿的那个必填格。
|
||||
*
|
||||
* ② **必填还空着就按不下提交**。同一单里,受理需求那张卡上一条料缺年用量(必填),
|
||||
* 人照样交了回来;这一单于是一路走到放行门被 L1 拦住,模型连拒五拍熔断。
|
||||
* 同一件事在提交那一刻拦下便宜得多 —— 那时缺的是哪一格就摆在他眼前。
|
||||
*
|
||||
* 测的是生产那两段本身(`writesOf` / `gateOf`),不在测里抄一份实现:
|
||||
* 抄一份的下场踩过 —— 生产改了、测没跟着改,测全绿而线上照旧。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { gateOf, rowChanged, writesOf } from '../routes/desk.ts';
|
||||
import { missingReadings } from '../wiring/engines.ts';
|
||||
|
||||
/** A1 受理记录的声明(裁到这一段用得着的几列):PN 与年用量必填 */
|
||||
const RECORD = {
|
||||
domain: 'quote_intake',
|
||||
object: 'record',
|
||||
fields: [
|
||||
{ name: 'rfq_ref', type: 'text', primaryKey: true },
|
||||
{ name: 'pn', type: 'text', primaryKey: true, label: '型号 PN', required: true },
|
||||
{ name: 'eau', type: 'number', primaryKey: false, label: '年用量 EAU', required: true },
|
||||
{ name: 'brand', type: 'text', primaryKey: false, label: '制造商', expect: true },
|
||||
],
|
||||
};
|
||||
const VERDICT = {
|
||||
domain: 'quote_intake',
|
||||
object: 'verdict',
|
||||
fields: [{ name: 'rfq_ref', type: 'text', primaryKey: true }],
|
||||
};
|
||||
|
||||
describe('件上按保存:按地址收', () => {
|
||||
it('**地址在声明里就写** —— 当下开着的是哪张卡、带没带块,一概不问', () => {
|
||||
const got = writesOf(
|
||||
[RECORD, VERDICT],
|
||||
{ 'quote_intake.record': [{ pn: 'NE555P', eau: '30000' }] },
|
||||
);
|
||||
expect(got).toHaveLength(1);
|
||||
expect(got[0]?.declared.object).toBe('record');
|
||||
expect(got[0]?.lines).toEqual([{ pn: 'NE555P', eau: '30000' }]);
|
||||
});
|
||||
|
||||
it('一次收好几张表 —— 需求表与分类表在同一份件上,各写各的', () => {
|
||||
const got = writesOf([RECORD, VERDICT], {
|
||||
'quote_intake.record': [{ pn: 'A', eau: '1' }],
|
||||
'quote_intake.verdict': [{ rfq_ref: 'R1' }],
|
||||
});
|
||||
expect(got.map((x) => x.declared.object)).toEqual(['record', 'verdict']);
|
||||
});
|
||||
|
||||
it('**活动没声明过的地址不写** —— 载荷从浏览器来,门得在服务端', () => {
|
||||
expect(writesOf([RECORD], { 'customer.profile': [{ name: '改我' }] })).toEqual([]);
|
||||
});
|
||||
|
||||
it('地址不成形的不写 —— 「域.对象」两段,多一段少一段都不认', () => {
|
||||
expect(writesOf([RECORD], { record: [{ pn: 'A' }] })).toEqual([]);
|
||||
expect(writesOf([RECORD], { 'a.b.c': [{ pn: 'A' }] })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('提交闸:必填没填就交不了', () => {
|
||||
const gate = (rows: Array<Record<string, unknown>>) =>
|
||||
gateOf(missingReadings({ 'mine:quote_intake.record': rows }, [RECORD]), [RECORD]);
|
||||
|
||||
it('一格不缺 → 交得了', () => {
|
||||
expect(gate([{ rfq_ref: 'R1', pn: 'NE555P', eau: 30000 }]).ready).toBe(true);
|
||||
});
|
||||
|
||||
it('**缺必填 → 交不了,并说清差在哪一行哪一格**', () => {
|
||||
const got = gate([
|
||||
{ rfq_ref: 'R1', pn: 'NE555P', eau: 30000 },
|
||||
{ rfq_ref: 'R1', pn: 'DAG2400L321504XZB01-R0', eau: null },
|
||||
]);
|
||||
expect(got.ready).toBe(false);
|
||||
expect(got.why).toContain('1 条必填');
|
||||
expect(got.blanks).toEqual(['R1/DAG2400L321504XZB01-R0 缺年用量 EAU']);
|
||||
});
|
||||
|
||||
it('**选填缺不拦单** —— 那一问不能省,但它不是交不了的理由', () => {
|
||||
expect(gate([{ rfq_ref: 'R1', pn: 'NE555P', eau: 30000, brand: '' }]).ready).toBe(true);
|
||||
});
|
||||
|
||||
it('空串与 null 同等看待 —— 模型用 `""` 表示「原文没给」', () => {
|
||||
expect(gate([{ rfq_ref: 'R1', pn: 'NE555P', eau: '' }]).ready).toBe(false);
|
||||
});
|
||||
|
||||
it('**与放行门的 L1 同一个数** —— 两处各算各的,就会出现「卡上说能交、放行门说不行」', () => {
|
||||
const rows = [{ rfq_ref: 'R1', pn: 'A', eau: 1 }, { rfq_ref: 'R1', pn: 'B', eau: null }];
|
||||
const readings = missingReadings({ 'mine:quote_intake.record': rows }, [RECORD]);
|
||||
// L1 判据读的就是这个键:`quote_intake_record_missing = 0`
|
||||
expect(readings['quote_intake_record_missing']).toBe(1);
|
||||
expect(gateOf(readings, [RECORD]).ready).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('只写真改了的那几行', () => {
|
||||
const row = { rfq_ref: 'R1', pn: 'NE555P', eau: 30000, brand: 'TI' };
|
||||
|
||||
it('**一个字没改就不算一次修订** —— 件上按保存,整张表都回传上来,它不知道人动了哪一格', () => {
|
||||
expect(rowChanged(row, { eau: 30000, brand: 'TI' })).toBe(false);
|
||||
});
|
||||
|
||||
it('改了一格就算 —— 只按值比,不按有没有回传比', () => {
|
||||
expect(rowChanged(row, { eau: 45000, brand: 'TI' })).toBe(true);
|
||||
});
|
||||
|
||||
it('**数字与它的字符串写法相等** —— 台账里躺的是 30000,人手填回来的是 "30000"', () => {
|
||||
expect(rowChanged(row, { eau: '30000' })).toBe(false);
|
||||
});
|
||||
|
||||
it('空与没有相等 —— 空格子回传成 `""`,而台账里那一列压根没写过', () => {
|
||||
expect(rowChanged(row, { remark: '' })).toBe(false);
|
||||
expect(rowChanged({ ...row, remark: null }, { remark: '' })).toBe(false);
|
||||
});
|
||||
|
||||
it('台账里还没有这一行 → 是新增,当然算改', () => {
|
||||
expect(rowChanged(undefined, { eau: 30000 })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* **增删行**(2026-08-02 用户:「编辑列表应该还能增删行」)。
|
||||
*
|
||||
* 手改本来就是**兜 AI 的错**的那一手,而错不止「这一格填错了」:
|
||||
* 「这一行根本不该在」(客户点名本轮跳过的料被写进了有效清单)与「漏了一行」
|
||||
* 是同一类错 —— 只给改格子,前两种就没有出口,那一单会一路走到放行门被判据拦住,
|
||||
* 而人明明一眼就看出问题在哪(实测那一单 13 次拒回、234k token 才熔断)。
|
||||
*/
|
||||
describe('增删行:删要说出来,不靠「没回传就是删了」', () => {
|
||||
it('**删走 `drops`,与改走 `tables` 分开** —— 两份都按地址过同一道声明门', () => {
|
||||
const drops = writesOf([RECORD], { 'quote_intake.record': [{ pn: 'NE555P', rfq_ref: 'R1' }] });
|
||||
expect(drops).toHaveLength(1);
|
||||
expect(drops[0]?.lines).toEqual([{ pn: 'NE555P', rfq_ref: 'R1' }]);
|
||||
});
|
||||
|
||||
it('删的地址也得在声明里 —— 载荷从浏览器来,撤行比改格子更该有门', () => {
|
||||
expect(writesOf([RECORD], { 'customer.profile': [{ name: '撤我' }] })).toEqual([]);
|
||||
});
|
||||
|
||||
it('**回传里没提到的行不算删** —— 一次不完整的回传不该变成一次删表', () => {
|
||||
// 三行的表只回传一行:那是「这次只说了这一行」,不是「另外两行撤掉」
|
||||
const got = writesOf([RECORD], { 'quote_intake.record': [{ pn: 'A' }] });
|
||||
expect(got[0]?.lines).toHaveLength(1); // 删与不删由 drops 说了算,tables 不承载删的语义
|
||||
});
|
||||
|
||||
it('新增行照样过归型与必填 —— 加一行只填了 PN,闸照旧拦着', () => {
|
||||
const readings = missingReadings(
|
||||
{ 'mine:quote_intake.record': [{ rfq_ref: 'R1', pn: '老料', eau: 1 }, { rfq_ref: 'R1', pn: '人加的新料' }] },
|
||||
[RECORD],
|
||||
);
|
||||
const got = gateOf(readings, [RECORD]);
|
||||
expect(got.ready).toBe(false);
|
||||
expect(got.blanks).toEqual(['R1/人加的新料 缺年用量 EAU']);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* **自增主键由基座生成**(2026-08-02 用户:「联系人ID自增啊,不用填」)。
|
||||
*
|
||||
* 联系人编码不承载业务含义,只是「这家客户下的第几位」。让人填是白占一格
|
||||
* 他答不上来的表单(他哪知道已经有几位);让模型编更糟 —— 编重了就是覆盖别人那一行,
|
||||
* 而主数据覆盖错了后面每一单都跟着错。
|
||||
*/
|
||||
describe('自增主键:前缀-两位序号', () => {
|
||||
/** 与 `routes/desk.ts` 的 `autoKeys` 同一口径 —— 改一处这里要跟着改 */
|
||||
const gen = (others: string[], given: Record<string, string>, count: number, names: string[]) => {
|
||||
const prefix = others.map((k) => given[k] ?? '').filter((v) => v !== '').join('-');
|
||||
const out: Record<string, string> = {};
|
||||
for (const [i, n] of names.entries()) {
|
||||
const seq = String(count + 1 + i).padStart(2, '0');
|
||||
out[n] = prefix === '' ? seq : `${prefix}-${seq}`;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
it('**照种子里那个写法**:第一位联系人是 `客户编码-01`', () => {
|
||||
expect(gen(['customer_code'], { customer_code: 'CU-9001' }, 0, ['contact_code'])).toEqual({
|
||||
contact_code: 'CU-9001-01',
|
||||
});
|
||||
});
|
||||
|
||||
it('这家客户已经有两位 → 新的是 -03,不跟谁撞', () => {
|
||||
expect(gen(['customer_code'], { customer_code: 'CU-0142' }, 2, ['contact_code'])['contact_code']).toBe('CU-0142-03');
|
||||
});
|
||||
|
||||
it('同一单里连加两位 → 各自拿到 -01 / -02(后一位不覆盖前一位)', () => {
|
||||
expect(Object.values(gen(['customer_code'], { customer_code: 'CU-1' }, 0, ['a', 'b']))).toEqual([
|
||||
'CU-1-01',
|
||||
'CU-1-02',
|
||||
]);
|
||||
});
|
||||
|
||||
it('没有其余主键(它自己就是唯一那把键)→ 退回纯序号', () => {
|
||||
expect(gen([], {}, 4, ['seq'])).toEqual({ seq: '05' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user