feat(w05): 件③ 落账确认与看板重绑 —— 签之前只核对,硬约束不符逐条点名

Task 9: 新增 landing-confirm(T_release 的终局验收工单,全只读,答「这一
签下去发生什么」——落账去向/结论卡/四态分布/硬约束逐条点名/待验项/发口),
三张件此刻齐了;activity-ledger 看板的 data 声明与 HTML 一并重绑到新九
张表(P-01/P-10 两域随重写撤下),解封 3 条等待三张件齐的 skip。

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 20:53:23 +08:00
co-authored by Claude Opus 5
parent 04675e814d
commit 4bd1a04511
5 changed files with 379 additions and 219 deletions
@@ -104,11 +104,11 @@ describe('清单本身合规', () => {
});
/*
* SKIP(2026-08-29,Task 2 → Task 7 部分交付):`input-supplement` 已随 Task 7 补回
* `assets.viewTemplates`(`VIEWS` 已更新),但 `difference-review` / `landing-confirm`
* 两张还在等 Task 8 / 9 —— `new Set(...)` 的整体相等要三张都在才成立,继续跳过。
* 解封(2026-08-29,Task 9):`landing-confirm` 已随 Task 9 补回 —— `input-supplement`
* (Task 7)/`difference-review`(Task 8)/`landing-confirm`(Task 9)三张全在了,
* `new Set(...)` 的整体相等条件此刻才第一次成立。
*/
it.skip('三张工单视图 + 一张运营侧看板都声明了,剧本每一步引用的 view 都在其中', () => {
it('三张工单视图 + 一张运营侧看板都声明了,剧本每一步引用的 view 都在其中', () => {
expect(new Set(manifest.assets.viewTemplates.map((v) => v.name))).toEqual(new Set([...VIEWS, BOARD_VIEW]));
for (const step of manifest.assets.businessModel.steps as ReadonlyArray<{ view?: string }>) {
if (step.view !== undefined) expect(VIEWS, step.view).toContain(step.view);
@@ -378,7 +378,15 @@ describe('报告模版:编辑面与只读面各就各位', () => {
}
});
/* SKIP(2026-08-29,Task 2 → Task 8/9):`difference-review` / `landing-confirm` 两张局面已撤下(新的两张由 Task 8/9 重做);它们绑的字段(recommendation_level 已只剩三档、analysis_status 已不带 tally)到时也会一并对齐 */
/*
* SKIP(2026-08-29,Task 9 复核后维持 skip):`difference-review` 那半句(第 383-385 行)
* 这次实测确已成立(与「件②」组的等价断言重复,不必再靠这条揭封);但 `landing-confirm`
* 那半句(第 386-390 行)测的 `request_status`/`current_stage` 是 `demand.request` 的列 ——
* 这个活动的清单里从没声明过 `demand.request`(见「台账声明」组「这个活动不碰 P-01 / P-10 /
* 客户档案」那条,已通过),`landing-confirm` 全只读、答的是「进哪几张表各几行」不是需求状态回写,
* 这两个字段**不属于新设计的任何一步**。这不是「还没轮到」,是测的东西这次重写里压根不存在 ——
* 一个 `it` 体绑死两半、拆不开,继续整条跳过,不强救。
*/
it.skip('枚举列只用 data-class-of 投影,不摆 ASCII 原值', () => {
expect(viewHtml('difference-review')).toContain('data-class-of="difference_status"');
expect(viewHtml('difference-review')).toContain('data-class-of="recommendation_level"');
@@ -395,13 +403,8 @@ describe('报告模版:编辑面与只读面各就各位', () => {
await expect(host.base.interaction.pushHtml('<div><script>x()</script></div>')).rejects.toThrow(/无脚本执行权/);
});
/*
* SKIP(2026-08-29,Task 2 → Task 7 部分交付):VIEWS 现在是三张新局面的名单,`input-supplement`
* 已随 Task 7 补回、这一条本身可以对它验(外壳确实是 page/card),但 `difference-review` /
* `landing-confirm` 两张还没有(viewHtml 返回空串,`toContain('class="card-head"')` 会假),
* 循环体没法只验一部分 —— 等 Task 8/9 补齐三张再一起解封。
*/
it.skip('三张模版的外壳都是搬自目标件的 page/card,不再含 E04/I01 那套视觉底(.proto-root)的痕迹', () => {
/* 解封(2026-08-29,Task 9):`landing-confirm` 补齐,三张全能验了 —— 都走同一个 `doc()` 壳。 */
it('三张模版的外壳都是搬自目标件的 page/card,不再含 E04/I01 那套视觉底(.proto-root)的痕迹', () => {
for (const view of VIEWS) {
const html = viewHtml(view);
expect(html, view).toContain('class="card-head"');
@@ -549,6 +552,68 @@ describe('件② 差异评估结论审核', () => {
});
});
/*
* 件③ 落账确认(2026-08-29,Task 9):T_release 的终局验收工单,`view: 'landing-confirm'` 指向它。
* **全只读,答「这一签下去发生什么」(spec §6.3)**:进哪几张表各几行(摆人读名)· 结论卡
* (等级 + 一句话)· 四态分布并把硬约束不符的逐条点名 · 待验项(P-06 的测试条件与后续动作)·
* 发哪一口。复用文件顶部已有的 `viewHtml` 短写。
*/
describe('件③ 落账确认', () => {
const html = viewHtml('landing-confirm');
it('在 viewTemplates 里,T_release 指向它', () => {
expect(html).not.toBe('');
expect((manifest.assets.businessModel.steps as ReadonlyArray<{ item: string; view?: string }>)
.find((s) => s.item === 'T_release')?.view).toBe('landing-confirm');
});
it('**全只读** —— 签之前只核对,不改', () => {
expect(html).not.toContain('data-edit=');
expect(html).not.toContain('data-col=');
expect(html).not.toContain('<script');
});
it('把「进哪几张表各几行」摆出来,用的是基座读数', () => {
for (const reading of ['substitution_relation_rows', 'substitution_difference_item_rows', 'validation_record_rows']) {
expect(html).toContain(`data-slot="${reading}"`);
}
});
it('硬约束不符的逐条点名 —— 签字的人最该看见的就是它', () => {
expect(html).toContain('data-rows="mine:substitution.difference_item"');
/*
* **筛选交给样式表,不是 `data-by`** —— `data-by` 过滤的是「子列 = 外层那一行的父列」,
* 这里没有外层行可依。做法:整表逐行渲出来,行上挂 `data-class-of="judgement_class"`,
* 由 CSS 只显 `.v-hard_mismatch` 那几行(判断留在样式表,模版不判)。
*/
expect(html).toContain('data-class-of="judgement_class"');
});
/*
* `judgement_class` 的 tally 值 `hard_mismatch` 带下划线 —— `classToken`/`vtok` 只留
* `[a-z0-9-]`(**吃掉下划线**),真正生效的选择器是 `.v-hardmismatch` 不是手写直觉的
* `.v-hard_mismatch`(2026-08-27 已在这个包里踩过一次同款事故,`vtok` 是这条回归网)。
*/
it('过滤用的选择器是从值算出来的(vtok),不是手写的 —— 不许踩「带下划线的值显示成待定」那个坑', () => {
expect(html).not.toContain('.v-hard_mismatch');
expect(html).toContain('.v-hardmismatch');
});
});
describe('运营中心看板', () => {
const board = manifest.assets.viewTemplates.find((v) => v.name === 'activity-ledger');
it('取数声明改绑新表,不再引已退役的那几张', () => {
const json = JSON.stringify(board?.data ?? {});
for (const gone of ['demand.request', 'parts.document', 'sub_eval.pn_check', 'sub_eval.input_gap', 'sub_eval.verdict']) {
expect(json).not.toContain(gone);
}
for (const kept of ['sub_eval.case', 'substitution.relation', 'substitution.difference_item']) {
expect(json).toContain(kept);
}
});
});
/*
* 2026-08-27 实测账,两条都真截图 + SQL 复核过:
* ① **带下划线的枚举值一律显示成「待定」**——平台有两套互相打架的消毒规则:
@@ -561,10 +626,15 @@ describe('件② 差异评估结论审核', () => {
* 静态 `class="req"` 把整列都标了必填。
*/
/*
* SKIP(2026-08-29,Task 2 → Task 7-9 补回):`BADGED` 列表里的每一项都指着已撤下的五张局面
* (p08-master-create 等)或已改动的字段(recommendation_level 掉了 alternate 档、
* analysis_status 不再带 tally、`sub_eval.pn_check` 整个改名 `sub_eval.part`)。
* 这整组「回归网」要等 Task 7–9 把新三张件补回来、字段清单也重新对齐后才谈得上继续钉。
* SKIP(2026-08-29,Task 9 复核后维持 skip):三张新件与看板全部到位后复查过 ——
* `BADGED` 列表 35 条里没有一条能原样救活:半数指着已彻底撤下的局面(`p08-master-create` /
* `eval-input-list`,Task 7 范围内已不存在)或已退役的字段(`substitution_type` 整列退休、
* `${D}.pn_check` 整个改名 `${D}.part`、`demand.request`/`parts.document` 两域这次重写
* 本活动已不碰),另一半虽然指向真实存在的 view/字段(`difference-review`/`landing-confirm`
* 的 `recommendation_level`/`risk_level`/`difference_status`/`constraint_type` 等),
* 但混在同一个数组里没法只救活这几行 —— 拆开重写等于另起一份测试,超出「回归网」这条 skip
* 本该做的事。这几条真正要钉的口径,「件②」「件③」两组已经用**显式点名**的断言钉过一遍
* (逐条明细六列只读 / judgement_class 的 vtok 选择器),不必靠这条泛化清单再验一遍。
*/
describe.skip('徽标别名与值域相关的坑(2026-08-27 实测账,回归网)', () => {
/** 必须与 `apps/api/src/modules/interaction/internal/domain/view-render.ts` 的 `classToken` 一字不差 */
@@ -773,13 +843,13 @@ describe('activity-ledger:关键台账数据看板', () => {
const declaredAddresses = new Set(manifest.declarations.ledger.map((l) => `${l.domain}.${l.object}`));
/*
* SKIP(2026-08-29,Task 2 → Task 9 补回):看板的 `data` 声明(`views/ledger-board.ts` 的
* `LEDGER_BOARD_DATA`)还圈着已退休的旧台账(`sub_eval.pn_check` / `sub_eval.verdict` /
* `demand.request` / `customer.profile` / `parts.document`)—— Task 9「运营中心看板重绑」
* 会把它换成新九张表;这条取数声明的自洽性检查到那时再验。html 自身的结构测试(下面三条)
* 不依赖 `declarations.ledger`,仍然有效、继续跑。
* 解封(2026-08-29,Task 9「运营中心看板重绑」):`LEDGER_BOARD_DATA` 已换成新九张表 ——
* 案卷两张(`sub_eval.case`/`sub_eval.part`)+ 字段字典两张(`parts.chip_type`/`parts.type_param`)+
* 参数对照两张(`parts.profile`/`parts.parameter`)+ 明细/关系(`substitution.difference_item`/
* `substitution.relation`)+ P-06(`validation.record`,含同料号历史 `validation.history`),
* 这条取数声明的自洽性检查此刻才第一次能验。
*/
it.skip('取数声明:案号台账是 record 且 key 是它的 caseKey、前缀 W05-;每条 rows 的表都已声明;from 只指 case 或前面的 as.列', () => {
it('取数声明:案号台账是 record 且 key 是它的 caseKey、前缀 W05-;每条 rows 的表都已声明;from 只指 case 或前面的 as.列', () => {
expect(data).toBeDefined();
if (data === undefined) return;
const caseLedger = manifest.declarations.ledger.find((l) => `${l.domain}.${l.object}` === data.case.ledger);
@@ -803,18 +873,23 @@ describe('activity-ledger:关键台账数据看板', () => {
}
seen.add(entry.as);
}
// 六张主表 + 案号两张 + 同料号历史都圈了
// 案卷两张 + 字段字典两张 + 参数对照两张 + 明细/关系 + P-06(含历史)都圈了
expect([...seen]).toEqual(
expect.arrayContaining([`${D}.verdict`, `${D}.pn_check`, 'demand.request', 'customer.profile', 'parts.profile', 'parts.parameter', 'parts.document', 'substitution.relation', 'substitution.difference_item', 'validation.record', 'validation.history']),
expect.arrayContaining([`${D}.case`, `${D}.part`, 'parts.chip_type', 'parts.type_param', 'parts.profile', 'parts.parameter', 'substitution.relation', 'substitution.difference_item', 'validation.record', 'validation.history']),
);
});
it('html 里每个 data-rows 键都是声明里的一个 as(行由基座圈过,不是整表);嵌套只用 data-by 父键过滤', () => {
const as = new Set(data?.rows.map((r) => r.as) ?? []);
const keys = [...html.matchAll(/data-rows="([^"]+)"/g)].map((m) => m[1] ?? '');
expect(keys.length).toBeGreaterThan(8);
/*
* 2026-08-29 Task 9:门槛从「> 8」(旧六张主表设计)降到「> 6」——新设计收成 8 处绑定
* (P-08 主表 + 两处嵌套查表 / 字段字典 / 关系 / 明细 / P-06 本单 + 历史),门槛仍要求
* 「不止一两处」这条哨兵语义,只是数字跟着新设计的真实形状改小,不是放水。
*/
expect(keys.length).toBeGreaterThan(6);
for (const key of keys) expect(as.has(key), key).toBe(true);
expect(html).toContain('<span class="pnrow" data-rows="sub_eval.pn_check" data-by="internal_pn">');
expect(html).toContain(`<span class="pnrow" data-rows="${D}.part" data-by="internal_pn">`);
expect(html).toContain('<div class="pchips" data-rows="parts.parameter" data-by="internal_pn">');
expect(html).toContain('<tbody class="hist" data-rows="validation.history">');
expect(html).not.toContain('mine:');
@@ -833,15 +908,24 @@ describe('activity-ledger:关键台账数据看板', () => {
expect(html).not.toContain('data-col=');
});
it('目标件六张表的表头与页头要素都在;MATCH 行淡显、放弃的候选只在有 abandoned 时露面 —— 都是 class 交样式表,模版不判', () => {
for (const header of ['<th>内部料号</th>', '<th>替代关系 ID</th>', '<th>参数项</th>', '<th>关联料号</th>', '<th>验证 ID</th>', '<th>标准化参数集</th>']) expect(html).toContain(header);
/*
* 2026-08-29 Task 9 改窄:六张表收成新九张(P-01/P-10 两张连同「关联需求」「关联料号」
* 两列一起退休 —— 本活动已不碰 demand.request / parts.document);案号台账从 `${D}.verdict`
* 改名 `${D}.case`;「放弃的候选」这一概念随多候选并行设计一起退役(新设计一次评估恰一对一
* 候选,没有「本轮放弃的候选」这件事)。补一条**已退役地址不再出现**的回归网,同「运营中心看板」组同款纪律。
*/
it('新九张表的表头与页头要素都在;MATCH 行淡显 —— 都是 class 交样式表,模版不判', () => {
for (const header of ['<th>内部料号</th>', '<th>替代关系 ID</th>', '<th>参数项</th>', '<th>验证 ID</th>', '<th>标准化参数集</th>', '<th>参数名</th>']) expect(html).toContain(header);
expect(html).toContain('data-slot="case:instanceNo"');
expect(html).toContain('data-slot="case:ref"');
expect(html).toContain(`data-slot="${D}.verdict.original_internal_pn"`);
expect(html).toContain('data-slot="demand.request.request_id"');
expect(html).toContain('data-slot="substitution.relation.original_internal_pn"');
expect(html).toContain(`data-slot="${D}.case.summary"`);
expect(html).toContain('<tr data-class-of="difference_status">');
expect(html).toContain('tr.v-match td{opacity:.55}');
expect(html).toContain('.aband:has(i.v-abandoned){display:block}');
expect(html).toContain('data-slot="substitution_difference_item_difference_status_unknown"');
// 已退役的旧域 / 旧字段一律不出现
for (const gone of ['demand.request', 'parts.document', `${D}.pn_check`, `${D}.verdict`, 'substitution_type', 'customer_approval', 'v-abandoned']) {
expect(html, gone).not.toContain(gone);
}
});
});