fix(w05): Task 7b 评审 Critical —— data-edit 挂外层 table 后「加一行」克隆到表头,不是数据行

外层 data-edit 是为了让 collect() 同时收到静态种子行与动态行,副作用是 ViewContainer.bodyOf()
的单个 querySelector 从此解析到 <table> 本身,addRow() 接着在这个 host 里找 tr:last-of-type ——
CSS :last-of-type 按各自父元素判定,<thead> 里孤零零的那个 <tr> 在它自己的父元素里必然「最后」,
若 <thead> 排在数据行前面就会被文档序优先命中:克隆出来的是四个 <th> 表头文字、零个
input[data-col],还被 appendChild 到 <table> 底下、不在任何 <tbody> 里。parts.type_param 那张
更要紧——它的加行功能在这次改动之前本来是好的(data-edit 只挂 tbody 时,querySelectorAll 的
搜索范围本就不含 thead),外层 data-edit 一挂就成了真回归。

修法(不改 ViewContainer.tsx/view-render.ts):把两张表的 <thead> 挪到两个 <tbody> 之后。CSS 的
table-header-group 排版规则视觉上永远排最前、不看它在 DOM 里排第几(与 HTML5 允许 tfoot 写在
thead 之后、tbody 之前同一条原理)——呈现不变,而 querySelector/:last-of-type 认的是文档序,
thead 排最后,选择器就再也碰不到表头行。

