fix(w05): 评审回来的一条 Critical、一条 Important、两条 Minor

Critical:S1 数据工单的 gateLedgers 带着 parts.chip_type,而这张件没有任何手段
写它(选类型走 data-options 下拉现读,不是编辑表)—— mine:parts.chip_type 永远
是空数组,desk.ts 的 emptyGate 永远判「这一单还一行都没有」,确认钮永久按不下去,
三张真被编辑的表填得再全也一样。删掉 parts.chip_type,只留真被这张件写过的三张;
注释写明这是永久决定,不是等 Task 7b 补了新建类型的编辑表就该还原(主路是从下拉
挑已有类型,那时这张表天然零行,「零行=没干完」这条闸的前提对它压根不成立)。
新增 apps/api 侧回归探针(调用生产 emptyGate,不抄实现):证明旧配置确实会永久
卡死、新配置在同一份数据下真的放行。

Important:BIND_COMMON 的序号计数器规则只认 .tbl-wrap,而三张表的外壳是
.jc-tbl-wrap(继承自已删除的旧 P08_MASTER_CREATE 同一个坑)—— 两个 class 对不上,
「#」列在只读态与编辑态下逐行空白。补上 .jc-tbl-wrap,钉一条结构测试。

Minor:①block③ 的注释「只两栏可改」没说全 —— internal_pn/param_name 也走 cell()
(主键格必挂 data-col 是既定约定,行的钥匙),补全这句话;②data-class-of="gate_reason"
在中文自由文本上永远算不出 v-token,是源码里躺着的一个永远不起作用的属性——删掉,
显隐只留 :has(:empty) 一处,brief 给的那条断言同步改成断真实机制。

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-29 19:11:03 +08:00
co-authored by Claude Opus 5
parent fdcd1f88a7
commit 40daa6c9f7
5 changed files with 128 additions and 17 deletions
@@ -442,14 +442,40 @@ describe('件① 评估输入补全', () => {
}
});
it('模版不算不判:没有内联脚本,红带的显隐交给样式表', () => {
/*
* 2026-08-29 评审 Minor:brief 原话是「红带显隐用 data-class-of」,但 gate_reason 是
* 中文自由文本 —— classToken 只留 [a-z0-9-],中文字符全被剥空,不论空串还是有内容的
* 中文句子算出来的 token 都是空的,这条路本来就走不通(不是本任务的臆断,评审读了
* view-render.ts 的 classToken 实现确认)。真正管显隐的是 data-blank 出的空文本节点
* 配 CSS :has(:empty) ——源码里不留一个永远不起作用的 data-class-of 属性。
*/
it('模版不算不判:没有内联脚本,红带的显隐交给样式表(:has(:empty),不是 gate_reason 上算不出来的 data-class-of)', () => {
expect(html).not.toContain('<script');
expect(html).toContain('data-class-of="gate_reason"');
expect(html).not.toContain('data-class-of="gate_reason"');
expect(html).toContain('class="jc-gate-text" data-slot="gate_reason" data-blank');
expect(html).toContain('.jc-gate-bar:has(.jc-gate-text:empty)');
});
it('表宽超出要能横着滑 —— 不许写成 overflow:hidden(E04/I01 抄错过一次)', () => {
expect(html).not.toMatch(/\.jc-tbl-wrap\s*\{[^}]*overflow\s*:\s*hidden/);
});
/*
* 2026-08-29 评审 Important:BIND_COMMON 的序号计数器规则原来只认 `.tbl-wrap`
* (继承自已删除的旧 P08_MASTER_CREATE,同一个 wrapper class,同一个坑),
* 而这张件的三张表全用平台的 `.jc-tbl-wrap` —— 两个 class 对不上,计数器永不触发,
* 只读态与编辑态下「#」列逐行都是空白。钉死 `.jc-tbl-wrap` 现在也接了这五条规则,
* 且这张件确实只用 `.jc-tbl-wrap`(没有表在用没打这条补丁就会数不出来的 `.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)—— 补丁真的接在会用到的类上
expect(html).not.toContain('class="tbl-wrap"');
const jcTblWrapCount = html.match(/class="jc-tbl-wrap"/g)?.length ?? 0;
expect(jcTblWrapCount).toBe(3);
});
});
/*
@@ -188,11 +188,21 @@ describe('S1 配方', () => {
expect(when).toContain('parts_parameter_missing > 0');
});
it('卡:数据工单、四张表都在闸上、且有 basis(基座拒发 basis 为空的工单)', () => {
/*
* 2026-08-29 评审 Critical(Task 7 走查):`gateLedgers` 一度带着 `parts.chip_type`,
* 而 `input-supplement` 这张件没有任何 `data-edit="parts.chip_type"`(选类型走
* `data-options` 下拉现读,不写这张表)—— `mine:parts.chip_type` 永远是空数组,
* `desk.ts` 的 `emptyGate` 会永远判「这一单还一行都没有」,提交钮永久按不下去。
* **`parts.chip_type` 永久不列进这张卡的闸**(不是等新建类型的编辑表补上就该还原 ——
* 见 `case-model.ts` 该处注释:主路是从下拉挑一个已有类型,那时这张表天然零行,
* `emptyGate`「这张卡就是来建它的」这条语义对一张只是有时候才写的表根本不成立)。
*/
it('卡:数据工单、三张真被这张件写过的表在闸上(parts.chip_type 不在——它没有编辑手段,列了就永久卡死)、且有 basis(基座拒发 basis 为空的工单)', () => {
const card = acts[4];
expect(card?.kind === 'card' && card.as).toBe('execution');
expect(card?.kind === 'card' && card.variant).toBe('data');
expect(card?.kind === 'card' && card.gateLedgers).toEqual(['parts.chip_type', 'parts.type_param', 'parts.profile', 'parts.parameter']);
expect(card?.kind === 'card' && card.gateLedgers).toEqual(['parts.type_param', 'parts.profile', 'parts.parameter']);
expect((card?.kind === 'card' ? card.gateLedgers ?? [] : [])).not.toContain('parts.chip_type');
expect((card?.kind === 'card' ? card.basis ?? [] : []).length).toBeGreaterThan(0);
expect((card?.kind === 'card' ? card.guide ?? [] : []).length).toBeGreaterThanOrEqual(3);
});
@@ -241,7 +241,8 @@ export const ASSESSMENT_STEPS = [
intent: '核对两颗料在不在库、归没归同一个类型、参数值齐不齐;缺什么就派一张数据工单让人补',
guide: '',
/* Task 7:发给人的数据工单摆的就是这一张 —— 三块竖排(存在性与主数据 / 芯片类型字段 / 参数值),
与 auto.do 第 ⑤ 笔 `card` 的 `gateLedgers` 同源(parts.chip_type / parts.type_param / parts.profile / parts.parameter)。 */
与 auto.do 第 ⑤ 笔 `card` 的 `gateLedgers` 同源(parts.type_param / parts.profile / parts.parameter ——
`parts.chip_type` 不列入,理由见那笔 `card` 定义处的注释)。 */
view: 'input-supplement',
writes: ['sub_eval.part', 'sub_eval.case', 'parts.profile', 'parts.parameter'],
reads: ['originalInternalPn', 'candidateInternalPn', 'case:openedAt', 'parts.profile', 'parts.parameter', 'parts.chip_type', 'parts.type_param'],
@@ -409,7 +410,20 @@ export const ASSESSMENT_STEPS = [
'参数值填**原厂规格书上的值**,查不到就留空 —— 留空会被红带点名,但**编一个值比留空坏得多**。',
],
standard: '红带消失、提交钮变绿:两颗料都在库、归了同一个类型、每个核心参数字段都有值。',
gateLedgers: ['parts.chip_type', 'parts.type_param', 'parts.profile', 'parts.parameter'],
/*
* **`parts.chip_type` 永久不列在这里** —— 不是等 Task 7b 补了「新建类型」那张编辑表
* 就该加回来(2026-08-29 评审逮到的 Critical:这张件此刻没有任何 `data-edit`
* 能写 `parts.chip_type`,列了它,`mine:parts.chip_type` 永远是空数组,
* `desk.ts` 的 `emptyGate` 就永远判「这一单还一行都没有 —— 这张卡就是来建它的」,
* 提交钮当场且永久按不下去,三张表填得再全也一样)。
* 更深一层:即便 Task 7b 真的补上了那张编辑表,也不该把它放回来 —— `emptyGate`
* 的语义是「这张卡**就是来建**这张表的,一行都没有 = 没干活」,而
* `parts.chip_type` 的主路是**从下拉挑一个已有类型**,那时 `mine:parts.chip_type`
* 天然就是零行(没新建、没问题);只有走「新建类型」那条岔路时才会写它。
* 一张**只是有时候才写**的表,套「零行 = 没干完」这条闸的前提本身就不成立 ——
* 该拦的是 `parts.type_param`(核心参数字段清单,新建类型时**一定**要写)。
*/
gateLedgers: ['parts.type_param', 'parts.profile', 'parts.parameter'],
},
{ kind: 'say' as const, text: '已核对两颗料的在库状态与参数齐备情况。' },
],
@@ -57,12 +57,16 @@ html:not(.editable) .jc-editgate, html:not(.editable) .jc-addrow { display: none
/* 序号列交给 CSS 计数器 —— 目标件的 JS 脚本在增删行后重写 .jc-rn-num / .num 的文本,
沙箱零脚本,新增的行 / 删掉的行让计数器自己算对,不必比对哪一行手改过数字。
两种列名都要接(.rn 用于 P08/P10/执行工单 —— 目标件里那三份自己就用它;
.num 用于两份验收工单 —— 目标件里那两份自己就用它,静态数字,这里换成动态计数)。 */
.tbl-wrap table { counter-reset: rn; }
.tbl-wrap table td.rn { counter-increment: rn; }
.tbl-wrap table td.rn .jc-rn-num::before { content: counter(rn); }
.tbl-wrap table td.num { counter-increment: rn; }
.tbl-wrap table td.num::before { content: counter(rn); }
.num 用于两份验收工单 —— 目标件里那两份自己就用它,静态数字,这里换成动态计数)。
两种包装类都要接(2026-08-29 评审 Important 逮到):数据工单 / 执行件的表用平台的
.jc-tbl-wrap,两份验收件照目标件用 .tbl-wrap —— 这五条规则原来只认了 .tbl-wrap,
input-supplement 的三张表(以及已删除的旧 P08/P10 三张)全走 .jc-tbl-wrap,
序号列因而从来没数过,只读态与编辑态下 # 列逐行都是空白。 */
.jc-tbl-wrap table, .tbl-wrap table { counter-reset: rn; }
.jc-tbl-wrap table td.rn, .tbl-wrap table td.rn { counter-increment: rn; }
.jc-tbl-wrap table td.rn .jc-rn-num::before, .tbl-wrap table td.rn .jc-rn-num::before { content: counter(rn); }
.jc-tbl-wrap table td.num, .tbl-wrap table td.num { counter-increment: rn; }
.jc-tbl-wrap table td.num::before, .tbl-wrap table td.num::before { content: counter(rn); }
/* 缺项红带 —— 目标件没有这个概念(它是静态 demo,不会「缺东西」);
这是本仓的必填提示机制,与提交闸同源,配色用目标件自己 :root 里那两个 --danger 变量
(四份目标件的 :root 对这两个变量给的是同一组值,借用不算改目标件的数值)。 */
@@ -81,8 +85,12 @@ html.editable .jc-gapbar.v-0 { display: none; }
* 中文字符全被剥空,v-<token> 永远是空 token(既取不出 v-0 也取不出任何有意义的档位),
* 靠它去挑「空 → 隐藏」这条路本身就走不通(核对过 view-render.ts 的 classToken 实现)。
* 改用更结实的信号:data-slot 配 data-blank 让空值真的渲成空文本节点,CSS 用 :has(:empty)
* 认「这句话是不是空的」—— gate_reason 本身还是照 brief 摆 data-class-of(留作未来若换成
* 枚举型信号时的钩子,且不影响任何断言),但真正管显隐的是下面这条 :empty 规则。
* 认「这句话是不是空的」—— 真正管显隐的是下面这条 :empty 规则。
*
* 不给这个元素再挂那个按值投影的属性了(2026-08-29 评审 Minor 修正,第一版留了它):
* 上面这段已经交代清楚它在这个字段上永远算不出 v-token,留着就是源码里躺着一个
* 永远不起作用的标记 —— 下一个读的人会以为它有用(那也是一种界面/源码层面的「撒谎」)。
* 判断只留一处:data-blank 出的空文本节点 + CSS :empty。
*/
.jc-gate-bar { display: flex; align-items: flex-start; gap: 8px; margin: 0 0 14px; padding: 10px 14px;
border-radius: 9px; background: #fff5f5; border: 1px solid #f3b9b5; color: var(--danger); font-size: 12.5px; line-height: 1.65; }
@@ -234,8 +242,13 @@ const badge = (klass: string, col: string, wrapTwin = false): string => {
*/
const optionSelect = (col: string, address: string, valueCol: string, labelCol: string, required = false): string =>
`<select class="jc-cell" data-col="${col}"${required ? ' required' : ''} data-options="${address}" data-option-value="${valueCol}" data-option-label="${labelCol}"></select>`;
/** 顶部「还差什么」红带 —— 见 BIND_COMMON 里 `.jc-gate-bar` 的显隐说明(它不是 gapbar 那种数值判断) */
const gateBar = (): string => `<div class="jc-gate-bar" data-class-of="gate_reason">
/**
* 顶部「还差什么」红带 —— 见 BIND_COMMON 里 `.jc-gate-bar` 的显隐说明(它不是 gapbar 那种
* 数值判断)。**不挂 `data-class-of="gate_reason"`**:这个字段是中文自由文本,`classToken`
* 在它上面永远算不出 v-token,挂了也是一个永远不起作用的属性;显隐靠 `.jc-gate-text`
* 的 `data-blank` + CSS `:has(:empty)`。
*/
const gateBar = (): string => `<div class="jc-gate-bar">
<span><b>还差什么</b> —— <span class="jc-gate-text" data-slot="gate_reason" data-blank></span></span>
</div>`;
@@ -325,7 +338,18 @@ const TYPE_PARAM_TABLE = `<div class="jc-tbl-wrap"><table>
/**
* 块③ 参数值 —— `parts.parameter`,一字段一行(行由 S1 配方按 `sub_eval.part × parts.type_param`
* 铺,`prune:true` 会把不属于这个组合的孤行清掉,故这张表也**人不加行**)。
* **只两栏可改**(brief 原话):值 / 说明;`param_unit` 特意留只读 —— 配方每拍都从
*
* **2026-08-29 评审 Minor 修正这条注释**:「只两栏可改」这句话本身没错,但没说全 ——
* `internal_pn` / `param_name` 也走 `cell()`(带 `data-col`,会渲出可改的输入孪生件),
* 所以严格数起来编辑态是 4/5 列都有孪生输入框,不是字面的「两栏」。这是**故意的**、
* 不是漏改:**主键格必挂 `data-col`**是本仓的既定约定(它不是编辑面,是行的钥匙 ——
* 漏了 `data-col`,保存时这一行凑不出主键,`resolveRowKey` 认不出它是哪一行,人改的
* 值可能被错配到别的行上,2026-08-02 踩过)。「只两栏可改」说的是**这张表业务上真正
* 要人填的信息**只有值与说明,`internal_pn`/`param_name` 虽然技术上可编辑,但那两格
* 的内容由 S1 配方按启动参数与字典铺定,人正常操作不会去改它们(与 `cell()` 在别处
* 承载主键的用法完全一致,如块①的 `internal_pn`)。
*
* `param_unit` 是唯一**真正**只读、连孪生输入框都不给的一列:配方每拍都从
* `parts.type_param` 字典**直接复制**这一格(`{ col: 'param_unit' }`,不是自查现值的
* `lookup...else` 那种自保写法),手改了也会在下一拍被字典值覆盖回去,给它配可编辑的
* 输入孪生件只会让人误以为改了真的算数。