fix(engine): 打不回的验收卡不摆那颗钮 —— 全 auto 的放行步没有模型能接住那句意见

用户:「评审放行与落账确认这一步的验收卡不应该有重做的选项,因为这里都没有接入 ai」。

W05 的 T_release 整个由配方跑完(guide 是空串,不问模型),件又是只读的 ——
打回之后没有任何东西能接住那句意见:配方只会原样再提交一次,撞在「打回之后台账没动过
就不许再提交」那道闸上,连撞两拍落成一张「连续撞在同一条判据上」的兜底卡。
一颗按下去只会把单子推进死胡同的钮,不如不摆(墙要配路;没有路就别立那扇门)。

· 契约:AcceptanceWorkOrder.redoable(不写 = 打得回,老卡老行为);
· 装配层按放行那一步的声明现算 —— releaseRedoable 纯函数(有 auto 且 guide 空 → 打不回;
  半 auto 与模型驱动照旧打得回,那儿真有模型会读意见),与 decisionCardFace /
  executionCardVariant 同一种做法、同一个测试位;
· 卡面收起理由框与那颗钮,换一句真路:「这一步只落账,退不回上一步 —— 有异议就别确认,
  去终止这一单重开」;
· **服务端才是门**:redoable:false 的卡收到非 pass 的回传当场拒(redo-unavailable),
  直调 API 绕不过;拒回把真路指出来。

