From 445aa5ef225d30827a83c1ff36647c3b259e5eb9 Mon Sep 17 00:00:00 2001 From: xcs Date: Mon, 31 Aug 2026 23:26:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(w05):=20=E9=AA=8C=E6=94=B6=E5=B7=A5?= =?UTF-8?q?=E5=8D=95=E7=9A=84=E9=9D=A2=E5=AD=94=E5=9C=A8=E8=A3=85=E9=85=8D?= =?UTF-8?q?=E5=B1=82=E8=A2=AB=E4=B8=A2=E6=8E=89=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E4=B8=A4=E5=A4=B4=E9=83=BD=E6=9C=89=E6=B5=8B,=E4=B8=AD?= =?UTF-8?q?=E9=97=B4=E9=82=A3=E4=B8=80=E6=AE=B5=E6=8A=8A=E5=80=BC=E6=89=94?= =?UTF-8?q?=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户实测:「你怎么给我发成了执行工单,我需要的是验收工单,然后标题那一块需要是 验收卡的那种绿色」。 一、面孔:engines.ts 发执行工单那一处是一句写死只认 'data' 的三目,配方递下来的 'accept' 静默丢掉 —— 对话区照旧摆「执行工单」。**配方那一头有测(variant 原样递出去)、 前端那一头有测(accept 该穿哪副面孔)、e2e 按标题驱动也全绿,唯独中间没有。** 修:抽成纯函数 executionCardVariant(与旁边 decisionCardFace 同一种做法、同一个测试位), 认 data / accept、拒 review、认不出的当没写;4 测变异验证过。 e2e 同批改**认面孔不认标题**(试跑观测面的 cards 补 variant,契约变更)—— 标题是活动自己写的字符串,卡穿哪副面孔由装配层决定,那一次正是两者对不上。 二、颜色:件① 验收工单那一副整套强调色换成验收绿 #0aa294(令牌从 ACCENT_LEDGER 现改选择器得来,与件②③、对话区验收卡一处真源),题头那条写死的浅蓝渐变单独给一条绿的; 数据工单那一副仍是蓝。真渲染核过两副 class 与两套令牌。 验收:e2e w05 5/5 released、被拒 0(面孔断言这一轮真在跑);2563 测、四门禁绿; 2.16.0 装库。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet --- CLAUDE.md | 14 +++++++ .../__tests__/activity.test.ts | 15 ++++++++ .../src/manifest.ts | 2 +- .../src/views/report.ts | 3 +- .../src/views/style.ts | 10 +++++ .../contracts/src/api/governance.d.ts | 4 ++ .../app/__tests__/execution-card-face.test.ts | 37 +++++++++++++++++++ apps/api/src/app/routes/governance-ops.ts | 1 + apps/api/src/app/wiring/engines.ts | 24 +++++++++++- packages/contracts/src/api/governance.ts | 7 ++++ tools/e2e-a2.mjs | 18 ++++++++- 11 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 apps/api/src/app/__tests__/execution-card-face.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index cf306d0..7fc510f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1834,6 +1834,20 @@ .dossiers 三列网格(1fr·76px·1fr),中间列是实体动画道 —— ⇄ 徽呼吸 + 双点绕徽轨道 对转(sw-orbit),总线虚线在道里连两卡;窄屏单列收道。浏览器两帧相位实证。 +- **「你怎么给我发成了执行工单」—— 面孔在真发卡那条路上被丢掉**(2026-08-31 用户实测, + `part-substitution-assessment@2.16.0`):装配层发执行工单那一处是一句**写死只认 `'data'`** + 的三目(`engines.ts`),配方递下来的 `variant: 'accept'` 静默丢掉 —— 对话区照旧摆「执行工单」。 + **两头都有测,唯独中间没有**:配方那一头断言了「variant 原样递出去」,前端那一头断言了 + 「accept 该穿哪副面孔」,e2e 按卡的**标题**驱动也照样全绿 —— 三处都对,而它们中间那一段把值扔了。 + 修:抽成纯函数 `executionCardVariant`(与旁边的 `decisionCardFace` 同一种做法、同一个测试位), + 认 data / accept、拒 review、认不出的当没写;新测 4 条变异验证过。 + e2e 同批**改认面孔不认标题**(试跑观测面的 `cards` 补 `variant`,契约变更):标题是活动自己写的 + 字符串,卡穿哪副面孔由装配层决定,两者可以对不上 —— 那一次正是「标题叫验收工单、发出去是执行工单」。 + **面孔要在真发卡那条路上断言。** + 同批(用户第二句「标题那一块需要是验收卡的那种绿色」):件① 验收工单那一副整套强调色换成 + **验收绿 #0aa294**(令牌从 `ACCENT_LEDGER` 现改选择器得来,数值与件②③、对话区验收卡一处真源), + 题头那条写死的浅蓝渐变单独给一条绿的;数据工单那一副仍是蓝。真渲染核过两副 class 与两套令牌。 + - **起单不许选到同一颗料;S1 数据齐了也叫人一次**(2026-08-31 用户两条, `part-substitution-assessment@2.15.0`): ①**两格互斥**(「创建活动的启动参数里面不应该能选到同样的两个芯片」)—— 查下来不只是没意义, diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts index f148333..d5a0933 100644 --- a/activities/part-substitution-assessment/__tests__/activity.test.ts +++ b/activities/part-substitution-assessment/__tests__/activity.test.ts @@ -864,6 +864,21 @@ describe('件① 评估输入补全', () => { expect(colOf('sub_eval.case', 'order_kind')?.tally).toEqual(['data', 'accept']); }); + /* + * 2026-08-31 用户:「标题那一块需要是验收卡的那种绿色」—— 验收工单那一副整套强调色换成 + * 验收绿 #0aa294(与件②③、对话区验收卡同一枚),题头那条写死的浅蓝渐变单独给一条绿的。 + * 令牌从 `ACCENT_LEDGER` 现改选择器得来:**数值只有一处真源**,件②③ 改色时这一副跟着走。 + */ + it('验收工单那一副是验收绿:整套令牌换成 #0aa294,题头渐变也换掉(数值与件②③ 同源)', () => { + expect(html).toContain('.card.v-accept{'); + expect(html).toContain('.card.v-accept.hd-light .card-head{background:linear-gradient('); + /* 蓝那一套仍在(数据工单那一副),两套各就各位 */ + expect(html).toContain('--acc:#2e6bff'); + const acceptBlock = html.slice(html.indexOf('.card.v-accept{'), html.indexOf('.card.v-accept.hd-light')); + expect(acceptBlock).toContain('--acc:#0aa294'); + expect(acceptBlock).toContain('--ledger:#0aa294'); + }); + it('缺项点名全并进顶条「还差什么」,正文不再有逐块红带', () => { expect(html).toContain('class="jc-gate-sub" data-class-of="parts_profile_missing"'); expect(html).toContain('data-slot="parts_profile_missing_keys"'); diff --git a/activities/part-substitution-assessment/src/manifest.ts b/activities/part-substitution-assessment/src/manifest.ts index 2dbd94e..5c162f7 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.15.0', + packageVersion: '2.16.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 17675d9..1b7eea4 100644 --- a/activities/part-substitution-assessment/src/views/report.ts +++ b/activities/part-substitution-assessment/src/views/report.ts @@ -23,7 +23,7 @@ * 印章 hero + 落账去向 + 四态计数与硬约束点名 + 待验项。 */ import { vtok, badgeCss } from './badge.ts'; -import { SHELL_CSS, ACCENT_DATA, ACCENT_LEDGER } from './style.ts'; +import { SHELL_CSS, ACCENT_DATA, ACCENT_LEDGER, ACCENT_ACCEPT } from './style.ts'; import { D } from '../constants.ts'; /* ==================== 徽标配色(值与文案成对交给 badgeCss,选择器从值算,不手写) ==================== */ @@ -205,6 +205,7 @@ ${badgeCss('.rl', [ .v-accept .k-data { display: none; } .v-accept .k-acc { display: inline; } .v-accept .fabs { display: none; } +${ACCENT_ACCEPT} `; diff --git a/activities/part-substitution-assessment/src/views/style.ts b/activities/part-substitution-assessment/src/views/style.ts index 1d58e25..c28ef00 100644 --- a/activities/part-substitution-assessment/src/views/style.ts +++ b/activities/part-substitution-assessment/src/views/style.ts @@ -42,6 +42,16 @@ export const ACCENT_LEDGER = ` } `; +/** + * **验收工单那一副的强调色**(2026-08-31 用户:「标题那一块需要是验收卡的那种绿色」)—— + * 件① 一张两副面孔:数据工单蓝、验收工单绿。整套令牌从 `ACCENT_LEDGER` **现改选择器**得来, + * 数值只有一处真源(件②③ 与对话区验收卡同一枚 #0aa294);题头那条浅色渐变是写死的蓝白, + * 单独给一条绿的override —— `.card.v-accept`(0,2,0)比 `:root`(0,1,0)特异,先后顺序不吃亏。 + */ +export const ACCENT_ACCEPT = `${ACCENT_LEDGER.replace(':root{', '.card.v-accept{')} +.card.v-accept.hd-light .card-head{background:linear-gradient(120deg,#e9f9f5 0%,#fff 55%,#e3f7f1 100%);border-bottom-color:var(--acc-deep)} +`; + export const SHELL_CSS = ` /* ================= 令牌与底 ================= */ :root{ diff --git a/api-report/packages/contracts/src/api/governance.d.ts b/api-report/packages/contracts/src/api/governance.d.ts index 4ccea05..57c04ec 100644 --- a/api-report/packages/contracts/src/api/governance.d.ts +++ b/api-report/packages/contracts/src/api/governance.d.ts @@ -2835,6 +2835,10 @@ export declare const testActivityStateRoute: { execution: "execution"; acceptance: "acceptance"; }>; + variant: z.ZodOptional>; title: z.ZodString; done: z.ZodBoolean; result: z.ZodOptional; diff --git a/apps/api/src/app/__tests__/execution-card-face.test.ts b/apps/api/src/app/__tests__/execution-card-face.test.ts new file mode 100644 index 0000000..7f37986 --- /dev/null +++ b/apps/api/src/app/__tests__/execution-card-face.test.ts @@ -0,0 +1,37 @@ +/** + * **执行工单的面孔在装配层这一处定**(2026-08-31,用户实测:「你怎么给我发成了执行工单, + * 我需要的是验收工单」)。 + * + * 病:配方递下来 `variant: 'accept'`(验收工单 —— 件只读、人只确认),而装配层这一处 + * 是一句写死只认 `'data'` 的三目,别的值**静默丢掉** —— 对话区照旧摆「执行工单」。 + * + * 更值得记的是**两头都有测,唯独中间没有**:配方那一头断言了「variant 原样递出去」, + * 前端那一头断言了「accept 该穿哪副面孔」,e2e 按卡的**标题**驱动也照样绿 —— + * 三处都对,而它们中间这一段把值扔了。**面孔要在真发卡那条路上断言。** + */ +import { describe, expect, it } from 'vitest'; +import { executionCardVariant } from '../wiring/engines.ts'; + +describe('执行工单的两副变体面孔', () => { + it('data 数据工单 / accept 验收工单,两个都认', () => { + expect(executionCardVariant('data', undefined)).toBe('data'); + expect(executionCardVariant('accept', undefined)).toBe('accept'); + }); + + it('剧本声明优先于卡骨架 —— 模型抄卡会把 variant 抄丢(2026-08-27 实测)', () => { + expect(executionCardVariant(undefined, 'data')).toBe('data'); + expect(executionCardVariant('data', 'accept')).toBe('accept'); + }); + + it('`review` 是决策卡那一路的面孔,不许串到执行工单上', () => { + expect(executionCardVariant('review', undefined)).toBeUndefined(); + expect(executionCardVariant(undefined, 'review')).toBeUndefined(); + }); + + it('没写 / 认不出的值:普通执行工单,不把脏东西端下去', () => { + expect(executionCardVariant(undefined, undefined)).toBeUndefined(); + expect(executionCardVariant('', undefined)).toBeUndefined(); + expect(executionCardVariant({ variant: 'accept' }, undefined)).toBeUndefined(); + expect(executionCardVariant(42, undefined)).toBeUndefined(); + }); +}); diff --git a/apps/api/src/app/routes/governance-ops.ts b/apps/api/src/app/routes/governance-ops.ts index 800cea3..5564dc1 100644 --- a/apps/api/src/app/routes/governance-ops.ts +++ b/apps/api/src/app/routes/governance-ops.ts @@ -712,6 +712,7 @@ export function registerGovernanceOpsRoutes(app: Hono, container: Contai cards: orders.map((o) => ({ id: o.id, mode: o.mode, + ...(o.mode === 'execution' && o.variant !== undefined ? { variant: o.variant } : {}), title: o.title, done: o.done !== undefined, ...(o.done?.result === undefined ? {} : { result: String(o.done.result) }), diff --git a/apps/api/src/app/wiring/engines.ts b/apps/api/src/app/wiring/engines.ts index 78b459a..6b7d002 100644 --- a/apps/api/src/app/wiring/engines.ts +++ b/apps/api/src/app/wiring/engines.ts @@ -242,6 +242,27 @@ export function createOnlyViolation( * 而 `checkChatCardReply` 又要求 `checked ⊇ checklist`:卡按不下去。**只在 `reviewFace` 为真时** * 才退回剧本声明的清单 —— 非审核面孔的卡,清单就是没有,不发明一条按不下去的。 */ +/** + * **执行工单的面孔** —— 两副变体:`data` 数据工单(件上的表就是要建的记录,确认即入库)/ + * `accept` 验收工单(件只读,人只核对确认)。`review` 是决策卡那一路的面孔,不许串到这儿来; + * 认不出的值一律当没写(普通执行工单),不把脏东西端下去。 + * + * **剧本声明(`PlanStep.cardVariant`)优先于卡骨架**:模型抄卡时会把 `variant` 抄丢 + * (2026-08-27 实测:同一张卡 gateLedgers 抄到了、variant 没有)。但卡自己带的值同样作数 —— + * 配方那一路没有模型可抄丢,而**一个步骤里两笔条件互斥的 card**(缺东西派数据工单 / + * 都齐了派验收工单,2026-08-31)本来就不可能由一个 step 级的 `cardVariant` 说清。 + * + * **这一处此前是内联的三目,写死只认 `'data'`**(2026-08-31 用户实测:「你怎么给我发成了 + * 执行工单」)—— 配方递下来的 `accept` 在这儿被静默丢掉,对话区照旧摆「执行工单」。 + * 配方那一头与前端那一头都有测,唯独**真发卡这条路**上没有:面孔要在这里断言。 + */ +export function executionCardVariant(fromCard: unknown, fromStep: unknown): 'data' | 'accept' | undefined { + for (const value of [fromStep, fromCard]) { + if (value === 'data' || value === 'accept') return value; + } + return undefined; +} + export function decisionCardFace( input: { readonly variant?: 'review' | undefined; readonly checklist?: ReadonlyArray | undefined }, step: { readonly cardVariant?: string | undefined; readonly acceptanceChecklist?: ReadonlyArray | undefined } | undefined, @@ -1913,8 +1934,7 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring { : (await deps.registry.manifestOf((await taskContext.of(input.taskId)).activityId)).assets.businessModel.steps?.find( (s) => s.item === input.itemId, )?.cardVariant; - // 执行工单这一路只认 data(review 是决策卡那一路的面孔) - const variant = stepVariant === 'data' || input.card['variant'] === 'data' ? ('data' as const) : undefined; + const variant = executionCardVariant(input.card['variant'], stepVariant); const issued = await deps.humanTool.issueExecution(context, { title, basis: basis.length > 0 ? basis : [{ k: '来源', v: '案例模型当下局面' }], diff --git a/packages/contracts/src/api/governance.ts b/packages/contracts/src/api/governance.ts index 122ab78..0f7f29f 100644 --- a/packages/contracts/src/api/governance.ts +++ b/packages/contracts/src/api/governance.ts @@ -1060,6 +1060,13 @@ export const testActivityStateRoute = { z.object({ id: z.string(), mode: z.enum(['execution', 'acceptance']), + /** + * 执行工单的**面孔**:`data` 数据工单 / `accept` 验收工单(2026-08-31 补)。 + * 观测面得看得见这张卡是以什么身份发出去的 —— 面孔在装配层那一处决定, + * 而它此前恰恰因为「配方那头与前端那头都有测、中间没有」漏过一次 + * (配方递 `accept`,装配层写死只认 `data`,人收到的是「执行工单」)。 + */ + variant: z.enum(['data', 'accept']).optional(), title: z.string(), done: z.boolean(), result: z.string().optional(), diff --git a/tools/e2e-a2.mjs b/tools/e2e-a2.mjs index ab27e09..d388e2d 100644 --- a/tools/e2e-a2.mjs +++ b/tools/e2e-a2.mjs @@ -1335,6 +1335,8 @@ async function runCase(suite, kase) { let dataCardSeen = false; /** w05「都在库」:匹配**验收工单**那一段的执行卡出现过 —— 数据齐了也要叫人一次 */ let acceptCardSeen = false; + /** 那张卡的**面孔**不对(标题是验收工单,发出去却是执行工单)—— 2026-08-31 真撞过一次 */ + let acceptFaceWrong = false; /** w05「类型不一致·硬拦」:`sub_eval.case.gate_reason` 命中过 `kase.expectGateReason` 那个词 */ let gateReasonMatched = false; /** `kase.customFill` 只跑一次(w05「候选缺库·新建类型」:两阶段写 —— 先铸 type_id,读回来再引用它) */ @@ -1419,7 +1421,20 @@ async function runCase(suite, kase) { dataCardSeen = (body.cards ?? []).some((c) => (suite.stages ?? []).some((s) => s.kind !== 'accept' && s.match.test(c.title ?? ''))); } if (kase.expectAcceptCard && !acceptCardSeen) { - acceptCardSeen = (body.cards ?? []).some((c) => (suite.stages ?? []).some((s) => s.kind === 'accept' && s.match.test(c.title ?? ''))); + /* + * **认面孔,不认标题**(2026-08-31 用户实测:「你怎么给我发成了执行工单」)—— + * 标题是活动自己写的字符串,卡穿的是哪副面孔由装配层决定,两者可以对不上: + * 那一次配方递了 `accept`、标题也叫「验收工单 · …」,而装配层把 variant 丢了, + * 人收到的是一张执行工单 —— 按标题驱动的这套脚本当时照样全绿。 + */ + const seen = (body.cards ?? []).find((c) => (suite.stages ?? []).some((s) => s.kind === 'accept' && s.match.test(c.title ?? ''))); + if (seen !== undefined) { + acceptCardSeen = true; + if (seen.variant !== 'accept') { + console.log(` ⚠️ 这张卡的标题是验收工单,发出去穿的却是「${seen.variant ?? '执行工单'}」的面孔 —— 判失败`); + acceptFaceWrong = true; + } + } } // w05「类型不一致·硬拦」:gate_reason 那句话要点了名(latch 住,答完卡草稿就清了,过了这一拍再查就晚了) if (kase.expectGateReason && !gateReasonMatched) { @@ -1663,6 +1678,7 @@ async function runCase(suite, kase) { console.log(' ⚠️ 数据齐了也该叫人一次(只读的验收工单),一张都没见到 —— 判失败(多半是 S1 又被拍首的 doneWhen 扫描收掉了)'); ok = false; } + if (acceptFaceWrong) ok = false; if (kase.expectGateReason && !gateReasonMatched) { console.log(` ⚠️ gate_reason 没点名(期望命中 ${kase.expectGateReason})—— 判失败`); ok = false;