fix(w05): 数据工单三病一改 —— 档案卡上桌、类型守卫、prune 跳过已落地

2026-08-31 用户实测三连 + 「设计只是头上加了动画,操作感没变」:

① 类型「自己给上了」:data-by 行缺键落回外层同名字段(debt I3),查名 pill 摆出
  案卷锚(原 PN)的类型 —— 这一行根本没有的值。守卫认 selectCurrent 按行作用域写的
  option[selected]:真选上才摆名,没选摆「待选择」。守卫必须单层 :has ——
  :has() 里嵌 :has() 是非法选择器,整条被浏览器静默丢弃(第一版栽过,预览截图逮回)。

② 「品牌…点保存根本不会保存」:块① 行源是 sub_eval.part,那张案卷表没有主数据列,
  renderTemplate 只认行作用域 —— 保存真落草稿,显示与孪生件永远空白(四步重写后的
  潜伏缺陷:e2e 走 API、浏览器走查在 swap 版上,两条验证线都照不到,真人一填才现形)。
  行源改 mine:parts.profile 本身(S1 ④ prune:true 恒有两行),行里天生有全部列。

③ 「改回同一类型,下面还是两个类型」:②③ 自投影为绕「已落地行撤不掉」的死锁
  (2026-08-30 e2e 案例 2)特意不带 prune,没落地的陈旧行也跟着积在屏上。
  治在平台:draftDrop 加配方专用旗 skipLanded(跳过并出声「N 行已随数据工单入库、
  这一笔不清它」,不炸;模型那一路的硬拒一个字不变),②③ 恢复 prune:true ——
  死锁与积行两头都收,debt「prune 与确认即入库的架构性冲突」销账。
  quiet 拍仍不撤行(F3),清理落在每次确认工单那一拍。

④ 设计:块① 从 11 列横滑表推翻成电路板工作台 —— 深色板底上两张可就地编辑的
  芯片档案卡(SOIC 封装 SVG + 角色/在库徽 + 类型行 + 双栏主数据格),编辑态整卡
  变表单;三级占位(必填红/建议橘)上卡与参数值表;参数值表所属料号列加角色徽。
  嵌套的角色/在库查表全只读(嵌套行里有 data-col 会被 collect 并成一条,测钉死)。

契约:loop-engine port draftDrop +skipLanded/+landed(纯新增,CONTRACT.md 已记,
api-report 已刷)。测:107 包内(三锚变异验证)+ auto-runner skipLanded(变异验证)
+ 2520 全仓;2.7.0 装库 SQL 核过(档案卡宿主/守卫/胶囊/S1 四笔 prune)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-31 14:12:10 +08:00
co-authored by Claude Fable 5
parent e2db7cfa01
commit 5a7bb9b58f
14 changed files with 280 additions and 103 deletions
@@ -103,8 +103,14 @@ function harness(seed: Record<string, unknown> = {}, override: { analyze?: Analy
});
return Promise.resolve({ changed });
},
draftDrop: (i: { domain: string; object: string; key: Record<string, string> }) => {
draftDrop: (i: { domain: string; object: string; key: Record<string, string>; skipLanded?: boolean }) => {
const id = `${i.domain}.${i.object}|${Object.values(i.key).join('|')}`;
/* 生产真有的落地守卫(engines.ts):已落地的行,配方带 skipLanded 就跳过,别的路硬拒 */
const row = drafts.get(id);
if (row?.landed === true) {
if (i.skipLanded === true) return Promise.resolve({ dropped: false, landed: true });
return Promise.reject(new Error('这一行是本单在数据工单里建好、人确认后已入库的,撤不掉'));
}
return Promise.resolve({ dropped: drafts.delete(id) });
},
},
@@ -244,6 +250,28 @@ describe('配方执行器', () => {
expect(line).not.toContain('W05-1');
});
it('prune:已落地的陈旧行跳过不炸,没落地的照撤(2026-08-31 skipLanded)', async () => {
const { runner, drafts, notes, ctx } = harness({});
/* 铺 A、B、C 三行;B 随「数据工单确认即入库」落了地(landed),B、C 随后都不在投影源里了 */
await runner.run(
{ do: [{ kind: 'project', into: 'sub_eval.part', from: { rows: [{ pn: 'A' }, { pn: 'B' }, { pn: 'C' }] }, fields: { eval_ref: 'W05-1', internal_pn: { col: 'pn' } } }] },
ctx,
);
for (const d of drafts.values()) if (d.key['internal_pn'] === 'B') d.landed = true;
const out = await runner.run(
{ do: [{ kind: 'project', into: 'sub_eval.part', prune: true, from: { rows: [{ pn: 'A' }] }, fields: { eval_ref: 'W05-1', internal_pn: { col: 'pn' } } }] },
ctx,
);
/* 不炸(从前这里整个配方抛错停摆);C(没落地)撤掉,B(已落地)原地留下 */
expect(out.failedAt).toBeUndefined();
expect(out.dropped).toBe(1);
expect([...drafts.values()].map((d) => d.key['internal_pn']).sort()).toEqual(['A', 'B']);
const line = notes.find((n) => n.includes('撤掉 1 行'));
expect(line).toContain('撤掉 1 行(C)');
/* 跳过也要出声:B 已入库、这一笔不清它 —— 实情说给人听(debt 那条的原话要求) */
expect(line).toContain('1 行已随数据工单入库、这一笔不清它');
});
it('say:进工作日志,归系统', async () => {
const { runner, notes, ctx } = harness({ n: 2 });
await runner.run({ do: [{ kind: 'say', text: { cat: ['已核对 ', { in: 'n' }, ' 颗料'] } }] }, ctx);