Archived
fix(w05): S3 补一笔只重算派生列 —— 人改了四态,结论才跟得上
浏览器走查实测:件上承诺「你改动下表里任何一条四态,这五项当场重算」, 而把使能阈值由 mismatch 改成 match 保存后,同一行的 judgement_class 仍是 hard_mismatch、risk_level 仍 high —— 行内自相矛盾,结论纹丝不动 (仍「不推荐 / 高风险 / 1 项硬约束不符」)。 断链在:S3 复用的 RELATION_PROJECTION 算结论读的是 judgement_class 的 tally, 而 judgement_class 由 difference_status 在差异明细那一笔的 when 链推出来 —— 那一笔只在 S2。人改的 difference_status 没有任何东西把它转成 judgement_class。 当初不把那一笔放进 S3 是有真理由的:它的 difference_status 走 lookup:'result:items', 而 result: 只在 analyze 那一拍存在,在 S3 重跑会把人刚改的值抹成空。 修法:加 DIFFERENCE_ITEM_RECOMPUTE 作 S3 的第一笔 —— each(mine:…) 不 prune (避开「已落地行不许 prune」),fields 只有两个主键列 + JUDGEMENT_DERIVED_FIELDS (judgement_class / risk_level),**一列都不多写**,稀疏 patch 绝不碰 difference_status 与人可改的叙述列;排在 RELATION_PROJECTION 之前。 when 链提成 JUDGEMENT_DERIVED_FIELDS 一处真源,S2 与 S3 spread 同一个, 新测试用 toBe 断言引用相等(不是形状相同)。 **旧测试为什么漏掉**:w05-s3-review-recompute.test.ts 的 HUMAN_EDIT mock 在同一个 patch 里同时写了 difference_status 和 judgement_class —— **把系统本该做的翻译自己 先算好了**,那座缺失的桥就此被掩盖。现改为只写人真能碰的列(difference_status + evidence_note),并加修前/修后成对断言。 变异验证两路都真红:①拆掉配方里那一笔 → 2/3 红,复原 → 3/3 绿; ②git stash 真 case-model.ts → steps.test.ts 5 条结构测试红,pop → 38/38 绿。 包版本 2.1.8(已装,SQL 核过)。pnpm gates 退出码 0(2454 测)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -345,17 +345,50 @@ 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', 'card', 'say']);
|
||||
expect(s3[0]?.kind === 'project' && s3[0].into).toBe('substitution.relation');
|
||||
expect(s3[1]?.kind === 'project' && s3[1].into).toBe('validation.record');
|
||||
it('S3 五笔:差异明细重算 → 关系 → 验证记录 → 卡 → 交代 —— 人改的四态先落地,结论才有真数据可读', () => {
|
||||
expect(s3.map((a) => a.kind)).toEqual(['project', 'project', 'project', 'card', 'say']);
|
||||
expect(s3[0]?.kind === 'project' && s3[0].into).toBe('substitution.difference_item');
|
||||
expect(s3[1]?.kind === 'project' && s3[1].into).toBe('substitution.relation');
|
||||
expect(s3[2]?.kind === 'project' && s3[2].into).toBe('validation.record');
|
||||
});
|
||||
|
||||
/*
|
||||
* **这是 2026-08-30 task-12「结论纹丝不动」缺陷的回归钉**:S3 从前直接从
|
||||
* `RELATION_PROJECTION`/`VALIDATION_PROJECTION` 起步,人改的 `difference_status`
|
||||
* 没有任何一笔把它转译成 `judgement_class`,两笔投影读到的 tally 纹丝不动。
|
||||
* `s3[0]` 必须是**排在关系 / 验证记录投影之前**的差异明细重算笔。
|
||||
*/
|
||||
it('差异明细重算笔:只列主键与两个派生列,不碰 difference_status 自己、也不碰人能改的叙事列', () => {
|
||||
const recompute = s3[0];
|
||||
expect(recompute?.kind === 'project' && recompute.from).toEqual({ each: 'mine:substitution.difference_item' });
|
||||
/* `recompute` 经 `s3[0]` 的 tuple 定位精确 narrow 到这一笔自己的字面量类型,它压根没有
|
||||
`prune` 这个键(不是「值为 undefined」,是键不存在)—— `in` 判存在性,不做属性访问 */
|
||||
expect('prune' in (recompute ?? {})).toBe(false);
|
||||
const fields = recompute?.kind === 'project' ? recompute.fields : undefined;
|
||||
/* 只有两把主键(定位这一行)+ 两个派生列(judgement_class/risk_level)—— 一个都不多写:
|
||||
difference_status / impact_statement / evidence_note / unknown_disposition(人可以就地改的那几列)
|
||||
一个都不在这份 fields 里,靠 draftWrite 的稀疏 patch 语义保证不被这一笔碰到。 */
|
||||
expect(Object.keys(fields ?? {}).sort()).toEqual(['difference_item_id', 'judgement_class', 'risk_level', 'substitution_relation_id']);
|
||||
});
|
||||
|
||||
/*
|
||||
* **判定分类 / 风险等级的 when 链只许有一处真源**:S3 的重算笔与 S2 差异明细创建笔的
|
||||
* `judgement_class`/`risk_level` 必须是**同一个对象引用**(都来自 `...JUDGEMENT_DERIVED_FIELDS`
|
||||
* 展开)——各写一份早晚分叉,而分叉的那一刻屏上会同时摆着两个互相矛盾的规则。
|
||||
*/
|
||||
it('S3 重算笔与 S2 差异明细创建笔共用同一份 judgement_class / risk_level when 链', () => {
|
||||
const s3Fields = s3[0]?.kind === 'project' ? s3[0].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']);
|
||||
expect(s3Fields?.['risk_level']).toBe(s2Fields?.['risk_level']);
|
||||
});
|
||||
|
||||
it('**S2 与 S3 用的是同一份投影** —— 各写一份早晚分叉', () => {
|
||||
// S2 的 do 数组里 RELATION_PROJECTION 出现两次(acts[1]/acts[3],同一个引用)——
|
||||
// 与 S3 比对随便挑一个都行,这里挑 acts[3](紧跟在差异明细之后、值是真的那一次)
|
||||
expect(s3[0]).toBe(s2[3]);
|
||||
expect(s3[1]).toBe(s2[4]);
|
||||
expect(s3[1]).toBe(s2[3]);
|
||||
expect(s3[2]).toBe(s2[4]);
|
||||
});
|
||||
|
||||
/*
|
||||
@@ -371,7 +404,7 @@ describe('S3 / S4 配方', () => {
|
||||
});
|
||||
|
||||
it('S3 的卡:审核面孔、单一选项、挂本步的件', () => {
|
||||
const card = s3[2];
|
||||
const card = s3[3];
|
||||
expect(card?.kind === 'card' && card.as).toBe('decide');
|
||||
expect(card?.kind === 'card' && card.variant).toBe('review');
|
||||
expect(card?.kind === 'card' ? card.options ?? [] : []).toHaveLength(1);
|
||||
|
||||
Reference in New Issue
Block a user