Archived
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:
@@ -1749,6 +1749,28 @@
|
||||
104 包内测 + 2516 全仓绿,两条新锚变异验证过,2.6.0 装库 SQL 核过。
|
||||
(2.5.0 是同日另一会话的启动表单芯片类型联动 —— 两会话同 checkout,版本号先问库。)
|
||||
|
||||
- **W05 数据工单三病一改:档案卡上桌,prune 跳过已落地**(2026-08-31 用户实测三连 +
|
||||
「设计只是头上加了动画」,`part-substitution-assessment@2.7.0`):
|
||||
①「类型自己给上了」= 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 恒有两行),行里天生有全部列。
|
||||
③「改回同一类型,下面还是两个类型」= ②③ 自投影为绕「已落地行撤不掉」的死锁特意不带
|
||||
prune,没落地的陈旧行也跟着积。治在平台:`draftDrop` 加配方专用旗 `skipLanded`
|
||||
(跳过并出声「N 行已随数据工单入库、这一笔不清它」,不炸),②③ 恢复 prune:true ——
|
||||
H35 案例 2 不再死锁,debt「prune 与确认即入库的架构性冲突」销账。quiet 拍仍不撤行(F3),
|
||||
清理落在每次确认工单那一拍。
|
||||
④设计:块① 从 11 列横滑表推翻成**电路板工作台** —— 深色板底上两张可就地编辑的芯片
|
||||
档案卡(SOIC 封装 SVG + 角色/在库徽 + 类型选择行 + 双栏主数据格),编辑态整卡变表单;
|
||||
三级占位(必填红/建议橘)上卡与参数值表(徽标包装的必填格用单层 `:has` 认裸 class 补胶囊);
|
||||
参数值表所属料号列加角色徽。测:107 包内(守卫/行源/嵌套零 data-col 三锚变异验证)+
|
||||
auto-runner skipLanded(变异验证)+ 2520 全仓;2.7.0 装库 SQL 核过。
|
||||
另:1838474「在库」是真的 —— 用户此前某单 land_master 落过它(正式表 chip_type_id 为空)。
|
||||
|
||||
## 文档权威地图
|
||||
|
||||
| 层 | 文件 | 说明 |
|
||||
|
||||
@@ -548,13 +548,46 @@ describe('件① 评估输入补全', () => {
|
||||
expect(steps.find((s) => s.item === 'S1_inputs')?.view).toBe('input-supplement');
|
||||
});
|
||||
|
||||
it('四块都在,行都绑本单草稿(不是整表)', () => {
|
||||
it('四块都在,行都绑本单草稿(不是整表);档案卡行源是 parts.profile 本身', () => {
|
||||
expect(html).toContain('data-rows="mine:sub_eval.part"');
|
||||
expect(html).toContain('data-rows="mine:parts.profile"');
|
||||
expect(html).toContain('data-rows="mine:parts.parameter"');
|
||||
expect(html).toContain('data-rows="mine:parts.chip_type"');
|
||||
expect(html).toContain('data-rows="mine:parts.type_param"');
|
||||
});
|
||||
|
||||
/*
|
||||
* 2026-08-31 用户实测三连的回归网(视图侧两条,配方侧第三条在 steps.test.ts):
|
||||
* ① 「品牌…点保存根本不会保存」—— 病根:块① 行源曾是 `mine:sub_eval.part`,那张案卷表
|
||||
* 没有品牌这些列,renderTemplate 只认行作用域,显示与孪生件的值永远空白。行源必须是
|
||||
* `mine:parts.profile` 本身(S1 第④笔 prune:true 保证恒有两行)。
|
||||
* ② 「明明没选类型,界面自己给上了」—— data-by 行里缺键落回外层同名字段(debt I3),
|
||||
* 查名 pill 摆出这一行没有的类型。守卫认 selectCurrent 按这一行自己的值写下的
|
||||
* option[selected]:真选上才摆名,没选摆「待选择」。
|
||||
*/
|
||||
it('档案卡:行源 = mine:parts.profile、一个宿主一道闸;嵌套的角色/在库查表全只读(嵌套行里有 data-col 会被 collect 并成一条)', () => {
|
||||
expect((html.match(/data-edit="parts\.profile"/g) ?? []).length).toBe(1);
|
||||
expect(html).toContain('data-rows="mine:parts.profile" data-edit="parts.profile"');
|
||||
expect(html).not.toContain('data-rows="mine:sub_eval.part" data-edit="parts.profile"');
|
||||
const nested = [...html.matchAll(/<span data-rows="mine:sub_eval\.part" data-by="internal_pn=internal_pn">((?:(?!<\/span>)[\s\S])*?)<\/span>/g)];
|
||||
expect(nested.length).toBeGreaterThanOrEqual(2);
|
||||
for (const m of nested) expect(m[1] ?? '').not.toContain('data-col');
|
||||
});
|
||||
|
||||
it('类型名带守卫:这一行的下拉真有选中项才摆名,没选摆「待选择」—— data-by 外层回落的谎话进不了屏(单层 :has,嵌套 :has 是非法选择器会被整条丢弃)', () => {
|
||||
expect(html).toContain('.dz-type:not(:has(option[selected])) .tsel > span{display:none}');
|
||||
expect(html).toContain('待选择');
|
||||
/* 回归网:不许再写回嵌套 :has 的形 —— 那条选择器非法,浏览器静默丢弃,守卫等于没有 */
|
||||
expect(html).not.toContain(':has(+ select.jc-cell:not(:has(');
|
||||
});
|
||||
|
||||
it('三级占位:必填红 / 建议橘 —— 空格子直说要不要填(档案卡与参数值表两处)', () => {
|
||||
expect(html).toContain('.fcell.req .jc-vcell:empty::after{content:"必填"');
|
||||
expect(html).toContain('.fcell.sug .jc-vcell:empty::after{content:"建议"');
|
||||
expect(html).toContain('td.pv .jc-vcell:empty::after{content:"必填"');
|
||||
expect(html).toContain('td.sg .jc-vcell:empty::after{content:"建议"');
|
||||
});
|
||||
|
||||
it('芯片类型是从台账现读的下拉 —— 写死等于「只能选出包那天就有的那几种」(块①的 chip_type_id 与块②b 的 type_id 都是)', () => {
|
||||
expect(html).toContain('data-options="parts.chip_type"');
|
||||
expect(html).toContain('data-option-value="type_id"');
|
||||
@@ -626,7 +659,7 @@ describe('件① 评估输入补全', () => {
|
||||
expect(html).toContain('data-col="param_name"');
|
||||
});
|
||||
|
||||
it('可改的四张表各有一道编辑闸', () => {
|
||||
it('可改的三张表 + 档案卡各有一道编辑闸', () => {
|
||||
for (const address of ['parts.profile', 'parts.chip_type', 'parts.type_param', 'parts.parameter']) {
|
||||
expect(html).toContain(`data-edit="${address}"`);
|
||||
}
|
||||
@@ -711,14 +744,14 @@ describe('件① 评估输入补全', () => {
|
||||
* 只读态与编辑态下「#」列逐行都是空白。钉死 `.jc-tbl-wrap` 现在也接了这五条规则,
|
||||
* 且这张件确实只用 `.jc-tbl-wrap`(没有表在用没打这条补丁就会数不出来的 `.tbl-wrap`)。
|
||||
*/
|
||||
it('四张表的序号列真的会数:.jc-tbl-wrap 也接了 CSS 计数器(不再只认 .tbl-wrap)', () => {
|
||||
it('三张表的序号列真的会数:.jc-tbl-wrap 也接了 CSS 计数器(不再只认 .tbl-wrap)', () => {
|
||||
expect(html).toMatch(/\.jc-tbl-wrap\s+table\s*,\s*\.tbl-wrap\s+table\s*\{\s*counter-reset:\s*rn;?\s*\}/);
|
||||
expect(html).toMatch(/\.jc-tbl-wrap\s+table\s+td\.rn\s*,\s*\.tbl-wrap\s+table\s+td\.rn\s*\{\s*counter-increment:\s*rn;?\s*\}/);
|
||||
expect(html).toMatch(/\.jc-tbl-wrap\s+table\s+td\.rn\s+\.jc-rn-num::before\s*,\s*\.tbl-wrap\s+table\s+td\.rn\s+\.jc-rn-num::before\s*\{\s*content:\s*counter\(rn\);?\s*\}/);
|
||||
// 这张件的四张表都走 .jc-tbl-wrap(没有 .tbl-wrap)—— 补丁真的接在会用到的类上(Task 7b 新增 parts.chip_type 这一张,3→4)
|
||||
// 这张件的三张表都走 .jc-tbl-wrap(没有 .tbl-wrap)—— 2026-08-31 块① 换成档案卡(不是表),4→3
|
||||
expect(html).not.toContain('class="tbl-wrap"');
|
||||
const jcTblWrapCount = html.match(/class="jc-tbl-wrap"/g)?.length ?? 0;
|
||||
expect(jcTblWrapCount).toBe(4);
|
||||
expect(jcTblWrapCount).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -158,23 +158,24 @@ describe('S1 配方', () => {
|
||||
* 原样投一遍(值不变,幂等)——目的不是产出新数据,是让 `mine:` 这两张表不再天然为零行,
|
||||
* 好让它们能安全回到下面 ⑦ 卡的 `gateLedgers` 上(闸不会再拿「零行」误判)。
|
||||
*
|
||||
* **不带 `prune`**(2026-08-30 第二处修正,e2e 案例 2 撞出来的):这两张表在 ⑦ 的
|
||||
* `gateLedgers` 上,一旦人交回数据工单就随「数据工单确认即入库」落进正式表——已落地的行
|
||||
* `draftDrop`(`prune` 的撤行机制)撤不掉,配方原样抛错。字典类的表允许历史选型留在
|
||||
* 草稿层(不算错),不像 `parts.profile`/`parts.parameter` 那几笔(它们仍然 `prune:true`,
|
||||
* 见下面「主数据骨架」那笔)必须清掉不再相关的陈旧行。
|
||||
* **带 `prune`,已落地的行由基座 `skipLanded` 跳过**(2026-08-31 改回)。曾特意不带
|
||||
* `prune`(2026-08-30 e2e 案例 2:已落地的旧类型行撤不掉,配方整单炸)—— 代价是
|
||||
* **没落地的**陈旧行也不清:人把候选从 LDO 改回 DC-DC,LDO 的字典/字段行积在屏上
|
||||
* (2026-08-31 用户实测点名「下面还是显示两个类型」)。现在 `draftDrop` 对配方这一路
|
||||
* 跳过已落地的行(loop-engine/port.ts),prune 回来:没落地的陈旧选型照撤,
|
||||
* 已落地的原地留下 —— 案例 2 不再炸,用户那条路清得干净。
|
||||
*/
|
||||
it('②③ 两笔自投影都读 sub_eval.part 的 chip_type_id 去查字典/字段清单,原样抄回自己(不带 prune)', () => {
|
||||
it('②③ 两笔自投影都读 sub_eval.part 的 chip_type_id 去查字典/字段清单,原样抄回自己(带 prune,已落地由基座跳过)', () => {
|
||||
const chipType = acts[1];
|
||||
expect(chipType?.kind === 'project' && chipType.into).toBe('parts.chip_type');
|
||||
expect(chipType?.kind === 'project' && chipType.prune).toBeUndefined();
|
||||
expect(chipType?.kind === 'project' && chipType.prune).toBe(true);
|
||||
expect(chipType?.kind === 'project' && chipType.from).toMatchObject({
|
||||
each: 'mine:sub_eval.part',
|
||||
then: { each: 'parts.chip_type', where: { type_id: { outer: 'chip_type_id' } } },
|
||||
});
|
||||
const typeParam = acts[2];
|
||||
expect(typeParam?.kind === 'project' && typeParam.into).toBe('parts.type_param');
|
||||
expect(typeParam?.kind === 'project' && typeParam.prune).toBeUndefined();
|
||||
expect(typeParam?.kind === 'project' && typeParam.prune).toBe(true);
|
||||
expect(typeParam?.kind === 'project' && typeParam.from).toMatchObject({
|
||||
each: 'mine:sub_eval.part',
|
||||
then: { each: 'parts.type_param', where: { type_id: { outer: 'chip_type_id' } } },
|
||||
|
||||
@@ -400,18 +400,17 @@ export const ASSESSMENT_STEPS = [
|
||||
kind: 'project' as const,
|
||||
into: 'parts.chip_type',
|
||||
/*
|
||||
* **不带 `prune`**(2026-08-30 第二处修正,e2e 案例 2 撞出来的第二个坑)——
|
||||
* `parts.chip_type` 在 ⑦ 的 `gateLedgers` 上,人交回数据工单那一刻这张表就跟着
|
||||
* 「数据工单确认即入库」落进正式表(`landsOnConfirm`);已落地的行 `draftDrop`
|
||||
* (`prune` 的撤行机制)撤不掉,配方原样抛 `AutoRecipeError`(原文:「这一行是本单
|
||||
* 在数据工单里建好、人确认后已入库的,撤不掉」)。案例 2(类型不一致·硬拦)第一次
|
||||
* 提交时 LM2596S-5.0 还在 type_id=1、随首次确认落地;人接着把它改挂到 type_2 后,
|
||||
* 这一笔重投只剩 type_2 一行,`prune:true` 想把已经落地的 type_1 那行撤掉 —— 直接炸。
|
||||
* 去掉 `prune` 就不会再触发这条撤行:陈旧的选型历史留在草稿层不是错误(字典行本就
|
||||
* 该积累、不该因为这一单后来改了主意就被追溯撤销),`parts.profile`/`parts.parameter`
|
||||
* 那几笔**仍然** `prune:true`(它们是这一单自己的骨架,换料/换类型确实要清旧行,
|
||||
* 且它们不会像字典这样跨料共享、也不会被单独确认入库)。
|
||||
* **带 `prune`,已落地的行由基座跳过**(2026-08-31 改回)。这一笔曾特意不带
|
||||
* `prune`(2026-08-30 e2e 案例 2:类型行随「数据工单确认即入库」落地后,人改了
|
||||
* 类型选择,`prune` 想撤已落地的旧行,`draftDrop` 硬拒、配方整单炸)—— 当时的
|
||||
* 代价是**没落地的**陈旧行也跟着不清:人在件上把候选从 LDO 改回 DC-DC,LDO 的
|
||||
* 字典行与字段行积在屏上,「下面还是显示两个类型」(2026-08-31 用户实测点名)。
|
||||
* 病根在撤行机制一刀切,不在 prune 本身 —— 现在 `draftDrop` 对配方这一路带
|
||||
* `skipLanded`(见 loop-engine/port.ts):已落地的行原地留下(它已是世界的一部分,
|
||||
* 字典行本就该积累),**没落地的陈旧选型照撤** —— 案例 2 那条路不再炸,
|
||||
* 用户点名的那条路清得干净。撤行只在非 quiet 拍跑(人正编辑时不动他的行,F3)。
|
||||
*/
|
||||
prune: true,
|
||||
from: {
|
||||
each: 'mine:sub_eval.part',
|
||||
then: { each: 'parts.chip_type', where: { type_id: { outer: 'chip_type_id' } } },
|
||||
@@ -435,16 +434,14 @@ export const ASSESSMENT_STEPS = [
|
||||
* 人在件上新增的字段行本就已经在 `mine:parts.type_param` 里,这一笔重新投一遍
|
||||
* 原样命中、照样幂等跳过,不会覆盖人刚填的内容。
|
||||
*
|
||||
* **同样不带 `prune`**(与 ② 同一处修正,同一个理由:字段清单也在 ⑥ 的
|
||||
* `gateLedgers` 上,人交回数据工单那一刻就会跟着落地;已落地的行 `prune` 撤不掉,
|
||||
* 带上 `prune:true` 会在「先前定过某个类型的字段清单、后来改选了另一个类型」这条
|
||||
* 路上原样复现 H35 e2e 案例 2 那个死锁)。历史选型的陈旧字段行留在草稿层不算错,
|
||||
* 与 ② 的字典行是同一条道理。
|
||||
* **同样带 `prune`(已落地的由基座 `skipLanded` 跳过)**—— 与 ② 同一次改回、
|
||||
* 同一个理由,见 ② 头上的大注释:改选类型后,旧类型**没落地**的字段行照撤,
|
||||
* 已随数据工单确认落地的原地留下,H35 案例 2 的死锁不再触发。
|
||||
*/
|
||||
{
|
||||
kind: 'project' as const,
|
||||
into: 'parts.type_param',
|
||||
/* 同上一笔同一个理由,同一次修正(含"不带 prune") —— 见 ② 那笔头上的大注释,不重复贴 */
|
||||
prune: true,
|
||||
from: {
|
||||
each: 'mine:sub_eval.part',
|
||||
then: { each: 'parts.type_param', where: { type_id: { outer: 'chip_type_id' } } },
|
||||
|
||||
@@ -54,7 +54,7 @@ export const manifest: ActivityManifest = {
|
||||
activityId: ACTIVITY_ID,
|
||||
name: 'ACT-WORK-05 · 差异评估',
|
||||
/* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */
|
||||
packageVersion: '2.6.0',
|
||||
packageVersion: '2.7.0',
|
||||
category: '需求分析与方案匹配',
|
||||
sdkVersion: '0.1.0-m5',
|
||||
checksum: 'dev-checksum-part-substitution-assessment',
|
||||
|
||||
@@ -70,7 +70,7 @@ const editGate = (address: string, label: string): string => `<div class="jc-edi
|
||||
<button class="jc-egbtn eg-save" type="button" data-edit-save="${address}">保存</button>
|
||||
</div>`;
|
||||
const gapbar = (object: string, what: string): string => `<div class="jc-gapbar" data-class-of="${object}_missing">
|
||||
<span><b>还差 <span data-slot="${object}_missing"></span> 行${what}没填</b> —— <span data-slot="${object}_missing_keys"></span>。按「编辑」开表,红框那几格就是要填的。补齐了才确认得了。</span>
|
||||
<span><b>还差 <span data-slot="${object}_missing"></span> 行${what}没填</b> —— <span data-slot="${object}_missing_keys"></span>。按「编辑」打开,带 * 的必填格填上,补齐了才确认得了。</span>
|
||||
</div>`;
|
||||
const addrow = (address: string, label: string, hint: string): string => `<div class="jc-addrow">
|
||||
<button type="button" data-edit-add="${address}">${label}</button>
|
||||
@@ -157,65 +157,52 @@ ${badgeCss('.rl', [
|
||||
])}
|
||||
`;
|
||||
|
||||
/**
|
||||
* hero 场景①「建档台」:两颗料的芯片铭牌直接由 `mine:sub_eval.part` 铺出来(一行一块),
|
||||
* 铭牌上是角色 / 料号 / 在库状态 —— hero 不是装饰画,摆的就是这一单的两颗料。
|
||||
* 行容器用 `<section>`(行模版里只有 div/span/svg,撞不上宿主自己的闭合标签);
|
||||
* 铭牌整块挂 `data-class-of="exists_status"`,不在库的那块虚线描边(判断留在样式表)。
|
||||
*/
|
||||
const STAGE_LAB = `<section class="stage stage-lab" data-rows="mine:${D}.part"><article class="hchip" data-class-of="exists_status">
|
||||
${CHIP}
|
||||
<span class="rl" data-class-of="role"></span>
|
||||
<span class="hchip-pn mono" data-slot="internal_pn" data-blank></span>
|
||||
<span class="hchip-badges"><span class="stx" data-class-of="exists_status"></span></span>
|
||||
</article></section>`;
|
||||
|
||||
/**
|
||||
* 块① 要评估什么 —— 两颗料(原 PN / 候选 PN)的存在性与主数据。
|
||||
* 块① 要评估什么 —— 电路板工作台:两颗料各一张**可就地编辑的档案卡**。
|
||||
*
|
||||
* **行由 `sub_eval.part` 铺、写向 `parts.profile`**(`data-rows="mine:sub_eval.part"
|
||||
* data-edit="parts.profile"`,两个不同的台账地址同时出现在一个行容器上,收值的授权面
|
||||
* 只认 `data-edit`,与行从哪来无关 —— `apps/api/src/app/routes/desk.ts` 的
|
||||
* `writesOf`/`collect` 按 `data-edit` 地址找容器、按容器内 `data-col` 收值,`data-rows`
|
||||
* 只管这一屏摆几行、摆谁的)。选它做行源是因为**角色(原/候选)与在库状态**只长在
|
||||
* `sub_eval.part` 上;`internal_pn` 挂 `data-col` 之后,保存时按它认作 `parts.profile`
|
||||
* 的主键。**人不加行**(要建哪颗料是启动参数定死的事),没有「+加一行」/删行按钮。
|
||||
* **行源改成 `mine:parts.profile` 本身**(2026-08-31,用户实测「品牌…点保存根本不会保存」
|
||||
* 的病根):从前行源是 `mine:sub_eval.part`,而那张案卷表上只有角色/在库/类型三列 ——
|
||||
* `renderTemplate` 只认行作用域,品牌/生命周期这些 `data-slot` 与孪生件的值**永远是空白**;
|
||||
* 人填了、保存也真落了草稿,件上却读不出来,像没保存。S1 配方第④笔(prune:true)保证
|
||||
* `parts.profile` 草稿恒有两行(原/候选各一,值回填现值),行里天生有全部主数据列 ——
|
||||
* 显示与孪生件从此有值可读,保存 → wake → 重推,当场看得见。
|
||||
*
|
||||
* 芯片类型选择器**长在每一行里**(不是页面级单选):`chip_type_id` 是逐颗料各自一份的
|
||||
* 字段,写入目标仍是 `parts.profile`。只读态用嵌套 `data-by` 从 `parts.chip_type` 整表
|
||||
* 按 `type_id` 查出人读的 `type_name`(取值,不是算);编辑态换成 `optionSelect`(选项现读)。
|
||||
* **角色 / 在库状态是嵌套只读查表**(`mine:sub_eval.part` 按 `internal_pn` 对行):
|
||||
* 嵌套行里**一个 `data-col` 都不许有** —— 嵌套 data-rows 里的输入格会被 `collect()`
|
||||
* 并成一条记录(平台测试 view-container-collect-div-rows 钉过的机制),只读徽标无此虞。
|
||||
*
|
||||
* **类型名带守卫,不再撒谎**(2026-08-31 用户:「明明还没选类型,界面自己就给上了」):
|
||||
* `data-by` 在行里缺键时会落回外层同名字段(debt I3)—— 候选还没选类型时,查名 pill
|
||||
* 摆出的是**案卷锚(原 PN)的类型**。守卫认 `selectCurrent` 按**这一行自己的值**写下的
|
||||
* `option[selected]`(行作用域求值,不受外层回落污染):这一行真选上了才摆名,
|
||||
* 没选摆「待选择」胶囊 —— 判断全在样式表(与被退回的换版面那版同一招,机制经过实测)。
|
||||
*
|
||||
* 人不加行(要评估哪两颗是启动参数定死的),没有「+加一行」;整卡一个宿主一道闸
|
||||
* (`bodyOf` 是单 querySelector),`.on` 落在 `data-edit` 元素自身(非表格容器)。
|
||||
*/
|
||||
const PART_TABLE = `<div class="jc-tbl-wrap"><table>
|
||||
<thead><tr>
|
||||
<th style="width:38px">#</th>
|
||||
<th data-h="角色">角色</th>
|
||||
<th class="req" data-h="内部料号">内部料号</th>
|
||||
<th data-h="在库状态">在库状态</th>
|
||||
<th class="req" data-h="芯片类型">芯片类型</th>
|
||||
<th class="req" data-h="品牌名称">品牌</th>
|
||||
<th class="req" data-h="生命周期状态">生命周期</th>
|
||||
<th data-h="制造商名称">制造商</th>
|
||||
<th data-h="规格描述">规格描述</th>
|
||||
<th data-h="Datasheet引用">Datasheet 引用</th>
|
||||
<th data-h="Datasheet版本">Datasheet 版本</th>
|
||||
</tr></thead>
|
||||
<tbody data-rows="mine:${D}.part" data-edit="parts.profile"><tr>
|
||||
<td class="rn"><span class="jc-rn-num"></span></td>
|
||||
<td><span class="jc-vcell ro"><span class="rl" data-class-of="role"></span></span></td>
|
||||
<td>${cell('internal_pn', 'mono')}</td>
|
||||
<td><span class="jc-vcell ro">${badge('stx', 'exists_status')}</span></td>
|
||||
<td>
|
||||
<span class="jc-vcell"><span data-rows="parts.chip_type" data-by="type_id=chip_type_id"><i data-slot="type_name" data-blank></i></span></span>
|
||||
const DOSSIERS = `<section class="board"><article class="dossiers" data-rows="mine:parts.profile" data-edit="parts.profile"><section class="dossier">
|
||||
<header class="dz-top">
|
||||
${CHIP}
|
||||
<span class="dz-id">
|
||||
<span class="dz-role"><span data-rows="mine:${D}.part" data-by="internal_pn=internal_pn"><i class="rl" data-class-of="role"></i><i class="stx" data-class-of="exists_status"></i></span></span>
|
||||
<span class="dz-pn">${cell('internal_pn', 'mono')}</span>
|
||||
</span>
|
||||
</header>
|
||||
<div class="dz-type">
|
||||
<i class="dz-lb">芯片类型 *</i>
|
||||
<span class="jc-vcell tsel"><span data-rows="parts.chip_type" data-by="type_id=chip_type_id"><i data-slot="type_name" data-blank></i></span></span>
|
||||
${optionSelect('chip_type_id', 'parts.chip_type', 'type_id', 'type_name', true)}
|
||||
</td>
|
||||
<td>${cell('brand_name')}</td>
|
||||
<td>${badge('stx', 'lifecycle_status', true)}${select('lifecycle_status', [['active', 'Active'], ['nrnd', 'NRND'], ['eol', 'EOL'], ['discontinued', '停产'], ['risk', '风险']], true)}</td>
|
||||
<td>${cell('manufacturer_name')}</td>
|
||||
<td>${cell('specification_description')}</td>
|
||||
<td>${cell('datasheet_ref', 'mono')}</td>
|
||||
<td>${cell('datasheet_version')}</td>
|
||||
</tr></tbody>
|
||||
</table></div>`;
|
||||
</div>
|
||||
<div class="dz-grid">
|
||||
<label class="fcell req"><i>品牌</i>${cell('brand_name')}</label>
|
||||
<label class="fcell req"><i>生命周期</i>${badge('stx', 'lifecycle_status', true)}${select('lifecycle_status', [['active', 'Active'], ['nrnd', 'NRND'], ['eol', 'EOL'], ['discontinued', '停产'], ['risk', '风险']], true)}</label>
|
||||
<label class="fcell sug"><i>制造商</i>${cell('manufacturer_name')}</label>
|
||||
<label class="fcell sug"><i>Datasheet 版本</i>${cell('datasheet_version')}</label>
|
||||
<label class="fcell sug wide"><i>规格描述</i>${cell('specification_description')}</label>
|
||||
<label class="fcell sug wide"><i>Datasheet 引用</i>${cell('datasheet_ref', 'mono')}</label>
|
||||
</div>
|
||||
</section></article></section>`;
|
||||
|
||||
/**
|
||||
* 块②a 新建芯片类型 —— `parts.chip_type`。类型下拉(块①)里没有合适的,人在这里现造一个。
|
||||
@@ -335,11 +322,11 @@ const PARAMETER_TABLE = `<div class="jc-tbl-wrap"><table>
|
||||
</tr></thead>
|
||||
<tbody data-rows="mine:parts.parameter" data-edit="parts.parameter"><tr>
|
||||
<td class="rn"><span class="jc-rn-num"></span></td>
|
||||
<td>${cell('internal_pn', 'mono')}</td>
|
||||
<td><span class="jc-vcell ro pkrole"><span data-rows="mine:${D}.part" data-by="internal_pn=internal_pn"><i class="rl" data-class-of="role"></i></span></span>${cell('internal_pn', 'mono')}</td>
|
||||
<td>${cell('param_name')}</td>
|
||||
<td>${cell('param_value', 'mono')}</td>
|
||||
<td class="pv">${cell('param_value', 'mono')}</td>
|
||||
<td>${ro('param_unit')}</td>
|
||||
<td>${cell('param_note')}</td>
|
||||
<td class="sg">${cell('param_note')}</td>
|
||||
</tr></tbody>
|
||||
</table></div>`;
|
||||
|
||||
@@ -350,22 +337,22 @@ export const INPUT_SUPPLEMENT = doc({
|
||||
kicker: 'SS02 · ACT-WORK-05 差异评估',
|
||||
title: '差异评估的输入还差几格',
|
||||
step: '步骤 1/4 · 校验入参与补数据',
|
||||
stage: STAGE_LAB,
|
||||
stage: '',
|
||||
footTag: '工单类型:数据',
|
||||
body:
|
||||
gateBar() +
|
||||
zone('做什么', '核对两颗料在不在库、归没归同一个芯片类型、参数值齐不齐;缺什么就在下面补齐', ZIC_TASK) +
|
||||
note([
|
||||
'<span class="need">模板定位</span>:本单是「数据工单」——目标台账 = 芯片主数据(P-08 主记录 + 标准化参数子表),核心参数字段清单挂在芯片类型字典上。',
|
||||
'<span class="need">触发原因</span>:上面铭牌里两块是本次评估要比对的原 PN 与候选 PN —— 至少有一颗不在库、两颗归的类型不一致、或必填格还空着,顶部那条红带才会出现。',
|
||||
'<span class="done">已绑定</span>:两颗料的内部料号已按启动参数铺进下表,不必你重填料号本身,只需核对 / 补齐其余各格;在库的料现值已回填,只是查漏补缺。',
|
||||
'<span class="need">触发原因</span>:下面电路板上两张档案卡就是本次要比对的原 PN 与候选 PN —— 至少有一颗不在库、两颗归的类型不一致、或必填格还空着,顶部那条红带才会出现。',
|
||||
'<span class="done">已绑定</span>:两颗料的内部料号已按启动参数铺上档案卡,不必你重填料号本身,只需核对 / 补齐其余各格;在库的料现值已回填,只是查漏补缺。',
|
||||
'<span class="need">纪律</span>:两颗料必须归到同一个芯片类型下才谈得上逐项比对;类型下拉里没有合适的就在【芯片类型】区新建一个,并把它的核心参数字段一并定下来;查不到的参数值留空(会被红带点名),不要编一个值顶替。',
|
||||
]) +
|
||||
zone('要评估什么', '两颗料的存在性与主数据(每行一颗,人不加行,*为必填列)', ZIC_TABLE) +
|
||||
editGate('parts.profile', '两颗料的主数据核对 / 补齐,点「编辑」开表') +
|
||||
zone('要评估什么', '两颗料各一张档案卡,直接在卡上核对 / 补齐(人不加行,*为必填)', ZIC_TABLE) +
|
||||
editGate('parts.profile', '两张档案卡上的主数据核对 / 补齐,点「编辑」开卡') +
|
||||
gapbar('parts_profile', '必填') +
|
||||
PART_TABLE +
|
||||
zone('芯片类型', '上面下拉选不到合适的类型,就在这里新建一个(选填,*为必填列)', ZIC_TABLE) +
|
||||
DOSSIERS +
|
||||
zone('芯片类型', '本单选用 / 新建的类型;卡上改选之后,旧选择在你确认工单那一刻自动清掉(已入库的留档)', ZIC_TABLE) +
|
||||
editGate('parts.chip_type', '新建一个类型:表尾那一行填类型名称与说明,保存后立刻出现在上面的下拉里') +
|
||||
gapbar('parts_chip_type', '必填') +
|
||||
CHIP_TYPE_TABLE +
|
||||
|
||||
@@ -246,9 +246,61 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
|
||||
*{animation:none !important;transition:none !important}
|
||||
}
|
||||
|
||||
/* ================= 电路板工作台(件① 的两张档案卡)================= */
|
||||
.board{position:relative;margin:0 0 6px;padding:20px 16px 18px;border-radius:16px;
|
||||
background:
|
||||
repeating-linear-gradient(0deg, rgba(255,255,255,.03) 0 1px, transparent 1px 26px),
|
||||
repeating-linear-gradient(90deg, rgba(255,255,255,.03) 0 1px, transparent 1px 26px),
|
||||
radial-gradient(700px 260px at 16% -30%, var(--hero-tint), transparent 62%),
|
||||
linear-gradient(158deg, #13212f 0%, var(--board) 58%, #0f1d2b 100%)}
|
||||
.board::before{content:"";position:absolute;left:4%;right:4%;top:50%;border-top:2px dashed var(--glow-line)}
|
||||
.board::after{content:"";position:absolute;top:50%;left:4%;width:9px;height:9px;margin-top:-4px;border-radius:999px;background:var(--glow);box-shadow:0 0 14px 3px var(--glow-line),0 0 4px 1px var(--glow);animation:sig 3.4s linear infinite}
|
||||
.dossiers{position:relative;z-index:1;display:grid;grid-template-columns:repeat(auto-fit,minmax(272px,1fr));gap:16px}
|
||||
.dossier{display:flex;flex-direction:column;gap:11px;background:rgba(255,255,255,.97);border-radius:16px;padding:15px 16px 14px;box-shadow:0 14px 30px rgba(3,10,18,.5),inset 0 1px 0 rgba(255,255,255,.7);transition:box-shadow .18s,transform .18s}
|
||||
.dossier:hover{transform:translateY(-2px);box-shadow:0 18px 36px rgba(3,10,18,.55),inset 0 1px 0 rgba(255,255,255,.7)}
|
||||
.dz-top{display:flex;align-items:center;gap:12px}
|
||||
.dz-top .csvg{width:64px;flex:none}
|
||||
.dz-id{display:flex;flex-direction:column;gap:4px;min-width:0}
|
||||
.dz-role{display:flex;gap:6px;flex-wrap:wrap}
|
||||
.dz-pn{font-size:15px;font-weight:800;overflow-wrap:anywhere;line-height:1.3}
|
||||
.dz-pn .jc-vcell{font-size:15px;font-weight:800;color:var(--ink)}
|
||||
.dz-type{display:flex;align-items:center;gap:9px;flex-wrap:wrap;padding:8px 10px;border-radius:10px;background:var(--acc-wash);border:1px solid var(--acc-line)}
|
||||
.dz-lb{flex:none;font-style:normal;font-size:10.5px;font-weight:800;letter-spacing:.05em;color:var(--ink-4)}
|
||||
.dz-type .jc-cell{flex:1 1 140px;width:auto}
|
||||
/* 类型名守卫:这一行的下拉真有选中项(selectCurrent 按行作用域写的 option[selected])才摆名 ——
|
||||
行里没选,data-by 落回外层的名一个字都不摆,换「待选择」胶囊。
|
||||
守卫挂在共同祖先 .dz-type 上、单层 :has —— :has() 里再嵌 :has() 是非法选择器,整条会被浏览器
|
||||
静默丢弃(第一版就栽在这:守卫写了等于没写,XL 那张卡照样摆出它没有的类型)。 */
|
||||
.tsel{flex:1 1 auto;min-width:0}
|
||||
.tsel > span > i{display:inline-block;font-size:11.5px;font-weight:800;color:var(--acc-deep);background:var(--acc-soft);border-radius:999px;padding:1px 11px}
|
||||
.dz-type:not(:has(option[selected])) .tsel > span{display:none}
|
||||
.dz-type:not(:has(option[selected])) .tsel::after{content:"待选择 · 点「编辑」挑一个";display:inline-block;font-size:11px;font-weight:700;color:var(--amber);background:var(--amber-soft);border:1px dashed #eeda9e;border-radius:999px;padding:1px 10px}
|
||||
.dz-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px 12px}
|
||||
.fcell{display:flex;flex-direction:column;gap:3px;min-width:0}
|
||||
.fcell.wide{grid-column:1 / -1}
|
||||
.fcell > i{font-style:normal;font-size:10.5px;font-weight:800;letter-spacing:.05em;color:var(--ink-4)}
|
||||
.fcell.req > i::after{content:" *";color:var(--danger)}
|
||||
.fcell .jc-vcell{font-size:12.5px}
|
||||
/* 三级占位:必填红 / 建议橘(空格子直说要不要填,不摆一枚含糊的破折号) */
|
||||
.fcell.req .jc-vcell:empty::after{content:"必填";font-size:10.5px;font-weight:800;color:var(--danger);background:var(--danger-soft);border-radius:999px;padding:0 9px}
|
||||
/* 徽标包装的必填格(如生命周期):没值时徽标 class 恰为裸 "stx"(class-of 没投出 v-*),
|
||||
wrapper 不为 :empty,上一条够不着 —— 认裸 class 补同一枚胶囊(单层 :has) */
|
||||
.fcell.req .jc-vcell:has(> [class="stx"])::after{content:"必填";font-size:10.5px;font-weight:800;color:var(--danger);background:var(--danger-soft);border-radius:999px;padding:0 9px}
|
||||
.fcell.sug .jc-vcell:empty::after{content:"建议";font-size:10.5px;font-weight:800;color:var(--amber);background:var(--amber-soft);border-radius:999px;padding:0 9px}
|
||||
/* 档案卡的编辑态(非表格容器,.on 落在 data-edit 元素自身) */
|
||||
.dossiers.on .jc-vcell{display:none}
|
||||
.dossiers.on .jc-vcell.ro{display:block}
|
||||
.dossiers.on .jc-cell{display:block}
|
||||
.dossiers.on .dz-role{display:flex}
|
||||
/* 参数值表:所属料号列的角色徽 + 值/说明列的三级占位 */
|
||||
.pkrole{margin-bottom:2px}
|
||||
.jc-tbl-wrap td.pv .jc-vcell:empty::after{content:"必填";font-size:10.5px;font-weight:800;color:var(--danger);background:var(--danger-soft);border-radius:999px;padding:0 9px}
|
||||
.jc-tbl-wrap td.sg .jc-vcell:empty::after{content:"建议";font-size:10.5px;font-weight:800;color:var(--amber);background:var(--amber-soft);border-radius:999px;padding:0 9px}
|
||||
|
||||
/* ================= 窄屏 ================= */
|
||||
@media(max-width:620px){
|
||||
.card-head{padding:16px 14px 18px}
|
||||
.dz-grid{grid-template-columns:1fr}
|
||||
.card-bd{padding:16px 14px 6px}
|
||||
.hero-title{font-size:19.5px}
|
||||
.stage{gap:14px}
|
||||
|
||||
@@ -92,8 +92,21 @@ export type TaskDataGateway = {
|
||||
domain: string;
|
||||
object: string;
|
||||
key: Record<string, string | number>;
|
||||
/**
|
||||
* **已落地的行:跳过,不拒**(2026-08-31,配方专用 —— 与 `draftWrite` 的两个旗同族)。
|
||||
* 「数据工单确认即入库」把行落进正式表后,草稿层那一笔只是「本单写过它」的记号;
|
||||
* 撤记号正式表一个字不动,读面反而少一行 —— 所以对人/模型那一路它是硬拒(拒回是
|
||||
* 行动指南)。而配方 `prune` 的语义是「陈旧的投影行清掉」:已落地 = 已经是世界的
|
||||
* 一部分,不是陈旧草稿 —— 对配方来说跳过是常态,抛错就是整单停摆(2026-08-30
|
||||
* e2e 案例 2 的死锁:人确认入库后改了类型选择,prune 想撤已落地的旧类型行,当场炸;
|
||||
* 当时以「②③ 不带 prune」绕开,代价是**没落地的**陈旧行也跟着不清了 ——
|
||||
* 2026-08-31 用户实测点名的「改回同一类型,下面还是两个类型」正是这半截代价)。
|
||||
* 模型那一路不传这个旗:行为一个字不变。
|
||||
*/
|
||||
skipLanded?: boolean | undefined;
|
||||
}): Promise<{
|
||||
dropped: boolean;
|
||||
landed?: boolean;
|
||||
}>;
|
||||
};
|
||||
/**
|
||||
|
||||
@@ -1203,7 +1203,7 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
* 回 `dropped: false` = 草稿层里根本没有这一行 —— 多半是主键写法对不上
|
||||
* (`DAG…` 与 `DAG…-R0` 是两行);如实说比静默成功有用。
|
||||
*/
|
||||
async draftDrop(input: { taskId: TaskId; domain: string; object: string; key: Record<string, string | number> }) {
|
||||
async draftDrop(input: { taskId: TaskId; domain: string; object: string; key: Record<string, string | number>; skipLanded?: boolean | undefined }) {
|
||||
const allowed = await taskData.writable(input.taskId);
|
||||
const target = allowed.find((a) => a.domain === input.domain && a.object === input.object);
|
||||
if (target === undefined) {
|
||||
@@ -1226,6 +1226,12 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
Object.keys(input.key).every((k) => String(d.key[k] ?? '') === String(input.key[k] ?? '')),
|
||||
);
|
||||
if (landedHit !== undefined) {
|
||||
/*
|
||||
* 配方 `prune` 传 `skipLanded`:已落地 = 已经是世界的一部分,不是陈旧草稿 ——
|
||||
* 跳过是常态,不该整单炸掉(port.ts 上的大注释)。模型那一路不传,照旧硬拒:
|
||||
* 那句拒回是它的行动指南。
|
||||
*/
|
||||
if (input.skipLanded === true) return { dropped: false, landed: true };
|
||||
throw new Error(
|
||||
`${target.label}里这一行(${Object.entries(input.key).map(([k, v]) => `${k}=${String(v)}`).join('、')})` +
|
||||
'是本单在数据工单里建好、人确认后已入库的,撤不掉;要改由人在件上改,你接着办下一件事。',
|
||||
|
||||
@@ -177,6 +177,12 @@
|
||||
跳过的行照旧算「这一笔要的行」,`prune` 不许撤它。
|
||||
- `writable` 多一个可选 `caseKey`(单标识列):摆给人看的行标识要**去掉单号那一段**
|
||||
(2026-08-25 那条口径,基座那一侧是 `rowLabel`)—— 人正在这一单里看,单号是脚下的地面。
|
||||
- `draftDrop` 多一个可选入参 `skipLanded` 与可选出参 `landed`(2026-08-31,同族第三个旗):
|
||||
已随「数据工单确认即入库」落地的行,配方 `prune` 传 `skipLanded` 时回 `{dropped:false, landed:true}`
|
||||
而不是抛 —— 已落地 = 已是世界的一部分,不是陈旧草稿,跳过是配方的常态;抛出去就是整单停摆
|
||||
(2026-08-30 e2e 案例 2 的死锁,当时以「W05 ②③ 两笔不带 prune」绕开,代价是**没落地的**
|
||||
陈旧行也不清 —— 2026-08-31 用户实测点名的「改回同一类型,下面还是两个类型」)。
|
||||
模型那一路不传:那句「已入库撤不掉」的拒回是它的行动指南,一个字不变。
|
||||
|
||||
同批三条不变量:
|
||||
- **配方由 `liveStep` 驱动,不由 `current` 的兜底驱动**:兜底(在场的第一个有剧本的项)不过滤
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -208,6 +208,8 @@ export function createAutoRunner(deps: AutoRunnerDeps): {
|
||||
kept.add(keyText(key));
|
||||
}
|
||||
let localDropped = 0;
|
||||
/** 已随「数据工单确认」落地、prune 不动它的行数 —— 跳过要说给人听(debt 那条的原话要求) */
|
||||
let localLandedKept = 0;
|
||||
/*
|
||||
* **人正在件上编辑的时候不 prune**(2026-08-29 终审 F3)。
|
||||
* `prune` 的语义是「这一笔投影出来的行就是这张表的全部,多出来的撤掉」——
|
||||
@@ -221,14 +223,23 @@ export function createAutoRunner(deps: AutoRunnerDeps): {
|
||||
const key: Record<string, string> = {};
|
||||
for (const column of decl.keys) key[column] = String(stale[column] ?? '');
|
||||
if (kept.has(keyText(key))) continue;
|
||||
const { dropped: gone } = await deps.data.draftDrop({ taskId: context.taskId, domain: decl.domain, object: decl.object, key });
|
||||
/*
|
||||
* `skipLanded`:已随「数据工单确认」落进正式表的行不撤、也不炸(2026-08-31)。
|
||||
* 从前这里不传,已落地的陈旧行让整个配方抛错停摆(2026-08-30 e2e 案例 2),
|
||||
* ②③ 那两笔被迫整个放弃 prune —— 连**没落地的**陈旧行(人改了类型选择,
|
||||
* 旧类型的字典/字段行)也跟着积在屏上。现在已落地的原地留下(它是世界的
|
||||
* 一部分),没落地的照撤 —— 两种行各得其所。
|
||||
*/
|
||||
const { dropped: gone, landed } = await deps.data.draftDrop({ taskId: context.taskId, domain: decl.domain, object: decl.object, key, skipLanded: true });
|
||||
if (gone) {
|
||||
localDropped += 1;
|
||||
dropNames.push(rowName(key, decl.caseKey));
|
||||
} else if (landed === true) {
|
||||
localLandedKept += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (localWrote > 0 || localDropped > 0 || localExisting > 0 || localUnkeyed > 0) {
|
||||
if (localWrote > 0 || localDropped > 0 || localExisting > 0 || localUnkeyed > 0 || localLandedKept > 0) {
|
||||
/*
|
||||
* **kind: 'observation' + `internal: true`**(2026-08-29 终审 I5,控制端裁定 F2)。
|
||||
* `kind` 必须是 `observation`:`result` 不在 `task-log.ts` 的 `SKELETON` 里,进不了工作日志(R10)。
|
||||
@@ -253,7 +264,9 @@ export function createAutoRunner(deps: AutoRunnerDeps): {
|
||||
/* 撤了要点名(M2):不点名的话,下一拍的读者(人或模型)对不上是哪几行没了 */
|
||||
(localDropped === 0 ? '' : `,撤掉 ${localDropped} 行(${nameList(dropNames)})`) +
|
||||
/* 少写了几行要出声(M1):静默 continue 的那几行,表里少了谁也查不出为什么 */
|
||||
(localUnkeyed === 0 ? '' : `,${localUnkeyed} 行因主键算不出来没写(主键列:${decl.keys.join('、')})`),
|
||||
(localUnkeyed === 0 ? '' : `,${localUnkeyed} 行因主键算不出来没写(主键列:${decl.keys.join('、')})`) +
|
||||
/* 已落地的陈旧行跳过也要出声 —— 「这几行已经入库、这一笔没法帮你清」的实情说给人听 */
|
||||
(localLandedKept === 0 ? '' : `,${localLandedKept} 行已随数据工单入库、这一笔不清它`),
|
||||
detail: {
|
||||
auto: `${context.stepId}#${index + 1}`,
|
||||
ledger: action.into,
|
||||
|
||||
@@ -92,7 +92,19 @@ export type TaskDataGateway = {
|
||||
domain: string;
|
||||
object: string;
|
||||
key: Record<string, string | number>;
|
||||
}): Promise<{ dropped: boolean }>;
|
||||
/**
|
||||
* **已落地的行:跳过,不拒**(2026-08-31,配方专用 —— 与 `draftWrite` 的两个旗同族)。
|
||||
* 「数据工单确认即入库」把行落进正式表后,草稿层那一笔只是「本单写过它」的记号;
|
||||
* 撤记号正式表一个字不动,读面反而少一行 —— 所以对人/模型那一路它是硬拒(拒回是
|
||||
* 行动指南)。而配方 `prune` 的语义是「陈旧的投影行清掉」:已落地 = 已经是世界的
|
||||
* 一部分,不是陈旧草稿 —— 对配方来说跳过是常态,抛错就是整单停摆(2026-08-30
|
||||
* e2e 案例 2 的死锁:人确认入库后改了类型选择,prune 想撤已落地的旧类型行,当场炸;
|
||||
* 当时以「②③ 不带 prune」绕开,代价是**没落地的**陈旧行也跟着不清了 ——
|
||||
* 2026-08-31 用户实测点名的「改回同一类型,下面还是两个类型」正是这半截代价)。
|
||||
* 模型那一路不传这个旗:行为一个字不变。
|
||||
*/
|
||||
skipLanded?: boolean | undefined;
|
||||
}): Promise<{ dropped: boolean; landed?: boolean }>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+8
-1
@@ -1669,7 +1669,14 @@ B 表却**不在**同一份 `only` 名单里——B 表对应的那一行仍留
|
||||
有),先看确认那张卡的 `gateLedgers`/绑定 `blocks` 是不是只列了「主表」、漏了它
|
||||
`refersTo` 指向的「字典表」。
|
||||
|
||||
## `prune` 与「数据工单确认即入库」的架构性冲突(2026-08-30,ACT-WORK-05 案例 2 真跑撞见)
|
||||
## ~~`prune` 与「数据工单确认即入库」的架构性冲突~~(2026-08-30 撞见;2026-08-31 已治,销账)
|
||||
|
||||
**治法即下文「要治」的第二条**:`draftDrop` 加配方专用旗 `skipLanded`(loop-engine/port.ts),
|
||||
`prune` 遇已落地行**跳过并出声**(观察痕「N 行已随数据工单入库、这一笔不清它」),不再抛
|
||||
`AutoRecipeError`;W05 ②③ 两笔自投影随之恢复 `prune:true` —— 没落地的陈旧选型照撤
|
||||
(2026-08-31 用户实测点名的「改回同一类型,下面还是两个类型」就此清掉),已落地的原地留下
|
||||
(它已是世界的一部分,字典行本就该积累)。模型那一路的硬拒一个字不变(拒回是它的行动指南)。
|
||||
测:auto-runner「prune:已落地的陈旧行跳过不炸」(变异验证过)。原始记录留档如下。
|
||||
|
||||
**现象**:数据工单**确认落地之后**再改动会影响行集合的字段(如 W05 的芯片类型),
|
||||
`project` 动作的 `prune: true` 要把「新计算出的集合里没有」的陈旧行撤掉——而那些行如果
|
||||
|
||||
Reference in New Issue
Block a user