Compare commits

...
Author SHA1 Message Date
xcsandClaude Fable 5 7ed997187e fix(engine): 确认闸条件档在发卡装配被丢 —— 类型不一致的卡从前点得动
CI / ci (push) Failing after 11s
活动声明了 gateWhen,发卡装配的手写清单没点它的名:落库载荷 undefined,
按钮禁用与服务端拒收一起无声失效(手写字段清单第四次丢值,与 variant
上次被丢的同一段)。抽 cardConditionGate 校验着转交(5 测);e2e w05
数据卡阶段补末端断言 expectCardGateWhen。活体验证:载荷带条件 → 闸关 →
API 强行确认 400;e2e 案例 0 全绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-09-01 11:09:48 +08:00
4 changed files with 103 additions and 0 deletions
+16
View File
@@ -2026,6 +2026,22 @@
**副产**:e2e 样例 3「原PN缺库」的前提已被污染(`LM2596S-3.3` 现在真在库里,多半是白天 ops 起单
落的主数据),那一例实际走的也是验收工单那条路 —— 名字与实测不符,清库存那件事记在这儿。
- **「类型不一致还能点下一步」的病根:确认闸条件在发卡装配被丢 —— 手写字段清单第四次丢值**
(2026-09-01 用户:「两个芯片类型不一样,页面上方有红色提示,右边派发的卡片还可以点击下一步」):
活动白纸黑字声明了 `gateWhen`(2026-08-31 加的确认闸条件档),而**发卡装配那段手写清单**
(engines.ts 的 human 口 —— `variant` 上次被丢的同一段)逐项点名转交 `gateLedgers`/`variant`,
没点 `gateWhen`/`gateWhenSay` 的名:落库工单载荷里它是 undefined(稳定库任务 43 法证),
按钮禁用与服务端拒收读的都是它,**一起无声失效**;红带是视图画的所以照亮 ——
屏上两处说着相反的话。此前 fields / blocks / redoable 三次同款,这次的教训:
**中段是不可信卡骨架的缝,不能整包 spread,那就抽纯函数校验着转交并在 variant 的
测试位旁边钉死**(`cardConditionGate`,5 测:转交原样 / 不发明键 / 脏值不端 /
say 单独无意义 / when 可无 say)。**末端断言照 redoable 那课补上**:e2e w05 数据卡
阶段 `expectCardGateWhen` —— 卡一到手查 desk 载荷,丢了当场判失败。
活体验证(测试台真单):载荷带 `gateWhen` → detail 闸 `ready:false` 带活动那句话 →
API 强行确认 **400**;e2e w05 案例 0 全绿(released、被拒 0、模型 2 次)。
活动侧零改动(`GATE_OPEN_WHEN` 含类型不一致,mismatch 时发的必是带闸的数据卡;
⑧ 验收卡的件只读、碰不上这形)。门禁 exit 0(2595 测)。
- **「终止活动」接口被蒸馏吊 13–45 秒到报错:收尾不等模型**(2026-09-01 用户:「点击终止后
接口迟迟不返回,还会报错,但实例确实终止了」):法证在库里 —— 四单被终止的任务,
`finished_at` 与蒸馏条目 `created_at` 差 13.2 / 36.3 / 43.4 / 44.6 秒:`finish` 把终态
@@ -0,0 +1,44 @@
/**
* **确认闸的条件档要在真发卡那条路上转交**(2026-09-01 用户实测:「两个芯片类型不一样,
* 页面上方有红色提示,右边派发的卡片还可以点击下一步」)。
*
* 病:发卡装配那段手写清单逐项点名转交 `gateLedgers`/`variant`,而 `gateWhen`/`gateWhenSay`
* 没被点名 —— 配方白纸黑字声明了「类型不一致不许确认」,落库的工单载荷里那两个字段是
* undefined(任务 43 法证)。红带是视图画的所以照亮;按钮禁用与服务端拒收读的都是
* 存下的 `order.gateWhen`,一起无声失效。**手写字段清单第四次丢值**(fields / blocks /
* redoable 之后),而且丢在 `variant` 上次被丢的同一段。
*
* 这个缝取的是模型也会路过的不可信卡骨架,逐项校验转交是设计 —— 所以治法不是整包 spread,
* 是把这两项的取用抽成纯函数、在 variant 的测试位旁边钉死。
*/
import { describe, expect, it } from 'vitest';
import { cardConditionGate } from '../wiring/engines.ts';
describe('cardConditionGate —— 条件档从卡骨架到工单载荷', () => {
it('声明了就转交,一个字不改', () => {
expect(cardConditionGate({ gateWhen: 'parts_profile_distinct_chip_type_id <= 1', gateWhenSay: '两颗料不是同一个芯片类型' })).toEqual({
gateWhen: 'parts_profile_distinct_chip_type_id <= 1',
gateWhenSay: '两颗料不是同一个芯片类型',
});
});
it('没声明就什么都不带 —— 不发明键', () => {
expect(cardConditionGate({})).toEqual({});
expect(cardConditionGate({ gateLedgers: ['parts.profile'] })).toEqual({});
});
it('脏值不端下去:非字符串 / 空白串一律丢', () => {
expect(cardConditionGate({ gateWhen: 42, gateWhenSay: '话' })).toEqual({});
expect(cardConditionGate({ gateWhen: ' ', gateWhenSay: '话' })).toEqual({});
expect(cardConditionGate({ gateWhen: null })).toEqual({});
});
it('只有那句话没有条件 = 什么都不带(say 单独存在没有意义,评估端只认 when)', () => {
expect(cardConditionGate({ gateWhenSay: '两颗料不是同一个芯片类型' })).toEqual({});
});
it('条件在、话没写:条件照转交,拒回用兜底话术', () => {
expect(cardConditionGate({ gateWhen: 'a > 1' })).toEqual({ gateWhen: 'a > 1' });
expect(cardConditionGate({ gateWhen: 'a > 1', gateWhenSay: '' })).toEqual({ gateWhen: 'a > 1' });
});
});
+20
View File
@@ -279,6 +279,24 @@ export function executionCardVariant(fromCard: unknown, fromStep: unknown): 'dat
return undefined;
}
/**
* **确认闸的条件档从卡骨架到工单载荷**(2026-09-01 用户实测:「两个芯片类型不一样…
* 右边派发的卡片还可以点击下一步」)。配方声明了 `gateWhen`/`gateWhenSay`,发卡装配的
* 手写清单没点它们的名 —— 落库的工单载荷里两个字段是 undefined,按钮禁用与服务端拒收
* 读的都是它,一起无声失效(**手写字段清单第四次丢值**,且丢在 `variant` 上次被丢的同一段)。
* 这个缝取的是模型也会路过的不可信卡骨架,不能整包 spread —— 校验着转交:
* 条件必须是非空字符串;那句话只在有条件时才有意义(评估端只认 `when`)。
*/
export function cardConditionGate(card: Readonly<Record<string, unknown>>): { gateWhen?: string; gateWhenSay?: string } {
const when = card['gateWhen'];
if (typeof when !== 'string' || when.trim() === '') return {};
const say = card['gateWhenSay'];
return {
gateWhen: when,
...(typeof say === 'string' && say.trim() !== '' ? { gateWhenSay: say } : {}),
};
}
export function decisionCardFace(
input: { readonly variant?: 'review' | undefined; readonly checklist?: ReadonlyArray<string> | undefined },
step: { readonly cardVariant?: string | undefined; readonly acceptanceChecklist?: ReadonlyArray<string> | undefined } | undefined,
@@ -1974,6 +1992,8 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
...(blocks === undefined ? {} : { blocks: blocks as ExecutionWorkOrderDraft['blocks'] }),
...(gateLedgers.length === 0 ? {} : { gateLedgers }),
...(variant === undefined ? {} : { variant }),
/* 条件档(gateWhen/gateWhenSay)—— 校验着转交,见 cardConditionGate 的账 */
...cardConditionGate(input.card),
});
// 打扰率的计量对象:每发一张卡就是调了一次人 —— 不计就等于没有上限
await taskStore.addConsumption(input.taskId, { humanCalls: 1 });
+23
View File
@@ -615,6 +615,13 @@ const SUITES = {
rowKey: 'internal_pn',
blankCol: 'brand_name',
expectGateBlock: true,
/*
* **确认闸的条件档必须跟到工单载荷上**(2026-09-01 用户实测:「两个芯片类型不一样…
* 卡片还可以点击下一步」)。配方声明了 `gateWhen`,发卡装配的手写清单没点它的名 ——
* 落库载荷里是 undefined,按钮禁用与服务端拒收一起无声失效(手写字段清单第四次丢值,
* 与 variant 上次被丢的是同一段)。这类丢值只有末端断言拦得住:卡一到手就查载荷。
*/
expectCardGateWhen: true,
edits: [],
extraTables: [
/*
@@ -1340,6 +1347,9 @@ async function runCase(suite, kase) {
let fileCheck;
let gateLeaked = false;
const gateChecked = new Set();
/** 确认闸的条件档(gateWhen)没跟到工单载荷上 —— 类型不一致将拦不住(末端断言,见 w05 数据卡阶段) */
let gateWhenMissing = false;
let gateWhenChecked = false;
let decisions = 0;
missedRows.clear();
asked.length = 0;
@@ -1540,6 +1550,15 @@ async function runCase(suite, kase) {
: ` ⚠️ 缺着必填还放过去了:提交闸失效`,
);
}
if (stage.expectCardGateWhen === true && !gateWhenChecked) {
gateWhenChecked = true;
const { body: det } = await api(`/api/desk/tasks/${taskId}`);
const wo = (det?.workOrders ?? []).find((o) => o.id === card.id);
if (wo?.gateWhen === undefined) {
gateWhenMissing = true;
console.log(' ⚠️ 确认闸的条件档(gateWhen)没跟到工单载荷上 —— 类型不一致将拦不住,判失败');
} else console.log(' 确认闸条件已随卡落库(gateWhen 在载荷上)');
}
/*
* **两阶段自定义填表**(w05「候选缺库·新建类型」专用)—— 在空手交卷的闸测过**之后**、
* 常规 `buildEdits` 之前跑一次:先铸 `parts.chip_type` 的自增 `type_id`,读回来才知道
@@ -1675,6 +1694,10 @@ async function runCase(suite, kase) {
console.log(' ⚠️ 数据工单确认即入库那道门没兑现 —— 判失败');
ok = false;
}
if (gateWhenMissing) {
console.log(' ⚠️ 确认闸条件(gateWhen)被链上丢掉 —— 判失败');
ok = false;
}
// 「超限才叫人」是 A4 的要害:该叫的那一单没叫,等于底价门禁是摆设
if (missedRows.size > 0) {
console.log(` ⚠️ 案例点名要填的行,整例跑完也没在件上出现过(${missedRows.size} 处:${[...missedRows].slice(0, 3).join('、')}…)—— 判失败`);