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
@@ -54,7 +54,7 @@ export const manifest: ActivityManifest = {
activityId: ACTIVITY_ID,
name: 'ACT-WORK-05 · 差异评估',
/* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */
packageVersion: '2.1.0',
packageVersion: '2.1.1',
category: '需求分析与方案匹配',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-part-substitution-assessment',
@@ -234,12 +234,27 @@ export const manifest: ActivityManifest = {
* `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`)。
* 唯一先例 `contact_code` 一直是 `text`。**`resolveRowKey`/`coerceToColumn` 这两处
* 平台代码本身仍带着这两个坑没修**(下一个建同样组合主键的人会重新踩上,已记
* `docs/debt.md`)——这里只是在活动自己的声明层绕开,不是修复。
*
* 改成 `text` 为什么是安全的(此列与下面 5 处同引用列一起改,保持同一条身份链路
* 全程一种类型):**`ledger.__draft`(任务级草稿层)才是 JSONB**
* (`ledger-store.ts` 的 `DRAFT_TABLE`/`DRAFT_DDL`:`key`/`patch`/`baseline` 三列都是
* `jsonb`)——但 `master`/`record` 这类**正式表是真实的物理列**(同文件 `PG_TYPE`
* 映射,`integer → bigint`,`create table`/`add column`/`alter column ... type ...`
* 都拼的是真 PG 类型)。改声明安全,靠的是 `schema-merge.ts` 的**单调加宽 lattice**
* (`SCALAR_CHAIN.integer = ['integer', 'number', 'text']`,`widen()`
* 判定 `integer → text` 是**合法加宽**、不是类型冲突)配合 `widen-column` 那支生成的
* 显式转换(`alter table … alter column … type text using col::text`——整数转文本
* 沿 PG 的 `bigint::text` 转型,没有失败路径)。这次重装(`activities:install`)会
* 把 `ledger.parts__chip_type.type_id` 等六列从 `bigint` 真的 `ALTER … TYPE text`
* 过去一遍。
*
* 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 }),