Archived
feat(w05): 重做的意见交回给 AI 重跑那一步 —— 不再只是把派生列重算一遍
用户:「唯一调用 ai 的这一步,使用重做后是否没有将重做原因带回去重新让 ai 重跑这一步?」 确实没有:S3 那一轮只跑三笔纯投影重算,人写的那句话除了留痕之外无人读 —— AI 判过的四态一个字不会变,除非他自己在件上逐格改。 平台:引擎把「人这次打回写了什么」注给配方(`case_redo_note`)。 · 下划线不是 case: 前缀 —— S-FEEL 的名字不许带冒号,而它最要紧的用法就是当 when 条件用; · **没打回也永远绑着空串**:未绑定的名字 `!= ""` 求值为真,不绑就等于那几笔每一轮都跑 (拿真 @sfeel 钉了这条,桩也照真语义写,否则钉不住); · 静态审认这个名字 —— 它当场拦下过一次未登记的引用,这道门做对了事。 W05@2.17.0:S3 多两笔,都钉在 `case_redo_note != ""` 上 —— 带意见重判(入参多带上一轮四态/影响/依据/UNKNOWN 去向 + 他那句话;判法与第一轮同一份 ASSESS_RULES,后接复核纪律:没被点到、也确实没判错的行原样带回,别趁重判把人改过的冲掉) + 判完落回明细(与第一轮同一笔投影,只多一个 when)。判法、出参形状、落明细抽成三个 共享常量,S2 与 S3 共用 —— 各写一份早晚分叉。 验收:e2e w05 5/5 released、被拒 0;案例 0(先点名驳回、再通过)模型调用恰 2 次, 其余四例仍是 1 次 —— 没被打回就不多花一次模型。auto-runner 3 测(含变异验证)、包内 +1。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -316,6 +316,12 @@ function boundNames(manifest: AcceptanceAuditInput): ReadonlySet<string> {
|
||||
}
|
||||
// 决策卡回答读数(2026-08-27):人在决策卡上选了什么,基座按留痕数 —— 审核通过与否不再由模型自写的旗说了算
|
||||
for (const name of ['decision_last', 'decision_approve_total', 'decision_reject_total', 'decision_custom_total']) out.add(name);
|
||||
/*
|
||||
* **人把这一步打回重做时写下的那句话**(2026-09-01)—— 引擎每一拍都注给配方(没打回就是空串,
|
||||
* 见 `auto-runner.ts` 的 `AutoContext.redoNote`)。活动据它决定「这一轮要不要把意见交回给 AI 重判」。
|
||||
* 名字用下划线而不是 `case:` 前缀:S-FEEL 的名字不许带冒号,而它最要紧的用法就是当 `when` 条件用。
|
||||
*/
|
||||
out.add('case_redo_note');
|
||||
for (const id of manifest.assets.businessModel.cmmnXml.match(/caseFileItem\s+id="([^"]+)"/g) ?? []) {
|
||||
const m = /id="([^"]+)"/.exec(id);
|
||||
if (m?.[1] !== undefined) out.add(m[1]);
|
||||
|
||||
@@ -134,6 +134,12 @@ function harness(seed: Record<string, unknown> = {}, override: { analyze?: Analy
|
||||
if (expr === 'never') return false;
|
||||
if (expr === 'always') return true;
|
||||
if (expr === 'found_ref != ""') return String(scope['found_ref'] ?? '') !== '';
|
||||
/*
|
||||
* 人打回时写的那句话:空串 = 没打回(2026-09-01)。**照真 S-FEEL 的语义写**——
|
||||
* 未绑定的名字 `!= ""` 求值为 **true**(见本 describe 里那条直接拿 @sfeel 验的用例);
|
||||
* 桩若在这儿手滑写成「没有就当空串」,「必须永远绑上」这条就永远钉不住。
|
||||
*/
|
||||
if (expr === 'case_redo_note != ""') return !('case_redo_note' in scope) || String(scope['case_redo_note']) !== '';
|
||||
throw new Error(`没教过这句:${expr}`);
|
||||
},
|
||||
expression: () => undefined,
|
||||
@@ -383,6 +389,57 @@ describe('analyze 动词', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* **人打回时写的那句话要到得了配方**(2026-09-01 用户定:「重做后是否没有将重做原因带回去
|
||||
* 重新让 ai 重跑这一步?」)—— 配方据它决定这一轮要不要把意见交回给 AI 重判。
|
||||
* 名字是 `case_redo_note`(下划线,不是 `case:` 前缀):S-FEEL 的名字不许带冒号,
|
||||
* 而这句话最要紧的用法恰恰是当 `when` 条件用。
|
||||
*/
|
||||
describe('case_redo_note —— 人打回写的那句话', () => {
|
||||
const ROW = { kind: 'project' as const, into: 'sub_eval.part', from: { once: true as const } };
|
||||
|
||||
it('打回了:那句话进作用域,`when` 与 `{in:}` 都读得到', async () => {
|
||||
const { runner, drafts, ctx } = harness({});
|
||||
const out = await runner.run(
|
||||
{
|
||||
do: [{ ...ROW, when: 'case_redo_note != ""', fields: { eval_ref: 'W05-1', internal_pn: 'LM2596S-5.0', note: { in: 'case_redo_note' } } }],
|
||||
},
|
||||
{ ...ctx, redoNote: '使能阈值那条请写明验证任务号' },
|
||||
);
|
||||
expect(out.wrote).toBe(1);
|
||||
expect([...drafts.values()][0]?.patch['note']).toBe('使能阈值那条请写明验证任务号');
|
||||
});
|
||||
|
||||
it('没打回:这个名字**仍然绑着**(空串)—— 只在被打回时才跑的那几笔因此关得住', async () => {
|
||||
const { runner, drafts, ctx } = harness({});
|
||||
const out = await runner.run(
|
||||
{
|
||||
do: [
|
||||
{ ...ROW, when: 'case_redo_note != ""', fields: { eval_ref: 'W05-1', internal_pn: 'REDO-ONLY', role: 'x' } },
|
||||
{ ...ROW, fields: { eval_ref: 'W05-1', internal_pn: 'ALWAYS', note: { in: 'case_redo_note' } } },
|
||||
],
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
/* 第一笔被 when 挡住(绑了空串);第二笔照跑,那句话取出来是空串 */
|
||||
expect(out.wrote).toBe(1);
|
||||
const rows = [...drafts.values()];
|
||||
expect(rows.map((d) => d.key['internal_pn'])).toEqual(['ALWAYS']);
|
||||
expect(rows[0]?.patch['note']).toBe('');
|
||||
});
|
||||
|
||||
/*
|
||||
* **为什么非绑不可**——用**真** S-FEEL(不是本文件顶部那个只认几句话的桩)把两种下场摆出来:
|
||||
* 不绑,「只在被打回时才跑」的那几笔**每一轮都会跑**(未绑定的名字 `!= ""` 求值为真);
|
||||
* 绑上空串才关得住。本仓踩过同一个坑不止一次(S1 的 `doneWhen`、`gate_reason` 那条)。
|
||||
*/
|
||||
it('不绑的下场:真 S-FEEL 里未绑定的名字 `!= ""` 求值为 **true**', () => {
|
||||
expect(evaluateCondition('case_redo_note != ""', {})).toBe(true);
|
||||
expect(evaluateCondition('case_redo_note != ""', { case_redo_note: '' })).toBe(false);
|
||||
expect(evaluateCondition('case_redo_note != ""', { case_redo_note: '有意见' })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('card 与 submit', () => {
|
||||
it('execution:发一张数据工单,itemId 挂在这一步上,basis 现算', async () => {
|
||||
const { runner, cards, ctx } = harness({ pn: 'XL2596S-5.0' });
|
||||
|
||||
@@ -43,6 +43,16 @@ export type AutoContext = {
|
||||
readonly quiet: boolean;
|
||||
/** 这一步已经发过卡(worldTouched) */
|
||||
readonly carded: boolean;
|
||||
/**
|
||||
* **人把这一步打回重做时写下的那句话**(2026-08-31 用户定:「重做后要把原因带回去,
|
||||
* 让 AI 重跑这一步」)—— 没有打回就是空串,**永远绑得上**:S-FEEL 里未绑定的名字参与
|
||||
* 比较会翻车(`!= ""` 求值为真),那正好会让「只在被打回时才跑」的那几笔每一轮都跑。
|
||||
*
|
||||
* 配方里的名字是 **`case_redo_note`**(下划线,不是 `case:` 前缀)—— S-FEEL 的名字只认
|
||||
* `[A-Za-z0-9_一-龥]`(`packages/sfeel/src/lexer.ts` 的 NAME_PART),带冒号的名字
|
||||
* 写不进 `when` 条件;而这句话最要紧的用法恰恰是当条件用。
|
||||
*/
|
||||
readonly redoNote?: string | undefined;
|
||||
};
|
||||
|
||||
export type AutoOutcome = {
|
||||
@@ -98,7 +108,9 @@ export function createAutoRunner(deps: AutoRunnerDeps): {
|
||||
async run(recipe, ctx) {
|
||||
const writable = await deps.data.writable(ctx.taskId);
|
||||
const results: Record<string, unknown[]> = {};
|
||||
let scope: Record<string, unknown> = { ...(await deps.data.view(ctx.taskId)), ...results };
|
||||
/* 人打回时写的那句话:没有就是空串(见 `AutoContext.redoNote` 的头注 —— 必须永远绑得上) */
|
||||
const redoNote = { case_redo_note: ctx.redoNote ?? '' };
|
||||
let scope: Record<string, unknown> = { ...(await deps.data.view(ctx.taskId)), ...redoNote, ...results };
|
||||
let dirty = false;
|
||||
let wrote = 0;
|
||||
let dropped = 0;
|
||||
@@ -391,7 +403,7 @@ export function createAutoRunner(deps: AutoRunnerDeps): {
|
||||
for (const [index, action] of recipe.do.entries()) {
|
||||
try {
|
||||
if (dirty) {
|
||||
scope = { ...(await deps.data.view(ctx.taskId)), ...results };
|
||||
scope = { ...(await deps.data.view(ctx.taskId)), ...redoNote, ...results };
|
||||
dirty = false;
|
||||
}
|
||||
/*
|
||||
|
||||
@@ -2102,6 +2102,8 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
|
||||
humanCallCap: (await deps.tasks.of(taskId)).humanCallCap,
|
||||
quiet: false,
|
||||
carded: touchedItem && settled,
|
||||
/* 打回那句话交给配方(`case_redo_note`)—— 活动据它决定这一轮要不要把意见交回给 AI 重跑 */
|
||||
...(settled || answer?.chose === undefined ? {} : { redoNote: answer.chose.startsWith('custom:') ? answer.chose.slice(7).trim() : answer.chose }),
|
||||
});
|
||||
if (auto.failedAt !== undefined) {
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user