From 59afcbfdbd176baa15f403db4ddb3f9f364b46b8 Mon Sep 17 00:00:00 2001 From: xcs Date: Sat, 29 Aug 2026 19:50:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(w05):=20=E4=BB=B6=E2=91=A1=20=E5=B7=AE?= =?UTF-8?q?=E5=BC=82=E7=BB=93=E8=AE=BA=E5=AE=A1=E6=A0=B8=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E5=8F=AF=E6=94=B9=E7=9A=84=E6=98=AF=E5=9B=9B?= =?UTF-8?q?=E6=80=81,=E6=91=98=E8=A6=81=E4=B8=8E=E7=AD=89=E7=BA=A7?= =?UTF-8?q?=E5=8F=AA=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 推荐理由 / 摘要 / 风险 / 验证要求都是规则算出来的:让人直接改它们, 等于让他给自己没做的判断盖章。要改就改上面那一条四态,系统重算。 同批修一条与本次设计矛盾的既有断言:「差异审核与落账确认只读」原把 difference-review 也算只读,而 spec §4.4/§6.2 已把 S3 改成可就地编辑 ——改窄成只管 landing-confirm,differerence-review 的只读面改由新增的 「件②」测试组精确覆盖。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet --- .../__tests__/activity.test.ts | 63 ++++- .../src/case-model.ts | 5 + .../src/manifest.ts | 13 +- .../src/views/report.ts | 246 +++++++++++++++++- 4 files changed, 313 insertions(+), 14 deletions(-) diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts index 39a4a42..5c6dbbe 100644 --- a/activities/part-substitution-assessment/__tests__/activity.test.ts +++ b/activities/part-substitution-assessment/__tests__/activity.test.ts @@ -351,16 +351,25 @@ describe('报告模版:编辑面与只读面各就各位', () => { expect(html).toContain(`data-rows="mine:${D}.pn_check"`); }); - it('差异审核与落账确认只读:没有任何 data-edit / data-col', () => { - for (const name of ['difference-review', 'landing-confirm']) { - expect(viewHtml(name), name).not.toContain('data-edit='); - expect(viewHtml(name), name).not.toContain('data-col='); - } + /* + * 2026-08-29 Task 8 改窄:这条原本把 `difference-review` 也算作「只读」,与 `landing-confirm` + * 并列断言零 `data-edit`/`data-col`。而 spec §4.4/§6.2(同一次重写的权威设计,同日写定) + * 明确把 S3 从纯只读改成「人就地改四态,配方重算」—— brief 给的 Step 1 测试(见下面 + * 「件② 差异评估结论审核」组)直接要求 `data-edit="substitution.difference_item"` 与 + * `data-col="difference_status"` 等必须存在,与这条断言原样矛盾。这条断言写在那处设计 + * 定稿之前(或是与 landing-confirm 一起想当然地假设了两者都只读),现改窄成只管 + * `landing-confirm`(它确实全只读,spec §6.3)。`difference-review` 的「只读面在哪」 + * 改由「件②」组精确覆盖(哪几列可改、`substitution.relation` 那半张不许有 data-edit)。 + */ + it('落账确认只读:没有任何 data-edit / data-col(差异审核已按 spec §4.4 变为可就地编辑,断言见「件②」组)', () => { + expect(viewHtml('landing-confirm')).not.toContain('data-edit='); + expect(viewHtml('landing-confirm')).not.toContain('data-col='); }); - it('每个可编辑格子的 data-col 都在对应台账对象的声明里(input-supplement 一屏三张表,三张台账对象的声明并起来看)', () => { + it('每个可编辑格子的 data-col 都在对应台账对象的声明里(input-supplement 一屏三张表,三张台账对象的声明并起来看;difference-review 一张表)', () => { const cases: Array<[string, Array<[string, string]>]> = [ ['input-supplement', [['parts', 'profile'], ['parts', 'type_param'], ['parts', 'parameter']]], + ['difference-review', [['substitution', 'difference_item']]], ]; for (const [view, objects] of cases) { const cols = new Set([...viewHtml(view).matchAll(/data-col="([^"]+)"/g)].map((m) => m[1] ?? '')); @@ -478,6 +487,48 @@ describe('件① 评估输入补全', () => { }); }); +/* + * 件② 差异评估结论审核(2026-08-29,Task 8):S3 的验收工单,`view: 'difference-review'` + * 指向它,`cardVariant: 'review'`。绑定契约(spec §6.2):对决头(原 PN → 候选 PN、类型徽)+ + * 四态分布带 + 推荐等级大徽章只读;逐条明细可就地改(四态 / 影响说明 / 判断依据 / UNKNOWN 去向); + * 收口四格(推荐理由 / 差异摘要 / 风险 / 验证要求)只读 —— 它们是配方按规则算的,人要改就去改 + * 上面那一条四态,系统重算。复用文件顶部已有的 `viewHtml` 短写。 + */ +describe('件② 差异评估结论审核', () => { + const html = viewHtml('difference-review'); + + it('在 viewTemplates 里,S3 指向它,面孔是审核卡', () => { + expect(html).not.toBe(''); + const s3 = (manifest.assets.businessModel.steps as ReadonlyArray<{ item: string; view?: string; cardVariant?: string }>) + .find((s) => s.item === 'S3_review'); + expect(s3?.view).toBe('difference-review'); + expect(s3?.cardVariant).toBe('review'); + }); + + it('逐条明细可改,且**改的是四态**', () => { + expect(html).toContain('data-rows="mine:substitution.difference_item"'); + expect(html).toContain('data-edit="substitution.difference_item"'); + expect(html).toContain('data-col="difference_status"'); + for (const col of ['impact_statement', 'evidence_note', 'unknown_disposition']) { + expect(html).toContain(`data-col="${col}"`); + } + expect(html).toContain('data-col="difference_item_id"'); + }); + + it('**推荐理由 / 摘要 / 风险 / 验证要求只读** —— 让人直接改摘要,等于让他给自己没做的判断盖章', () => { + for (const col of ['recommendation_reason', 'difference_summary', 'verification_requirement']) { + expect(html).toContain(`data-slot="${col}"`); + expect(html).not.toContain(`data-col="${col}"`); + } + expect(html).not.toContain('data-edit="substitution.relation"'); + }); + + it('等级与四态用 data-class-of 上色 —— 判断留在样式表,模版不判', () => { + expect(html).toContain('data-class-of="recommendation_level"'); + expect(html).not.toContain('提交 / 确认:本工单只承载内容;确认请在对话区的【执行卡】中进行。确认后活动自动从「等待数据」唤醒。'), }); + +/* ==================== 局面:差异评估结论审核(difference-review,S3 的验收工单) ==================== */ + +/* + * **这一段是本活动自己的平台绑定,不是「抽目标件」**(与 BIND_P08 不同源)。 + * 老目标件(`docs/SS02-ACT-WORK-05-html/ACT-WORK-05-验收工单-01-差异评估结论审核.html`) + * 整页只读——「通过 / 驳回」写死在对话区的验收卡里,demo 本身从没有过「点开编辑」这回事, + * 所以它被机械抽出来的 `ACC1_CSS` 里没有 `.jc-vcell`/`.jc-cell`/`.jc-editgate`/`table.on` + * 这一整套(`P08_CSS` 有,是因为「数据工单-01」目标件自己的原型 demo 就带点击编辑的 JS)。 + * spec §4.4/§6.2 把这一步从纯只读改成「人就地改四态,配方重算」——这是这次重写新加的能力, + * 我们不能假装老目标件本来就有、只是「漏抄」了它。尺寸按 ACC1 的表格字号(13px)调, + * 强调色用页面自己的 `--ledger`(验收绿),不借 P08 的 `--data`(蓝)。 + */ +const BIND_ACC1_EDIT = ` +.jc-vcell{display:block;font-size:13px;color:var(--ink);line-height:1.6;overflow-wrap:anywhere} +.jc-vcell.mono{font-family:var(--mono);font-size:12px;color:var(--ink-2)} +.jc-vcell.ro{display:block} +.jc-cell{display:none;width:100%;min-width:64px;border:1px solid var(--line);border-radius:6px;padding:6px 8px;font:inherit;font-size:12.5px;color:var(--ink);background:#fff} +.jc-cell:focus{border-color:var(--ledger);box-shadow:0 0 0 3px var(--ledger-soft);outline:none} +table.on td>.jc-vcell{display:none} +table.on td>.jc-vcell.ro{display:block} +table.on td>.jc-cell{display:block} +/* 主键的隐藏钥匙格(substitution_relation_id / difference_item_id)—— 不给人看(一串内部编号), + 但必须真在 DOM 里挂 data-col:漏了钥匙,人保存的行服务端凑不出主键(2026-08-02 栽过)。 */ +.rowkey{display:none} +th.req::after{content:" *";color:var(--danger);font-size:11px} +.desc{color:var(--ink-3);font-size:12px;line-height:1.5} +.jc-editgate{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:14px 0 10px;padding:9px 12px;border-radius:9px;background:var(--soft);border:1px solid var(--line-soft);font-size:12px;color:var(--ink-3)} +.jc-editgate-t{flex:1 1 auto;min-width:0} +.jc-editgate-t b{color:var(--ink-2);font-weight:700} +.jc-egbtn{flex:none;display:inline-flex;align-items:center;gap:5px;cursor:pointer;border:1px solid var(--line);border-radius:7px;padding:6px 13px;font:inherit;font-size:12px;font-weight:700;color:var(--ink-2);background:#fff;transition:border-color .16s,color .16s,background .16s} +.jc-egbtn:hover{border-color:var(--ledger);color:var(--ledger)} +.jc-egbtn.save{color:#fff;background:var(--ledger);border-color:var(--ledger)} +.jc-egbtn.save:hover{filter:brightness(1.06);color:#fff} +.jc-editgate .eg-cancel,.jc-editgate .eg-save{display:none} +.jc-editgate.on{background:var(--ledger-soft);border-color:var(--ledger-line);color:#0f6a5d} +.jc-editgate.on .eg-open{display:none} +.jc-editgate.on .eg-cancel,.jc-editgate.on .eg-save{display:inline-flex} +`; + +/** + * 徽标别名 —— 四态(可改列的只读孪生)/ 约束类型(只读)/ 推荐等级(只读,规则算的)/ 替代风险(只读)。 + * 全部走 `badgeCss()`(与 `BIND_P08` 同一套机制):选择器与文案从 `[值, 人读名, 颜色]` 算, + * 不手写 —— `data-class-of` 消毒出来的是 `v-<值>`,手写选择器对不上是本仓踩过三次的坑。 + */ +const DIFF_STATUS_OPTIONS: ReadonlyArray = [ + ['match', 'MATCH'], + ['mismatch', 'MISMATCH'], + ['unknown', 'UNKNOWN'], + ['conditional', 'CONDITIONAL'], +]; +const BIND_ACC1_BADGE = ` +/* 四态(substitution.difference_item.difference_status)—— 徽标文案与下面下拉框的选项同一份字面量表(陷阱表:上下两态得说同一个字) */ +${badgeCss('.st', [ + ['match', 'MATCH', OK], + ['mismatch', 'MISMATCH', DANGER], + ['unknown', 'UNKNOWN', AMBER], + ['conditional', 'CONDITIONAL', 'color: var(--brand-deep); background: var(--brand-wash); border-color: var(--brand-soft);'], +])} +/* 约束类型(hard/soft)—— 只读,与 input-supplement 同一套颜色语义 */ +${badgeCss('.ct', [ + ['hard', '硬约束', DANGER], + ['soft', '软约束', MUTED], +])} +/* 推荐等级(substitution.relation.recommendation_level)—— 规则算的,人不改;.rec-lg 放大成头部大徽章 */ +${badgeCss('.rec', [ + ['recommended', '推荐', OK], + ['conditional', '有条件推荐', AMBER], + ['not_recommended', '不推荐', DANGER], +])} +.rec.rec-lg{font-size:14px;padding:6px 18px;letter-spacing:.02em} +/* 替代风险(收口四格的「风险」,substitution.relation.risk_level)—— 走徽标而不裸摆英文枚举值 */ +${badgeCss('.risk-badge', [ + ['low', '低风险', OK], + ['medium', '中风险', AMBER], + ['high', '高风险', DANGER], +])} +`; + +/** + * 对决头(原 PN → 候选 PN,共享同一个芯片类型)+ 四态分布带 + 收口四格的 kv 行样式 —— spec §6.2「顶」「下」。 + * 分布条的宽度梯子照 quote-pricing 的 `.jc-distbar` 抄同一招(`data-class-of` 把条数投影成 `v-`, + * 梯子在样式表里取宽;模版仍只取值不算)。 + */ +const BIND_ACC1_LAYOUT = ` +.duel{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin:0 0 16px;padding:16px 18px;border:1px solid var(--ledger-line);border-radius:12px;background:linear-gradient(120deg,#f5fdfb 0%,#fff 60%)} +.duel-pn{flex:1 1 190px;min-width:150px} +.duel-role{display:block;font-size:10.5px;font-weight:800;letter-spacing:.1em;color:var(--ink-4);margin-bottom:4px} +.duel-name{display:block;font-size:18px;font-weight:800;color:var(--ink);font-family:var(--mono);overflow-wrap:anywhere} +.duel-mid{flex:none;display:flex;flex-direction:column;align-items:center;gap:6px} +.duel-arrow{font-size:20px;color:var(--ledger);font-weight:800;line-height:1} +.duel-type{font-size:11px;font-weight:700;color:var(--ink-3);background:var(--soft);border:1px solid var(--line-soft);border-radius:999px;padding:2px 10px;white-space:nowrap} +.duel-rec{flex:none} +@media(max-width:640px){.duel{flex-direction:column;align-items:stretch}.duel-mid{flex-direction:row;justify-content:center}} +.dist{margin:0 0 18px} +.dist-bar{display:flex;height:10px;border-radius:999px;overflow:hidden;background:var(--soft);border:1px solid var(--line-soft)} +.dist-bar>i{flex:0;display:block} +.dist-bar>i.v-0,.dist-bar>i:not([class*=" v-"]){display:none} +.dist-bar>i.v-1{flex:1}.dist-bar>i.v-2{flex:2}.dist-bar>i.v-3{flex:3}.dist-bar>i.v-4{flex:4} +.dist-bar>i.v-5{flex:5}.dist-bar>i.v-6{flex:6}.dist-bar>i.v-7{flex:7}.dist-bar>i.v-8{flex:8} +.dist-bar>i.v-9{flex:9}.dist-bar>i.v-10{flex:10}.dist-bar>i.v-11{flex:11}.dist-bar>i.v-12{flex:12} +.dist-bar>i.v-13{flex:13}.dist-bar>i.v-14{flex:14}.dist-bar>i.v-15{flex:15}.dist-bar>i.v-16{flex:16} +.dist-bar>i.v-17{flex:17}.dist-bar>i.v-18{flex:18}.dist-bar>i.v-19{flex:19}.dist-bar>i.v-20{flex:20} +.dist-bar>i.m{background:var(--ok)}.dist-bar>i.x{background:var(--danger)}.dist-bar>i.u{background:var(--amber)}.dist-bar>i.c{background:var(--brand-deep)} +.dist-lg{display:flex;flex-wrap:wrap;gap:14px;margin-top:8px;font-size:11.5px;color:var(--ink-3)} +.dist-lg em{display:inline-block;width:9px;height:9px;border-radius:3px;margin-right:5px;vertical-align:-1px} +.dist-lg em.m{background:var(--ok)}.dist-lg em.x{background:var(--danger)}.dist-lg em.u{background:var(--amber)}.dist-lg em.c{background:var(--brand-deep)} +.dist-lg b{color:var(--ink);font-weight:800} +.kv{display:flex;gap:10px;padding:9px 0;border-bottom:1px solid var(--line-soft);font-size:12.5px} +.kv:last-child{border-bottom:0} +.kv i{flex:0 0 84px;font-style:normal;color:var(--ink-4);font-size:11px;padding-top:1px} +.kv span{flex:1 1 auto;color:var(--ink-2);line-height:1.6;overflow-wrap:anywhere} +.kv span:empty::after{content:"—";color:var(--ink-4)} +.summary-box{border:1px solid var(--line-soft);border-radius:10px;background:var(--soft);padding:2px 16px;margin-bottom:4px} +`; + +/** + * 对决头 —— **只读**。`original_internal_pn`/`candidate_internal_pn` 是 `substitution.relation` + * 这一单唯一一行草稿写下的字段,任务视图把它们摊到了顶层作用域(裸 `data-slot` 就取得到, + * 与 `eval_ref`/`gate_reason` 同一条道理)。类型名沿用 `input-supplement` 的 `PART_TABLE` + * 已验证过的查表写法:`data-rows="parts.chip_type" data-by="type_id=chip_type_id"` —— + * `chip_type_id` 在两颗料上必须相等(S2 的入场哨兵已经把关过),这里只需查一次。 + */ +const DUEL = `
+
+ 原内部料号 + +
+
+ 芯片类型 · + → +
+
+ 候选内部料号 + +
+
+
`; + +/** + * 四态分布带 —— 四个基座 tally 读数各自身兼两职:`data-slot` 摆计数字面值, + * `data-class-of` 把同一个数投影成宽度梯子的 class(`v-`)。两个属性长在同一个元素上, + * 与 `quote-pricing` 的 `.jc-distbar` 完全同款(见该文件 `DIST` 常量)。 + */ +const DIST_PREFIX = 'substitution_difference_item_difference_status_'; +const DIST = `
+
+ + + + +
+
+ MATCH · + MISMATCH · + UNKNOWN · + CONDITIONAL · +
+
`; + +/** + * 逐条差异明细 —— **唯一可改的一张表**。四态 / 影响说明 / 判断依据 / UNKNOWN 去向四列可改, + * 其余(参数项 / 说明 / 约束 / 原值 / 候选值 / 单位)只读。 + * + * **两枚主键都挂 `data-col`**(`substitution_relation_id` + `difference_item_id`,复合主键, + * 与 `input-supplement` 的 `PARAMETER_TABLE` 同一条约定):它们不摆给人看(一串内部编号, + * 对审核没有信息量),裹进 `.rowkey`(`display:none`)藏起来 —— 但必须真的在 DOM 里, + * 人保存这一行时服务端 `resolveRowKey` 才凑得出主键。表头补一个不显示文字的 `` 与它对位, + * 首尾列数对得上。 + */ +const DIFF_ITEM_TABLE = `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#参数项参数说明约束原 PN 值候选 PN 值单位结论(四态)影响说明判断依据UNKNOWN 去向
${badge('ct', 'constraint_type')}${badge('st', 'difference_status', true)}${select('difference_status', DIFF_STATUS_OPTIONS, true)}${cell('impact_statement')}${cell('evidence_note')}${cell('unknown_disposition')}${cell('difference_item_id')}${cell('substitution_relation_id')}
`; + +/** + * 收口四格 —— **只读**。推荐理由 / 差异摘要 / 验证要求三格是自由文本,裸 `data-slot`; + * 风险走徽标(`data-class-of`,避免直显 `low`/`medium`/`high` 这类英文枚举原值, + * 与「陷阱表:枚举列不许 data-slot 直显机器码」同一条纪律)。四格全部读 `substitution.relation`, + * 但**这一段没有任何 `data-edit`**——它们是配方按 §3.5 规则算出来的,要改就去改上面的四态。 + */ +const CLOSING_SUMMARY = `
+
推荐理由
+
差异摘要
+
替代风险
+
验证要求
+
`; + +export const DIFFERENCE_REVIEW = doc({ + css: ACC1_CSS, + alias: BIND_ACC1_EDIT + BIND_ACC1_BADGE + BIND_ACC1_LAYOUT, + badge: '验收', + tag: '验收工单', + kicker: 'SS02 · ACT-WORK-05 差异评估 · 验收工单 1/2', + title: '差异评估结论审核', + step: '步骤 3/4 · 结论审核', + footTag: '工单类型:验收', + body: + zone('AI 结论说明', '结论是怎么算出来的', ZIC_TASK) + + note([ + 'AI 已完成的判定:按差异规则(锁定版本 ),对两颗料在同一芯片类型下的每一条核心参数逐一比对,给出 MATCH / MISMATCH / UNKNOWN / CONDITIONAL 四态之一 —— 硬约束不满足即 MISMATCH,证据不足即 UNKNOWN,不会被措辞改判成 MATCH。', + '系统按规则算出:推荐等级、替代风险、差异摘要、推荐理由、验证要求 —— 这五项不是 AI 编的,是规则按下面四态分布算出来的;你改动下表里任何一条四态,这五项当场重算。', + '就地核对:逐条对过原 PN 与候选 PN 的取值、硬约束是否含糊放过、UNKNOWN 是否有验证去向 —— 要改就在下表里改四态、影响说明、判断依据、UNKNOWN 去向,改完保存即生效,不必重新调用 AI。', + ]) + + DUEL + + DIST + + zone('逐条差异明细', '一行一参数,与原 PN 逐项对比;结论 / 影响说明 / 判断依据 / UNKNOWN 去向可就地改', ZIC_TABLE) + + editGate('substitution.difference_item', '逐条核对结论、影响说明、判断依据,点「编辑」开表') + + gapbar('substitution_difference_item', '必填') + + DIFF_ITEM_TABLE + + zone('收口结论', '系统按规则算出,要改就去改上面的四态 —— 让人直接改这几格,等于让他给自己没做的判断盖章', ZIC_TASK) + + CLOSING_SUMMARY + + flow('审批:本工单只承载内容;逐条核对无误后,请在对话区的【验收卡】中确认通过。通过后进入落账放行(T_release),签字后正式落账并发口。'), +});