diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts
index 57825e8..ecb11ef 100644
--- a/activities/part-substitution-assessment/__tests__/activity.test.ts
+++ b/activities/part-substitution-assessment/__tests__/activity.test.ts
@@ -185,15 +185,13 @@ describe('案例模型与清单的对齐', () => {
for (const name of mentioned) expect(ITEMS, `说明书提到但不存在的计划项: ${name}`).toContain(name);
});
- it('两种决策卡骨架都在说明书里:候选放弃/创建(create/abandon)与审核(approve/reject/custom:)', () => {
+ it('说明书里只剩审核那一种决策卡(approve/reject/custom:);候选放弃/创建那张已退役 —— 缺库一律自动派数据工单(2026-08-27 用户定)', () => {
const n = manifest.assets.businessModel.narrative;
- expect(n).toContain('"kind":"decide","itemId":"S0d_p08_master"');
- expect(n).toContain('"value":"create"');
- expect(n).toContain('"value":"abandon"');
- expect(n).toContain('"kind":"decide","itemId":"S4_review"');
- expect(n).toContain('"approve"');
- expect(n).toContain('"reject"');
- expect(n).toContain('"custom:"');
+ expect(n).not.toContain('"value":"create"');
+ expect(n).not.toContain('"value":"abandon"');
+ expect(n).toContain('"value":"approve"');
+ expect(n).toContain('"value":"reject"');
+ expect(n).toContain('custom:');
});
it('T_release 剧本里 judge criterion 的原文逐字对得上 acceptanceCriteria 的 label', () => {
diff --git a/activities/part-substitution-assessment/__tests__/steps.test.ts b/activities/part-substitution-assessment/__tests__/steps.test.ts
index 6a4c220..5c5e9fa 100644
--- a/activities/part-substitution-assessment/__tests__/steps.test.ts
+++ b/activities/part-substitution-assessment/__tests__/steps.test.ts
@@ -139,7 +139,9 @@ describe('doneWhen —— 开局不许真,干完必须真', () => {
describe('说明书与剧本的骨架', () => {
it('五张卡的骨架都住在说明书里(压拍之后只有说明书还在)', () => {
expect(NARRATIVE).toContain('"kind":"entry","itemId":"S0_bind"');
- expect(NARRATIVE).toContain('"kind":"decide","itemId":"S0d_p08_master"');
+ expect(NARRATIVE).not.toContain('"kind":"decide","itemId":"S0d_p08_master"');
+ // 两张数据工单都得标 variant:"data" —— 对话区凭它换成「数据工单」那副面孔
+ expect(NARRATIVE.match(/"variant":"data"/g)?.length).toBe(2);
expect(NARRATIVE).toContain('"title":"数据工单 · 芯片主数据创建(P-08)');
expect(NARRATIVE).toContain('"title":"数据工单 · 原厂技术资料创建(P-10)');
expect(NARRATIVE).toContain('"title":"评估输入与资料补齐确认');
@@ -166,22 +168,21 @@ describe('说明书与剧本的骨架', () => {
expect(secondPass).toMatch(/收不了步|收不掉/);
});
- it('S0 剧本写明都在库时数据工单两项本就不会进「当下可为」;S0d_p08_master 剧本按候选总数判断该不该问人(=1 必建、≥2 才问、原 PN 硬阻塞)', () => {
+ it('S0 剧本写明都在库时数据工单两项本就不会进「当下可为」;S0d_p08_master 剧本写明 found 的一颗都不建', () => {
const s0 = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? '';
expect(s0).toContain('当下可为');
- expect(s0).toContain('不要对计划项用');
+ expect(s0).toMatch(/都在库/);
const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
- expect(s0d).toContain('硬阻塞');
- expect(s0d).toContain('候选总数 = 1');
+ expect(s0d).toContain('absent');
+ expect(s0d).toMatch(/found" 的一行都不建/);
});
- it('S0d_p08_master 剧本按候选总数(不是「不在库的颗数」)判断要不要问人 —— 候选总数 ≥2 时逐颗不在库的都要问(2026-08-27 实测逼出的措辞修正)', () => {
+ it('S0d_p08_master:查无记录一律派数据工单,不问人 —— 决策卡退役(2026-08-27 用户定:「只要有缺失就要自动派数据工单不需要人选择」)', () => {
const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
- expect(s0d).toContain('候选总数');
- expect(s0d).toContain('不在库');
- expect(s0d).toMatch(/候选总数\s*≥\s*2/);
- // 反面提示:「不在库的只有一颗」不该被读成跳过决策卡的理由
- expect(s0d).toContain('不是跳过决策卡的理由');
+ expect(s0d).toMatch(/一律建,不问人/);
+ expect(s0d).toMatch(/exists_status = "found" 的一行都不建/);
+ expect(s0d).not.toMatch(/发一张决策卡问人/);
+ expect(s0d).not.toMatch(/候选总数\s*≥\s*2/);
});
it('s_bound 哨兵真的检查了「有料号不在库」这件事(都在库时门不该开;退回旧写法会红)', () => {
@@ -208,11 +209,6 @@ describe('说明书与剧本的骨架', () => {
expect(p10?.doneWhen).toBeUndefined();
});
- it('候选放弃到只剩最后一颗时不许再放弃(至少留一颗,防止全放弃死锁)', () => {
- const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
- expect(s0d).toContain('至少留一颗候选');
- });
-
it('P-08 交回之后先 finish 再发 P-10 卡,顺序写死进两段 guide(2026-08-27 实测:漏收步会让工作区画面停在 P-08 那两张表,P-10 卡发出后人却看不见资料表、提交闸拦死)', () => {
const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(s0d).toContain('finish S0d_p08_master');
@@ -239,8 +235,8 @@ describe('说明书与剧本的骨架', () => {
it('数据工单的名单只认 absent,「建哪几颗」与「要不要问人」分开写(2026-08-27 实测:原 PN 缺库那单,模型把在库的候选派成了数据工单,缺的那颗没人建)', () => {
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
- expect(g).toMatch(/问题一 · 建哪几颗/);
- expect(g).toMatch(/问题二 · 建之前要不要先问人/);
+ expect(g).toMatch(/【名单】/);
+ expect(g).toMatch(/一律建,不问人/);
expect(g).toMatch(/exists_status = "found" 的一行都不建/);
});
@@ -300,8 +296,8 @@ describe('说明书与剧本的骨架', () => {
it('第二颗缺库的候选也要先问(每一颗都有被放弃的权利)', () => {
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
- expect(g).toMatch(/这一颗也要先发一张决策卡问人/);
- expect(g).toMatch(/第一颗问过了不代表第二颗也问过/);
+ expect(g).toMatch(/不问人,直接建/);
+ expect(g).not.toMatch(/这一颗也要先发一张决策卡问人/);
});
it('created 认的是「卡交回来了」,不是「表里有行」—— 写骨架 ≠ 建好了(2026-08-27 e2e:模型铺完骨架就标 created、直接 finish,撞「从没给它发过卡」连拒五拍)', () => {
@@ -311,6 +307,23 @@ describe('说明书与剧本的骨架', () => {
expect(g).not.toMatch(/只有它在 \\`parts\.profile\\` 里真有一行/);
});
+ it('件上三处样式毛病钉死(2026-08-27 用户:表显示不全不能右滑 / 编辑态内部料号只读字与输入框并排 / input 与 select 没对齐)', () => {
+ const html = manifest.assets.viewTemplates.find((v) => v.name === 'p08-master-create')?.html ?? '';
+ expect(html).toContain('.jc-tbl-wrap { overflow-x: auto;');
+ expect(html).toContain('#tbl1.on td > .jc-vcell:not(.ro) { display: none; }');
+ expect(html).toContain('input.jc-cell, select.jc-cell { height: 32px;');
+ // 徽标不再包 .ro(编辑态要随只读格一起藏,只留下拉框)
+ expect(html).not.toContain(' {
+ for (const name of ['p08-master-create', 'p10-document-create'] as const) {
+ const html = manifest.assets.viewTemplates.find((v) => v.name === name)?.html ?? '';
+ expect(html, name).not.toContain('class="thd"');
+ for (const th of html.match(/
]*>[^<]*<\/th>/g) ?? []) expect(th, name).not.toMatch(/[a-z]_[a-z]/);
+ }
+ });
+
it('五段进度,每段名 ≤10 字;除放行外每项恰属一段', () => {
expect(ASSESSMENT_PHASES).toHaveLength(5);
for (const p of ASSESSMENT_PHASES) expect(p.name.length, p.name).toBeLessThanOrEqual(10);
diff --git a/activities/part-substitution-assessment/src/case-model.ts b/activities/part-substitution-assessment/src/case-model.ts
index 6d0dd6f..25e2dd9 100644
--- a/activities/part-substitution-assessment/src/case-model.ts
+++ b/activities/part-substitution-assessment/src/case-model.ts
@@ -196,8 +196,8 @@ export const NARRATIVE = `§0 开案:**判断候选料号在技术上能不能
**存在性检查与数据工单**:启动时逐个料号在 \`parts.profile\` 里查(原 PN + 每个候选)。查无记录 = 「完全没有」,
要人从零建整条主数据(表 1 主记录 + 表 2 标准化参数子表 \`parts.parameter\`)—— 发**数据工单**(一张单:P-08 一张、P-10 一张);
-PN 在库但个别字段缺 = 「有而不全」,归执行工单 01 补。**该不该问人,按候选总数判断,不是按不在库的颗数判断**:
-原 PN 缺 = 硬阻塞必须建、不问;候选总数 = 1 时那一颗必须建、不问;候选总数 ≥ 2 时,每一颗不在库的候选都要先问人放弃还是创建。
+PN 在库但个别字段缺 = 「有而不全」,归执行工单 01 补。**查无记录的一律派数据工单,不问人**(2026-08-27 用户定:
+「只要有缺失就要自动派数据工单,不需要人选择」)—— 原 PN 缺是硬阻塞、候选缺同样直接建,**没有「放弃这颗候选」这个岔路口**。
**库里都有的话这两项的入场前提本就不成立**(此单不走这条),不会出现在你的「当下可为」里,别发空单。
**六步走**:绑定与存在性核对 → (等待数据)→ 人补评估输入 → 标准化对照表 → 逐项评估 → 人审结论(可循环)→ 拟落账 → 人放行。
@@ -219,20 +219,14 @@ PN 在库但个别字段缺 = 「有而不全」,归执行工单 01 补。**该
把九个计划项的全名摆到人脸上,他一个字也用不上(2026-08-27 实测)。
人确认那一下基座已把这一行落进草稿层,**你不许再 write 一遍 customer.profile**。
-② 候选不在库、且候选总数 ≥2 时,一颗一张决策卡(按候选数组顺序,一次只发一张;候选总数 = 1 时不问、直接建):
-{"kind":"decide","itemId":"S0d_p08_master",
- "ask":"候选 在芯片主数据里查无记录。是从零建它的主数据(派数据工单),还是本轮放弃这颗候选?",
- "options":[{"value":"create","label":"创建主数据(派数据工单)","recommended":true},
- {"value":"abandon","label":"放弃这颗候选"}],"allowCustom":true}
-\`value\` 只有 \`"create"\` / \`"abandon"\` / \`"custom:"\` 开头三种:\`"custom:"\` 开头按放弃处理、\`note\` 就是放弃原因;
-点了 \`"abandon"\` 没写原因,\`abandon_reason\` 写「负责人本轮放弃(未注明原因)」。
+② (已退役)候选缺库不再问人 —— 2026-08-27 用户定「只要有缺失就自动派数据工单」;所有查无记录的料号直接进③那张卡。
③ 数据工单 · P-08(所有要建的料号一起,一张单;零输入,填报全在工作区的件上)——
**发这张卡之前,\`parts.profile\` 与 \`parts.parameter\` 必须先有骨架行**(名单里每颗料各一行 + 它的参数各一行)。
不管这一拍是不是顺手把上一步收掉的(压拍常发生,那时这一步的剧本还没摊开),写骨架与发卡都得在同一串里,顺序不能反 ——
件上一行都没有的话,人打开是一张空表、什么也补不了,而这一单的主数据从头到尾不会存在
(实测走到 S2 才被引用检查拦下,那时两张数据工单都已经「办完」了)
-{"kind":"human","itemId":"S0d_p08_master","workOrder":"execution","card":{
+{"kind":"human","itemId":"S0d_p08_master","workOrder":"execution","card":{"variant":"data",
"title":"数据工单 · 芯片主数据创建(P-08):<料号列表,顿号分隔>",
"ask":"这些料号在芯片主数据里查无记录,请在工作区的件上把整条记录建齐(一颗一行,可加行)。",
"steps":["表 1 每一行:品牌、规格描述、封装、生命周期状态为必填,缺一格确认不了",
@@ -251,7 +245,7 @@ PN 在库但个别字段缺 = 「有而不全」,归执行工单 01 补。**该
**与③同一条:发这张卡之前 \`parts.document\` 必须先有骨架行**(名单里每颗料至少一行,
key 是 {internal_pn, doc_seq:"01"});一行都没有的话人打开是空表、什么也补不了,
提交闸会如实拦住(「这一单还一行都没有」),这一单就卡在这儿了(2026-08-27 实测)。
-{"kind":"human","itemId":"S0d_p10_document","workOrder":"execution","card":{
+{"kind":"human","itemId":"S0d_p10_document","workOrder":"execution","card":{"variant":"data",
"title":"数据工单 · 原厂技术资料创建(P-10):<料号列表>",
"ask":"请把这些料号的 datasheet / 承诺函等资料记录补进原厂技术资料库(一份资料一行,可加行)。",
"steps":["资料类型、标题、版本、文件链接、资料状态为必填;证据定位写页码或章节,差异评估的证据引用会指向它",
@@ -357,7 +351,7 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不
},
{
item: 'S0d_p08_master',
- intent: '料号不在库:派 P-08 数据工单让人从零建芯片主数据 + 标准化参数子表;候选总数 ≥2 才逐颗问放弃还是创建',
+ intent: '料号不在库:派 P-08 数据工单让人从零建芯片主数据 + 标准化参数子表 —— 查无记录一律派,不问人',
writes: ['parts.profile', 'parts.parameter', 'parts.document', `${D}.pn_check`],
reads: [`${D}.pn_check`, 'candidateInternalPns', 'originalInternalPn', 'datasheets', 'parts.profile', 'parts.parameter', 'parts.document'],
view: 'p08-master-create',
@@ -365,25 +359,11 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不
/* 触达人,不给 doneWhen;人工激活:发卡就算启动 */
guide: `这一项只在 \`${D}.pn_check\` 里有 absent 行时走 —— 都在库时入场前提本就不成立,这一项与下一项 S0d_p10_document 都不会进你的「当下可为」。**先分岔,再动作**:
-【先数两个数,再分岔】(先看轨迹:这一项里已经发过哪些卡、人答了什么,【当前这一步】下面那行引擎事实说得清楚)
-· **候选总数**:入参 \`candidateInternalPns\` 的长度(\`${D}.pn_check\` 里 role = "candidate" 的行数也是它,不随本单进展变化);
-· **不在库的候选数**:\`${D}.pn_check\` 里 role = "candidate" 且 exists_status = "absent" 的行数(这个数会随着人放弃/创建而变小)。
-**这里要分开答两个问题,别混成一个**:
-
-**问题一 · 建哪几颗?** 名单 = \`${D}.pn_check\` 里 **exists_status = "absent" 的每一行**,不管它是原 PN 还是候选。
+【名单】= \`${D}.pn_check\` 里 **exists_status = "absent" 的每一行**,不管它是原 PN 还是候选 —— **一律建,不问人**
+(2026-08-27 用户定:只要有缺失就自动派数据工单;从前那张「派数据工单还是放弃这颗候选」的决策卡已退役,别再发)。
**exists_status = "found" 的一行都不建** —— 那几颗的主数据本来就在库里,给它们派数据工单等于让人把已有的东西再填一遍,
而真正缺的那颗还是没人建、单子照样卡死。名单为空(一行 absent 都没有)这一项根本不会出现在你的「当下可为」里。
-
-**问题二 · 建之前要不要先问人?** 只有 **absent 的候选**才谈得上问,**看的是候选总数,不是不在库的颗数**
-——**「不在库的只有一颗」不是跳过决策卡的理由**:
-· 原 PN absent → **硬阻塞**,不问人,直接进名单(它是评估基准,没有它整单无从谈起);
-· **候选总数 = 1** → 那一颗候选**如果是 absent** 就不许放弃、不问人,直接进名单(候选是 found 的话它压根不在名单里,没有可问的);
-· **候选总数 ≥ 2** → **每一颗 absent 的候选都要先发一张决策卡问人**(一次一张,按候选数组顺序;哪怕这一单不在库的候选只有一颗,
- 只要候选总数 ≥ 2 就要问)照说明书②;人选 "abandon"/"custom:" 的,把那一行 \`${D}.pn_check\` 改成
- exists_status = "abandoned" + abandon_reason(同 PK 覆盖写),这颗不再评估;选 "create" 的进「建」的名单。
-**至少留一颗候选**:放弃到只剩最后一颗还是 absent 的候选时,那一颗按「候选总数 = 1」办 —— 不再发决策卡问,直接进「建」的名单
-(候选全被放弃,存在性核对永远凑不齐、单子会死锁,这一条就是防它)。
-**别把同一颗候选问两遍**:决策卡答过就按答案办。
+**名单里的料号全放进同一张 P-08 卡**(标题列全料号,件上一颗一行,参数各归各的料号)—— 别一颗一张。
【「建」的名单定了 → 写骨架行,发 P-08 数据工单(一张单,件上两张表)】
1. 给名单里每颗料写一行 \`parts.profile\` 骨架(表 1):key {internal_pn},patch 只带你确有依据的列 —— 有 \`datasheets[].text\` 就从里面读
@@ -414,11 +394,8 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不
门上还有一条逐颗对账(建好几颗就得有几行主数据、几行资料),标了却没建,这一步照样过不去。
【P-08 那张确认回来了】**先看名单里还有没有 absent 的料号**(\`${D}.pn_check\` 现读):
-· **还有** → **这一项千万别 finish**。先回到上面【问题二】那一档:候选总数 ≥2 的话,
- **这一颗也要先发一张决策卡问人**(照说明书②,人选 create 才建 —— 每一颗 absent 的候选都有被放弃的权利,
- 第一颗问过了不代表第二颗也问过);人选 create 之后再把它的骨架写好(profile + parameter,有 datasheet 就预填),
- 发一张 P-08 卡(标题带那一颗的料号)—— 同一个计划项可以先后发好几张卡,一颗一张也行。
- 原 PN 与「候选总数 = 1」那两档不问,直接建。
+· **还有** → **这一项千万别 finish**。把还 absent 的那几颗的骨架写好(profile + parameter,有 datasheet 就预填),
+ 再发一张 P-08 卡(标题列上它们的料号)—— 不问人,直接建;同一个计划项可以先后发好几张卡。
**收掉了就回不去了**:这一项一旦 Completed,再想给它挂第二张卡会被拒「这一步已经办完收掉了」,
而那颗料从此没人建,后面写替代关系时被引用检查逐颗拦下(2026-08-27 实测,一单连拒四次)。
· **一颗都不剩了** → 才走下面这两步。
diff --git a/activities/part-substitution-assessment/src/manifest.ts b/activities/part-substitution-assessment/src/manifest.ts
index dd4d2a9..afbc7b1 100644
--- a/activities/part-substitution-assessment/src/manifest.ts
+++ b/activities/part-substitution-assessment/src/manifest.ts
@@ -89,7 +89,7 @@ export const manifest: ActivityManifest = {
identity: {
activityId: ACTIVITY_ID,
name: 'ACT-WORK-05 · 差异评估',
- packageVersion: '1.3.6',
+ packageVersion: '1.4.0',
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 d3b355e..45e0520 100644
--- a/activities/part-substitution-assessment/src/views/report.ts
+++ b/activities/part-substitution-assessment/src/views/report.ts
@@ -29,6 +29,14 @@ import { D } from '../constants.ts';
* 绑定层公共开关 —— 五张模版共用,与目标件自己的样式表分开存(那份要保持逐字节可比)
* ==================================================================================== */
const BIND_COMMON = `
+/* ── 2026-08-27 用户四连指出的件上毛病,病根三处都在「目标件的样式与平台的编辑态没对上」 ── */
+/* ① 表宽超出就得能横着滑(E04/I01 08-23 同款):目标件把 overflow-x:auto 写在 .tbl-wrap 上,而平台包装是 .jc-tbl-wrap,谁都没管到 */
+.jc-tbl-wrap { overflow-x: auto; overflow-y: hidden; }
+/* ② 编辑态只留输入格:目标件 #tbl1 .jc-vcell{display:inline}(id 特异度)盖过了 table.on td>.jc-vcell{display:none},
+ 于是表 1 每格都是「只读字 + 输入框」并排 —— 内部料号那格恰好有字,人看见的就是它 */
+#tbl1.on td > .jc-vcell:not(.ro) { display: none; }
+/* ③ 一行里的 input 与 select 同高同线:select 天生比 input 矮 4px,编辑态里一排格子高低不齐 */
+input.jc-cell, select.jc-cell { height: 32px; box-sizing: border-box; padding-top: 0; padding-bottom: 0; vertical-align: middle; }
/* 只在「待处理」这一态露面;归档 / 验收只读态即便 HTML 里还带着 data-edit 也不摆闸和加行钮
(目标件是静态 demo,不区分这两态;html.editable 是平台在 iframe 根节点上真开的关) */
html:not(.editable) .jc-editgate, html:not(.editable) .jc-addrow { display: none !important; }
@@ -305,10 +313,14 @@ const cell = (col: string, cls = ''): string => ``;
const select = (col: string, options: ReadonlyArray, required = false): string =>
``;
-/** 枚举徽标(可改表里用,旁边挨着 `.jc-cell` 孪生件时要包一层 `.ro` 才不被编辑态藏起来) */
-const badge = (klass: string, col: string, wrapRo = false): string => {
+/**
+ * 枚举徽标。可改表里挨着 ` |