S3 结论审核那张审核卡不受影响 —— 它的重做真会带着意见重判(昨夜那一轮)。
测:card-rules 3 条 + releaseRedoable 4 条,两处变异验证;e2e w05 5/5 released、被拒 0。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-09-01 01:34:16 +08:00
co-authored by Claude Fable 5
parent 8314148ebc
commit cec19adc85
15 changed files with 179 additions and 26 deletions
@@ -32,6 +32,7 @@
- **调人预算环外强制**:超过契约声明的打扰上限即 `HumanCallBudgetExceededError`,
活动代码无从自查自守
- **落地失败卡是执行工单的 preset**,不构成第五种卡
- **打不回的验收卡**(2026-09-01 用户定):`AcceptanceWorkOrder.redoable === false` 时,`verdict !== 'pass'` 的回传当场拒(`redo-unavailable`)。什么时候是 false:那一步整个由配方跑完、一个字的 `guide` 都没有 —— 打回之后没有模型会读那句意见、件也只读,配方只会原样重提并撞在「打回之后台账没动过」那道闸上。卡面把钮收起来只是给人看的,**这里才是门**;拒回把真路指出来(别签,去终止重开)。判断在装配层 `releaseRedoable`(纯函数,有测)。
- **人工验收清单点全才能通过**(2026-08-27 用户定):验收工单 / 审核卡带 `checklist`(剧本 `acceptanceChecklist`,
每份工单固定)时,「通过」的回传必须带 `checked ⊇ checklist`,否则 `checklist-incomplete` 拒回;驳回不看清单。
审核卡只拦「通过」那一个选项(`reviewApproveValue`:值叫 approve 的 / 推荐的 / 第一个)。点过的条目落进
@@ -95,6 +95,30 @@ describe('工单形态机检', () => {
});
});
/*
* **打不回的验收卡**(2026-09-01 用户定:「评审放行与落账确认这一步的验收卡不应该有重做的选项,
* 因为这里都没有接入 ai」)。卡面把钮收起来只是给人看的,这里才是门 —— 直调 API 交「重做」,
* 单子会被推进死胡同:没有模型读那句意见,配方只会原样重提,撞在「打回之后台账没动过」那道闸上。
*/
describe('打不回的验收卡', () => {
const redo: WorkOrderReply = { mode: 'acceptance', workOrderId: OID, verdict: 'redo', values: {}, comment: '再看看', selectedPorts: {} };
it('redoable:false → 交「重做」当场拒,拒回把真路指出来', () => {
const v = checkWorkOrderReply(acceptance({ redoable: false }), redo);
expect(v.map((i) => i.code)).toContain('redo-unavailable');
expect(v.find((i) => i.code === 'redo-unavailable')?.message).toContain('终止');
});
it('同一张卡「通过」照收 —— 拦的是打回,不是这张卡', () => {
const pass: WorkOrderReply = { mode: 'acceptance', workOrderId: OID, verdict: 'pass', values: {}, selectedPorts: {} };
expect(checkWorkOrderReply(acceptance({ redoable: false }), pass).map((i) => i.code)).not.toContain('redo-unavailable');
});
it('没写这面旗(老卡)照旧打得回', () => {
expect(checkWorkOrderReply(acceptance(), redo).map((i) => i.code)).not.toContain('redo-unavailable');
});
});
describe('到达面两路', () => {
it('负责人的卡进任务对话流', () => {
expect(arrivalOf('s1', 's1')).toBe('task-chat');
@@ -133,6 +133,18 @@ export function checkWorkOrderReply(order: WorkOrder, reply: WorkOrderReply): Ca
if (rejectionNeedsComment(reply)) {
out.push({ code: 'comment-required', message: '驳回必附意见(OS 05)' });
}
/*
* **打不回的卡就是打不回**(2026-09-01 用户定:「评审放行与落账确认这一步的验收卡不应该有
* 重做的选项,因为这里都没有接入 ai」)。卡面把那颗钮收起来只是给人看的,**这里才是门** ——
* 直调 API 把「重做」交回来,单子会被推进死胡同(没有模型读那句意见,配方只会原样重提,
* 撞在「打回之后台账没动过」那道闸上)。拒回把真路指出来:别签、去终止重开。
*/
if (reply.verdict !== 'pass' && (order as AcceptanceWorkOrder).redoable === false) {
out.push({
code: 'redo-unavailable',
message: '这一步打不回:它整个由配方跑完,没有模型会读你的意见,件也只读 —— 有异议就别确认,去终止这一单重开。',
});
}
/*
* **恰选一口,随放行一并确认**(AM 1.2)—— 与「驳回必附意见」同族的收单规矩。
* 工单投影自己声明了选择组(portChoice),放行回传就必须带组内恰一口:
+2
View File
@@ -50,6 +50,8 @@ export type SubmitForReleaseInput = {
readonly criteria: readonly CriterionCheck[];
/** 人工验收清单(剧本声明,固定):卡上一条一枚开关,全部达标才能通过 */
readonly checklist?: ReadonlyArray<string>;
/** 这张卡打回得了吗 —— `false` = 打回之后没人接得住(见契约 `AcceptanceWorkOrder.redoable`) */
readonly redoable?: boolean;
/** 报告可修订:AI 拟值已填的表单字段 */
readonly fields?: ReadonlyArray<Record<string, unknown>>;
/**
@@ -144,6 +144,8 @@ export function createReleaseService(deps: ReleaseServiceDeps): ReleaseService {
actions: report.verified.map((v) => ({ label: v.label, effect: v.effect, detail: v.detail })),
// 人工验收清单:人核这份工单要走的步骤(剧本声明,固定)
...(input.checklist === undefined || input.checklist.length === 0 ? {} : { checklist: input.checklist }),
/* 打回得了吗 —— 只在明确说「打不回」时才带下去,老调用方一个字不用改 */
...(input.redoable === false ? { redoable: false } : {}),
/*
* 可修订字段 —— 验收报告「AI 已填好、可编辑」改的就是它们。
*
+2
View File
@@ -80,6 +80,8 @@ export type CardGateway = {
actions: ReadonlyArray<{ label: string; effect: 'read' | 'write'; detail: string }>;
/** 人工验收清单 —— 原样带上卡 */
checklist?: ReadonlyArray<string> | undefined;
/** 这张卡打回得了吗(`false` = 那颗钮不摆)—— 端口不声明它,值就在装配层被丢掉,这行不能省 */
redoable?: boolean | undefined;
/**
* 可修订字段 —— 验收报告「AI 已填好、可编辑」改的就是它们(带 `ai` 拟值与 `ledger` 落回地址)。
* **端口不声明它,值就在装配层被丢掉**:上游一路传下来,而 TypeScript 对条件展开