Archived
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:
@@ -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` 那种自保写法),手改了也会在下一拍被字典值覆盖回去,给它配可编辑的
|
||||
* 输入孪生件只会让人误以为改了真的算数。
|
||||
|
||||
@@ -308,6 +308,43 @@ describe('闸看哪几张表:卡上绑的优先,没绑回落到必交付物声
|
||||
expect(emptyGate({}, [{ domain: 'a', object: 'b' }], [])).toBeUndefined();
|
||||
});
|
||||
|
||||
/*
|
||||
* **W05 `input-supplement` 回归网**(2026-08-29 评审 Critical:确认钮永久按不下去)。
|
||||
*
|
||||
* 那张数据工单卡的 `gateLedgers` 一度带着 `parts.chip_type`,而件上没有任何
|
||||
* `data-edit="parts.chip_type"`(选类型走的是 `data-options` 下拉现读,不写这张表)——
|
||||
* `mine:parts.chip_type` 因而永远是空数组,`emptyGate` 永远判「这一单还一行都没有」,
|
||||
* 人把另外三张表填得再全,提交钮也按不下去。这条钉死两件事:
|
||||
* ①**复现旧配置确实会永久卡死**(不是猜的);②**修完(去掉 chip_type)之后闸真的开得了**——
|
||||
* 同一份 `mine`(chip_type 全程零行,profile/type_param/parameter 都已填好)在新
|
||||
* `gateLedgers` 下必须放行。ledger 的 domain/object/label 抄自
|
||||
* `activities/part-substitution-assessment/src/manifest.ts` 的声明原文。
|
||||
*/
|
||||
it('W05 input-supplement:gateLedgers 若带 parts.chip_type 会永久卡死(它没有编辑手段可写)——去掉之后,三张真被编辑的表填齐即放行', () => {
|
||||
const watched = [
|
||||
{ domain: 'parts', object: 'chip_type', label: '芯片类型字典' },
|
||||
{ domain: 'parts', object: 'type_param', label: '芯片类型核心参数字段' },
|
||||
{ domain: 'parts', object: 'profile', label: '芯片主数据(P-08)' },
|
||||
{ domain: 'parts', object: 'parameter', label: '标准化参数集(P-08 子表)' },
|
||||
];
|
||||
// parts.chip_type 从未被这张件写过 —— 无论闸怎么配,它在 mine 里恒是空数组
|
||||
const filledExceptChipType = {
|
||||
'mine:parts.type_param': [{ type_id: '1', param_name: '输入电压范围' }],
|
||||
'mine:parts.profile': [{ internal_pn: 'LM2596S-5.0' }, { internal_pn: 'XL2596S-5.0' }],
|
||||
'mine:parts.parameter': [{ internal_pn: 'LM2596S-5.0', param_name: '输入电压范围' }],
|
||||
};
|
||||
// 旧配置(已删除,这里复现只为证明它确实会卡死):chip_type 也在闸的名单里
|
||||
const buggyDeclared = ['parts.chip_type', 'parts.type_param', 'parts.profile', 'parts.parameter'];
|
||||
const stuck = emptyGate(filledExceptChipType, watched, buggyDeclared);
|
||||
expect(stuck?.ready).toBe(false);
|
||||
expect(stuck?.why).toContain('芯片类型字典');
|
||||
expect(stuck?.why).toContain('这张卡就是来建它的');
|
||||
|
||||
// 修完(case-model.ts 现在的真实值):chip_type 不列入闸的名单
|
||||
const fixedDeclared = ['parts.type_param', 'parts.profile', 'parts.parameter'];
|
||||
expect(emptyGate(filledExceptChipType, watched, fixedDeclared)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('优先级:绑的表 > 卡自报 > 交付物;空串与空白不算数', () => {
|
||||
expect(gateAddressesOf([table('quote_intake.record')], [{ ledgers: ['a.b'] }], ['parts.profile'])).toEqual(['quote_intake.record']);
|
||||
expect(gateAddressesOf(undefined, [{ ledgers: ['a.b'] }], ['', ' '])).toEqual(['a.b']);
|
||||
|
||||
Reference in New Issue
Block a user