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 => { +/** + * 枚举徽标。可改表里挨着 `