Archived
feat(w05): Task 7b —— 件① 补「新建芯片类型」,零平台改动
类型下拉里没有合适的,从前只有一句 guide 文字,没有可落笔的地方(parts.chip_type 全无
编辑面;parts.type_param 有个「加一行」但零行时 ViewContainer.addRow 找不到 tr:last-of-type
可克隆,按钮到不了岸)。
查证 Step 1 的坎(autoKeys 跨行自增键联动通不通)后确认:同一次保存请求内没有机制把父行
刚铸出来的自增键传给同批的子行,这条联动本身不需要通 —— 改成两步流程绕开(新建类型先保存,
type_id 铸出来后立刻出现在 data-options 现读的下拉里;定义核心参数字段时按类型名称选,
不必照抄编号)。
深挖过程中揪出两个从未被触发过的平台坑,根因都是 parts.chip_type.type_id 是本仓唯一
「auto:true 又是 integer」的主键:① resolveRowKey 的「③ 续号」步骤只认 isSeq(declared
type)不认 auto,会抢在 writeEditedRows 的 autoKeys() 前面把新行按草稿行数续号,撞上种子
数据 type_id=1/2(把「DC-DC 降压稳压器」悄悄改名);② autoKeys() 补零的字符串('03')经
引用列的 coerceToColumn(_,'integer') 会被 Number() 削掉前导零,refersTo/data-by 的字符串
比对全部失配。两处坑统一改声明(type_id 自身 + 五处引用列共 6 处,integer→text)一次性
绕开,desk.ts/ViewContainer.tsx/view-render.ts 零改动。
实现:parts.chip_type 新增可编辑表(动态行 + 一条不受 data-rows 管辖的静态种子行,
data-edit 两处写法保证 collect()/.on 切换与 renderRows 的 editable 检测都够得着);
parts.type_param 的 type_id 列从裸文本输入换成下拉(选项现读 parts.chip_type);两张表
各配一条静态种子行解决零行加不出第一行的问题。9 条新测试 + 3 条既有断言更新,变异验证
逐条附证据(拿掉种子行 / 拿掉 text 声明 / 拿掉下拉分别验证目标测试变红,过程中发现并修正
一条子串包含式断言测不出「只剩一份」的退化)。packageVersion 2.0.0→2.1.0。
pnpm gates 退出码 0(2410 passed | 34 skipped);E04/I01 包内测试同批全绿(92/92、55/55)—
本任务未改共享面,理论上不需要专门回归,仍按纪律核过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -132,8 +132,16 @@ describe('台账声明', () => {
|
||||
].sort());
|
||||
});
|
||||
|
||||
/*
|
||||
* Task 7b(2026-08-29):`type_id` 声明是 `text` 不是 `integer` —— 台账是 JSONB,不存在
|
||||
* 物理列类型这回事,这条纯粹是逻辑类型;改 text 是因为它是本仓唯一「auto:true 又是
|
||||
* 整数型」的主键,integer 会踩两个坑(`resolveRowKey` 的续号步骤抢在 `autoKeys()` 前面
|
||||
* 把它按草稿行数续成 1、撞种子的 `type_id=1`;`autoKeys()` 补零的字符串 `'03'` 经引用列的
|
||||
* `coerceToColumn(_, 'integer')` 会被 `Number()` 削掉前导零,`refersTo`/查表按字符串比对
|
||||
* 全部失配)。查证过程见 Task 7b 报告。
|
||||
*/
|
||||
it('类型字典:type_id 是自增主键 —— 人在件上新建类型时留空,由基座续号', () => {
|
||||
expect(colOf('parts.chip_type', 'type_id')).toMatchObject({ primaryKey: true, type: 'integer', auto: true });
|
||||
expect(colOf('parts.chip_type', 'type_id')).toMatchObject({ primaryKey: true, type: 'text', auto: true });
|
||||
expect(colOf('parts.chip_type', 'type_name')?.required).toBe(true);
|
||||
});
|
||||
|
||||
@@ -145,7 +153,7 @@ describe('台账声明', () => {
|
||||
});
|
||||
|
||||
it('P-08:类型必填并引用字典;createOnly 撤掉 —— 在库老料要能补上类型这一格', () => {
|
||||
expect(colOf('parts.profile', 'chip_type_id')).toMatchObject({ required: true, type: 'integer', refersTo: 'parts.chip_type.type_id' });
|
||||
expect(colOf('parts.profile', 'chip_type_id')).toMatchObject({ required: true, type: 'text', refersTo: 'parts.chip_type.type_id' });
|
||||
expect(ledgerOf('parts.profile')?.createOnly).toBeUndefined();
|
||||
/* 封装与工艺本来就是「核心参数」—— 它们该以类型字段的身份出现在字典里,不在主记录上另开一列 */
|
||||
for (const gone of ['package', 'process_technology', 'eol_pcn_status', 'application_scenarios']) {
|
||||
@@ -376,9 +384,9 @@ describe('报告模版:编辑面与只读面各就各位', () => {
|
||||
expect(viewHtml('landing-confirm')).not.toContain('data-edit-add=');
|
||||
});
|
||||
|
||||
it('每个可编辑格子的 data-col 都在对应台账对象的声明里(input-supplement 一屏三张表,三张台账对象的声明并起来看;difference-review 一张表)', () => {
|
||||
it('每个可编辑格子的 data-col 都在对应台账对象的声明里(input-supplement 一屏四张表,四张台账对象的声明并起来看;difference-review 一张表)', () => {
|
||||
const cases: Array<[string, Array<[string, string]>]> = [
|
||||
['input-supplement', [['parts', 'profile'], ['parts', 'type_param'], ['parts', 'parameter']]],
|
||||
['input-supplement', [['parts', 'profile'], ['parts', 'chip_type'], ['parts', 'type_param'], ['parts', 'parameter']]],
|
||||
['difference-review', [['substitution', 'difference_item']]],
|
||||
];
|
||||
for (const [view, objects] of cases) {
|
||||
@@ -425,9 +433,10 @@ describe('报告模版:编辑面与只读面各就各位', () => {
|
||||
});
|
||||
|
||||
/*
|
||||
* 件① 评估输入补全(2026-08-29,Task 7):S1 的配方缺什么就发一张数据工单,`view: 'input-supplement'`
|
||||
* 指向它。三块竖排——要评估什么(两颗料的存在性 + 主数据)/ 芯片类型(选型 + 新建类型时的核心参数
|
||||
* 字段清单)/ 参数值(逐条实测/规格值);顶部一条红带摆配方算好的 `gate_reason`。
|
||||
* 件① 评估输入补全(2026-08-29,Task 7;Task 7b 补②a 块):S1 的配方缺什么就发一张数据工单,
|
||||
* `view: 'input-supplement'` 指向它。四块竖排——要评估什么(两颗料的存在性 + 主数据)/
|
||||
* 芯片类型(选型 + 新建类型)/ 核心参数字段清单(新建类型时要填)/ 参数值(逐条实测/规格值);
|
||||
* 顶部一条红带摆配方算好的 `gate_reason`。
|
||||
* 复用文件顶部已有的 `viewHtml` 短写(与 brief 草稿里的 `viewOf` 同一件事,不重复定义一个)。
|
||||
*/
|
||||
describe('件① 评估输入补全', () => {
|
||||
@@ -439,18 +448,63 @@ describe('件① 评估输入补全', () => {
|
||||
expect(steps.find((s) => s.item === 'S1_inputs')?.view).toBe('input-supplement');
|
||||
});
|
||||
|
||||
it('三块都在,行都绑本单草稿(不是整表)', () => {
|
||||
it('四块都在,行都绑本单草稿(不是整表)', () => {
|
||||
expect(html).toContain('data-rows="mine:sub_eval.part"');
|
||||
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"');
|
||||
});
|
||||
|
||||
it('芯片类型是从台账现读的下拉 —— 写死等于「只能选出包那天就有的那几种」', () => {
|
||||
it('芯片类型是从台账现读的下拉 —— 写死等于「只能选出包那天就有的那几种」(块①的 chip_type_id 与块②b 的 type_id 都是)', () => {
|
||||
expect(html).toContain('data-options="parts.chip_type"');
|
||||
expect(html).toContain('data-option-value="type_id"');
|
||||
expect(html).toContain('data-option-label="type_name"');
|
||||
/* data-options 与 data-slot 不能共存(渲染时 data-slot 会被剥掉)—— 当下值由 data-col 定 */
|
||||
expect(/data-options="[^"]*"[^>]*data-slot=/.test(html)).toBe(false);
|
||||
/* 块①(sub_eval.part 行模版,只有一份行模版,被 data-rows 迭代)绑 chip_type_id 下拉 */
|
||||
expect(html).toContain('data-col="chip_type_id" required data-options="parts.chip_type"');
|
||||
/*
|
||||
* 块②b(type_param)绑 type_id 下拉的地方**该出现两次**——已有行的行模版(data-rows
|
||||
* 迭代)一份、种子行(字面量静态行,不受行数所限)一份;只数子串存在会被「只剩其中一份」
|
||||
* 这种退化蒙混过去(已用变异验证逮到:只把动态行模版那份换回裸文本输入,这条子串断言
|
||||
* 仍然为真,因为种子行那份还在——按出现次数数才钉得住两处都在)。
|
||||
*/
|
||||
const typeIdSelectCount = (html.match(/data-col="type_id" required data-options="parts\.chip_type"/g) ?? []).length;
|
||||
expect(typeIdSelectCount).toBe(2);
|
||||
});
|
||||
|
||||
/*
|
||||
* Task 7b:件① 新建类型(`parts.chip_type`)与新建字段(`parts.type_param`)两张表开局都是
|
||||
* 零行,`ViewContainer.addRow` 靠克隆 `tr:last-of-type` 添行,零行时找不到样子可克隆
|
||||
* (查证见 Task 7b 报告)。补一条**不受 `data-rows` 管辖的静态种子行**——两张表各一条,
|
||||
* 字面量手写 `data-col` 输入格,不经 `cell()`/`data-slot`,行数再少也在。
|
||||
*/
|
||||
it('**零行也加得出第一行**:两张表各带一条静态种子行(class="jc-row-static"),不靠 data-rows 生成', () => {
|
||||
const staticRows = html.match(/<tr class="jc-row-static">/g) ?? [];
|
||||
expect(staticRows).toHaveLength(2);
|
||||
/* 种子行里的输入格是手写的,不经 twinInputs —— 直接断言 data-col 存在即可 */
|
||||
expect(html).toContain('<input class="jc-cell" data-col="type_name" placeholder="新类型名称">');
|
||||
expect(html).toContain('<input class="jc-cell" data-col="description"');
|
||||
expect(html).toContain('<input class="jc-cell" data-col="param_name" placeholder="参数名">');
|
||||
expect(html).toContain('<input class="jc-cell" data-col="param_desc"');
|
||||
expect(html).toContain('<select class="jc-cell" data-col="constraint_type">');
|
||||
expect(html).toContain('<input class="jc-cell" data-col="param_unit"');
|
||||
/* 种子行只在编辑态露面 —— 读的时候不该看见一行空壳子(界面不撒谎) */
|
||||
expect(html).toContain('table:not(.on) tr.jc-row-static { display: none; }');
|
||||
});
|
||||
|
||||
/*
|
||||
* `auto:true` 的主键(`type_id`)**不该出现在种子行里**——自增列不问人(与联系人编码
|
||||
* `contact_code` 同一条纪律)。种子行只有 `type_name`/`description` 两个手写输入格。
|
||||
*/
|
||||
it('**新建类型的种子行不问 type_id**(自增列不问人):已有行的 type_id 只读、没有孪生输入框', () => {
|
||||
expect(html).toContain('系统生成');
|
||||
// 「系统生成」只出现在 chip_type 那条种子行里 —— 精确抓出那一行,断言里面没有 data-col="type_id"
|
||||
const chipTypeStaticRow = /<tr class="jc-row-static">(?:(?!<\/tr>)[\s\S])*?系统生成(?:(?!<\/tr>)[\s\S])*?<\/tr>/.exec(html)?.[0] ?? '';
|
||||
expect(chipTypeStaticRow).not.toBe('');
|
||||
expect(chipTypeStaticRow).not.toContain('data-col="type_id"');
|
||||
expect(chipTypeStaticRow).toContain('data-col="type_name"');
|
||||
expect(chipTypeStaticRow).toContain('data-col="description"');
|
||||
});
|
||||
|
||||
it('**主键列必挂 `data-col`** —— 它不是编辑面,是行的钥匙(漏了会把人改的行错配到别行上)', () => {
|
||||
@@ -458,12 +512,26 @@ describe('件① 评估输入补全', () => {
|
||||
expect(html).toContain('data-col="param_name"');
|
||||
});
|
||||
|
||||
it('可改的三张表各有一道编辑闸', () => {
|
||||
for (const address of ['parts.profile', 'parts.type_param', 'parts.parameter']) {
|
||||
it('可改的四张表各有一道编辑闸', () => {
|
||||
for (const address of ['parts.profile', 'parts.chip_type', 'parts.type_param', 'parts.parameter']) {
|
||||
expect(html).toContain(`data-edit="${address}"`);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Task 7b:`data-edit="parts.chip_type"`(与 `parts.type_param` 同款)两处都写 ——
|
||||
* 外层 `<table>` 一处(给 `ViewContainer.collect()`/`.on` 切换看,它按
|
||||
* `document.querySelector` 找唯一宿主,要同时收到已有行与种子行两处的输入)、
|
||||
* 内层 `data-rows` 那个 `<tbody>` 一处(给 `renderRows` 的 `editable` 检测看,
|
||||
* 它只认与 `data-rows` 同一个标签自带的属性,不看祖先)。
|
||||
*/
|
||||
it('新建类型 / 新建字段两张表的 data-edit 各出现两次(外层 table + 内层 data-rows tbody)', () => {
|
||||
for (const address of ['parts.chip_type', 'parts.type_param']) {
|
||||
const count = (html.match(new RegExp(`data-edit="${address}"`, 'g')) ?? []).length;
|
||||
expect(count, address).toBe(2);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* 2026-08-29 评审 Minor:brief 原话是「红带显隐用 data-class-of」,但 gate_reason 是
|
||||
* 中文自由文本 —— classToken 只留 [a-z0-9-],中文字符全被剥空,不论空串还是有内容的
|
||||
@@ -489,14 +557,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)—— 补丁真的接在会用到的类上
|
||||
// 这张件的四张表都走 .jc-tbl-wrap(没有 .tbl-wrap)—— 补丁真的接在会用到的类上(Task 7b 新增 parts.chip_type 这一张,3→4)
|
||||
expect(html).not.toContain('class="tbl-wrap"');
|
||||
const jcTblWrapCount = html.match(/class="jc-tbl-wrap"/g)?.length ?? 0;
|
||||
expect(jcTblWrapCount).toBe(3);
|
||||
expect(jcTblWrapCount).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -736,30 +804,40 @@ describe.skip('徽标别名与值域相关的坑(2026-08-27 实测账,回归网)
|
||||
* 与 `sub_eval.input_gap` 两张随重写整个退休,不再有对应条目。这两条 Critical 回归网继续钉在
|
||||
* 新件上:主键(internal_pn / type_id+param_name)都挂了 `data-col`,必填列都有可填的控件。
|
||||
*/
|
||||
describe('可编辑件的主键与必填列(2026-08-27 复审:主键丢钥匙 / 必填填不了,两条 Critical 回归)', () => {
|
||||
/** input-supplement 一屏三张真能编辑的表与它们各自绑定的台账对象 */
|
||||
describe('可编辑件的主键与必填列(2026-08-27 复审:主键丢钥匙 / 必填填不了,两条 Critical 回归;Task 7b 补 parts.chip_type)', () => {
|
||||
/** input-supplement 一屏四张真能编辑的表与它们各自绑定的台账对象(Task 7b 加入 parts.chip_type) */
|
||||
const EDITABLE: ReadonlyArray<{ view: string; domain: string; object: string }> = [
|
||||
{ view: 'input-supplement', domain: 'parts', object: 'profile' },
|
||||
{ view: 'input-supplement', domain: 'parts', object: 'chip_type' },
|
||||
{ view: 'input-supplement', domain: 'parts', object: 'type_param' },
|
||||
{ view: 'input-supplement', domain: 'parts', object: 'parameter' },
|
||||
];
|
||||
|
||||
it('每一个主键列都有 data-col —— 它不是编辑面,是行的钥匙,丢了就整行保存不下去', () => {
|
||||
it('每一个非豁免的主键列都有 data-col —— 它不是编辑面,是行的钥匙,丢了就整行保存不下去', () => {
|
||||
for (const { view, domain, object } of EDITABLE) {
|
||||
const table = byAddress(domain, object);
|
||||
/*
|
||||
* `caseKey` 声明的那一列(如 `eval_ref`)是例外:它是「一单一号,全表当然一致」的
|
||||
* 案号型主键(与 I01 的 `round_ref` 同款),件上压根不摆,服务端 `resolveRowKey`
|
||||
* 按「全表一致」自动补上(`apps/api/src/app/routes/desk.ts` 注释里点名的那条路)——
|
||||
* 摆出来给人填反而是错的(那会让人以为能按行改案号)。真正逐行不同、必须挂
|
||||
* `data-col` 的,是主键里除 caseKey 之外的那几列(如 `item_key`)。
|
||||
* 两类主键列豁免,不必挂 `data-col`:
|
||||
* ① `caseKey` 声明的那一列(如 `eval_ref`)—— 「一单一号,全表当然一致」的案号型
|
||||
* 主键(与 I01 的 `round_ref` 同款),件上压根不摆,服务端 `resolveRowKey` 按
|
||||
* 「全表一致」自动补上(`apps/api/src/app/routes/desk.ts` 注释里点名的那条路)——
|
||||
* 摆出来给人填反而是错的(那会让人以为能按行改案号)。
|
||||
* ② `auto:true` 的那一列(如 `parts.chip_type.type_id`,Task 7b 补)—— 自增列不问人
|
||||
* (与联系人编码 `contact_code` 同一条纪律),值由 `writeEditedRows` 的
|
||||
* `autoKeys()` 在写入那一刻续号,件上给它挂 `data-col` 反而是在问人一个他答不上
|
||||
* 来的号(该表种子行只带 `type_name`/`description` 两格,不带 `type_id`)。
|
||||
* 真正逐行不同、必须挂 `data-col` 的,是主键里除这两类之外的那几列。
|
||||
*/
|
||||
const pk = (table?.fields.filter((f) => f.primaryKey).map((f) => f.name) ?? []).filter((name) => name !== table?.caseKey);
|
||||
expect(pk.length, `${domain}.${object} 没有非 caseKey 的主键列`).toBeGreaterThan(0);
|
||||
const pk = (table?.fields.filter((f) => f.primaryKey).map((f) => f.name) ?? []).filter(
|
||||
(name) => name !== table?.caseKey && table?.fields.find((f) => f.name === name)?.auto !== true,
|
||||
);
|
||||
const html = viewHtml(view);
|
||||
for (const col of pk) {
|
||||
expect(html, `${view}: 主键列 ${domain}.${object}.${col} 缺 data-col`).toContain(`data-col="${col}"`);
|
||||
}
|
||||
// 至少一类主键真存在(caseKey/auto 全豁免的表,如 chip_type,pk 允许为空数组——上面循环体自然跳过)
|
||||
const allPk = table?.fields.filter((f) => f.primaryKey).map((f) => f.name) ?? [];
|
||||
expect(allPk.length, `${domain}.${object} 没有任何主键列`).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user