Archived
fix(w05): 终审两条 Critical + 地基缺陷的另一半 + 文档与欠账收口
**C1 新建芯片类型会改写既有字典行(共享主数据被静默污染)**
链条逐行复核成立:件上种子行漏挂 data-col → barred 与 isSeq 双保护同时失效
(isSeq 那条是因为 Task 7b 把 type_id 从 integer 改成了 text)→ 落进「全表一致」,
而 H35 的自投影恰好保证这张表只有一行 → 人新建的类型被认成既有 type_id=1 →
确认时随范围放行把既有字典行改名并落进正式表,屏上只说「改了 1 行」。
三条各自成立的裁定叠出来的静默污染。
修法:种子行改成 disabled 的真实 <input data-col=type_id>(desk.ts 零改动),
格子里摆「由系统生成」——不能一声不吭地拿掉,那样人会以为这张档少了一格。
测试不止断言结果:另加两条用**真实 resolveRowKey** 重放病灶/修法两种 cells 形状,
直接证明因果机制。平台根治(全表一致应排除 auto 列)记 debt。
**C2 两个启动参数都选填 → 留空则整单永久无解**
gate_reason 说「在下面把两格补上」,而块① 没有加一行、也不在 gateLedgers ——
一句指向空气的指路。改为必填;第一档改口为唯一真无解的那条路径(两格填了同一个号)。
**I1 地基缺陷只修了一半:recipe.let 同样按对象键序求值**
与 fields 是一模一样的假设,同样住 jsonb 列、同样被 PG 重排;今天不炸只因 W05 那五条
互不引用,而 ADR-014 是给后续所有活动用的。新增 orderLets/letReferences
(把 {in:X} 视为依赖 —— 语义与 fields 不同,故单独实现),auto-runner 与静态审同步接入。
**I2 审核卡按「重做」照样收步推进** —— 坐实为真实平台缺陷(「卡答过了就收步」快道不看裁决),
并证明活动侧加选项救不了、chosenGuidance 那条也救不了(那是喂模型的提示词,
而这条快道存在的目的就是跳过模型)。按裁定只留复现测试,平台未改。
**I4** 同一堵墙撞第二次即发兜底执行工单交人 + 立监听(头一次软化留痕),
不再空转到 exhausted 而人无处可改。
debt +4(C1 根治 / modules→app / thead-after-tbody / data-by 落回外层),
并补写此前被两处引用却从不存在的「refersTo 传递闭包」条目。
文档:ADR-014:201「按声明顺序求值」改对(照它写配方的人会重犯本轮的地基缺陷)、
陈旧注释四处、spec 四处分叉逐条判断处置、端到端测试记录标为历史、
CLAUDE.md 纠正「新建类型端到端通了」那句(它没通过真人路)并更新版本与文档表。
包版本 2.1.9(已装,SQL 直查库内 version/required/gate_reason/视图 HTML 四项核实)。
pnpm gates 退出码 0(2476 通过 / 34 跳过);E04 92/92、I01 55/55 无回归。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -159,6 +159,13 @@ const REDO_MARK = '验收被打回重做';
|
||||
const REFUSE_MARK = '打回之后台账一个字没改';
|
||||
/** 配方办不下去那张兜底卡的题头 —— 认它靠这个前缀,不靠措辞 */
|
||||
const AUTO_FAIL_MARK = '配方办不下去';
|
||||
/**
|
||||
* 「提交放行被拒」那条痕的抬头(2026-08-30 终审 I4 补的常量,行为不是新写的 ——
|
||||
* 这句话原来就是硬编码在两处:`autoCompleteReady` 判齐快道 / `auto.submit` 那一支,
|
||||
* 见 :156 那条既有注释「判齐快道那条是『提交放行被拒:…』」)。命名它是为了下面
|
||||
* `auto.submit` 那一支新增的「同一堵墙撞第二次就交人」能扫痕认出「这是不是同一句话」。
|
||||
*/
|
||||
const AUTO_SUBMIT_REFUSE_MARK = '提交放行被拒';
|
||||
|
||||
export function toolInputHash(input: unknown): string {
|
||||
const text = JSON.stringify(input) ?? '';
|
||||
@@ -2133,13 +2140,78 @@ export function createTurnRunner(deps: TurnRunnerDeps): LoopEngineService {
|
||||
return { ...raised, enabled: stateEnabled, navigation: { kind: 'converged' }, status: 'awaiting' };
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
/*
|
||||
* **同一堵墙撞第二次就交人,别空转到 exhausted**(2026-08-30 终审 I4)。
|
||||
*
|
||||
* 这一支从没有任何模型会读到「提交放行被拒」这句话:全 auto 的步
|
||||
* (`current.guide.trim() === ''`)压根不问模型;半 auto 的步即便有 `guide`,
|
||||
* `auto.submit !== undefined` 也会在问模型**之前**直接 `return`(见上面这个
|
||||
* `if` 分支所在的位置——它在"半 auto 落回问模型"那段代码之前)。而失败的原因
|
||||
* (`acceptanceCriteria` 里未过的机检/裁量)在**这一步自己**这一层无法自愈:
|
||||
* 台账已经不会再变(往前一步的项都已收掉,没有活的编辑面),下一拍重跑同一份
|
||||
* 配方只会拿到同一份数据、算出同一个 `rationale`、撞回同一堵墙 —— 纯粹烧步数预算,
|
||||
* 直到 `caps.steps` 耗尽才停(task-service 的 `markProgress` 每拍都记,
|
||||
* `stallAfterMs` 那道熔断因此永远不会提前触发)。
|
||||
*
|
||||
* **头一次只留痕,当一次软化**(与「重发已答过的卡」「同一份入参重调」同族的
|
||||
* 软硬两段):万一真有什么会在两拍之间变(如另一进程写了同一份台账),给它一次
|
||||
* 机会。**同一句话第二次出现**(靠 `AUTO_SUBMIT_REFUSE_MARK` 前缀 + 原话逐字比对,
|
||||
* 不是靠拍数计数——串位/措辞抖动过一次就会被误判成"不同的墙")就当死墙:
|
||||
* 发一张兜底卡把原话摆给负责人、立监听,照 `auto.failedAt` 那条(上面)同一个模式——
|
||||
* 两者都是「配方自己走不下去,得请人来看」,卡的形状理应一致。
|
||||
*/
|
||||
const already = (await deps.memory.window(taskId, STALL_WINDOW)).some(
|
||||
(t) => t.kind === 'result' && t.title === `${AUTO_SUBMIT_REFUSE_MARK}:${reason}`,
|
||||
);
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'result',
|
||||
title: `提交放行被拒:${reason}`,
|
||||
title: `${AUTO_SUBMIT_REFUSE_MARK}:${reason}`,
|
||||
detail: { auto: current.item, hint: '配方的 submit 没过落地预检 —— 按理由补齐必交付物或修正台账,下一拍再提交' },
|
||||
});
|
||||
return { runtime: stateRuntime, enabled: stateEnabled, navigation: { kind: 'converged' }, status: 'running' };
|
||||
if (!already) {
|
||||
return { runtime: stateRuntime, enabled: stateEnabled, navigation: { kind: 'converged' }, status: 'running' };
|
||||
}
|
||||
const stuck = await deps.tasks.of(taskId);
|
||||
try {
|
||||
const card = await deps.cards.issue({
|
||||
taskId,
|
||||
ownerId: stuck.ownerId,
|
||||
humanCallCap: stuck.humanCallCap,
|
||||
workOrder: 'execution',
|
||||
card: {
|
||||
title: `${AUTO_FAIL_MARK}:「${current.item}」提交放行连续撞在同一条判据上`,
|
||||
ask:
|
||||
'这一步由系统按配方确定性提交,连续两拍撞在同一条判据上 —— 这一步没有模型会读这条拒回、' +
|
||||
'也没有可编辑的画面能让你就地改(台账在更早一步就已经收口)。看下面的原话判断该怎么办:' +
|
||||
'数据确实还缺什么就去别处把它补齐(这一单仍开着,补完下一拍会自动重试);' +
|
||||
'判据或配方本身有问题就找开发改完重跑。',
|
||||
basis: [
|
||||
{ k: '这一步', v: current.item },
|
||||
{ k: '拒回原话', v: reason },
|
||||
],
|
||||
fields: ['decision'],
|
||||
},
|
||||
});
|
||||
await deps.listeners.register({ taskId, eventType: 'human', keyFilter: { cardId: card.id }, deadline: 'P3D' });
|
||||
} catch (issueError) {
|
||||
const issueReason = issueError instanceof Error ? issueError.message : String(issueError);
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'result',
|
||||
title: `提交放行连续被拒,连兜底卡都没能发出去:${issueReason}`,
|
||||
detail: {
|
||||
auto: current.item,
|
||||
hint: `本该交给人的那句话:「${reason}」。发不出去多半是调人预算已经触顶(去治理台把这一单的调人预算提上去)。`,
|
||||
},
|
||||
});
|
||||
}
|
||||
return {
|
||||
runtime: stateRuntime,
|
||||
enabled: stateEnabled,
|
||||
navigation: { kind: 'breakpoint', gaps: [...stateEnabled.gaps] },
|
||||
status: 'awaiting',
|
||||
};
|
||||
}
|
||||
}
|
||||
if (auto.issuedCardId !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user