fix(w05): 调人预算提到 24;「一条都没点到」的打回不再整表重判

用户:「怎么直接最后落帐的步骤直接没了?」—— 那一单终态是 exhausted:结论审核连打回四次,
每打回一次配方就重发一张审核卡、每张 +1 humanCalls,第八次触顶,放行那一步再没机会出现。
屏上看着像「那一步没了」,其实是单子先被烧死了。

① 预算 8 → 24(用户当场定「那你就把预算提高」):常路只要 3 次(补数据 / 审核 / 放行),
   而审核每打回一次就多叫一次人。授权面与资源面同一个数;测里改成「两处一致 + 下限 12」,
   不再钉死某个具体数字。**这是配置不是机制** —— 机制上的正解是「人自己打回引出的那张卡
   不算打扰」(他不是被打扰,是他叫的),已记 debt。

② 「一条都没点到 → 整表算被点名」当天上线、当天撤掉:他那句是「收口结论里面的推荐理由
   改为非常不推荐」—— 说的根本不是某条参数,是收口那四格(本来就能在件上直接改)。
   整表重判的下场:AI 拿这句话把 5 条参数的结论改成「不匹配」,每条依据都写着
   「按审核人意见调整为不匹配:推荐理由改为非常不推荐」——一句与它们无关的话,改了五行。
   现在点不到名就一条都不改,并照实说清怎么才办得成(点参数名 / 收口四格在件上改)。
   重判与写回同批加「真点到了名」的前提,免得白烧一次模型。

