Archived
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:
@@ -133,12 +133,18 @@ describe('台账声明', () => {
|
||||
});
|
||||
|
||||
/*
|
||||
* 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 报告。
|
||||
* Task 7b(2026-08-29):`type_id` 声明是 `text` 不是 `integer` —— 它是本仓唯一
|
||||
* 「auto:true 又是整数型」的主键,integer 会踩两个平台坑(仍未修,见 docs/debt.md):
|
||||
* ① `resolveRowKey` 的续号步骤只认 declared type 是不是 integer/number(`isSeq`),
|
||||
* 不看 `auto`,抢在 `autoKeys()` 前面把它按草稿行数续成 1、撞种子的 `type_id=1`;
|
||||
* ② `autoKeys()` 补零的字符串 `'03'` 经引用列的 `coerceToColumn(_, 'integer')` 会被
|
||||
* `Number()` 削掉前导零,`refersTo`/查表按字符串比对全部失配。
|
||||
* **改 text 不是因为「台账没有物理列类型」**——`master`/`record` 是真实物理列
|
||||
* (`ledger-store.ts` 的 `PG_TYPE` 映射,`integer → bigint`);只有任务级草稿层
|
||||
* `ledger.__draft` 才是 JSONB。安全的真正原因是 `schema-merge.ts` 的单调加宽 lattice
|
||||
* 把 `integer → text` 判定成合法加宽(不是类型冲突),`widen-column` 那支生成的是
|
||||
* `alter column … type text using col::text`——bigint 转 text 沿 PG 的转型没有失败路径。
|
||||
* 查证过程见 Task 7b 报告。
|
||||
*/
|
||||
it('类型字典:type_id 是自增主键 —— 人在件上新建类型时留空,由基座续号', () => {
|
||||
expect(colOf('parts.chip_type', 'type_id')).toMatchObject({ primaryKey: true, type: 'text', auto: true });
|
||||
@@ -532,6 +538,46 @@ describe('件① 评估输入补全', () => {
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* **评审 Critical 回归修复的结构安全网**(2026-08-30):`data-edit` 两处写法
|
||||
* (上面那条)有个副作用——`ViewContainer.bodyOf()` 的 `document.querySelector`
|
||||
* 从此解析到外层 `<table>`,而 `addRow()` 接着在这个 host 里找 `tr:last-of-type`——
|
||||
* CSS `:last-of-type` 是按各自父元素判定的,`<thead>` 里孤零零的那个 `<tr>` 在它
|
||||
* 自己的父元素(`thead`)里必然「最后」,若 `<thead>` 排在数据行前面,它会被文档序
|
||||
* 优先命中:克隆出来的是表头文字,零个 `input[data-col]`,`parts.type_param`
|
||||
* 那张(改动前本来是好的)因此被做坏——一次真回归(评审用 jsdom 在 renderView() 真实
|
||||
* 输出上复现两次坐实)。
|
||||
*
|
||||
* 修法是把 `<thead>` 挪到两个 `<tbody>` 之后(CSS 的 `table-header-group` 排版规则:
|
||||
* 视觉上永远排最前,不看它在 DOM 里排第几——这条规则本来就是 HTML5 允许 `tfoot`
|
||||
* 写在 `thead` 之后、`tbody` 之前的原理)——`document.querySelector`/`:last-of-type`
|
||||
* 认的是文档序不是视觉序,`<thead>` 排在两个 `<tbody>` 后面,`tr:last-of-type` 的
|
||||
* 搜索就再也碰不到表头行。
|
||||
*
|
||||
* 走 DOM 选择器的那部分行为(渲染后 `addRow`/`bodyOf` 真的会不会命中表头)由
|
||||
* `apps/api/src/modules/interaction/__tests__/view-render-addrow-thead.test.ts`
|
||||
* 覆盖(activities 包按 ADR-004 只准 import @sdk/@contracts/@sfeel,够不着
|
||||
* `apps/api` 的 `renderView`,所以那份测试镜像了这份源码的结构,不是 import 它)——
|
||||
* 这里补的是**廉价的字符串安全网**,把这份源码与那份镜像测试的假设锁在一起:
|
||||
* 谁若把 `<thead>` 挪回两个 `<tbody>` 之前(哪怕只改一张表),这条测试立刻标红,
|
||||
* 提醒去核对 `view-render-addrow-thead.test.ts` 是否也需要同步。
|
||||
*/
|
||||
it('**回归安全网**:两张新表的 <thead> 都排在两个 <tbody> 之后(不是最前面)—— 与 view-render-addrow-thead.test.ts 的假设锁在一起', () => {
|
||||
for (const address of ['parts.chip_type', 'parts.type_param']) {
|
||||
const tableStart = html.indexOf(`<table data-edit="${address}">`);
|
||||
expect(tableStart, address).toBeGreaterThanOrEqual(0);
|
||||
const tableEnd = html.indexOf('</table>', tableStart);
|
||||
const tableHtml = html.slice(tableStart, tableEnd);
|
||||
const theadAt = tableHtml.indexOf('<thead>');
|
||||
const tbodyPositions = [...tableHtml.matchAll(/<tbody/g)].map((m) => m.index ?? -1);
|
||||
expect(theadAt, `${address}: 找不到 <thead>`).toBeGreaterThanOrEqual(0);
|
||||
expect(tbodyPositions.length, `${address}: 应有两个 <tbody>`).toBe(2);
|
||||
// <thead> 必须排在两个 <tbody> 的开标签**之后**(源码序;视觉呈现由 CSS 的
|
||||
// table-header-group 保证仍在最前,与源码序无关)
|
||||
for (const pos of tbodyPositions) expect(theadAt, address).toBeGreaterThan(pos);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* 2026-08-29 评审 Minor:brief 原话是「红带显隐用 data-class-of」,但 gate_reason 是
|
||||
* 中文自由文本 —— classToken 只留 [a-z0-9-],中文字符全被剥空,不论空串还是有内容的
|
||||
|
||||
Reference in New Issue
Block a user