补 6 条真 DOM 测试(apps/api/.../view-render-addrow-thead.test.ts):真实 renderView() 输出
在 jsdom 里挂起来,逐行复刻 bodyOf()/addRow() 的真实选择器链(它们没 export,双向 depcruise
规则也拦着直接 import),断言克隆样本落在 tbody 里、含 N 个具名 input[data-col],不是表头。
apps/api 只受 tsconfig.node.json 管(零 DOM lib),packages/ui-kit 又不许反向 import apps/*
(packages-are-leaves)——用 TS 标准的 per-file `/// <reference lib="dom" />` 只给这一份文件
挂 DOM 类型,不改 tsconfig 本身。activity.test.ts 另补一条字符串级结构安全网(thead 确实排在
两个 tbody 之后),把源码与镜像测试的假设锁在一起。变异验证两段输出(拿掉修复→3/6 真红,
含 report.ts 真实源码单独验证一次红/绿→改回来全绿)贴在报告里。

Important 两条:①两个平台坑(resolveRowKey 续号步骤不认 auto、coerceToColumn 削前导零)
记入 docs/debt.md,写清仍未修、只是被本活动的声明层绕开;②manifest.ts/activity.test.ts 里
「台账是 JSONB 不存在物理列类型」这句错的论据删掉——master/record 是真实物理列(bigint),
只有草稿层是 JSONB,安全的真正原因是 schema-merge.ts 的单调加宽 lattice 把 integer→text
判定成合法加宽,widen-column 生成的 ALTER TYPE text USING col::text 沿 PG 转型没有失败路径。

pnpm gates 退出码 0(2417 passed | 34 skipped);E04/I01 包内测试 92/92、55/55 全绿。
packageVersion 2.1.0 → 2.1.1。

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-30 00:23:47 +08:00
co-authored by Claude Opus 5
parent d9bcce9516
commit 57870329e6
5 changed files with 384 additions and 27 deletions
+41
View File
@@ -1458,3 +1458,44 @@ nudge:[{after:'PT2H',action:'nudge'}], retry:{times:2,backoff:'PT30S'}`)慢慢
与 `checkWhen` 现有的「S-FEEL 对未绑定的名字求值为假,这一笔将永远被跳过」同一条纪律,只是
`{in:X}` 的失败面目更隐蔽(空串会被拼接、被当合法值继续往下算,不像 S-FEEL 谓词那样直接短路)。
`auto` 是 ADR-014 新落地的机制,这个洞会在下一个用到跨步共享投影的活动上重复咬人。
## `auto:true` 又是整数型的主键,平台两处独立机制都会踩(2026-08-30,Task 7b 评审 Critical 逼出来的发现)
`resolveRowKey`(`apps/api/src/app/routes/desk.ts:111-181`)第③段「续号」只判断
declared type 是不是 `integer`/`number`(`isSeq`),**不看这一列是否 `auto:true`** ——
它是为 `req_seq` 这类「非自增、按行数续的序号型主键」(I01 的 `req_seq`)设计的,却对**任何**
整数型主键一视同仁。若某张表的主键**恰好**既是 `auto:true` 又是 `type:'integer'`,这一段会
**抢在** `writeEditedRows` 的 `mintable`/`autoKeys()`(真正为 `auto:true` 设计的续号器,数
`taskView` 正式版+草稿叠加、按声明的前缀+两位补零规则铸号)**前面**动手,拿「这一单自己的
草稿行数+1」铸一个号——这个数不数**正式表**里已有的行,新建的第一行几乎必然撞上任何已存在的
`type_id=1`(种子数据、或早前任务放行落地的行),**静默覆盖**那一行的其余字段。
`autoKeys()` 补零续出的号(如 `'03'`)还有第二个坑:**引用这一列的其它表**(外键列)若也声明
`type:'integer'`,写入时会被 `coerceToColumn`(`apps/api/src/app/wiring/engines.ts:590-598`)
按 `Number()` 解析,**削掉前导零**——`'03'` 存成整数 `3`,而被引用的原表仍是字符串 `'03'`,
`refersTo` 校验(`engines.ts:1119-1143`)与 `data-by` 嵌套查表(`view-render.ts` 的
`byParent`)全按 `String(...)` 比对,两边永远对不上:引用列的写入会被 `refersTo` 拒(「在字典里
不存在」),或下拉/查表显示不出刚建的那一行。
**本仓迄今唯一一个 `auto:true` 又是 `integer` 的主键是 `part-substitution-assessment`(W05)的
`parts.chip_type.type_id`**(`auto:true` 的既有先例 `contact_code`——E04/quote-intake 两处——
从来都是 `type:'text'`,没人先撞上这个组合)。Task 7b 补「件① 新建芯片类型」时踩上了(种子数据
`type_id: 1/2` 会被第一次新建的类型静默覆盖;引用列若也是 `integer` 会导致新建的类型写不进
`parts.profile.chip_type_id` 等)。**这次只是把 `type_id` 自身与五处引用列(`type_param.type_id`
/ `profile.chip_type_id` / `sub_eval.part.chip_type_id` / `sub_eval.case.chip_type_id` /
`substitution.relation.chip_type_id`)在活动自己的声明层统一改成 `text` 绕开——`resolveRowKey`
与 `coerceToColumn` 这两处平台代码本身一行没改,坑原样留着**。
**要治**(两处,各自独立就够,不必都做):
- `resolveRowKey` 第③段续号加一条 `if (input.autos?.includes(k)) continue;`(与第②段
`barred` 检查同一条精神:`auto:true` 的列有自己专用的续号器,不该被这段通用的
「整数型主键续号」抢跑)。
- 或者更保守地,给 `writeEditedRows`/`static-audit` 加一条装包期检查:某张表的主键**同时**
`primaryKey:true`、`auto:true`、`type:'integer'|'number'` 时报一条 warning,提醒作者
「这个组合会撞上 `resolveRowKey` 的续号步骤,改成 `text` 或联系平台修 `resolveRowKey`」——
比事后重新发现这条坑更便宜。
**排查法留一条**:某张 `auto:true` 主键表的**种子/早前数据在没人手动改过的情况下自己变了
名字/内容**,且改动发生在**该表第一次被本单新建一行**前后,先怀疑这条坑,查该主键是否
`type:'integer'` 且没有其它非 auto 主键列(单主键、无前缀的场景下,`resolveRowKey` 续出的
号从 `1` 起,最容易撞上种子数据里最早的几行)。