验收:e2e w05 5/5 released、被拒 0,案例 0 四条断言全绿;2.23.0 装库核过(humanCalls=24)。

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 02:55:05 +08:00
co-authored by Claude Fable 5
parent 3c563525f1
commit 4174c5f194
6 changed files with 119 additions and 50 deletions
@@ -113,14 +113,14 @@ describe('清单本身合规', () => {
/*
* 2026-08-29 重写:必交付物从三件(评估输入清单 / 差异评估结论 / 拟落账内容)收到两件 ——
* 「评估输入清单」那件连同它绑的三张旧单据(pn_check/input_gap/verdict)一起退休。
* 人环预算从 16(候选放弃/创建决策等岔路)收到 8(补数据 / 审核 / 放行三次调人,零工具、
* AI 只出场一次)—— 见「契约 > 预算」那条把 8 钉死;这里只需两个数上下一致。
* 人环预算:常路只要 3 次(补数据 / 审核 / 放行),**而审核每打回一次就多叫一次** ——
* 8 次实测三四轮就见底(2026-09-01 用户连打回四次,单子落 exhausted,屏上看着像「落账那一步没了」),
* 用户当场定「那你就把预算提高」→ 24。这里只需两个数上下一致(授权面与资源面不许分叉)。
*/
it('两件必交付物都有台账地址;人环预算恰为 8', () => {
it('两件必交付物都有台账地址;授权面与资源面的调人预算是同一个数', () => {
expect(manifest.contract.requiredDeliverables).toHaveLength(2);
for (const d of manifest.contract.requiredDeliverables) expect(d.ledger, d.key).toBeDefined();
expect(manifest.contract.authorization.humanCallBudget).toBe(8);
expect(manifest.contract.resources.budget.humanCalls).toBe(8);
expect(manifest.contract.authorization.humanCallBudget).toBe(manifest.contract.resources.budget.humanCalls);
});
/*
@@ -343,9 +343,14 @@ describe('契约', () => {
}
});
it('预算:调人三次(补数据 / 审核 / 放行),模型只出场一次', () => {
expect(manifest.contract.resources.budget.humanCalls).toBe(8);
expect(manifest.contract.authorization.humanCallBudget).toBe(8);
/*
* 常路调人三次(补数据 / 审核 / 放行),模型只出场一次 —— 而**审核可以打回,每打回一次就多叫一次人**,
* 所以预算按「常路 3 + 二十来轮打回」给(2026-09-01 用户定,见 manifest 那一行的注释)。
* 下限钉在 12:低于它,人点几次重做就把单子烧成 exhausted —— 落账那一步再也没机会出现。
*/
it('预算:常路调人三次,留足打回的余量;模型只出场一次', () => {
expect(manifest.contract.resources.budget.humanCalls).toBeGreaterThanOrEqual(12);
expect(manifest.contract.authorization.humanCallBudget).toBe(manifest.contract.resources.budget.humanCalls);
expect(manifest.contract.resources.models).toEqual(['deepseek-v4-flash']);
});
});
@@ -406,11 +406,11 @@ describe('S3 / S4 配方', () => {
const s4 = stepOf('T_release')?.auto?.do ?? [];
const s2 = stepOf('S2_assess')?.auto?.do ?? [];
it('S3 九笔:圈范围 → 兜底扩范围 → 复核重判 → 落明细 → 明细重算 → 关系 → 验证记录 → 交代 → 卡', () => {
expect(s3.map((a) => a.kind)).toEqual(['project', 'project', 'analyze', 'project', 'project', 'project', 'project', 'say', 'card']);
for (const at of [0, 1, 3, 4]) expect(s3[at]?.kind === 'project' && s3[at].into).toBe('substitution.difference_item');
expect(s3[5]?.kind === 'project' && s3[5].into).toBe('substitution.relation');
expect(s3[6]?.kind === 'project' && s3[6].into).toBe('validation.record');
it('S3 九笔:圈范围 → 复核重判 → 落明细 → 明细重算 → 关系 → 验证记录 → 两句交代 → 卡', () => {
expect(s3.map((a) => a.kind)).toEqual(['project', 'analyze', 'project', 'project', 'project', 'project', 'say', 'say', 'card']);
for (const at of [0, 2, 3]) expect(s3[at]?.kind === 'project' && s3[at].into).toBe('substitution.difference_item');
expect(s3[4]?.kind === 'project' && s3[4].into).toBe('substitution.relation');
expect(s3[5]?.kind === 'project' && s3[5].into).toBe('validation.record');
});
/*
@@ -427,9 +427,17 @@ describe('S3 / S4 配方', () => {
const act = s3[at];
return act !== undefined && 'when' in act ? act.when : undefined;
};
for (const at of [0, 1, 2, 3]) expect(String(whenOf(at) ?? ''), `第 ${at} 笔`).toContain('case_redo_note != ""');
for (const at of [0, 1, 2]) expect(String(whenOf(at) ?? ''), `第 ${at} 笔`).toContain('case_redo_note != ""');
/* 后面那几笔(重算 / 关系 / 验证)照旧每一轮都跑 —— 人就地改了四态也要跟着重算 */
for (const at of [4, 5, 6]) expect(whenOf(at), `第 ${at} 笔`).toBeUndefined();
for (const at of [3, 4, 5]) expect(whenOf(at), `第 ${at} 笔`).toBeUndefined();
/*
* **点不到名就一条都不改**(2026-09-01 当天上线当天撤的那一笔:整表重判)——
* 重判与写回都多一个前提「真点到了名」,免得一句与参数无关的话(实测:「收口结论里面的
* 推荐理由改为非常不推荐」)把 5 条参数的结论一起改掉,还白烧一次模型。
*/
for (const at of [1, 2]) {
expect(String(whenOf(at) ?? ''), `第 ${at} 笔`).toContain('substitution_difference_item_redo_scope_named > 0');
}
});
/*
@@ -438,19 +446,23 @@ describe('S3 / S4 配方', () => {
* 那一行;它改了没人问过的那条,`evidence_note` 里还写着「维持原判」。
* 现在:①按字面点名圈出 `named`;③入参**只有** `named` 的行;④写回也只写这几行。
*/
it('圈范围:点名的标 named,其余 kept;一条都没点到时整表算被点名', () => {
it('圈范围:点名的标 named,其余 kept —— 一条都没点到时什么都不改,只把话说清楚', () => {
const mark = s3[0];
const widen = s3[1];
const markFields: Readonly<Record<string, unknown>> = mark?.kind === 'project' ? mark.fields : {};
expect(markFields['redo_scope']).toEqual({ when: 'contains(case_redo_note, parameter_name)', then: 'named', else: 'kept' });
/* 兜底那一笔读的是上一笔刚写下的 tally 读数 —— 不重新算一遍谁被点到 */
expect(widen?.when).toBe('case_redo_note != "" and substitution_difference_item_redo_scope_named = 0');
expect(widen?.kind === 'project' ? widen.fields['redo_scope'] : undefined).toBe('named');
/* 没点到名的那一轮:一句交代,告诉他怎么才办得成 —— 而不是猜「他指的是全部」 */
const noHit = s3[7];
expect(noHit?.kind === 'say' && noHit.when).toBe('case_redo_note != "" and substitution_difference_item_redo_scope_named = 0');
const text = noHit?.kind === 'say' ? String(noHit.text) : '';
expect(text).toContain('一条都没改');
expect(text).toContain('参数名');
/* 收口四格不归 AI 判 —— 那几格人在件上直接改(2026-08-31 立的口径) */
expect(text).toContain('件上直接改');
});
it('重判与写回都只认 named 的行 —— 没点名的行 AI 连见都见不到,自然改不着', () => {
const redo = s3[2];
const land = s3[3];
const redo = s3[1];
const land = s3[2];
const named = { each: 'mine:substitution.difference_item', where: { redo_scope: 'named' } };
expect(redo?.kind === 'analyze' ? redo.input.from : undefined).toEqual(named);
expect(land?.kind === 'project' ? land.from : undefined).toEqual(named);
@@ -493,7 +505,7 @@ describe('S3 / S4 配方', () => {
* `s3[0]` 必须是**排在关系 / 验证记录投影之前**的差异明细重算笔。
*/
it('差异明细重算笔:只列主键与两个派生列,不碰 difference_status 自己、也不碰人能改的叙事列', () => {
const recompute = s3[4];
const recompute = s3[3];
expect(recompute?.kind === 'project' && recompute.from).toEqual({ each: 'mine:substitution.difference_item' });
/* `recompute` 经 `s3[0]` 的 tuple 定位精确 narrow 到这一笔自己的字面量类型,它压根没有
`prune` 这个键(不是「值为 undefined」,是键不存在)—— `in` 判存在性,不做属性访问 */
@@ -511,7 +523,7 @@ describe('S3 / S4 配方', () => {
* 展开)——各写一份早晚分叉,而分叉的那一刻屏上会同时摆着两个互相矛盾的规则。
*/
it('S3 重算笔与 S2 差异明细创建笔共用同一份 judgement_class / risk_level when 链', () => {
const s3Fields = s3[4]?.kind === 'project' ? s3[4].fields : undefined;
const s3Fields = s3[3]?.kind === 'project' ? s3[3].fields : undefined;
const s2Fields = s2[2]?.kind === 'project' ? s2[2].fields : undefined;
expect(s2Fields?.['judgement_class']).toBeDefined();
expect(s3Fields?.['judgement_class']).toBe(s2Fields?.['judgement_class']);
@@ -532,7 +544,7 @@ describe('S3 / S4 配方', () => {
* 「人改四态 → 等级跟着变」这条链不能断),且与 S2 那份是**同一个 when 链引用**(不许分叉)。
*/
it('**S3 的关系笔只重算判定** —— 收口四格(理由/摘要/风险/验证要求)一列都不许出现,否则人改完一保存就被冲掉', () => {
const relation = s3[5];
const relation = s3[4];
expect(relation?.kind).toBe('project');
const fieldsOf = (act: (typeof s3)[number] | undefined): Readonly<Record<string, unknown>> =>
act?.kind === 'project' ? act.fields : {};
@@ -547,7 +559,7 @@ describe('S3 / S4 配方', () => {
/* 主键带回:同 PK 覆盖,不是新开一行 */
expect(fields['substitution_relation_id']).toBeDefined();
/* 验证记录那一笔仍与 S2 同一份(它不涉及人可改的格子) */
expect(s3[6]).toBe(s2[4]);
expect(s3[5]).toBe(s2[4]);
});
/*
@@ -574,8 +586,8 @@ describe('S3 / S4 配方', () => {
/* **排在卡之前**:发了卡这一拍就收了,排在后面的 say 要等下一拍 —— 那时人早看完卡了 */
expect(kinds.lastIndexOf('say')).toBeLessThan(kinds.indexOf('card'));
/* 只在被打回那一轮说 —— 第一轮那句与卡自己的 ask 是同一件事,退役了 */
const redoSay = s3[7];
expect(redoSay?.kind === 'say' && redoSay.when).toBe('case_redo_note != ""');
const redoSay = s3[6];
expect(redoSay?.kind === 'say' && redoSay.when).toBe('case_redo_note != "" and substitution_difference_item_redo_scope_named > 0');
const text = redoSay?.kind === 'say' ? redoSay.text : undefined;
const parts = JSON.stringify(text);
expect(parts).toContain('按你的意见复核了');
@@ -456,21 +456,19 @@ const REDO_SCOPE_MARK = {
},
} as const;
/**
* ② 一条都没点到 → 整表都算被点名(「结论整体判得太松」这类意见说的是整批,不是某一条)。
* 读的是上一笔刚写下的 tally 读数(每一笔之前刷读数)—— 不重新算一遍谁被点到,同一件事只许有一处判定。
/*
* ~~② 一条都没点到 → 整表都算被点名~~(2026-09-01 当天上线、当天撤掉)。
*
* 当时想的是「『结论整体判得太松』这类意见说的是整批」。实测打脸(用户那一单):他写的是
* **「收口结论里面的推荐理由改为非常不推荐」** —— 说的根本不是某一条参数,而是收口那四格
* (推荐理由 / 差异摘要 / 风险 / 验证要求,那几格本来就能在件上直接改)。整表重判的下场:
* AI 拿着这句话把 **5 条参数**的结论改成了「不匹配」,每一条的依据都写着
* 「按审核人意见调整为不匹配:推荐理由改为非常不推荐。数据面:原料 4.5~40V…」——
* 一句与它们无关的话,改了五行。
*
* **点不到名的意见,宁可什么都不改**:这一轮不重判,把话说清楚(见下面那句 `say`)——
* 要重判某一条就点它的名,收口四格在件上直接改。**「猜他指的是全部」比「承认没对上」贵得多。**
*/
const REDO_SCOPE_WIDEN = {
kind: 'project' as const,
when: 'case_redo_note != "" and substitution_difference_item_redo_scope_named = 0',
into: 'substitution.difference_item',
from: { each: 'mine:substitution.difference_item' },
fields: {
substitution_relation_id: { col: 'substitution_relation_id' },
difference_item_id: { col: 'difference_item_id' },
redo_scope: 'named',
},
} as const;
/**
* ③ **带着意见重判 —— 只判被点名的那几条**(整个活动第二次、也是最后一次模型出场)。
@@ -481,7 +479,7 @@ const REDO_SCOPE_WIDEN = {
*/
const REVIEW_REDO_ANALYZE = {
kind: 'analyze' as const,
when: 'case_redo_note != ""',
when: 'case_redo_note != "" and substitution_difference_item_redo_scope_named > 0',
model: 'deepseek-v4-flash',
into: 'items',
instruction: [
@@ -530,7 +528,7 @@ const REVIEW_REDO_ANALYZE = {
*/
const DIFFERENCE_ITEM_FROM_REDO = {
kind: 'project' as const,
when: 'case_redo_note != ""',
when: 'case_redo_note != "" and substitution_difference_item_redo_scope_named > 0',
into: 'substitution.difference_item',
from: { each: 'mine:substitution.difference_item', where: { redo_scope: 'named' } },
fields: {
@@ -941,7 +939,6 @@ export const ASSESSMENT_STEPS = [
* 判完落回明细,后面三笔重算照旧走:派生列跟着新四态变,等级、风险、验证记录跟着变。
*/
REDO_SCOPE_MARK,
REDO_SCOPE_WIDEN,
REVIEW_REDO_ANALYZE,
DIFFERENCE_ITEM_FROM_REDO,
/*
@@ -969,7 +966,7 @@ export const ASSESSMENT_STEPS = [
*/
{
kind: 'say' as const,
when: 'case_redo_note != ""',
when: 'case_redo_note != "" and substitution_difference_item_redo_scope_named > 0',
text: {
cat: [
'按你的意见复核了 ',
@@ -982,6 +979,18 @@ export const ASSESSMENT_STEPS = [
],
},
},
/*
* **点不到名就照实说** —— 这一轮一个字都没改,得让他知道为什么,以及怎么才办得成
* (2026-09-01 实测:他写「收口结论里面的推荐理由改为非常不推荐」,而那不是任何一条参数)。
*/
{
kind: 'say' as const,
when: 'case_redo_note != "" and substitution_difference_item_redo_scope_named = 0',
text:
'你这句意见我没能对上具体哪一条参数,所以这一轮**一条都没改**。' +
'要我重判某一条,请在意见里写上它的参数名(如「工作温度」);' +
'收口结论那四格(推荐理由 / 差异摘要 / 风险等级 / 验证要求)不归 AI 判 —— 在工作区的件上直接改就行,改完点「确认通过」。',
},
/*
* ~~第一轮那句「差异结论已送审,请在件上逐条核对」~~(2026-09-01 退役):
* 它与卡自己的 `ask` 说的是同一件事(「逐条差异结论已备好。请在工作区的件上逐条核对…」)——
@@ -54,7 +54,7 @@ export const manifest: ActivityManifest = {
activityId: ACTIVITY_ID,
name: 'ACT-WORK-05 · 差异评估',
/* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */
packageVersion: '2.21.0',
packageVersion: '2.23.0',
category: '需求分析与方案匹配',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-part-substitution-assessment',
@@ -269,15 +269,25 @@ export const manifest: ActivityManifest = {
authorization: {
discretion: [],
preauthorizedActions: [],
/* 调人三次(补数据 / 审核 / 放行);零工具、AI 只出场一次 —— 预算随之从 16/90 步收紧到 8/40 步 */
humanCallBudget: 8,
/* 与下面 `resources.budget.humanCalls` 同一个数 —— 常路调人三次(补数据 / 审核 / 放行),
余量给审核打回:每打回一次就多叫一次人(2026-09-01 用户定,见那一行的注释) */
humanCallBudget: 24,
},
resources: {
tools: [],
skills: [],
models: ['deepseek-v4-flash'],
roles: ['FAE/RI'],
budget: { steps: 40, tokens: 300_000, activeMinutes: 60, humanCalls: 8 },
/*
* **调人次数给足**(2026-09-01 用户定「那你就把预算提高」)—— 8 次在真人手里三四轮就见底:
* 常路已经要 3 次(数据/验收工单 1 + 结论审核 1 + 落账放行 1),而**审核每打回一次就多叫一次**,
* 实测他连打回四次,单子落 `exhausted` —— 屏上看着像「最后落账那一步直接没了」。
* 24 次 = 常路 3 + 二十来轮打回,人自己点得累了也到不了顶。
*
* **这是配置,不是机制**:机制上的正解是「人自己打回引出的那张卡不算打扰」(他不是被打扰,
* 是他叫的)—— 那要在发卡那条路上分「主动叫人」与「应他要求再问一遍」,记在 debt。
*/
budget: { steps: 40, tokens: 300_000, activeMinutes: 60, humanCalls: 24 },
},
},