diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts
index 9589f97..2cd42c2 100644
--- a/activities/part-substitution-assessment/__tests__/activity.test.ts
+++ b/activities/part-substitution-assessment/__tests__/activity.test.ts
@@ -2,8 +2,9 @@
* ACT-WORK-05 差异评估的包内自测 —— **只准 import @sdk / @contracts / @sfeel**。
* 案例活动没有 `run`,自测不跑回合;测的是清单本身与授权面对齐,结构照 E04 / I01。
*
- * 六组:清单本身合规 / 台账声明 / 案例模型与清单的对齐 / 报告模版:编辑面与只读面各就各位 /
- * 徽标别名与值域相关的坑(2026-08-27 实测账,回归网)/ 活动入口。
+ * 七组:清单本身合规 / 台账声明 / 案例模型与清单的对齐 / 报告模版:编辑面与只读面各就各位 /
+ * 徽标别名与值域相关的坑(2026-08-27 实测账,回归网)/ 可编辑件的主键与必填列
+ * (2026-08-27 复审,回归网)/ 活动入口。
*/
import { describe, expect, it } from 'vitest';
import { activityManifestSchema } from '@contracts';
@@ -304,6 +305,56 @@ describe('徽标别名与值域相关的坑(2026-08-27 实测账,回归网)', ()
});
});
+/*
+ * 2026-08-27 定向复审逮住的两条 Critical(都是这一轮版面重做新引入的回归,不是原有的坑):
+ * ① `sub_eval.input_gap` 的「键」(`item_key`)那一格丢了 `data-col` —— 一单 11 行,`item_key`
+ * 逐行不同,人保存**任何一行**时服务端 `resolveRowKey` 凑不出主键(它既不在件上、
+ * 又不是「全表一致」的那种案号型列),整格落不下去;
+ * ② `sub_eval.input_gap.status` 声明 `required: true`,件上又有「+ 加一行」——
+ * 降成只读徽标后,人新加的行永远没有格子能把这一列填上,`_missing` 恒为正、
+ * 提交闸永远拦着,这是「墙不配路」的死路形态。
+ * 两条各配一张回归网,防的是同一类事故再出现在别的字段上。
+ */
+describe('可编辑件的主键与必填列(2026-08-27 复审:主键丢钥匙 / 必填填不了,两条 Critical 回归)', () => {
+ /** 三张真能编辑的件与它们各自绑定的台账对象 */
+ const EDITABLE: ReadonlyArray<{ view: string; domain: string; object: string }> = [
+ { view: 'p08-master-create', domain: 'parts', object: 'profile' },
+ { view: 'p10-document-create', domain: 'parts', object: 'document' },
+ { view: 'eval-input-list', domain: D, object: 'input_gap' },
+ ];
+
+ it('每一个主键列都有 data-col —— 它不是编辑面,是行的钥匙,丢了就整行保存不下去', () => {
+ for (const { view, domain, object } of EDITABLE) {
+ const table = byAddress(domain, object);
+ /*
+ * `caseKey` 声明的那一列(如 `eval_ref`)是例外:它是「一单一号,全表当然一致」的
+ * 案号型主键(与 I01 的 `round_ref` 同款),件上压根不摆,服务端 `resolveRowKey`
+ * 按「全表一致」自动补上(`apps/api/src/app/routes/desk.ts` 注释里点名的那条路)——
+ * 摆出来给人填反而是错的(那会让人以为能按行改案号)。真正逐行不同、必须挂
+ * `data-col` 的,是主键里除 caseKey 之外的那几列(如 `item_key`)。
+ */
+ const pk = (table?.fields.filter((f) => f.primaryKey).map((f) => f.name) ?? []).filter((name) => name !== table?.caseKey);
+ expect(pk.length, `${domain}.${object} 没有非 caseKey 的主键列`).toBeGreaterThan(0);
+ const html = viewHtml(view);
+ for (const col of pk) {
+ expect(html, `${view}: 主键列 ${domain}.${object}.${col} 缺 data-col`).toContain(`data-col="${col}"`);
+ }
+ }
+ });
+
+ it('可编辑件上,每一个 required:true 的列都有真能填的控件(data-col)—— 必填但填不了是墙不配路', () => {
+ for (const { view, domain, object } of EDITABLE) {
+ const table = byAddress(domain, object);
+ const required = table?.fields.filter((f) => f.required === true).map((f) => f.name) ?? [];
+ expect(required.length, `${domain}.${object} 没有声明 required 列`).toBeGreaterThan(0);
+ const html = viewHtml(view);
+ for (const col of required) {
+ expect(html, `${view}: 必填列 ${domain}.${object}.${col} 没有可填的控件`).toContain(`data-col="${col}"`);
+ }
+ }
+ });
+});
+
describe('活动入口', () => {
it('没有 run —— 行为全在清单里', () => {
expect(activity.run).toBeUndefined();
diff --git a/activities/part-substitution-assessment/src/manifest.ts b/activities/part-substitution-assessment/src/manifest.ts
index 320fe38..b3d2d8f 100644
--- a/activities/part-substitution-assessment/src/manifest.ts
+++ b/activities/part-substitution-assessment/src/manifest.ts
@@ -84,7 +84,7 @@ export const manifest: ActivityManifest = {
identity: {
activityId: ACTIVITY_ID,
name: 'ACT-WORK-05 · 差异评估',
- packageVersion: '1.1.0',
+ packageVersion: '1.1.1',
category: '需求分析与方案匹配',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-part-substitution-assessment',
diff --git a/activities/part-substitution-assessment/src/views/report.ts b/activities/part-substitution-assessment/src/views/report.ts
index 4983aaa..115ab82 100644
--- a/activities/part-substitution-assessment/src/views/report.ts
+++ b/activities/part-substitution-assessment/src/views/report.ts
@@ -436,6 +436,7 @@ const PN_CHECK_TABLE = `
const GAP_TABLE = `
| # |
+ 键 |
输入项 |
来源 |
状态 |
@@ -444,9 +445,10 @@ const GAP_TABLE = `
|
+ ${cell('item_key', 'mono')} |
${cell('input_item')} |
${cell('source_ledger', 'mono')} |
- ${badge('st', 'status', true)} |
+ ${select('status', [['gap', '待补'], ['default', '默认·可改'], ['ok', '齐备']], true)}${badge('st', 'status')} |
${cell('detail')} |
${cell('supplement')} |
@@ -473,7 +475,7 @@ export const EVAL_INPUT_LIST = doc({
gapbar(`${D}_input_gap`, '补充内容') +
editGate(`${D}.input_gap`, `还有 项待补 / 待确认,点「编辑」在「补充内容」列填入 —— 已绑定那几行的「输入项/来源/绑定值」抄的是启动入参,发现记录本身有误请退回触发方重新启动,不要在这里改`) +
GAP_TABLE +
- addrow(`${D}.input_gap`, '+ 加一行(补充资料项)', '新加行的「输入项」与「补充内容」为必填;删一行 = 该输入项不适用;补不了的写「留 UNKNOWN」或「挂验证任务」') +
+ addrow(`${D}.input_gap`, '+ 加一行(补充资料项)', '新行的「键」必须写(它是这一行的钥匙,写一个英文短名,如 thermal_note);「输入项」与「补充内容」也是必填;删一行 = 该输入项不适用;补不了的写「留 UNKNOWN」或「挂验证任务」') +
flow(
'审批 / 提交:本工单只承载内容;「提交」请在对话区的【执行卡】中进行。提交后去向:进入步骤 2 · AI 读取与标准化,仍缺失的项将按 UNKNOWN 进入差异评估。注意:启动参数如有误,需退回触发方修订后重新触发,本单不可改。',
),