feat(w05): 差异评估结论审核的收口四格可就地改 —— 判定归规则,话归人

用户:「差异评估结论审核这个界面的收口结论下面这 4 个字段也需要可以编辑」

这四格(推荐理由 / 差异摘要 / 替代风险 / 验证要求)原本是**故意**只读的,理由写在源码里:
「让人直接改这几格,等于让他给自己没做的判断盖章」。用户改了这个口径 —— 审核人对结论
有最终发言权。原理由留在注释里备查。

**光把格子打开等于没开**:`RELATION_PROJECTION` 每跑一次就按规则把这四列重写一遍,
而人在件上按保存会触发 quiet 拍、quiet 恰恰跑 project —— 人刚写下的话当场被规则值冲掉,
那是「让你改、改完被冲掉」,比不给改更糟(基座不记「谁写的」,配方分不清一格是人改的
还是自己写的,所以只能从配方这一侧下手)。

改法:S3 换用新的 `RELATION_RECOMPUTE` —— 与 `DIFFERENCE_ITEM_RECOMPUTE` 同一手法,
**只列主键 + recommendation_level**。patch 是稀疏的、DB 侧是 jsonb 浅合并,没列的列原样
留着,人改过的四格因此保得住。**推荐等级仍然重算且不容手改**:它是规则按四态分布算出来
的判据核心(件② 判据表 / 件③ 落账结论 / 出口谓词都读它),「人改四态 → 等级跟着变」
这条链不能断。分工因此是:**判定归规则,话归人**。

代价说在明处,件上那段说明已改口:改完四态,这四格不会自动跟着改口(否则刚写的话就没了)
——「四态改完,记得回头看看这四格还对不对」。界面不撒谎优先于自动。

件上:三句话走 cell(只读份 + 孪生输入),风险只读态摆徽标、编辑态换 select(枚举列不许
直显 low/medium/high),整块一个宿主一道闸,主键钥匙藏在 .rowkey 里但真挂 data-col。
.summary-box 不是表格 —— setOpen 对非表格容器把 .on 落在 data-edit 元素自身,CSS 按
.summary-box.on 写。

测:件上四格可改 + 一个宿主一道闸 + 钥匙在场;S3 重算笔四列一个都不许出现、判定仍在且
与 S2 共用同一条 when 链。四条变异各红一条(S3 退回用 RELATION_PROJECTION / 混进
difference_summary / 混进 risk_level / 判定不再重算)。pnpm gates 退出码 0(2495 测)。
包版本 2.3.0 -> 2.4.0。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-31 11:53:33 +08:00
co-authored by Claude Opus 5
parent e5ca9caaa6
commit 1e3e5d40c6
5 changed files with 139 additions and 28 deletions
@@ -401,10 +401,35 @@ describe('S3 / S4 配方', () => {
expect(s3Fields?.['risk_level']).toBe(s2Fields?.['risk_level']);
});
it('**S2 与 S3 用的是同一份投影** —— 各写一份早晚分叉', () => {
// S2 的 do 数组里 RELATION_PROJECTION 出现两次(acts[1]/acts[3],同一个引用)——
// 与 S3 比对随便挑一个都行,这里挑 acts[3](紧跟在差异明细之后、值是真的那一次)
expect(s3[1]).toBe(s2[3]);
/**
* **S3 的关系笔只重算判定,收口四格一个字不碰**(2026-08-31 用户:「差异评估结论审核这个
* 界面的收口结论下面这 4 个字段也需要可以编辑」)。
*
* 此前 S2 与 S3 共用同一份 `RELATION_PROJECTION`(「各写一份早晚分叉」)。件上把四格打开
* 之后那条口径不能再照搬:那一笔每跑一次就按规则重写这四列,而人在件上按保存会触发
* quiet 拍、quiet 恰恰跑 `project` —— 人刚写下的话当场被冲掉,等于格子白开。
*
* 现在 S3 换成 `RELATION_RECOMPUTE`:只列主键 + `recommendation_level`。`patch` 是稀疏的、
* DB 侧是 jsonb 浅合并,没列的列原样留着 —— **这条断言的要害就是「没列」**,列了就等于没改。
* 而 `recommendation_level` 必须仍在(判据核心:件② 判据表 / 件③ 落账结论 / 出口谓词都读它,
* 「人改四态 → 等级跟着变」这条链不能断),且与 S2 那份是**同一个 when 链引用**(不许分叉)。
*/
it('**S3 的关系笔只重算判定** —— 收口四格(理由/摘要/风险/验证要求)一列都不许出现,否则人改完一保存就被冲掉', () => {
const relation = s3[1];
expect(relation?.kind).toBe('project');
const fieldsOf = (act: (typeof s3)[number] | undefined): Readonly<Record<string, unknown>> =>
act?.kind === 'project' ? act.fields : {};
const fields = fieldsOf(relation);
for (const col of ['recommendation_reason', 'difference_summary', 'risk_level', 'verification_requirement']) {
expect(Object.keys(fields), col).not.toContain(col);
}
/* 判定仍要重算,且与 S2 那份共用同一条 when 链 —— 两处各写一份早晚分叉 */
const s2Relation = fieldsOf(s2[3]);
expect(fields['recommendation_level']).toBeDefined();
expect(fields['recommendation_level']).toBe(s2Relation['recommendation_level']);
/* 主键带回:同 PK 覆盖,不是新开一行 */
expect(fields['substitution_relation_id']).toBeDefined();
/* 验证记录那一笔仍与 S2 同一份(它不涉及人可改的格子) */
expect(s3[2]).toBe(s2[4]);
});