diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts
index 9de3950..8d1ec51 100644
--- a/activities/part-substitution-assessment/__tests__/activity.test.ts
+++ b/activities/part-substitution-assessment/__tests__/activity.test.ts
@@ -450,7 +450,7 @@ describe('报告模版:编辑面与只读面各就各位', () => {
it('每个可编辑格子的 data-col 都在对应台账对象的声明里(input-supplement 一屏四张表,四张台账对象的声明并起来看;difference-review 一张表)', () => {
const cases: Array<[string, Array<[string, string]>]> = [
- ['input-supplement', [['parts', 'profile'], ['parts', 'chip_type'], ['parts', 'type_param'], ['parts', 'parameter']]],
+ ['input-supplement', [['parts', 'profile'], ['parts', 'parameter']]],
/* 件② 2026-08-31 起多一张:收口四格可就地改,写向 substitution.relation */
['difference-review', [['substitution', 'difference_item'], ['substitution', 'relation']]],
];
@@ -548,12 +548,12 @@ describe('件① 评估输入补全', () => {
expect(steps.find((s) => s.item === 'S1_inputs')?.view).toBe('input-supplement');
});
- it('四块都在,行都绑本单草稿(不是整表);档案卡行源是 parts.profile 本身', () => {
+ it('三块都在,行都绑本单草稿(不是整表);档案卡行源是 parts.profile 本身(类型字典只读整表,不再有 mine: 镜像)', () => {
expect(html).toContain('data-rows="mine:sub_eval.part"');
expect(html).toContain('data-rows="mine:parts.profile"');
expect(html).toContain('data-rows="mine:parts.parameter"');
- expect(html).toContain('data-rows="mine:parts.chip_type"');
- expect(html).toContain('data-rows="mine:parts.type_param"');
+ expect(html).not.toContain('data-rows="mine:parts.chip_type"');
+ expect(html).not.toContain('data-rows="mine:parts.type_param"');
});
/*
@@ -597,16 +597,14 @@ describe('件① 评估输入补全', () => {
/* 全只读:选类型在档案卡,建类型在抽屉 —— 基准卡上一个编辑面都不许有 */
expect(basis).not.toContain('data-col');
expect(basis).not.toContain('data-edit');
- /* 基准卡不再有表格:类型 + 字段全是瓷贴 */
+ /* 基准卡不再有表格:类型 + 字段全是瓷贴;说明藏进悬浮气泡(降噪,2026-08-31) */
expect(basis).not.toContain('
');
- const labEnd = html.indexOf('', labAt);
- expect(labAt).toBeGreaterThanOrEqual(0);
- expect(html.indexOf('')).toBeGreaterThan(labAt);
- expect(html.indexOf('')).toBeGreaterThan(labAt);
- expect(labEnd).toBeGreaterThan(html.indexOf(''));
+ expect(html).toContain('.ftile:hover .ft-desc');
+ /* 新建类型抽屉已随「只选不建」退役(2026-08-31 用户定)—— 只剩说明折叠那一只 details */
+ expect(html).not.toContain('');
+ expect(html).not.toContain('data-edit="parts.chip_type"');
+ expect(html).not.toContain('data-edit="parts.type_param"');
});
/*
@@ -631,34 +629,28 @@ describe('件① 评估输入补全', () => {
expect(html).toContain('点击翻面');
});
- it('主画面无表格:说明折叠之前的正文一张 {
- const beforeDrawer = html.slice(0, html.indexOf(''));
- expect(beforeDrawer).not.toContain(' {
+ expect(html).not.toContain(' {
+ it('占位口径:正面必填红/建议橘;卡背值必填红,说明空着**什么都不摆**(2026-08-31 用户:「没写就什么都不要展示」)', () => {
expect(html).toContain('.fcell.req .jc-vcell:empty::after{content:"必填"');
expect(html).toContain('.fcell.sug .jc-vcell:empty::after{content:"建议"');
expect(html).toContain('.ptile .pv .jc-vcell:empty::after{content:"必填"');
- expect(html).toContain('.ptile .sg .jc-vcell:empty::after{content:"建议"');
+ expect(html).toContain('.ptile .sg .jc-vcell:empty{display:none}');
+ expect(html).not.toContain('.ptile .sg .jc-vcell:empty::after');
});
- it('芯片类型是从台账现读的下拉 —— 写死等于「只能选出包那天就有的那几种」(块①的 chip_type_id 与块②b 的 type_id 都是)', () => {
+ it('芯片类型是从台账现读的下拉 —— 写死等于「只能选出包那天就有的那几种」', () => {
expect(html).toContain('data-options="parts.chip_type"');
expect(html).toContain('data-option-value="type_id"');
expect(html).toContain('data-option-label="type_name"');
/* data-options 与 data-slot 不能共存(渲染时 data-slot 会被剥掉)—— 当下值由 data-col 定 */
expect(/data-options="[^"]*"[^>]*data-slot=/.test(html)).toBe(false);
- /* 块①(sub_eval.part 行模版,只有一份行模版,被 data-rows 迭代)绑 chip_type_id 下拉 */
+ /* 档案卡正面绑 chip_type_id 下拉 —— 只选不建(2026-08-31 用户定),type_id 的写入下拉已随抽屉退役 */
expect(html).toContain('data-col="chip_type_id" required data-options="parts.chip_type"');
- /*
- * 块②b(type_param)绑 type_id 下拉的地方**该出现两次**——已有行的行模版(data-rows
- * 迭代)一份、种子行(字面量静态行,不受行数所限)一份;只数子串存在会被「只剩其中一份」
- * 这种退化蒙混过去(已用变异验证逮到:只把动态行模版那份换回裸文本输入,这条子串断言
- * 仍然为真,因为种子行那份还在——按出现次数数才钉得住两处都在)。
- */
- const typeIdSelectCount = (html.match(/data-col="type_id" required data-options="parts\.chip_type"/g) ?? []).length;
- expect(typeIdSelectCount).toBe(2);
+ expect(html).not.toContain('data-col="type_id"');
});
/*
@@ -667,18 +659,14 @@ describe('件① 评估输入补全', () => {
* (查证见 Task 7b 报告)。补一条**不受 `data-rows` 管辖的静态种子行**——两张表各一条,
* 字面量手写 `data-col` 输入格,不经 `cell()`/`data-slot`,行数再少也在。
*/
- it('**零行也加得出第一行**:两张表各带一条静态种子行(class="jc-row-static"),不靠 data-rows 生成', () => {
- const staticRows = html.match(//g) ?? [];
- expect(staticRows).toHaveLength(2);
- /* 种子行里的输入格是手写的,不经 twinInputs —— 直接断言 data-col 存在即可 */
- expect(html).toContain('');
- expect(html).toContain('');
- expect(html).toContain('');
- expect(html).toContain(' {
+ expect(true).toBe(true);
});
/*
@@ -696,17 +684,14 @@ describe('件① 评估输入补全', () => {
* 钉住修法:`type_id` 必须挂 `data-col`,且必须是禁用的 —— 人看得见「由系统生成」
* 这句占位,但怎么点都改不了它的值。
*/
- it('**新建类型的种子行:type_id 挂 data-col、但禁用恒空**(2026-08-30 修 Critical 1 —— 漏挂会让 resolveRowKey 的"全表一致"把新建的类型误判成既有那一行,静默覆盖它)', () => {
- expect(html).toContain('由系统生成');
- // 「由系统生成」只出现在 chip_type 那条种子行里 —— 精确抓出那一行
- const chipTypeStaticRow = /
(?:(?!<\/tr>)[\s\S])*?由系统生成(?:(?!<\/tr>)[\s\S])*?<\/tr>/.exec(html)?.[0] ?? '';
- expect(chipTypeStaticRow).not.toBe('');
- /* 必须是一个真的 (collect() 只认 input/select/textarea[data-col]),
- 不能是纯文本 —— 否则这一格永远不会出现在回传的 cells 里,barred 的保护无从谈起 */
- expect(chipTypeStaticRow).toMatch(/]*data-col="type_id"[^>]*disabled/);
- /* disabled 意味着人怎么点都改不了它的值 —— 保存时这一格恒是空串,才谈得上「barred」 */
- expect(chipTypeStaticRow).toContain('data-col="type_name"');
- expect(chipTypeStaticRow).toContain('data-col="description"');
+ /*
+ * SKIP(2026-08-31 用户定「把添加芯片类型的功能从这个活动中移除,只能选择已有类型」):
+ * 新建类型抽屉连同两张编辑表(静态种子行 / type_id 禁用 data-col 的 Critical-1 修法 /
+ * thead 后置回归网 / data-edit 双写)整体退役 —— 这些钉的是已移除的创建路径,不是被放松的
+ * 断言。平台侧机制钉测(view-render-addrow-thead / view-table-edit)不随本活动退役。
+ */
+ it.skip('**新建类型的种子行:type_id 挂 data-col、但禁用恒空**(2026-08-30 修 Critical 1 —— 漏挂会让 resolveRowKey 的"全表一致"把新建的类型误判成既有那一行,静默覆盖它)', () => {
+ expect(true).toBe(true);
});
it('**主键列必挂 `data-col`** —— 它不是编辑面,是行的钥匙(漏了会把人改的行错配到别行上)', () => {
@@ -714,10 +699,28 @@ describe('件① 评估输入补全', () => {
expect(html).toContain('data-col="param_name"');
});
- it('可改的三张表 + 档案卡各有一道编辑闸', () => {
- for (const address of ['parts.profile', 'parts.chip_type', 'parts.type_param', 'parts.parameter']) {
- expect(html).toContain(`data-edit="${address}"`);
+ /*
+ * 2026-08-31 用户定:两行编辑闸退役,换成卡右上角的笔↔软盘 —— 笔开当前这一面的编辑
+ * (正面=主数据 / 背面=参数值),软盘保存,叉放弃。按钮就是平台认的 data-edit-open /
+ * data-edit-save / data-edit-cancel 触发器,快照与取消机制原样。
+ */
+ it('编辑收进卡角:笔(open)/软盘(save)/叉(cancel)正反两面各一套;地址只剩 profile 与 parameter', () => {
+ for (const address of ['parts.profile', 'parts.parameter']) {
+ expect(html).toContain(`data-edit-open="${address}"`);
+ expect(html).toContain(`data-edit-save="${address}"`);
+ expect(html).toContain(`data-edit-cancel="${address}"`);
}
+ /* 老的闸行不再出现在正文里(样式表里的基座规则给 ② 用,不算) */
+ expect(html.slice(html.indexOf(''))).not.toContain('jc-editgate');
+ /* 笔↔软盘的按面显隐由样式表判 */
+ expect(html).toContain('.dossiers.on .disk-f{display:grid}');
+ expect(html).toContain(".dossier:has(.pback.on) .disk-b{display:grid}");
+ /* 参数瓷贴的输入孪生件带占位提示(渲染器认 data-ph) */
+ expect(html).toContain('data-ph="规格书上的值 · 查不到留空"');
+ expect(html).toContain('data-ph="补充说明 · 选填"');
+ /* 题头回浅蓝渐变(2026-08-31 用户:「标题背景还是改成之前的那种蓝色渐变」) */
+ expect(html).toContain('class="card hd-light"');
+ expect(html).toContain('linear-gradient(120deg,#f0f5ff 0%,#fff 55%,#eef4ff 100%)');
});
/*
@@ -727,11 +730,14 @@ describe('件① 评估输入补全', () => {
* 内层 `data-rows` 那个 `` 一处(给 `renderRows` 的 `editable` 检测看,
* 它只认与 `data-rows` 同一个标签自带的属性,不看祖先)。
*/
- it('新建类型 / 新建字段两张表的 data-edit 各出现两次(外层 table + 内层 data-rows tbody)', () => {
- for (const address of ['parts.chip_type', 'parts.type_param']) {
- const count = (html.match(new RegExp(`data-edit="${address}"`, 'g')) ?? []).length;
- expect(count, address).toBe(2);
- }
+ /*
+ * SKIP(2026-08-31 用户定「把添加芯片类型的功能从这个活动中移除,只能选择已有类型」):
+ * 新建类型抽屉连同两张编辑表(静态种子行 / type_id 禁用 data-col 的 Critical-1 修法 /
+ * thead 后置回归网 / data-edit 双写)整体退役 —— 这些钉的是已移除的创建路径,不是被放松的
+ * 断言。平台侧机制钉测(view-render-addrow-thead / view-table-edit)不随本活动退役。
+ */
+ it.skip('新建类型 / 新建字段两张表的 data-edit 各出现两次(外层 table + 内层 data-rows tbody)', () => {
+ expect(true).toBe(true);
});
/*
@@ -758,20 +764,14 @@ describe('件① 评估输入补全', () => {
* 谁若把 `` 挪回两个 `` 之前(哪怕只改一张表),这条测试立刻标红,
* 提醒去核对 `view-render-addrow-thead.test.ts` 是否也需要同步。
*/
- it('**回归安全网**:两张新表的 都排在两个 之后(不是最前面)—— 与 view-render-addrow-thead.test.ts 的假设锁在一起', () => {
- for (const address of ['parts.chip_type', 'parts.type_param']) {
- const tableStart = html.indexOf(``);
- expect(tableStart, address).toBeGreaterThanOrEqual(0);
- const tableEnd = html.indexOf('
', tableStart);
- const tableHtml = html.slice(tableStart, tableEnd);
- const theadAt = tableHtml.indexOf('');
- const tbodyPositions = [...tableHtml.matchAll(/ m.index ?? -1);
- expect(theadAt, `${address}: 找不到 `).toBeGreaterThanOrEqual(0);
- expect(tbodyPositions.length, `${address}: 应有两个 `).toBe(2);
- // 必须排在两个 的开标签**之后**(源码序;视觉呈现由 CSS 的
- // table-header-group 保证仍在最前,与源码序无关)
- for (const pos of tbodyPositions) expect(theadAt, address).toBeGreaterThan(pos);
- }
+ /*
+ * SKIP(2026-08-31 用户定「把添加芯片类型的功能从这个活动中移除,只能选择已有类型」):
+ * 新建类型抽屉连同两张编辑表(静态种子行 / type_id 禁用 data-col 的 Critical-1 修法 /
+ * thead 后置回归网 / data-edit 双写)整体退役 —— 这些钉的是已移除的创建路径,不是被放松的
+ * 断言。平台侧机制钉测(view-render-addrow-thead / view-table-edit)不随本活动退役。
+ */
+ it.skip('**回归安全网**:两张新表的 都排在两个 之后(不是最前面)—— 与 view-render-addrow-thead.test.ts 的假设锁在一起', () => {
+ expect(true).toBe(true);
});
/*
@@ -799,14 +799,14 @@ describe('件① 评估输入补全', () => {
* 只读态与编辑态下「#」列逐行都是空白。钉死 `.jc-tbl-wrap` 现在也接了这五条规则,
* 且这张件确实只用 `.jc-tbl-wrap`(没有表在用没打这条补丁就会数不出来的 `.tbl-wrap`)。
*/
- it('抽屉里两张表的序号列真的会数:.jc-tbl-wrap 也接了 CSS 计数器(不再只认 .tbl-wrap)', () => {
- expect(html).toMatch(/\.jc-tbl-wrap\s+table\s*,\s*\.tbl-wrap\s+table\s*\{\s*counter-reset:\s*rn;?\s*\}/);
- expect(html).toMatch(/\.jc-tbl-wrap\s+table\s+td\.rn\s*,\s*\.tbl-wrap\s+table\s+td\.rn\s*\{\s*counter-increment:\s*rn;?\s*\}/);
- expect(html).toMatch(/\.jc-tbl-wrap\s+table\s+td\.rn\s+\.jc-rn-num::before\s*,\s*\.tbl-wrap\s+table\s+td\.rn\s+\.jc-rn-num::before\s*\{\s*content:\s*counter\(rn\);?\s*\}/);
- // 只剩抽屉里两张(2026-08-31 第三轮:基准卡瓷贴化、参数值上卡背 —— 主画面零表格)
- expect(html).not.toContain('class="tbl-wrap"');
- const jcTblWrapCount = html.match(/class="jc-tbl-wrap"/g)?.length ?? 0;
- expect(jcTblWrapCount).toBe(2);
+ /*
+ * SKIP(2026-08-31 用户定「把添加芯片类型的功能从这个活动中移除,只能选择已有类型」):
+ * 新建类型抽屉连同两张编辑表(静态种子行 / type_id 禁用 data-col 的 Critical-1 修法 /
+ * thead 后置回归网 / data-edit 双写)整体退役 —— 这些钉的是已移除的创建路径,不是被放松的
+ * 断言。平台侧机制钉测(view-render-addrow-thead / view-table-edit)不随本活动退役。
+ */
+ it.skip('抽屉里两张表的序号列真的会数:.jc-tbl-wrap 也接了 CSS 计数器(不再只认 .tbl-wrap)', () => {
+ expect(true).toBe(true);
});
});
@@ -1068,10 +1068,9 @@ describe.skip('徽标别名与值域相关的坑(2026-08-27 实测账,回归网)
*/
describe('可编辑件的主键与必填列(2026-08-27 复审:主键丢钥匙 / 必填填不了,两条 Critical 回归;Task 7b 补 parts.chip_type)', () => {
/** input-supplement 一屏四张真能编辑的表与它们各自绑定的台账对象(Task 7b 加入 parts.chip_type) */
+ /* 2026-08-31「只选不建」:chip_type / type_param 不再是可编辑面,从名单撤下 */
const EDITABLE: ReadonlyArray<{ view: string; domain: string; object: string }> = [
{ view: 'input-supplement', domain: 'parts', object: 'profile' },
- { view: 'input-supplement', domain: 'parts', object: 'chip_type' },
- { view: 'input-supplement', domain: 'parts', object: 'type_param' },
{ view: 'input-supplement', domain: 'parts', object: 'parameter' },
];
diff --git a/activities/part-substitution-assessment/__tests__/steps.test.ts b/activities/part-substitution-assessment/__tests__/steps.test.ts
index b463145..efe70da 100644
--- a/activities/part-substitution-assessment/__tests__/steps.test.ts
+++ b/activities/part-substitution-assessment/__tests__/steps.test.ts
@@ -146,10 +146,10 @@ describe('S1 配方', () => {
* 变成空字符串)见 `apps/api/src/modules/loop-engine/__tests__/w05-gate-reason-order.test.ts`
* ——这份文件只做结构断言,不跑引擎(活动包不许 import apps/*)。
*/
- it('八笔,次序是:两颗料 → 类型字典自投 → 字段清单自投 → 主数据骨架 → 参数骨架 → 案卷 → 卡 → 交代', () => {
- expect(kinds).toEqual(['project', 'project', 'project', 'project', 'project', 'project', 'card', 'say']);
+ it('六笔,次序是:两颗料 → 主数据骨架 → 参数骨架 → 案卷 → 卡 → 交代(类型字典/字段清单两笔自投影已随「只选不建」退役)', () => {
+ expect(kinds).toEqual(['project', 'project', 'project', 'project', 'card', 'say']);
expect(acts.map((a) => (a.kind === 'project' ? a.into : undefined)).filter(Boolean)).toEqual([
- 'sub_eval.part', 'parts.chip_type', 'parts.type_param', 'parts.profile', 'parts.parameter', 'sub_eval.case',
+ 'sub_eval.part', 'parts.profile', 'parts.parameter', 'sub_eval.case',
]);
});
@@ -165,21 +165,13 @@ describe('S1 配方', () => {
* 跳过已落地的行(loop-engine/port.ts),prune 回来:没落地的陈旧选型照撤,
* 已落地的原地留下 —— 案例 2 不再炸,用户那条路清得干净。
*/
- it('②③ 两笔自投影都读 sub_eval.part 的 chip_type_id 去查字典/字段清单,原样抄回自己(带 prune,已落地由基座跳过)', () => {
- const chipType = acts[1];
- expect(chipType?.kind === 'project' && chipType.into).toBe('parts.chip_type');
- expect(chipType?.kind === 'project' && chipType.prune).toBe(true);
- expect(chipType?.kind === 'project' && chipType.from).toMatchObject({
- each: 'mine:sub_eval.part',
- then: { each: 'parts.chip_type', where: { type_id: { outer: 'chip_type_id' } } },
- });
- const typeParam = acts[2];
- expect(typeParam?.kind === 'project' && typeParam.into).toBe('parts.type_param');
- expect(typeParam?.kind === 'project' && typeParam.prune).toBe(true);
- expect(typeParam?.kind === 'project' && typeParam.from).toMatchObject({
- each: 'mine:sub_eval.part',
- then: { each: 'parts.type_param', where: { type_id: { outer: 'chip_type_id' } } },
- });
+ /*
+ * SKIP(2026-08-31 用户定「只选不建」):②③ 两笔自投影连同 gateLedgers 上那两张表
+ * 一并退役 —— 类型只许从字典里选,选中的行本来就在正式表里,镜像与落地都失去了对象。
+ * 这条钉的机制(prune + skipLanded)仍活在 auto-runner 测试里,不随本活动退役。
+ */
+ it.skip('②③ 两笔自投影都读 sub_eval.part 的 chip_type_id 去查字典/字段清单,原样抄回自己(带 prune,已落地由基座跳过)', () => {
+ expect(true).toBe(true);
});
it('案号由 let 铸,只从本单开案时刻取材', () => {
@@ -203,7 +195,7 @@ describe('S1 配方', () => {
* 「加一行」),必须换成说实情、给真出路的话:没法比出差异,终止重开。
*/
it('gate_reason 第一档:两参必填之后只剩"填成同一个号"这条残余路径,文案不再指向不存在的"补两格"', () => {
- const kase = acts[5];
+ const kase = acts[3];
const fields = kase?.kind === 'project' ? kase.fields : undefined;
const gateReason = fields?.['gate_reason'] as { when?: string; then?: string } | undefined;
expect(gateReason?.when).toBe('sub_eval_part_rows < 2');
@@ -213,7 +205,7 @@ describe('S1 配方', () => {
});
it('参数骨架:两颗料都铺、值回填现值 —— 「在库但当年参数没录全」也要被拦住', () => {
- const params = acts[4];
+ const params = acts[2];
/* 'live':人加不了行的表,quiet 拍也撤 —— 改类型后旧参数行跟手消失(2026-08-31 用户点名「只会变多不会变少」) */
expect(params?.kind === 'project' && params.prune).toBe('live');
const from = params?.kind === 'project' ? params.from : undefined;
@@ -224,7 +216,7 @@ describe('S1 配方', () => {
});
it('**发卡的条件全是基座读数** —— 不许拿 `gate_reason != ""` 当条件', () => {
- const card = acts[6];
+ const card = acts[4];
const when = card?.kind === 'card' ? (card.when ?? '') : '';
/*
* S-FEEL 里**未绑定的名字 `!= ""` 求值为 true**,而 `gate_reason` 为空串时那一列压根不写、
@@ -245,16 +237,16 @@ describe('S1 配方', () => {
* (2026-08-30 真跑 e2e 撞见)。现在四张表都在闸上、也都会一起落地。
*/
it('卡:数据工单、四张表都在闸上(含 parts.chip_type/parts.type_param,Ruling H35)、且有 basis(基座拒发 basis 为空的工单)', () => {
- const card = acts[6];
+ const card = acts[4];
expect(card?.kind === 'card' && card.as).toBe('execution');
expect(card?.kind === 'card' && card.variant).toBe('data');
- expect(card?.kind === 'card' && card.gateLedgers).toEqual(['parts.chip_type', 'parts.type_param', 'parts.profile', 'parts.parameter']);
+ expect(card?.kind === 'card' && card.gateLedgers).toEqual(['parts.profile', 'parts.parameter']);
expect((card?.kind === 'card' ? card.basis ?? [] : []).length).toBeGreaterThan(0);
expect((card?.kind === 'card' ? card.guide ?? [] : []).length).toBeGreaterThanOrEqual(3);
});
it('发卡条件与 doneWhen 是同一件事的正反面', () => {
- const when = acts[6]?.kind === 'card' ? (acts[6].when ?? '') : '';
+ const when = acts[4]?.kind === 'card' ? (acts[4].when ?? '') : '';
for (const reading of [
'sub_eval_part_exists_status_found', 'sub_eval_part_missing',
'sub_eval_part_distinct_chip_type_id', 'parts_profile_missing', 'parts_parameter_missing',
diff --git a/activities/part-substitution-assessment/src/case-model.ts b/activities/part-substitution-assessment/src/case-model.ts
index c76d0e9..658f38e 100644
--- a/activities/part-substitution-assessment/src/case-model.ts
+++ b/activities/part-substitution-assessment/src/case-model.ts
@@ -339,7 +339,7 @@ export const ASSESSMENT_STEPS = [
与 auto.do 第 ⑥ 笔 `card` 的 `gateLedgers` 同源(parts.chip_type / parts.type_param /
parts.profile / parts.parameter —— 四张表都在闸上,理由见那笔 `card` 定义处的注释)。 */
view: 'input-supplement',
- writes: ['sub_eval.part', 'sub_eval.case', 'parts.chip_type', 'parts.type_param', 'parts.profile', 'parts.parameter'],
+ writes: ['sub_eval.part', 'sub_eval.case', 'parts.profile', 'parts.parameter'],
reads: ['originalInternalPn', 'candidateInternalPn', 'case:openedAt', 'parts.profile', 'parts.parameter', 'parts.chip_type', 'parts.type_param'],
tools: [],
doneWhen:
@@ -378,82 +378,12 @@ export const ASSESSMENT_STEPS = [
},
},
/*
- * ② **选中的那一行类型字典,原样投一遍**(2026-08-30,Ruling H35 —— 真跑 e2e 撞见的
- * 平台侧连带问题:「数据工单确认即入库」按 `gateLedgers` 整表并入正式表,而
- * `parts.chip_type` 从 Task 7 起就没列在 `gateLedgers` 里 —— 人一旦真的新建一个类型,
- * `parts.profile`/`parts.type_param` 引用着它一起落地,**字典行本身**却只留在这一单
- * 草稿里、从没落地过。下一个任务读到同一颗料,查这个 `chip_type_id` 时字典里空空如也,
- * `refersTo` 当场拒——是一条悬空引用,不是「查无此值」那种输入错误)。
- *
- * **这一笔不看「新建还是已有」,统统投一遍**(不选型的选也一样、新建的选也一样):
- * 读的是 `sub_eval.part` 每一行当下的 `chip_type_id`(空串时下面这层 `each` 天然查
- * 不到、不产生任何行 —— 人还没选类型的那颗不会被提前铺出假行),按它去
- * `parts.chip_type` 里找到那一行、原样把 `type_name`/`description` 抄回自己身上。
- * 值没变,`draftWrite` 同 PK 覆盖静默跳过(幂等,不产生多余的写入痕迹)——
- * 但**这一单的草稿层从此真的有这一行**,`mine:parts.chip_type` 不再天然为空。
- *
- * 这条把 `parts.chip_type` 重新放回下面 ⑥ 的 `gateLedgers` 铺平了路:闸不会再拿
- * 「零行」的旧理由误拦(那个前提不成立了),而选中的类型(新建的也好、已有的也好)
- * 会随「数据工单确认入库」一起落进正式表 —— 不再留悬空引用。
+ * ~~② 类型字典自投影 / ③ 字段清单自投影~~(2026-08-31 用户定「本活动只选类型、
+ * 不新增类型」后退役):这两笔当年(Ruling H35)是为「人新建的类型/字段要随数据
+ * 工单确认一起落地、且 mine: 不为空好让闸不误拦」而生 —— 类型只许从字典里选之后,
+ * 选中的类型本来就在正式表里,镜像与落地都失去了对象;`gateLedgers` 同批收窄到
+ * `parts.profile` / `parts.parameter` 两张真被人编辑的表。历史见 git(2.9.0 及更早)。
*/
- {
- kind: 'project' as const,
- into: 'parts.chip_type',
- /*
- * **带 `prune`,已落地的行由基座跳过**(2026-08-31 改回)。这一笔曾特意不带
- * `prune`(2026-08-30 e2e 案例 2:类型行随「数据工单确认即入库」落地后,人改了
- * 类型选择,`prune` 想撤已落地的旧行,`draftDrop` 硬拒、配方整单炸)—— 当时的
- * 代价是**没落地的**陈旧行也跟着不清:人在件上把候选从 LDO 改回 DC-DC,LDO 的
- * 字典行与字段行积在屏上,「下面还是显示两个类型」(2026-08-31 用户实测点名)。
- * 病根在撤行机制一刀切,不在 prune 本身 —— 现在 `draftDrop` 对配方这一路带
- * `skipLanded`(见 loop-engine/port.ts):已落地的行原地留下(它已是世界的一部分,
- * 字典行本就该积累),**没落地的陈旧选型照撤** —— 案例 2 那条路不再炸,
- * 用户点名的那条路清得干净。撤行只在非 quiet 拍跑(人正编辑时不动他的行,F3)。
- */
- prune: true,
- from: {
- each: 'mine:sub_eval.part',
- then: { each: 'parts.chip_type', where: { type_id: { outer: 'chip_type_id' } } },
- },
- fields: {
- type_id: { col: 'type_id' },
- type_name: { col: 'type_name' },
- description: { col: 'description' },
- },
- },
- /*
- * ③ **选中类型的核心参数字段清单,同一套道理原样投一遍**(2026-08-30,与 ② 同一次
- * 发现、同一个技法):`parts.type_param` 只有人在件上「新建类型时加字段」才会写它 ——
- * 挑一个已有类型时,这张表天然一行都没有,而它此刻仍在 ⑥ 的 `gateLedgers` 上,
- * 「零行 = 没干完」的闸对这条路一样是误判(2026-08-30 真跑 e2e 五案例实测坐实:
- * 「归到已有类型」的候选缺库 / 类型不一致 / 原PN缺库 / 两颗都缺库四例,全部卡死在
- * 同一条「芯片类型核心参数字段 这一单还一行都没有」)。
- *
- * 修法与 ② 完全同构:按当下选中的 `chip_type_id` 把那个类型的核心参数字段清单
- * 整套原样抄回自己身上(值没变,幂等,不产生多余写入)——**新建的类型也走这条路**:
- * 人在件上新增的字段行本就已经在 `mine:parts.type_param` 里,这一笔重新投一遍
- * 原样命中、照样幂等跳过,不会覆盖人刚填的内容。
- *
- * **同样带 `prune`(已落地的由基座 `skipLanded` 跳过)**—— 与 ② 同一次改回、
- * 同一个理由,见 ② 头上的大注释:改选类型后,旧类型**没落地**的字段行照撤,
- * 已随数据工单确认落地的原地留下,H35 案例 2 的死锁不再触发。
- */
- {
- kind: 'project' as const,
- into: 'parts.type_param',
- prune: true,
- from: {
- each: 'mine:sub_eval.part',
- then: { each: 'parts.type_param', where: { type_id: { outer: 'chip_type_id' } } },
- },
- fields: {
- type_id: { col: 'type_id' },
- param_name: { col: 'param_name' },
- param_desc: { col: 'param_desc' },
- constraint_type: { col: 'constraint_type' },
- param_unit: { col: 'param_unit' },
- },
- },
/*
* ④ 主数据骨架:**两颗料都铺、值回填现值**。
* 在库那颗回填出它此刻的样子(人不必重填);要建那颗全空等人填。
@@ -566,7 +496,7 @@ export const ASSESSMENT_STEPS = [
then: '有料号库里查不到 —— 在下面把它的主数据建起来。',
else: {
when: 'sub_eval_part_missing > 0',
- then: '有料号还没归芯片类型 —— 在下面给它选一个;没有合适的就新建一个,并把它的核心参数字段一并定下来。',
+ then: '有料号还没归芯片类型 —— 在档案卡上给它选一个;字典里没有合适的,先请数据管理员补进类型字典(本活动只选不建)。',
else: {
when: 'sub_eval_part_distinct_chip_type_id > 1',
then: '两颗料不是同一个芯片类型,逐项比对无从谈起 —— 换一颗料,或者把归错的那颗类型改对。',
@@ -601,29 +531,18 @@ export const ASSESSMENT_STEPS = [
],
guide: [
'两颗料必须归到**同一个芯片类型**下 —— 要比的核心参数字段由类型定,类型不同就没有可比的字段。',
- '类型下拉里没有合适的,就新建一个,并把它的**核心参数字段**一并定下来:每个字段要有名字、一句说明、以及硬约束还是软约束。',
- '说明写「这个参数为什么要紧」——**它是后面判定差异时唯一的解释来源**,写成参数名的复述等于没写。',
- '硬约束 = 不满足就不能用(烧芯片、超温域、装不上);软约束 = 影响表现但可权衡(功耗、噪声)。',
+ '类型**只从字典里选,本活动不新增类型**(2026-08-31 用户定)—— 字典里没有合适的,先请数据管理员补进字典,再回来选。',
+ '硬约束 = 不满足就不能用(烧芯片、超温域、装不上);软约束 = 影响表现但可权衡(功耗、噪声)—— 字段的硬软在字典上定死,件上「比对基准」卡可查。',
'参数值填**原厂规格书上的值**,查不到就留空 —— 留空会被红带点名,但**编一个值比留空坏得多**。',
],
standard: '红带消失、提交钮变绿:两颗料都在库、归了同一个类型、每个核心参数字段都有值。',
/*
- * **`parts.chip_type` 与 `parts.type_param` 都在闸上**(2026-08-30 改,Ruling H35)——
- * 从前 `parts.chip_type` 被永久排除在外,理由是「它没有编辑手段,`mine:` 天然为空,
- * 列了就永久卡死」;那条理由**从 ②③ 两笔自我投影加上之后就不再成立**——
- * `mine:parts.chip_type`/`mine:parts.type_param` 现在恒不为空(只要 `sub_eval.part`
- * 里已经选定了类型),闸不会再误判「一行都没有」。
- *
- * **既然要落地就必须在名单里,这不是碰运气**:「数据工单确认即入库」把 `gateLedgers`
- * 列出的表整批并入正式表;`parts.chip_type` 若不在这份名单里,人新建的类型字典行
- * 就只留在草稿层,而引用它的 `parts.profile`/`parts.type_param` 却随名单一起落地 ——
- * 落地那一刻起,正式表里就有一行指着字典里查无此号的悬空引用(2026-08-30 真跑
- * e2e 撞见的平台侧连带发现:范围放行没有对 `refersTo` 做传递闭包,并一张表却不
- * 并它引用的表,必然留下悬空引用;那条更普遍的平台缺陷已记 `docs/debt.md`,
- * 这里只在活动自己的声明层堵住这一处具体触发点)。四张表都在闸上、也都会一起
- * 落地,才是「选类型」这件事完整、闭环的样子。
+ * **闸只盯人真编辑的两张表**(2026-08-31 随「只选不建」收窄):从前 `parts.chip_type`/
+ * `parts.type_param` 也在名单上 —— 那是给「人新建的类型要随确认一起落地」铺的路
+ * (Ruling H35,史见 git)。类型只许从字典里选之后,字典行本来就在正式表里,
+ * 落地无对象、mine: 恒为空 —— 留在名单上反而会被「零行 = 没干完」的闸误拦死。
*/
- gateLedgers: ['parts.chip_type', 'parts.type_param', 'parts.profile', 'parts.parameter'],
+ gateLedgers: ['parts.profile', 'parts.parameter'],
},
{ kind: 'say' as const, text: '已核对两颗料的在库状态与参数齐备情况。' },
],
diff --git a/activities/part-substitution-assessment/src/manifest.ts b/activities/part-substitution-assessment/src/manifest.ts
index b98fa94..62833ee 100644
--- a/activities/part-substitution-assessment/src/manifest.ts
+++ b/activities/part-substitution-assessment/src/manifest.ts
@@ -54,7 +54,7 @@ export const manifest: ActivityManifest = {
activityId: ACTIVITY_ID,
name: 'ACT-WORK-05 · 差异评估',
/* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */
- packageVersion: '2.9.0',
+ packageVersion: '2.10.0',
category: '需求分析与方案匹配',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-part-substitution-assessment',
@@ -568,7 +568,7 @@ export const manifest: ActivityManifest = {
* LM2596S-3.3 刻意不在库。
*/
samples: [
- { name: '候选缺库·新建类型', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'XL2596S-5.0' }, expect: { ports: ['assessment-released'] } },
+ { name: '候选缺库·建档定型', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'XL2596S-5.0' }, expect: { ports: ['assessment-released'] } },
{ name: '都在库·零卡直分析', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'LM2596S-ADJ' }, expect: { ports: ['assessment-released'] } },
{ name: '类型不一致·硬拦', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'AMS1117-3.3' }, expect: { ports: ['assessment-released'] } },
{ name: '原PN缺库', input: { originalInternalPn: 'LM2596S-3.3', candidateInternalPn: 'LM2596S-ADJ' }, expect: { ports: ['assessment-released'] } },
diff --git a/activities/part-substitution-assessment/src/views/report.ts b/activities/part-substitution-assessment/src/views/report.ts
index c7fb846..495edd6 100644
--- a/activities/part-substitution-assessment/src/views/report.ts
+++ b/activities/part-substitution-assessment/src/views/report.ts
@@ -70,16 +70,12 @@ const editGate = (address: string, label: string): string => `保存
`;
const gapbar = (object: string, what: string): string => `
- 还差 行${what}没填 —— 。按「编辑」打开,带 * 的必填格填上,补齐了才确认得了。
+ 还差 行${what}没填 —— 。打开编辑,带 * 的必填格填上,补齐了才确认得了。
`;
-const addrow = (address: string, label: string, hint: string): string => `
-
- ${hint}
-
`;
-/** 可改格:只读那一份留空,渲染器自动配孪生 ``(手写 input 会不填值,人一点编辑就是白框) */
-const cell = (col: string, cls = ''): string => ``;
-/** 只读格:即便挂在可改的表里也不许被 `table.on` 藏起来 */
-const ro = (col: string, cls = ''): string => ``;
+/** 可改格:只读那一份留空,渲染器自动配孪生 ``(手写 input 会不填值,人一点编辑就是白框)。
+ * `ph` 落成孪生件的 placeholder(渲染器认 `data-ph`,2026-08-31)—— 短短一句说清这格填什么。 */
+const cell = (col: string, cls = '', ph = ''): string =>
+ ``;
const select = (col: string, options: ReadonlyArray, required = false): string =>
``;
/**
@@ -111,9 +107,9 @@ const gateBar = (): string => `
* 收件人 / 关联任务两格由引擎注入;工单编号即本次评估案号 `eval_ref`(页脚)。
* `stage` 是每张件自己的 hero 场景(SVG 舞台),`accent` 决定这张件的强调色。
*/
-const doc = (input: { accent: string; alias: string; tag: string; kicker: string; title: string; step: string; stage: string; body: string; footTag: string }): string =>
+const doc = (input: { accent: string; alias: string; tag: string; kicker: string; title: string; step: string; stage: string; body: string; footTag: string; headTone?: 'light' }): string =>
`
-
+
`;
+/** 卡角编辑钮的三枚图标:笔(开编辑)/ 软盘(保存)/ 叉(放弃)—— 2026-08-31 用户定的笔↔软盘交互 */
+const PEN_SVG =
+ '
';
+const DISK_SVG =
+ '
';
+const X_SVG =
+ '
';
+
/* ==================== 局面:评估输入补全(input-supplement,S1 的数据工单) ==================== */
/*
@@ -191,6 +195,14 @@ ${badgeCss('.rl', [
const DOSSIERS = `
点击翻面 · 核心参数点击翻回 · 主数据
+
+
+
+
+
+
+
+
@@ -218,8 +230,8 @@ const DOSSIERS = `
${cell('param_name')}
- ${cell('param_value', 'mono')}
- ${cell('param_note')}
+ ${cell('param_value', 'mono', '规格书上的值 · 查不到留空')}
+ ${cell('param_note', '', '补充说明 · 选填')}
${cell('internal_pn', 'mono')}
@@ -254,102 +266,13 @@ const BASIS_CARD = `
`;
-/**
- * 块②a 新建芯片类型 —— `parts.chip_type`。类型下拉(块①)里没有合适的,人在这里现造一个。
- * `type_id` 是自增主键 —— **人不填它**,自增列不问人;已有行只读态摆 `ro()`。
- *
- * **种子行里 `type_id` 必须挂一个真实的(禁用、恒空)`data-col`,不能干脆不给 ——
- * 这是 2026-08-30 终审 Critical 1 的病灶与修法**:第一版把这一格写成纯文本 `
`,
- * 不带 `data-col` —— 保存时 `collect()` 只认 `input[data-col]/select[data-col]/
- * textarea[data-col]`,`type_id` 压根不出现在回传的 `cells` 里;而 `desk.ts` 的
- * `resolveRowKey` 用 `k in cells` 判「这一列人碰过没有」,漏挂在它眼里等同「这张表没有
- * 这一列」,专为「auto 列件上摆着却空着」设计的 `barred` 保护从未触发,退回「全表一致」:
- * `parts.chip_type` 恰好一行时,人新建的类型被当场判成「就是那唯一一行」,一次保存就把
- * 种子字典行**静默改名** —— `autoKeys()` 续号器永远轮不到。修法:真的
- * ``(值恒空,只显示「由系统生成」占位)。
- * 回归钉在 `__tests__/activity.test.ts` 与 `apps/api/src/app/__tests__/view-table-edit.test.ts`。
- *
- * **零行照样加得出第一行**(这一块存在的全部理由):`mine:parts.chip_type` 每单开局是
- * 零行,`ViewContainer.addRow` 靠克隆 `tr:last-of-type` 添新行,零行时无样可克隆 ——
- * 多一个不受 `data-rows` 管辖的静态 ``,字面量摆一行输入格(不经 `cell()`),
- * 它不是数据行,永远在;填了按保存,`writeEditedRows` 认得出「主键全空 + 别的列有值」
- * (`mintable`),转给 `autoKeys()` 续号。
- *
- * `data-edit="parts.chip_type"` **两处都写** —— 外层 `` 一处(给
- * `ViewContainer.collect()` 与 `.on` 切换看,要同时收到已有行与种子行两处的输入,宿主
- * 必须是两者共同的祖先),内层 `data-rows` 那个 `` 一处(`renderRows` 判「这组行
- * 可编辑吗」只认与 `data-rows` 同一个标签自带的属性)。
- *
- * **`` 排在两个 `` 之后 —— 评审逮到的 Critical 回归修法,不是随手换的顺序**
- * (2026-08-30):`bodyOf()` 解析到外层 `` 后,`addRow()` 在这个 host 里找
- * `tr:last-of-type` —— `:last-of-type` 按各自父元素判定,`` 里那个 `` 必然
- * 「最后」,若 `` 排前面会被文档序优先命中:克隆出来的是表头文字。CSS 的
- * `table-header-group` 排版规则保证 `` 视觉上永远在最前,不看 DOM 序 —— 呈现
- * 一字不变,而选择器认文档序。证据与变异验证见
- * `apps/api/src/modules/interaction/__tests__/view-render-addrow-thead.test.ts`。
+/*
+ * ~~块②a 新建芯片类型 / 块②b 核心参数字段清单~~(2026-08-31 用户定:「把添加芯片类型的
+ * 功能从这个活动中移除,不能在这个活动中新增芯片类型,只能选择已有的芯片类型」)。
+ * 两张编辑表(静态种子行、加删行、thead 后置、type_id 禁用 data-col 那整套 Critical-1
+ * 修法)与「新建类型」抽屉一并退役 —— 类型字典只在治理侧维护,本活动只读它。
+ * 机制史与钉测见 git(2.9.0 及更早)与 view-render-addrow-thead.test.ts(平台侧仍在)。
*/
-const CHIP_TYPE_TABLE = ``;
-
-/**
- * 块②b 芯片类型的核心参数字段清单 —— `parts.type_param`,复合主键 `type_id + param_name`。
- * 新建的类型在这里把核心参数字段一条条定下来(名字 / 说明 / 硬软约束 / 单位)。
- *
- * `type_id` 是与块①同款的 `optionSelect`(选项从 `parts.chip_type` 现读,人挑的是
- * **类型名称**):块②a 新建的类型一旦保存,这份下拉立刻带上它 —— 不必谁记住那个号。
- * 静态种子行与块②a 同一套手法;这张表是唯一带「+加一行」/删行的一张,零行时那颗按钮
- * 同样够不到岸,种子行同样补。
- *
- * **`` 排在两个 `
` 之后,同 `CHIP_TYPE_TABLE` 那条大注释** —— 这张表更
- * 要紧:它的「加一行」在外层 `data-edit` 挂上之前本来是好的,是一次真回归。
- */
-const TYPE_PARAM_TABLE = `
-
- |
-
-
- ${optionSelect('type_id', 'parts.chip_type', 'type_id', 'type_name', true)}
- |
- ${cell('param_name')} |
- ${cell('param_desc')} |
- ${badge('ct', 'constraint_type', true)}${select('constraint_type', [['hard', '硬约束'], ['soft', '软约束']], true)} |
- ${cell('param_unit')} |
-
-
- |
- ${optionSelect('type_id', 'parts.chip_type', 'type_id', 'type_name', true)} |
- |
- |
- |
- |
-
-
- | # |
- 所属类型 |
- 参数名 |
- 参数说明 |
- 约束(硬/软) |
- 参考单位 |
-
-
`;
export const INPUT_SUPPLEMENT = doc({
accent: ACCENT_DATA,
@@ -359,6 +282,7 @@ export const INPUT_SUPPLEMENT = doc({
title: '差异评估的输入还差几格',
step: '步骤 1/4 · 校验入参与补数据',
stage: '',
+ headTone: 'light',
footTag: '工单类型:数据',
body:
gateBar() +
@@ -367,28 +291,15 @@ export const INPUT_SUPPLEMENT = doc({
'模板定位:本单是「数据工单」——目标台账 = 芯片主数据(P-08 主记录 + 标准化参数子表),核心参数字段清单挂在芯片类型字典上。',
'触发原因:下面电路板上两张档案卡就是本次要比对的原 PN 与候选 PN —— 至少有一颗不在库、两颗归的类型不一致、或必填格还空着,顶部那条红带才会出现。',
'已绑定:两颗料的内部料号已按启动参数铺上档案卡,不必你重填料号本身,只需核对 / 补齐其余各格;在库的料现值已回填,只是查漏补缺。',
- '纪律:两颗料必须归到同一个芯片类型下才谈得上逐项比对;类型下拉里没有合适的就在下面的抽屉里新建一个,并把它的核心参数字段一并定下来;查不到的参数值留空(会被红带点名),不要编一个值顶替。',
+ '纪律:两颗料必须归到同一个芯片类型下才谈得上逐项比对;类型只从字典里选,本活动不新增类型 —— 字典里没有合适的,先请数据管理员补进字典;查不到的参数值留空(会被红带点名),不要编一个值顶替。',
]) +
`` +
- zone('比对基准', '芯片类型 + 它的核心参数字段 —— 两颗料归到哪个类型,就按这些字段逐项比;档案卡上改选,这张卡当场跟着换', ZIC_TABLE) +
- BASIS_CARD +
- zone('芯片档案', '一颗芯片一张卡:正面主数据、背面核心参数值 —— 点卡翻面看另一面', ZIC_TABLE) +
- editGate('parts.profile', '正面主数据核对 / 补齐 —— 开编辑后卡停在正面') +
- editGate('parts.parameter', '背面参数值核对 / 补齐 —— 开编辑后两张卡自动翻到背面') +
+ zone('芯片档案', '一颗芯片一张卡:正面主数据、背面核心参数值 —— 点卡翻面看另一面;右上角的笔 = 编辑当前这一面,软盘 = 保存', ZIC_TABLE) +
gapbar('parts_profile', '必填') +
gapbar('parts_parameter', '必填') +
DOSSIERS +
- gapbar('parts_chip_type', '必填') +
- gapbar('parts_type_param', '必填') +
- `类型库里没有合适的?展开新建一个类型,并把它的核心参数字段定下来 —— 定完回到上面的档案卡挑它
` +
- editGate('parts.chip_type', '新建一个类型:表尾那一行填类型名称与说明,保存后立刻出现在档案卡的下拉里') +
- CHIP_TYPE_TABLE +
- addrow('parts.chip_type', '+ 再建一个类型', '两颗料若都要新建、且类型不同,先各自保存,「加一行」能再建一个') +
- `
这个类型逐条比对时要看哪些参数 —— 名字 / 说明 / 硬软约束(*为必填列)
` +
- editGate('parts.type_param', '字段名字 / 说明 / 硬软约束,点「编辑」核对;表尾那一行 / 「加一行」补字段') +
- TYPE_PARAM_TABLE +
- addrow('parts.type_param', '+ 加一行(补一个核心参数字段)', '所属类型 / 参数名 / 参数说明 / 约束类型必须写;参考单位选填') +
- `
` +
+ zone('比对基准', '芯片类型 + 它的核心参数字段 —— 两颗料归到哪个类型,就按这些字段逐项比;字段说明悬浮即看', ZIC_TABLE) +
+ BASIS_CARD +
flow('提交 / 确认:本工单只承载内容;确认请在对话区的【执行卡】中进行。确认后活动自动从「等待数据」唤醒。'),
});
diff --git a/activities/part-substitution-assessment/src/views/style.ts b/activities/part-substitution-assessment/src/views/style.ts
index 670c701..bc34a52 100644
--- a/activities/part-substitution-assessment/src/views/style.ts
+++ b/activities/part-substitution-assessment/src/views/style.ts
@@ -110,6 +110,15 @@ i{font-style:normal}
.seal-sum{flex:1 1 240px;min-width:200px;max-width:420px;margin:0;font-size:13px;line-height:1.85;color:var(--hero-dim)}
.seal-sum:empty::after{content:"—"}
+/* ── 浅色题头变体(件① 2026-08-31 用户:「标题背景还是改成之前的那种蓝色渐变」——
+ 渐变取自原目标件 card-head 那一条,深色电路夜景只留给验收两件)── */
+.hd-light .card-head{background:linear-gradient(120deg,#f0f5ff 0%,#fff 55%,#eef4ff 100%);color:var(--ink);border-bottom:2px solid var(--ink)}
+.hd-light .hero-sys{color:var(--ink-4)}
+.hd-light .hero-title{color:var(--ink)}
+.hd-light .hero-step{color:var(--ink-3);border-color:var(--line)}
+.hd-light .hero-meta{color:var(--ink-3)}
+.hd-light .hero-meta b{color:var(--ink)}
+
/* ================= 正文分区 ================= */
.card-bd{padding:20px 22px 8px}
.sec{display:flex;align-items:baseline;gap:9px;margin:26px 0 10px;flex-wrap:wrap}
@@ -324,6 +333,23 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
.dossiers:not(.on) .dossier:hover:has(.flip-tg:checked):not(:has(.pback.on)) .flip-in{transform:rotateY(168deg)}
}
+/* ── 卡角编辑钮:笔=编辑当前这一面,软盘=保存,叉=放弃(2026-08-31 用户定,顶掉两行编辑闸)──
+ 笔只在待处理态露面(html.editable,与从前的闸同一条纪律);软盘/叉只在对应面的编辑态露面。 */
+.fabs{position:absolute;top:10px;right:10px;z-index:5;display:flex;gap:6px}
+.fab{display:none;width:32px;height:32px;border-radius:999px;border:1px solid var(--acc-line);background:#fff;color:var(--acc-deep);cursor:pointer;place-items:center;padding:0;box-shadow:0 4px 12px rgba(18,38,63,.14);transition:border-color .16s,color .16s,box-shadow .16s}
+.fab:hover{border-color:var(--acc);box-shadow:0 6px 16px rgba(18,38,63,.22)}
+.fab:active{transform:scale(.94)}
+.fic{width:15px;height:15px;display:block;margin:auto}
+.fab.pen{color:var(--acc-deep)}
+.fab.disk{background:var(--acc);border-color:var(--acc);color:#fff}
+.fab.xbtn{color:var(--ink-3)}
+html.editable .dossiers:not(.on) .dossier:not(:has(.pback.on)):not(:has(.flip-tg:checked)) .pen-f{display:grid}
+html.editable .dossiers:not(.on) .dossier:has(.flip-tg:checked):not(:has(.pback.on)) .pen-b{display:grid}
+.dossiers.on .disk-f{display:grid}
+.dossiers.on .x-f{display:grid}
+.dossier:has(.pback.on) .disk-b{display:grid}
+.dossier:has(.pback.on) .x-b{display:grid}
+
/* ── 卡背:参数瓷贴 ── */
.pb-hd{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap}
.pb-hd b{font-size:14px;font-weight:800;color:var(--ink)}
@@ -337,7 +363,8 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
.pt-note .jc-vcell{font-size:10.5px;color:var(--ink-3)}
/* 三级占位:值必填红 / 说明建议橘 */
.ptile .pv .jc-vcell:empty::after{content:"必填";font-size:10.5px;font-weight:800;color:var(--danger);background:var(--danger-soft);border-radius:999px;padding:0 9px}
-.ptile .sg .jc-vcell:empty::after{content:"建议";font-size:10px;font-weight:800;color:var(--amber);background:var(--amber-soft);border-radius:999px;padding:0 8px}
+/* 说明空着**什么都不摆**(2026-08-31 用户:「没写就什么都不要展示」)—— 连兜底破折号一起收 */
+.ptile .sg .jc-vcell:empty{display:none}
/* 参数编辑态:值与说明换输入,参数名照旧当标签摆着;两枚主键孪生件藏死(钥匙不是编辑面)。
藏钥匙用包装类(.pt-name / .rowkey),不用「data-col 属性选择器」—— 那段选择器文本会原样
出现在共享样式表里,把「落账确认全只读」那类字面断言踩翻(2026-08-31 踩过,连注释也不许写)。 */
@@ -359,13 +386,19 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
.basis-hd .csvg{width:56px;flex:none}
.basis-id{display:flex;flex-direction:column;gap:5px;min-width:0;flex:1 1 auto}
.basis-in:has(.ftiles:empty) .ftiles{display:none}
-.ftiles{display:grid;grid-template-columns:1fr 1fr;gap:8px}
-.ftile{display:flex;flex-direction:column;gap:3px;border:1px solid var(--line-soft);border-radius:10px;background:#fff;padding:9px 11px;min-width:0}
-.ft-hd{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap}
-.ft-hd b{font-size:12.5px;font-weight:800;color:var(--ink)}
+/* 降噪(2026-08-31 用户:「乱糟糟…没必要显示全就不要显示完全,鼠标指上去后再显示」):
+ 字段收成一排丸(名 + 硬/软徽 + 单位),说明藏进悬浮气泡 —— 指上去才现身 */
+.ftiles{display:flex;flex-wrap:wrap;gap:8px}
+.ftile{position:relative;display:flex;flex-direction:row;align-items:center;gap:7px;border:1px solid var(--line-soft);border-radius:999px;background:#fff;padding:6px 13px;min-width:0;transition:border-color .16s,box-shadow .16s}
+.ftile:hover{border-color:var(--acc-line);box-shadow:0 4px 12px rgba(18,38,63,.1)}
+.ft-hd{display:flex;align-items:baseline;gap:7px;white-space:nowrap}
+.ft-hd b{font-size:12px;font-weight:800;color:var(--ink)}
.ft-unit{font-style:normal;font-size:10px;color:var(--ink-4);font-family:var(--mono)}
-.ft-desc{margin:0;font-size:11px;line-height:1.6;color:var(--ink-3)}
-.ft-desc:empty{display:none}
+.ft-desc{display:none;position:absolute;z-index:6;top:calc(100% + 7px);left:0;width:max-content;max-width:280px;margin:0;font-size:11px;line-height:1.65;color:var(--ink-2);background:#fff;border:1px solid var(--line);border-radius:9px;padding:8px 11px;box-shadow:0 10px 24px rgba(18,38,63,.16)}
+.ftile:hover .ft-desc:not(:empty){display:block}
+/* 类型说明:默认一行截断,悬浮题头才放开 */
+.basis-desc{display:block;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.basis-hd:hover .basis-desc{white-space:normal}
/* ================= 新建类型工作抽屉(details,零脚本)================= */
.lab{margin:0 0 6px;border:1.5px dashed var(--line);border-radius:12px;background:var(--soft)}
@@ -377,7 +410,6 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
.lab-caret{margin-left:auto;flex:none;width:8px;height:8px;border-right:2px solid var(--ink-4);border-bottom:2px solid var(--ink-4);transform:rotate(45deg);transition:transform .16s}
.lab[open] .lab-caret{transform:rotate(-135deg)}
.lab-bd{padding:2px 14px 14px}
-.lab-sub{margin:16px 0 8px;font-size:11.5px;color:var(--ink-3)}
.lab-brief{margin:0 0 16px}
.lab-brief .brief{margin:2px 14px 14px;border-left-width:3px}
diff --git a/apps/api/src/modules/interaction/__tests__/view-render.test.ts b/apps/api/src/modules/interaction/__tests__/view-render.test.ts
index a937a8c..f338772 100644
--- a/apps/api/src/modules/interaction/__tests__/view-render.test.ts
+++ b/apps/api/src/modules/interaction/__tests__/view-render.test.ts
@@ -542,3 +542,17 @@ describe('data-options:下拉的选项从台账现读', () => {
expect(out).toContain('');
});
});
+
+describe('孪生件占位提示:data-ph 落成 input 的 placeholder(2026-08-31,W05 参数瓷贴)', () => {
+ it('带 data-ph 的可改格,孪生 input 有 placeholder;不带的没有', () => {
+ const html =
+ '';
+ const out = renderView(html, { 'mine:parts.parameter': [{ param_value: '', param_note: '' }] });
+ expect(out).toContain('placeholder="规格书上的值,查不到留空"');
+ const notePart = out.slice(out.indexOf('data-col="param_note"'));
+ expect(notePart.slice(0, notePart.indexOf('/>'))).not.toContain('placeholder=');
+ });
+});
diff --git a/apps/api/src/modules/interaction/internal/domain/view-render.ts b/apps/api/src/modules/interaction/internal/domain/view-render.ts
index 4a0087e..08069a5 100644
--- a/apps/api/src/modules/interaction/internal/domain/view-render.ts
+++ b/apps/api/src/modules/interaction/internal/domain/view-render.ts
@@ -263,7 +263,9 @@ function twinInputs(inner: string, rowIndex: number): string {
.split(/\s+/)
.filter((c) => c === 'mono' || c === 'num')
.join(' ');
- const input = ``;
+ /* `data-ph`:模版给这一格的输入孪生件配一句占位提示(2026-08-31,W05 参数瓷贴要「短短一句说清这格填什么」) */
+ const ph = /\bdata-ph\s*=\s*"([^"]*)"/.exec(attrs)?.[1];
+ const input = ``;
// 只读那一份把 `data-col` 交出去 —— 它的活已经干完(告诉这里是哪一列),留着只是噪音
return `${all.replace(/\s*\bdata-col\s*=\s*"[^"]*"/, '')}${input}`;
});
diff --git a/apps/web/src/features/desk-task/internal/chat-bubbles.tsx b/apps/web/src/features/desk-task/internal/chat-bubbles.tsx
index a7e1d43..6da1dee 100644
--- a/apps/web/src/features/desk-task/internal/chat-bubbles.tsx
+++ b/apps/web/src/features/desk-task/internal/chat-bubbles.tsx
@@ -1051,7 +1051,8 @@ function ExecForm({ order, onReplyOrder, onLocate, orderValues, onField, face =
{order.ask}
- {order.basis.length > 0 && }
+ {/* 参考信息不再上卡(2026-08-31 用户定:「派发的工单参考信息这个板块不要显示」)——
+ basis 数据照旧随卡走(工单详情 / 归档件仍有),只是对话区这张卡不摆 */}
{order.fields.map((f) => (