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:
xcs
2026-08-29 23:19:55 +08:00
co-authored by Claude Opus 5
parent a408542bed
commit d9bcce9516
3 changed files with 243 additions and 44 deletions
@@ -54,7 +54,7 @@ export const manifest: ActivityManifest = {
activityId: ACTIVITY_ID,
name: 'ACT-WORK-05 · 差异评估',
/* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */
packageVersion: '2.0.0',
packageVersion: '2.1.0',
category: '需求分析与方案匹配',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-part-substitution-assessment',
@@ -219,7 +219,29 @@ export const manifest: ActivityManifest = {
kind: 'master',
label: '芯片类型字典',
fields: [
col({ name: 'type_id', type: 'integer', label: '类型编号', primaryKey: true, auto: true }),
/*
* **`type_id` 声明成 `text` 而不是 `integer`**(Task 7b,2026-08-29,件① 补「新建类型」
* 逼出来的两处平台级发现,记在这里免得下一个人重踩):
* ①`resolveRowKey`(`apps/api/src/app/routes/desk.ts`)的「③ 续号」只认 `isSeq`
* (declared type 是 integer/number),不看 `auto` —— 这个类型碰巧是**唯一**一个
* `auto:true` 又是 `integer` 的主键,于是它抢在 `writeEditedRows` 的 `autoKeys()`
* (真正的续号器,数official+草稿叠加、不会撞号)前面,自己按「本单草稿有几行」
* 铸了个从 1 起的号——种子已经占了 `type_id=1/2`,人第一次新建类型就会撞车,
* 把种子的「DC-DC 降压稳压器」悄悄改名。②即便①不发生,`autoKeys()` 的续号是
* 两位补零字符串(`'03'`),而**引用它的那几列**(`parts.profile.chip_type_id` 等)
* 若声明成 `integer`,写入时会被 `coerceToColumn` 按 `Number()` 解析掉前导零——
* `'03'` 存成 `parts.chip_type.type_id`、`3` 存成 `parts.profile.chip_type_id`,
* `refersTo`/`data-by` 那几处按 `String(...)` 比对的查找会全部失配(`'03' !== '3'`),
* 写入当场被 `refersTo` 拒、或下拉/查表显示不出人刚建的类型。
* **两处病根都是「integer 类型的自增列」这个此前从未出现过的组合触发的**——本仓
* 唯一先例 `contact_code` 一直是 `text`。改成 `text`(此列与下面 5 处同引用列一起改,
* 保持同一条身份链路全程一种类型)对存储没有影响(台账是 JSONB,没有物理列类型),
* 只是把逻辑类型改得诚实;S-FEEL 那几处只做等值/去重比较,不做数值运算,不受影响
* (已逐处核对:distinct 计数 / lookup·take / refersTo 皆按字符串比,`>` 只用在
* `distinct_chip_type_id` 这个计数列上,不用在值本身)。查证过程详见
* Task 7b 报告(`.superpowers/sdd/2026-08-29-act-work-05-rewrite/task-7b-report.md`)。
*/
col({ name: 'type_id', type: 'text', label: '类型编号', primaryKey: true, auto: true }),
col({ name: 'type_name', type: 'text', label: '类型名称', required: true }),
col({ name: 'description', type: 'text', label: '类型说明', expect: true }),
],
@@ -230,7 +252,8 @@ export const manifest: ActivityManifest = {
kind: 'master',
label: '芯片类型核心参数字段',
fields: [
col({ name: 'type_id', type: 'integer', label: '所属类型', primaryKey: true, refersTo: 'parts.chip_type.type_id' }),
/* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text——见上面那条大注释 */
col({ name: 'type_id', type: 'text', label: '所属类型', primaryKey: true, refersTo: 'parts.chip_type.type_id' }),
col({ name: 'param_name', type: 'text', label: '参数名', primaryKey: true }),
/* AI 分析时读它 —— 这一格说清「这个参数是什么意思」,是 S2 唯一一次 analyze 出场的入参之一 */
col({ name: 'param_desc', type: 'text', label: '参数说明', required: true }),
@@ -254,7 +277,8 @@ export const manifest: ActivityManifest = {
label: '芯片主数据(P-08)',
fields: [
col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }),
col({ name: 'chip_type_id', type: 'integer', label: '芯片类型', required: true, refersTo: 'parts.chip_type.type_id' }),
/* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text —— 见该字段的大注释 */
col({ name: 'chip_type_id', type: 'text', label: '芯片类型', required: true, refersTo: 'parts.chip_type.type_id' }),
col({ name: 'brand_name', type: 'text', label: '品牌名称', required: true }),
col({ name: 'lifecycle_status', type: 'text', label: '生命周期状态', required: true, tally: ['active', 'nrnd', 'eol', 'discontinued', 'risk'] }),
col({ name: 'specification_description', type: 'text', label: '规格描述', expect: true }),
@@ -314,7 +338,8 @@ export const manifest: ActivityManifest = {
col({ name: 'exists_status', type: 'text', label: '在库状态', required: true, tally: ['found', 'absent'] }),
col({
name: 'chip_type_id',
type: 'integer',
/* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text —— 见该字段的大注释 */
type: 'text',
label: '芯片类型',
required: true,
/*
@@ -334,7 +359,8 @@ export const manifest: ActivityManifest = {
label: '本单案卷',
fields: [
col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }),
col({ name: 'chip_type_id', type: 'integer', label: '本单锁定的芯片类型' }),
/* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text —— 见该字段的大注释 */
col({ name: 'chip_type_id', type: 'text', label: '本单锁定的芯片类型' }),
col({ name: 'gate_reason', type: 'text', label: '入参未就绪原因(件上摆的就是它)' }),
col({ name: 'summary', type: 'text', label: '结论摘要' }),
],
@@ -354,7 +380,8 @@ export const manifest: ActivityManifest = {
col({ name: 'eval_ref', type: 'text', label: '评估案号', required: true }),
col({ name: 'original_internal_pn', type: 'text', label: '原内部料号', required: true, refersTo: 'parts.profile.internal_pn' }),
col({ name: 'candidate_internal_pn', type: 'text', label: '候选内部料号', required: true, refersTo: 'parts.profile.internal_pn' }),
col({ name: 'chip_type_id', type: 'integer', label: '芯片类型', required: true }),
/* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text —— 见该字段的大注释 */
col({ name: 'chip_type_id', type: 'text', label: '芯片类型', required: true }),
col({ name: 'recommendation_level', type: 'text', label: '推荐等级', required: true, tally: ['recommended', 'conditional', 'not_recommended'] }),
col({ name: 'recommendation_reason', type: 'text', label: '推荐理由', required: true }),
col({ name: 'difference_summary', type: 'text', label: '差异摘要', required: true }),