# ACT-WORK-05 差异评估(`part-substitution-assessment`)实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 把 SS02 的 ACT-WORK-05「差异评估」做成本仓第三个真活动包:台账建进库、活动装进系统、脚本 e2e 五案例全绿,再在浏览器里把全流程全分支走一遍,每一轮的结论与修改记进 `docs/ACT-WORK-05-端到端测试记录.md`。 **Architecture:** 照 E04 / I01 已验证的骨架 —— 清单(契约七件 + 台账声明 + 样例)、案例模型(CMMN 8 项 + 说明书 + 逐步剧本 + 5 段进度)、两枚纯工具(逐参数比对 / 推荐定级,规则同时以 DMN 表随包冻结)、五张 HTML 模版(两张数据工单件 + 执行单件 + 两张验收件)。主数据(P-01/P-08/P-10/P-11/P-06)进平台基线 `MASTER_OBJECTS` 由安装通道物化到 `ledger` schema,活动清单只追加自己要用的列与标记;活动自持单据归 `sub_eval.*`。数据工单 = 零输入执行工单 + 工作区可增删行的件(每张单只对一张表);验收 01 = 决策卡循环;验收 02 = 终局放行。 **Tech Stack:** TypeScript(活动包只 import `@sdk` / `@contracts` / `@sfeel`)、vitest、node-postgres(ledger 动态 DDL)、Hono API、React 工作台、tools/e2e-a2.mjs(服务账号 e2e)、claude-in-chrome(浏览器走查)。 **Spec:** `docs/SS02-ACT-WORK-05-html/ACT-WORK-05-活动流程与工单说明.md` · `ACT-WORK-05-一键启动入参说明.md` · `SS02-SS03-SS04_Demo台账字段细化与统一方案.md`(字段口径唯一权威)· 四份目标件 HTML · `ACT-WORK-05-全流程分支图.html`(18 条分支)。开发纪律:`docs/活动开发规范.md`、`docs/HTML报告生成规范.md`、`CLAUDE.md` 铁规矩。 ## Global Constraints - 五道门禁以退出码验:`pnpm gates`(= gen:tiers → typecheck → lint → depcruise → api:check → test);不许以 echo 验。 - §12.5 禁止清单:no `any` / `as unknown as` / `@ts-ignore`;domain 零 IO;不删测试;不动门禁配置。 - 活动包**只准 import `@sdk` / `@contracts` / `@sfeel`**;产物文本黑名单(`process.env` / `fetch(` / `node:` / `eval(`);模版含 `_<值> / _distinct_<列> / tool_<名>_calls`);模型自写的旗只能占谓词一半。 - 触达世界的项(交人核对 / 拍板 / 放行)标 `manualActivationRule` 且**不给 doneWhen**;`issueAcceptance` 一任务只能发一次且即终局 → 非终局审核用 `decide` 循环。 - 执行卡**零输入**:不摆 `fields` / `blocks`,填报全在工作区的件上;一单同一张卡只发一次。 - 报告模版**不算不判**:`data-slot` / `data-rows="mine:域.对象"` / `data-col` / `data-edit` / `data-class-of` / `data-blank`;主键列必挂 `data-col`;枚举列存 ASCII、人读名由样式表出。 - 样例:第一条满载;样例名 ≤14 字。 - 试跑一律用服务账号 `bpos-e2e`(`KEYCLOAK_E2E_CLIENT_ID/SECRET`),测完 `pnpm --filter @app/api reset-runs`;浏览器走查用当前 Chrome 已登录会话。 - 提交只 `git add` 本任务点名的文件(工作树里有一批未提交的鉴权改动,别顺手带走);提交信息末尾带: `Co-Authored-By: Claude Fable 5 ` 与 `Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh`。 - 数据工单口径(用户 2026-08-26 定):**读数点概念作废,一张表一张单**;P-08 是一张表(参数集是它的一个字段);本活动最多 2 张数据工单(P-08 / P-10);S-03 / L-02(启动前)/ P-06(事实类)/ P-09 不派。 - 本活动只答技术可替代性:不做价格 / 交期 / 数量判断;`customer_approval_status` 只读边界,活动不声明其写权。 --- ## 0. 已锁定的设计决定(执行者不必再问;要改先改这里) | # | 决定 | 依据 | |---|---|---| | D1 | **守铁律②**:数据工单确认落**本单草稿层**,放行时并入正式表(试跑单 `landMaster` 默认开,放行后主数据真落库);本单后续步骤读得到本单草稿,所以「建了就能拿来评」不受影响。文档里「确认即正式入库、无草稿态」落成「无审核环节、放行并入」。 | `docs/活动开发规范.md:45` 铁律②;E04 录入卡同款 | | D2 | **数据工单 = 零输入执行工单 + 件**(`data-edit` 表绑 `mine:parts.profile` / `mine:parts.document`,可加行,提交闸 = 声明的必填);卡标题以「数据工单 ·」开头;一张表一张单(P-08 一张、P-10 一张);对话区叫「数据卡」的专属卡皮记 debt。 | 现成机制零基座改动 | | D3 | **P-08 一张表**:`normalized_parameter_set` 是 `parts.profile` 的 **text 列**,行语法 `参数名 \| 值 \| 单位 \| hard/soft \| 说明`(一行一参数),件上用 textarea 编辑;不建 `parts.parameter` 子表,不用 json(json 列对人填的字符串不归型,textarea 存进去是 JSON 字符串,读写两侧形状不一)。 | 用户 2026-08-26「都是一张表」;`engines.ts coerceToColumn` json 不归型 | | D4 | datasheet 作**起单选填文件入参** `datasheets`(`format:'file'`,与 A1 `attachments` 同形);传了 AI 在数据工单件上预填,没传人全填(文档 0c:同为合法路径);「件上上传」记 debt。 | 平台文件入口只有起单 `format:'file'` | | D5 | **一人兼 PW/FAE**:所有卡发给任务负责人;`roles: ['FAE/RI']`;派给第二个人记 debt。 | 平台一任务恰一名负责人 | | D6 | 存在性检查 = S0 读 `parts.profile` 整表(剧本 `reads`)逐个候选核对 + **`refersTo` 墙**(`substitution.relation.original/candidate_internal_pn → parts.profile.internal_pn`;查无当场拒并指路);不另做基座读数。 | E04 contact_code 同款 | | D7 | 主数据物化走**平台基线**(`seed-mvp-data.ts` 新增 4 域 6 表)+ 清单**追加声明**(必填 / tally / refersTo / requiredWhen 是活动的授权面);不写 SQL 迁移(ledger 表由声明动态物化)。 | `seed-activities.ts` 主数据先于活动物化 | | D8 | 枚举列一律 ASCII tally,人读名由样式表出(推荐等级 `recommended/conditional/alternate/not_recommended`;差异状态 `match/mismatch/unknown/conditional`;约束 `hard/soft`;生命周期 `active/nrnd/eol/discontinued/risk`)。 | `data-class-of` 只认 ASCII | | D9 | 验收 01 非终局 → `decide` 循环(E04 S4 同款);驳回意见可写 P-06「验证要求」行;验收 02 = `submit` 终局,件上摆落库预览(`touched:` 数据源)。 | `issueAcceptance` 只能一次 | | D10 | GOV 规则 = 包内两张 DMN(文档)+ 两枚纯工具(求值),`rule_version = 'R-2026.07'` 随包冻结。 | quote-intake P-03 同款 | ## 1. 现有数据库架构与「把台账建进库」的含义 **一库四 schema**(`db/init/001-schemas.sql`):`sys`(系统自持,Drizzle 迁移 0001–0033)· `ledger`(台账自生长,**唯一写者** `apps/api/src/modules/ledger/internal/adapter/ledger-store.ts`,动态 DDL)· `mastra` · `queue`。连接 `postgres://jcai:jcai@localhost:54329/jcai_wos`(容器 `jcai-postgres`,已在跑;本机没有 psql,一律 `docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc ""`)。 **台账怎么物化**(不写迁移,声明即建表): | 声明 | 物理落点 | |---|---| | `{domain, object}` | 表 `ledger.<域>__<对象>`(`tableNameOf`),表注释 = `label` | | `fields[].type` text / number / integer / boolean / date / timestamp / json | text / double precision / bigint / boolean / date / timestamptz / jsonb;列注释 = `label` | | `primaryKey: true` 各列 | `primary key (...)`;**主键一经建立不得由声明变更** | | `kind` / `caseKey` | `ledger.__object_meta(domain, object, kind, case_key)`;`reset-runs` 只清 `kind <> 'master'` 的表 | | 同名对象多份声明 | `schema-merge.ts` 最大化合并、只增不破坏(add-column / 放宽类型);`json` 与标量互斥即拦 | | 任务草稿 | `ledger.__draft(task_id, domain, object, key_hash, key, patch, baseline, tombstone, updated_at)`;`taskView` = 正式版 + 本单草稿;放行 `releaseWrite` 合并进正式表(试跑单 `land_master=false` 时 master 行只留缓存) | **谁在什么时候调 `applyDeclarations`**:`installRealActivities()`(`apps/api/src/app/seed-activities.ts`,`pnpm activities:install` 的真身)先物化 `MASTER_OBJECTS`(平台基线,不挂活动),再逐包 `registry.install(manifest)`(活动声明合并进同名表 + 建自持表 + 写 `__object_meta`),最后 `systemWrite` 播 `MASTER_SAMPLE_ROWS`(按主键幂等)。 **现状(2026-08-26 实查)**:`ledger` 里 33 个对象;主数据四域 customer / project / supplier / market(`market.part` 是空占位的老料号表,主键 `pn`,与 P-08 的 `internal_pn` 口径不同,**不复用**,记 debt 待并);E04/I01 各自的 record 表 caseKey=`round_ref`;A1–A7 的表还在但已卸载。 **本活动要建进库的**(全部经上面这条路,零 SQL): | 全景 | 物理表 | kind | 主键 | 由谁物化 | |---|---|---|---|---| | P-08 | `ledger.parts__profile` | master | `internal_pn` | 基线 + 清单追加 | | P-10 | `ledger.parts__document` | master | `internal_pn, doc_seq` | 基线 + 清单追加 | | P-01 | `ledger.demand__request` | master | `request_id` | 基线 + 清单追加 | | P-11 主 | `ledger.substitution__relation` | master | `substitution_relation_id` | 基线 + 清单追加 | | P-11 子 | `ledger.substitution__difference_item` | master | `substitution_relation_id, difference_item_id` | 基线 + 清单追加(`compare_result` / `unknown_disposition` 两列只在清单) | | P-06 | `ledger.validation__record` | master | `validation_id` | 基线 + 清单追加 | | 自持 | `ledger.sub_eval__pn_check` / `__input_gap` / `__verdict` | record(caseKey `eval_ref`) | 见 Task 3 | 清单 | 验收 SQL(Task 6 用): ```sql select domain||'.'||object||' '||kind||' '||coalesce(case_key,'') from ledger.__object_meta where domain in ('parts','demand','substitution','validation','sub_eval') order by 1; select string_agg(column_name||':'||data_type, ', ' order by ordinal_position) from information_schema.columns where table_schema='ledger' and table_name='parts__profile'; select conname||' '||pg_get_constraintdef(oid) from pg_constraint where conrelid='ledger.substitution__difference_item'::regclass; select internal_pn, brand_name, lifecycle_status from ledger.parts__profile order by 1; ``` ## 2. 文件结构 **新建** - `activities/part-substitution-assessment/package.json` · `src/index.ts` · `src/manifest.ts`(清单 + DMN)· `src/case-model.ts`(CMMN / NARRATIVE / STEPS / PHASES)· `src/skills.ts` · `src/views/prd-style.ts`(逐字节抄 I01)· `src/views/report.ts`(五张模版)· `__tests__/activity.test.ts` · `__tests__/steps.test.ts` - `apps/api/src/app/__tests__/first-party-substitution.test.ts`(两枚纯工具) - `docs/ACT-WORK-05-端到端测试记录.md`(Task 8 逐轮追加)· `docs/数据域字段说明-W05.md` **修改** - `apps/api/src/app/seed-mvp-data.ts`(+4 域 6 表 + 样例料号)· `apps/api/src/app/__tests__/seed-mvp-data.test.ts`(四域 → 八域) - `apps/api/src/app/tools/first-party.ts`(+2 handler)· `apps/api/src/app/seed-mvp.ts`(+2 工具契约) - `tools/build-activities.ts`(PACKAGES)· `apps/api/src/app/seed-activities.ts`(REAL_PACKAGES) - `tools/e2e-a2.mjs`(+`w05` 套件、`stageEdits`、`decide` 值) - `CLAUDE.md`(现状快照 + 文档地图)· `docs/debt.md` --- ### Task 0: 开分支、确认环境 **Files:** 无(只动 git 与环境) - [ ] **Step 1: 从 mvp-foundation 开功能分支**(工作树里已有的未提交改动跟着走,不碰它们) ```bash cd /Users/xcs/project2/tmp/BPOS-MVP git checkout -b feat/act-work-05 git status --short | wc -l # 记下这个数:本计划的提交只 add 自己点名的文件 ``` - [ ] **Step 2: 确认库与两套服务在跑** ```bash docker ps --format '{{.Names}} {{.Status}}' | grep jcai-postgres lsof -nP -iTCP:8787 -iTCP:5173 -iTCP:8790 -iTCP:5180 -sTCP:LISTEN | awk '{print $1,$9}' | sort -u ``` Expected: `jcai-postgres Up`;8787(dev api)与 5173(dev web)在;8790/5180 是 `./stable.sh` 起的稳定版(与 dev 连同一个库,推进器靠跨进程租约互斥 —— 跑 e2e / 浏览器走查时**先停它**,避免两台交替推同一单干扰观察:`./stable.sh stop`;走查完 `./stable.sh start`)。 - [ ] **Step 3: 确认 e2e 凭证在环境里**(值不打印) ```bash for k in KEYCLOAK_URL KEYCLOAK_REALM KEYCLOAK_E2E_CLIENT_ID KEYCLOAK_E2E_CLIENT_SECRET; do printf '%s=%s\n' "$k" "${(P)k:+set}"; done ``` Expected: 四个都 `set`。缺了就从开发会话 / `.stable/env` 抄进当前 shell(`.stable/env` 只有 `KEYCLOAK_URL/REALM/CLIENT_ID`,E2E 那两个要问用户要;拿不到就停在这一步,e2e 跑不了)。 --- ### Task 1: 主数据基线 —— P-08 / P-10 / P-01 / P-11 / P-06 进平台声明,加样例料号 **Files:** - Modify: `apps/api/src/app/seed-mvp-data.ts`(`MASTER_OBJECTS` 末尾追加 6 表;`MASTER_DOMAINS` 8 域;`MASTER_SAMPLE_ROWS` 末尾追加 4 行) - Modify: `apps/api/src/app/__tests__/seed-mvp-data.test.ts:18-19`(四域 → 八域) - Test: `apps/api/src/app/__tests__/seed-mvp-data.test.ts` **Interfaces:** - Produces: 物理表 `ledger.parts__profile` / `parts__document` / `demand__request` / `substitution__relation` / `substitution__difference_item` / `validation__record`(列名与类型见下);样例行 `parts.profile` × 2(`LM2596S-5.0`, `LM2596S-ADJ`)、`parts.document` × 2。Task 3 的清单只**追加**列,列名与类型必须与这里一字不差(同名异型会被 schema-merge 放宽或拦下)。 - [ ] **Step 1: 改测试 —— 八域** 在 `apps/api/src/app/__tests__/seed-mvp-data.test.ts` 把 `'只有客户 / 项目 / 供应商 / 市场四个域'` 那条改成: ```ts it('八个域:客户 / 项目 / 供应商 / 市场 + 料件 / 需求 / 替代 / 验证(SS02 差异评估 2026-08-26 加后四域)', () => { expect([...MASTER_DOMAINS]).toEqual(['customer', 'project', 'supplier', 'market', 'parts', 'demand', 'substitution', 'validation']); expect([...new Set(MASTER_OBJECTS.map((o) => o.domain))].sort()).toEqual([...MASTER_DOMAINS].sort()); }); it('SS02 差异评估要读写的六张主数据表都在基线里,主键照《统一方案》', () => { const pk = (domain: string, object: string) => MASTER_OBJECTS.find((o) => o.domain === domain && o.object === object)?.fields.filter((f) => f.primaryKey).map((f) => f.name); expect(pk('parts', 'profile')).toEqual(['internal_pn']); expect(pk('parts', 'document')).toEqual(['internal_pn', 'doc_seq']); expect(pk('demand', 'request')).toEqual(['request_id']); expect(pk('substitution', 'relation')).toEqual(['substitution_relation_id']); expect(pk('substitution', 'difference_item')).toEqual(['substitution_relation_id', 'difference_item_id']); expect(pk('validation', 'record')).toEqual(['validation_id']); }); it('样例料号:原 PN 与同族可调版在库,候选 XL2596S-5.0 刻意不播(数据工单那条路要试得出来)', () => { const parts = MASTER_SAMPLE_ROWS.filter((r) => r.address.domain === 'parts' && r.address.object === 'profile').map((r) => r.key['internal_pn']); expect(parts).toEqual(['LM2596S-5.0', 'LM2596S-ADJ']); expect(parts).not.toContain('XL2596S-5.0'); }); ``` 文件头 `import` 改成 `import { MASTER_DOMAINS, MASTER_OBJECTS, MASTER_SAMPLE_ROWS } from '../seed-mvp-data.ts';`,并把文件头注释里「只有这四个域,不多不少」改为「八个域,多一个就说明有人把单据塞进主数据了」。 - [ ] **Step 2: 跑测试确认它红** Run: `pnpm vitest run apps/api/src/app/__tests__/seed-mvp-data.test.ts` Expected: FAIL(`MASTER_DOMAINS` 仍是四域;`pk('parts','profile')` 为 undefined)。 - [ ] **Step 3: 在 `seed-mvp-data.ts` 的 `MASTER_OBJECTS` 数组末尾(`market.quote_index` 之后、`];` 之前)追加六张表** ```ts /* ── 料件:P-08 芯片主数据 / P-10 原厂技术资料库(SS02 差异评估 2026-08-26 加)── 字段名与《SS02-SS03-SS04_Demo台账字段细化与统一方案》§4.2 一字不差;全链路只认 internal_pn。 参数集是 P-08 的一个字段(用户定:一张表,不建子表),一行一参数: `参数名 | 值 | 单位 | hard/soft | 说明`。老的 market.part(主键 pn)不复用,记 debt 待并。 */ master('parts', 'profile', '芯片主数据(P-08)', [ f('internal_pn', 'text', '内部料号', true), f('customer_pn', 'text', '客户料号'), f('brand_name', 'text', '品牌名称'), f('manufacturer_name', 'text', '制造商名称'), f('specification_description', 'text', '规格描述'), f('normalized_parameter_set', 'text', '标准化参数集(一行一参数:参数名 | 值 | 单位 | hard/soft | 说明)'), f('package', 'text', '封装'), f('process_technology', 'text', '工艺'), f('lifecycle_status', 'text', '生命周期状态'), f('eol_pcn_status', 'text', 'EOL/PCN 状态'), f('datasheet_ref', 'text', 'Datasheet 引用'), f('datasheet_version', 'text', 'Datasheet 版本'), f('application_scenarios', 'text', '适配应用场景'), f('pin_to_pin_relation_ref', 'text', 'Pin-to-Pin 关系引用'), ]), master('parts', 'document', '原厂技术资料库(P-10)', [ f('internal_pn', 'text', '关联内部料号', true), f('doc_seq', 'text', '资料序号(同一料号下 01/02…)', true), f('document_type', 'text', '资料类型'), f('brand_name', 'text', '品牌名称'), f('document_title', 'text', '资料标题'), f('document_version', 'text', '资料版本'), f('document_date', 'date', '资料发布日期'), f('document_ref', 'text', '资料文件链接'), f('citation_locator', 'text', '证据定位(页码/章节/表格)'), f('source_contact_ref', 'text', '原厂支持联系人'), f('document_status', 'text', '资料状态'), ]), /* ── 需求:P-01 客户具体需求与项目任务主台账 ── */ master('demand', 'request', '客户需求主记录(P-01)', [ f('request_id', 'text', '需求编号', true), f('customer_code', 'text', '客户编码'), f('project_code', 'text', '项目编码'), f('requirement_version', 'text', '需求版本'), f('request_type', 'text', '需求类型'), f('request_description', 'text', '需求描述'), f('input_snapshot_ref', 'text', '输入快照'), f('bom_id', 'text', 'BOM ID'), f('bom_position', 'text', 'BOM 位'), f('original_internal_pn', 'text', '原内部料号'), f('customer_pn', 'text', '客户料号'), f('application_scenario', 'text', '应用场景'), f('technical_constraints', 'text', '技术约束'), f('risk_level', 'text', '风险等级'), f('verification_requirement', 'text', '验证/复测/确认要求'), f('request_status', 'text', '需求状态'), f('current_activity', 'text', '当前 Activity'), f('current_stage', 'text', '当前阶段'), f('next_action', 'text', '下一步动作'), f('output_ref', 'text', '标准输出链接'), ]), /* ── 替代:P-11 替代选型与批准关系库 —— 主记录 + 差异明细子表(《统一方案》§六:重复明细用子表)── */ master('substitution', 'relation', '替代关系主记录(P-11)', [ f('substitution_relation_id', 'text', '替代关系ID', true), f('request_id', 'text', '关联需求编号'), f('project_code', 'text', '关联项目编码'), f('bom_position', 'text', '关联 BOM 位'), f('original_internal_pn', 'text', '原内部料号'), f('candidate_internal_pn', 'text', '候选内部料号'), f('substitution_type', 'text', '替代类型'), f('recommendation_level', 'text', '推荐等级'), f('recommendation_reason', 'text', '推荐理由'), f('difference_summary', 'text', '差异摘要'), f('risk_level', 'text', '替代风险等级'), f('verification_requirement', 'text', '验证要求'), f('validation_id', 'text', '关联验证ID'), f('analysis_status', 'text', '分析状态'), /* 客户批准三列由独立审批流维护 —— 基线建列,差异评估活动**不声明写权** */ f('customer_approval_status', 'text', '客户批准状态'), f('customer_approval_at', 'timestamp', '客户批准时间'), f('customer_approval_ref', 'text', '客户批准凭证'), f('customer_feedback', 'text', '客户反馈'), f('analysis_report_ref', 'text', '替代分析报告'), f('confidence_score', 'number', '证据可信度'), f('rule_version', 'text', '差异规则版本'), ]), master('substitution', 'difference_item', '差异明细(P-11 子表)', [ f('substitution_relation_id', 'text', '替代关系ID', true), f('difference_item_id', 'text', '差异明细ID', true), f('parameter_name', 'text', '标准参数名'), f('original_value', 'text', '原物料参数值'), f('candidate_value', 'text', '候选物料参数值'), f('parameter_unit', 'text', '参数单位'), f('constraint_type', 'text', '约束类型'), f('difference_status', 'text', '差异状态'), f('impact_statement', 'text', '影响说明'), f('risk_level', 'text', '风险等级'), f('evidence_ref', 'text', '差异证据引用'), f('confidence', 'number', 'AI 置信度'), f('rule_version', 'text', '差异规则版本'), ]), /* ── 验证:P-06 样品交付与芯片验证记录(事实类:只能实测产生,数据工单不许建)── */ master('validation', 'record', '芯片验证记录(P-06)', [ f('validation_id', 'text', '验证ID', true), f('request_id', 'text', '关联需求编号'), f('customer_code', 'text', '客户编码'), f('validation_internal_pn', 'text', '验证内部料号'), f('validation_type', 'text', '验证类型'), f('test_condition', 'text', '测试条件'), f('test_parameters', 'text', '测试参数'), f('test_result', 'text', '测试结果'), f('validation_status', 'text', '验证状态'), f('issue_risk', 'text', '问题与风险'), f('sample_version', 'text', '样品版本'), f('sample_lot', 'text', '样品批次'), f('evidence_ref', 'text', '验证证据引用'), f('next_validation_action', 'text', '后续验证动作'), ]), ``` 并把 `MASTER_DOMAINS` 改为: ```ts /** 八域的呈现次序 —— 谁在买 → 为什么买 → 从哪买 → 买什么 → 料件 → 需求 → 替代 → 验证 */ export const MASTER_DOMAINS = ['customer', 'project', 'supplier', 'market', 'parts', 'demand', 'substitution', 'validation'] as const; ``` - [ ] **Step 4: 在 `MASTER_SAMPLE_ROWS` 数组末尾追加样例料号**(现有最后一个元素之后、`];` 之前) ```ts /* * ── SS02 差异评估的样例料号(2026-08-26)── * 只播**原 PN**与**同族可调版**两颗:「都在库」那条路靠它们;候选 XL2596S-5.0 与国产 DCDC * **刻意不播** —— 「候选缺库 → 派数据工单」「硬约束不推荐」两条路要试得出来,库里得真没有。 * 参数集一行一参数(参数名 | 值 | 单位 | hard/soft | 说明),与 P-08 声明的行语法同一份。 */ { address: { domain: 'parts', object: 'profile' }, key: { internal_pn: 'LM2596S-5.0' }, values: { internal_pn: 'LM2596S-5.0', brand_name: 'TI', manufacturer_name: 'Texas Instruments', specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz', normalized_parameter_set: [ '输入电压范围 | 4.5~40 | V | hard | 客户输入上限 40V', '输出电压 | 5.0 | V | hard | 固定版', '输出电流 | 3 | A | hard | 满载能力', '开关频率 | 150 | kHz | hard | 影响外围电感/电容', '静态电流 | 5 | mA | soft | 待机功耗', '工作温度 | -40~+85 | ℃ | hard | 工业温域', '使能阈值 | 1.4 | V | hard | ON/OFF 引脚阈值', ].join('\n'), package: 'TO-263-5', process_technology: 'BCD', lifecycle_status: 'nrnd', eol_pcn_status: 'PCN-2026-07:转 NRND,2027-01 起不接新设计', datasheet_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf', datasheet_version: 'Rev H (2023-09)', application_scenarios: '工业电源;消费电源', }, }, { address: { domain: 'parts', object: 'profile' }, key: { internal_pn: 'LM2596S-ADJ' }, values: { internal_pn: 'LM2596S-ADJ', brand_name: 'TI', manufacturer_name: 'Texas Instruments', specification_description: '降压型 DCDC 稳压器,3A,可调输出 1.23~37V,150kHz(LM2596 同族)', normalized_parameter_set: [ '输入电压范围 | 4.5~40 | V | hard | 与固定版同', '输出电压 | 1.23~37 | V | hard | 可调版,靠外围反馈网络定', '输出电流 | 3 | A | hard | 满载能力', '开关频率 | 150 | kHz | hard | 影响外围电感/电容', '静态电流 | 5 | mA | soft | 待机功耗', '工作温度 | -40~+85 | ℃ | hard | 工业温域', '使能阈值 | 1.4 | V | hard | ON/OFF 引脚阈值', ].join('\n'), package: 'TO-263-5', process_technology: 'BCD', lifecycle_status: 'active', datasheet_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf', datasheet_version: 'Rev H (2023-09)', application_scenarios: '工业电源;消费电源', }, }, { address: { domain: 'parts', object: 'document' }, key: { internal_pn: 'LM2596S-5.0', doc_seq: '01' }, values: { internal_pn: 'LM2596S-5.0', doc_seq: '01', document_type: 'datasheet', brand_name: 'TI', document_title: 'LM2596 SIMPLE SWITCHER Power Converter 150-kHz 3-A Step-Down Voltage Regulator', document_version: 'Rev H (2023-09)', document_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf', citation_locator: '§6.5 Electrical Characteristics 表 / §6.3 Recommended Operating Conditions', document_status: 'valid', }, }, { address: { domain: 'parts', object: 'document' }, key: { internal_pn: 'LM2596S-ADJ', doc_seq: '01' }, values: { internal_pn: 'LM2596S-ADJ', doc_seq: '01', document_type: 'datasheet', brand_name: 'TI', document_title: 'LM2596 SIMPLE SWITCHER Power Converter 150-kHz 3-A Step-Down Voltage Regulator', document_version: 'Rev H (2023-09)', document_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf', citation_locator: '§6.5 Electrical Characteristics 表(ADJ 列)', document_status: 'valid', }, }, ``` 同时把 `seed-activities.ts:143` 那行日志改成不依赖下标(新行插在数组末尾,`[1]` 仍是样例客户,不改也不炸;但下一个人再往前插就错)——改为: ```ts log(` 样例主数据已就位:${MASTER_SAMPLE_ROWS.length} 行(客户 / 项目 / 联系人 / 料号)`); ``` - [ ] **Step 5: 跑测试确认绿** Run: `pnpm vitest run apps/api/src/app/__tests__/seed-mvp-data.test.ts` Expected: PASS(含新加 3 条)。 - [ ] **Step 6: 类型检查 + 提交** ```bash pnpm -s typecheck git add apps/api/src/app/seed-mvp-data.ts apps/api/src/app/__tests__/seed-mvp-data.test.ts apps/api/src/app/seed-activities.ts git commit -m "feat(w05): 主数据基线加料件/需求/替代/验证四域六表(P-08/P-10/P-01/P-11/P-06)与样例料号 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh" ``` --- ### Task 2: 两枚纯工具 —— 逐参数比对(compare-parameters)与推荐定级(recommend) **Files:** - Modify: `apps/api/src/app/tools/first-party.ts`(在 `FIRST_PARTY_TOOLS` 之前加两段实现;数组末尾登记两条) - Create: `apps/api/src/app/tools-w05.ts`(两条工具契约 + 试运行样例) - Modify: `apps/api/src/app/seed-mvp.ts`(注册循环的数组加进 `W05_TOOLS`) - Test: `apps/api/src/app/__tests__/first-party-substitution.test.ts`(新建) **Interfaces:** - Produces 工具 `part-substitution-assessment.compare-parameters`: 入参 `{ candidates: Array<{ candidateInternalPn: string; pairs: Array<{ parameterName: string; originalValue: string; candidateValue: string; unit?: string; constraintType: 'hard'|'soft'; requirement?: string; evidenceRef?: string }> }> }` 出参 `{ ruleVersion: 'R-2026.07'; results: Array<{ candidateInternalPn; items: Array<{ parameterName; compareResult; differenceStatus: 'match'|'mismatch'|'unknown'|'conditional'; riskLevel: 'low'|'medium'|'high'; note }>; counts: { match; mismatch; unknown; conditional; hardMismatch; softMismatch; configurable } }> }` - Produces 工具 `part-substitution-assessment.recommend`: 入参 `{ candidates: Array<{ candidateInternalPn: string; counts: { match; mismatch; unknown; conditional; hardMismatch; softMismatch; configurable } }> }` 出参 `{ ruleVersion; results: Array<{ candidateInternalPn; recommendationLevel: 'recommended'|'conditional'|'alternate'|'not_recommended'; riskLevel; reason }> }` - 出参键与台账列同名之处:`differenceStatus → difference_status`、`riskLevel → risk_level`、`recommendationLevel → recommendation_level`(模型照抄,不必改名)。 - [ ] **Step 1: 写测试** `apps/api/src/app/__tests__/first-party-substitution.test.ts`: ```ts /** * ACT-WORK-05 差异评估的两枚纯工具 —— 「能算的不问 LLM」:同输入必同结论。 * 用例逐条对应《ACT-WORK-05-活动流程与工单说明》§八的示例矩阵: * A XL2596S-5.0 条件推荐(使能阈值未披露 → UNKNOWN)/ B LM2596S-ADJ 备选(可调版靠外围配成 5V)/ * C 国产 DCDC 不推荐(输入上限 32V < 客户 40V,硬约束 MISMATCH)。 */ import { describe, expect, it } from 'vitest'; import { FIRST_PARTY_TOOLS } from '../tools/first-party.ts'; const handler = (name: string) => { const found = FIRST_PARTY_TOOLS.find(([n]) => n === name); if (found === undefined) throw new Error(`没有工具 ${name}`); return found[1]; }; /** ToolHandler 的入参形状见 `apps/api/src/modules/toolbox/internal/adapter/toolbox-store.ts`:纯工具不用连接目标 */ const call = async (name: string, input: Record) => { const out = await handler(name)({ input, connection: {}, operation: undefined, idempotencyKey: undefined }); if (!out.ok) throw new Error(`工具失败:${JSON.stringify(out)}`); return out.output as Record; }; type Item = { parameterName: string; compareResult: string; differenceStatus: string; riskLevel: string }; type Result = { candidateInternalPn: string; items: Item[]; counts: Record }; const pair = (parameterName: string, originalValue: string, candidateValue: string, constraintType: 'hard' | 'soft', extra: Record = {}) => ({ parameterName, originalValue, candidateValue, constraintType, evidenceRef: 'P-08 · P-10 Rev H', ...extra, }); describe('compare-parameters:逐参数四态', () => { it('候选温域更宽 → covers → MATCH 低风险;固定 vs 可调 → configurable → CONDITIONAL;未披露 → UNKNOWN', async () => { const out = await call('part-substitution-assessment.compare-parameters', { candidates: [ { candidateInternalPn: 'XL2596S-5.0', pairs: [ pair('工作温度', '-40~+85', '-40~+125', 'hard', { unit: '℃' }), pair('输出电压', '5.0', '5.0', 'hard', { unit: 'V' }), pair('使能阈值', '1.4', '未披露', 'hard', { unit: 'V' }), pair('静态电流', '5', '4.2', 'soft', { unit: 'mA' }), ], }, { candidateInternalPn: 'LM2596S-ADJ', pairs: [pair('输出电压', '5.0', '1.23~37', 'hard', { unit: 'V' })], }, ], }); const [a, b] = out['results'] as Result[]; expect(out['ruleVersion']).toBe('R-2026.07'); const by = (r: Result, name: string) => r.items.find((i) => i.parameterName === name); expect(by(a, '工作温度')).toMatchObject({ compareResult: 'covers', differenceStatus: 'match', riskLevel: 'low' }); expect(by(a, '输出电压')).toMatchObject({ compareResult: 'equal', differenceStatus: 'match' }); expect(by(a, '使能阈值')).toMatchObject({ compareResult: 'unknown', differenceStatus: 'unknown', riskLevel: 'medium' }); expect(by(a, '静态电流')).toMatchObject({ differenceStatus: 'conditional' }); // 软约束数值不同 → 条件 expect(a.counts).toMatchObject({ match: 2, unknown: 1, conditional: 1, mismatch: 0, hardMismatch: 0 }); expect(by(b, '输出电压')).toMatchObject({ compareResult: 'configurable', differenceStatus: 'conditional' }); expect(b.counts['configurable']).toBe(1); }); it('硬约束范围变窄 → MISMATCH 高风险;客户约束(requirement)不满足也是 MISMATCH', async () => { const out = await call('part-substitution-assessment.compare-parameters', { candidates: [ { candidateInternalPn: 'CN-DCDC-32V', pairs: [ pair('输入电压范围', '4.5~40', '4.5~32', 'hard', { unit: 'V', requirement: '>= 40' }), pair('封装', 'TO-263-5', 'TO-263-5', 'hard'), ], }, ], }); const [c] = out['results'] as Result[]; expect(c.items[0]).toMatchObject({ compareResult: 'narrower', differenceStatus: 'mismatch', riskLevel: 'high' }); expect(c.items[1]).toMatchObject({ compareResult: 'equal', differenceStatus: 'match' }); expect(c.counts).toMatchObject({ mismatch: 1, hardMismatch: 1, match: 1 }); }); it('硬约束相等但没有证据引用 → 不判 MATCH,降为 CONDITIONAL(硬约束项未经证据支持不得判 MATCH)', async () => { const out = await call('part-substitution-assessment.compare-parameters', { candidates: [{ candidateInternalPn: 'X', pairs: [{ parameterName: '封装', originalValue: 'TO-263-5', candidateValue: 'TO-263-5', constraintType: 'hard' }] }], }); const [x] = out['results'] as Result[]; expect(x.items[0]).toMatchObject({ compareResult: 'equal', differenceStatus: 'conditional' }); }); it('数值带单位/括号注记也认得:「5.0(固定)」「3A」「150 kHz」;生命周期按活跃度比', async () => { const out = await call('part-substitution-assessment.compare-parameters', { candidates: [{ candidateInternalPn: 'X', pairs: [pair('输出电压', '5.0(固定)', '5.0 V', 'hard'), pair('输出电流', '3A', '3', 'hard'), pair('开关频率', '150 kHz', '150', 'hard'), pair('生命周期', 'nrnd', 'active', 'soft')] }], }); const [x] = out['results'] as Result[]; // 生命周期不是数值:候选比原料更活(nrnd → active)算 covers → MATCH,替 NRND 料本就是为了这个 expect(x.items.map((i) => i.differenceStatus)).toEqual(['match', 'match', 'match', 'match']); expect(x.items[3]?.compareResult).toBe('covers'); }); }); describe('recommend:推荐等级四档', () => { it('硬约束 MISMATCH → 不推荐;有 UNKNOWN → 条件推荐;只有 configurable → 备选;全 MATCH → 推荐', async () => { const out = await call('part-substitution-assessment.recommend', { candidates: [ { candidateInternalPn: 'C', counts: { match: 5, mismatch: 1, unknown: 0, conditional: 0, hardMismatch: 1, softMismatch: 0, configurable: 0 } }, { candidateInternalPn: 'A', counts: { match: 5, mismatch: 0, unknown: 1, conditional: 0, hardMismatch: 0, softMismatch: 0, configurable: 0 } }, { candidateInternalPn: 'B', counts: { match: 6, mismatch: 0, unknown: 0, conditional: 1, hardMismatch: 0, softMismatch: 0, configurable: 1 } }, { candidateInternalPn: 'D', counts: { match: 7, mismatch: 0, unknown: 0, conditional: 0, hardMismatch: 0, softMismatch: 0, configurable: 0 } }, ], }); const results = out['results'] as Array<{ candidateInternalPn: string; recommendationLevel: string; riskLevel: string }>; expect(results.map((r) => [r.candidateInternalPn, r.recommendationLevel, r.riskLevel])).toEqual([ ['C', 'not_recommended', 'high'], ['A', 'conditional', 'medium'], ['B', 'alternate', 'medium'], ['D', 'recommended', 'low'], ]); }); it('既有 UNKNOWN 又有 configurable:UNKNOWN 优先 → 条件推荐(验证闭环前不给备选)', async () => { const out = await call('part-substitution-assessment.recommend', { candidates: [{ candidateInternalPn: 'E', counts: { match: 1, mismatch: 0, unknown: 1, conditional: 1, hardMismatch: 0, softMismatch: 0, configurable: 1 } }], }); expect((out['results'] as Array<{ recommendationLevel: string }>)[0]?.recommendationLevel).toBe('conditional'); }); }); ``` - [ ] **Step 2: 跑测试确认红** Run: `pnpm vitest run apps/api/src/app/__tests__/first-party-substitution.test.ts` Expected: FAIL(`没有工具 part-substitution-assessment.compare-parameters`)。 - [ ] **Step 3: 在 `first-party.ts` 的 `export const FIRST_PARTY_TOOLS` 之前加实现** ```ts /* ── ACT-WORK-05 差异评估:逐参数比对 —— 纯工具(活动私有作用域)───────────────── * * 「能算的不问 LLM」:同输入必同结论。规则与活动包 manifest 里的 D01/D02 两张 DMN 同一份; * 这里是求值器,那里是随包冻结的文档。`RULE_VERSION` 随结论落账(rule_version)。 * * 比对语义(按硬/软约束): * equal / covers(候选范围 ⊇ 原范围) → match,低风险; * configurable(原是定值、候选是含它的可调范围)→ conditional(要改外围); * narrower / differs / partial → 硬:mismatch 高风险;软:conditional 中风险; * 任一侧未披露 → unknown 中风险; * 硬约束 equal/covers 但没给 evidenceRef → conditional(硬约束未经证据支持不得判 MATCH); * requirement(客户约束,如 ">= 40")候选不满足 → 一律 mismatch 高风险。 */ export const SUBSTITUTION_RULE_VERSION = 'R-2026.07'; type Parsed = { kind: 'number'; value: number } | { kind: 'range'; lo: number; hi: number } | { kind: 'text'; value: string } | { kind: 'unknown' }; type CompareResult = 'equal' | 'covers' | 'configurable' | 'narrower' | 'partial' | 'differs' | 'unknown'; type DifferenceStatus = 'match' | 'mismatch' | 'unknown' | 'conditional'; type RiskLevel = 'low' | 'medium' | 'high'; type ParamPair = { parameterName: string; originalValue: string; candidateValue: string; unit?: string; constraintType: 'hard' | 'soft'; requirement?: string; evidenceRef?: string; }; type CompareCounts = { match: number; mismatch: number; unknown: number; conditional: number; hardMismatch: number; softMismatch: number; configurable: number }; const UNKNOWN_WORDS = /^(未披露|未知|未给出|unknown|n\/?a|tbd|待定|-|—|–)$/i; const NUM = '[-+]?\\d+(?:\\.\\d+)?'; const RANGE_RE = new RegExp(`^(${NUM})\\s*(?:~|~|-|–|—|至|to)\\s*(${NUM})`, 'i'); const NUM_RE = new RegExp(`^(${NUM})`); export function parseParamValue(raw: unknown): Parsed { const s = String(raw ?? '').trim().replace(/\s+/g, ' '); if (s === '' || UNKNOWN_WORDS.test(s)) return { kind: 'unknown' }; const r = RANGE_RE.exec(s); if (r !== null) { const lo = Number(r[1]); const hi = Number(r[2]); if (Number.isFinite(lo) && Number.isFinite(hi)) return { kind: 'range', lo: Math.min(lo, hi), hi: Math.max(lo, hi) }; } const n = NUM_RE.exec(s); if (n !== null && Number.isFinite(Number(n[1]))) return { kind: 'number', value: Number(n[1]) }; return { kind: 'text', value: s.toLowerCase() }; } function compareParsed(o: Parsed, c: Parsed): CompareResult { if (o.kind === 'unknown' || c.kind === 'unknown') return 'unknown'; if (o.kind === 'range' && c.kind === 'range') { if (c.lo === o.lo && c.hi === o.hi) return 'equal'; if (c.lo <= o.lo && c.hi >= o.hi) return 'covers'; if (c.lo >= o.lo && c.hi <= o.hi) return 'narrower'; return c.hi < o.lo || c.lo > o.hi ? 'differs' : 'partial'; } if (o.kind === 'number' && c.kind === 'number') return Math.abs(o.value - c.value) <= 1e-9 ? 'equal' : 'differs'; if (o.kind === 'number' && c.kind === 'range') return c.lo <= o.value && o.value <= c.hi ? 'configurable' : 'differs'; if (o.kind === 'range' && c.kind === 'number') return o.lo <= c.value && c.value <= o.hi ? 'narrower' : 'differs'; if (o.kind === 'text' && c.kind === 'text') return o.value === c.value ? 'equal' : 'differs'; return 'differs'; } /** 客户约束:">= 40" / "<= 85" / "-40~85"(候选必须盖住);解析不了当没给 */ function satisfiesRequirement(requirement: string | undefined, c: Parsed): boolean | undefined { if (requirement === undefined || requirement.trim() === '' || c.kind === 'unknown' || c.kind === 'text') return undefined; const s = requirement.trim(); const ge = new RegExp(`^(?:>=|≥)\\s*(${NUM})$`).exec(s); const le = new RegExp(`^(?:<=|≤)\\s*(${NUM})$`).exec(s); const hi = c.kind === 'range' ? c.hi : c.value; const lo = c.kind === 'range' ? c.lo : c.value; if (ge !== null) return hi >= Number(ge[1]); if (le !== null) return lo <= Number(le[1]); const req = parseParamValue(s); if (req.kind === 'range') return lo <= req.lo && hi >= req.hi; if (req.kind === 'number') return lo <= req.value && hi >= req.value; return undefined; } /** 生命周期不是数值:候选比原料「更活」算 covers(替 NRND 料本就是为了这个),更差才是 differs */ const LIFECYCLE_ORDER: Record = { active: 3, nrnd: 2, risk: 2, eol: 1, discontinued: 0 }; function compareLifecycle(o: string, c: string): CompareResult | undefined { const ro = LIFECYCLE_ORDER[o.trim().toLowerCase()]; const rc = LIFECYCLE_ORDER[c.trim().toLowerCase()]; if (ro === undefined || rc === undefined) return undefined; return rc === ro ? 'equal' : rc > ro ? 'covers' : 'differs'; } function judgePair(p: ParamPair): { compareResult: CompareResult; differenceStatus: DifferenceStatus; riskLevel: RiskLevel; note: string } { const o = parseParamValue(p.originalValue); const c = parseParamValue(p.candidateValue); const hard = p.constraintType === 'hard'; let compareResult = compareParsed(o, c); if (/生命周期|lifecycle/i.test(p.parameterName)) compareResult = compareLifecycle(p.originalValue, p.candidateValue) ?? compareResult; const req = satisfiesRequirement(p.requirement, c); if (req === false) { // 客户约束不满足:比对结果照实留(narrower/differs),结论一律 MISMATCH 高风险 return { compareResult: compareResult === 'unknown' ? 'differs' : compareResult, differenceStatus: 'mismatch', riskLevel: 'high', note: `不满足客户约束 ${p.requirement ?? ''}` }; } if (compareResult === 'unknown') return { compareResult, differenceStatus: 'unknown', riskLevel: 'medium', note: '一侧未披露:标 UNKNOWN,挂验证任务或补来源' }; if (compareResult === 'equal' || compareResult === 'covers') { if (hard && (p.evidenceRef === undefined || p.evidenceRef.trim() === '')) { return { compareResult, differenceStatus: 'conditional', riskLevel: 'medium', note: '硬约束缺证据引用:未经证据支持不判 MATCH' }; } return { compareResult, differenceStatus: 'match', riskLevel: 'low', note: compareResult === 'covers' ? '候选范围盖住原范围' : '一致' }; } if (compareResult === 'configurable') return { compareResult, differenceStatus: 'conditional', riskLevel: 'medium', note: '候选可调,须改外围把它配成原值' }; if (compareResult === 'partial') return { compareResult, differenceStatus: 'conditional', riskLevel: hard ? 'medium' : 'low', note: '范围部分重叠,须按应用边界确认' }; compareResult = compareResult === 'narrower' ? 'narrower' : 'differs'; return hard ? { compareResult, differenceStatus: 'mismatch', riskLevel: 'high', note: '硬约束不一致' } : { compareResult, differenceStatus: 'conditional', riskLevel: 'medium', note: '软约束不一致,须评估影响' }; } const compareParameters: ToolHandler = async ({ input }) => { const candidates = Array.isArray(input['candidates']) ? (input['candidates'] as Array<{ candidateInternalPn: string; pairs: ParamPair[] }>) : []; const results = candidates.map((cand) => { const counts: CompareCounts = { match: 0, mismatch: 0, unknown: 0, conditional: 0, hardMismatch: 0, softMismatch: 0, configurable: 0 }; const items = (Array.isArray(cand.pairs) ? cand.pairs : []).map((p) => { const j = judgePair(p); counts[j.differenceStatus] += 1; if (j.differenceStatus === 'mismatch') counts[p.constraintType === 'hard' ? 'hardMismatch' : 'softMismatch'] += 1; if (j.compareResult === 'configurable') counts.configurable += 1; return { parameterName: p.parameterName, unit: p.unit ?? '', constraintType: p.constraintType, ...j }; }); return { candidateInternalPn: cand.candidateInternalPn, items, counts }; }); return { ok: true, output: { ruleVersion: SUBSTITUTION_RULE_VERSION, results } }; }; /** * 推荐定级(D02,FIRST):硬 MISMATCH → 不推荐;有 UNKNOWN → 条件推荐(等验证闭环); * 有 configurable(改外围配得成)→ 备选;其余 conditional → 条件推荐;全 MATCH → 推荐。与 manifest 里的 D02 表同一份规则。 */ const recommend: ToolHandler = async ({ input }) => { const candidates = Array.isArray(input['candidates']) ? (input['candidates'] as Array<{ candidateInternalPn: string; counts: Partial }>) : []; const results = candidates.map((cand) => { const n = (k: keyof CompareCounts) => Number(cand.counts?.[k] ?? 0); if (n('hardMismatch') > 0) return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'not_recommended', riskLevel: 'high', reason: `硬约束 MISMATCH ${n('hardMismatch')} 项:硬约束失败直接不推荐` }; if (n('unknown') > 0) return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'conditional', riskLevel: 'medium', reason: `UNKNOWN ${n('unknown')} 项待验证闭环,通过后可升级推荐` }; if (n('configurable') > 0) return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'alternate', riskLevel: 'medium', reason: `${n('configurable')} 项须改外围配成原值(另有 ${n('conditional') - n('configurable')} 项条件差异):客户接受改设计则启用` }; if (n('conditional') > 0) return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'conditional', riskLevel: 'medium', reason: `CONDITIONAL ${n('conditional')} 项须按应用边界确认` }; return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'recommended', riskLevel: 'low', reason: `${n('match')} 项全部 MATCH` }; }); return { ok: true, output: { ruleVersion: SUBSTITUTION_RULE_VERSION, results } }; }; ``` 在 `FIRST_PARTY_TOOLS` 数组末尾登记: ```ts ['part-substitution-assessment.compare-parameters', compareParameters], ['part-substitution-assessment.recommend', recommend], ``` - [ ] **Step 4: 跑测试确认绿** Run: `pnpm vitest run apps/api/src/app/__tests__/first-party-substitution.test.ts` Expected: PASS 6 条。 - [ ] **Step 5: 工具契约放独立文件 `apps/api/src/app/tools-w05.ts`(seed-mvp 与 seed-activities 都要用,放 seed-mvp 里会撞循环 import)** ```ts /** * ACT-WORK-05 差异评估的两枚纯工具契约(活动私有作用域)。 * 两处消费:`seed-mvp.ts`(db:seed 全套播种)与 `seed-activities.ts`(activities:install 装活动时顺带注册 —— * 「安装即用」包含它声明过的纯工具能调得动;从前 E04 的工具只在 db:seed 里注册,开发机上只跑 * activities:install 的人第一次跑活动就撞「工具不存在」)。 */ const PURE = { effect: 'read' as const, errors: [] as Array<{ code: string; retryable: boolean; message: string }>, idempotent: true, timeoutMs: 5_000, retries: 0, connectionKeys: [] as string[], pure: true, ownerActivityId: 'part-substitution-assessment', }; /** 逐参数比对 —— 出参键与台账列同名(difference_status / risk_level),模型照抄 */ export const COMPARE_PARAMETERS_TOOL = { ...PURE, name: 'part-substitution-assessment.compare-parameters', displayName: '替代料逐参数比对', version: '1.0.0', description: '按硬/软约束逐参数比对原 PN 与候选 PN,输出四态 difference_status(match/mismatch/unknown/conditional)、' + 'compare_result、risk_level 与各候选的计数。一次可比一批候选(candidates 数组),每对参数可带客户约束 requirement(如 ">= 40")与证据引用 evidenceRef;' + '硬约束没有证据引用不判 match。规则版本 R-2026.07。', input: { type: 'object', properties: { candidates: { type: 'array', description: '[{candidateInternalPn, pairs:[{parameterName, originalValue, candidateValue, unit?, constraintType:"hard"|"soft", requirement?, evidenceRef?}]}]', }, }, }, output: { type: 'object', properties: { ruleVersion: { type: 'string' }, results: { type: 'array', description: '[{candidateInternalPn, items:[{parameterName, unit, constraintType, compareResult, differenceStatus, riskLevel, note}], counts:{match,mismatch,unknown,conditional,hardMismatch,softMismatch,configurable}}]', }, }, }, }; /** 推荐定级 —— 出参 recommendationLevel / riskLevel / reason 直接落 P-11 */ export const RECOMMEND_TOOL = { ...PURE, name: 'part-substitution-assessment.recommend', displayName: '替代推荐定级', version: '1.0.0', description: '按每个候选的比对计数定推荐等级:硬约束 mismatch>0 → not_recommended;unknown>0 → conditional;' + '只有可调配成(configurable)→ alternate;其余 conditional → conditional;全 match → recommended。', input: { type: 'object', properties: { candidates: { type: 'array', description: '[{candidateInternalPn, counts:{match,mismatch,unknown,conditional,hardMismatch,softMismatch,configurable}}]' }, }, }, output: { type: 'object', properties: { ruleVersion: { type: 'string' }, results: { type: 'array', description: '[{candidateInternalPn, recommendationLevel, riskLevel, reason}]' }, }, }, }; export const W05_TOOLS = [COMPARE_PARAMETERS_TOOL, RECOMMEND_TOOL] as const; /** 试运行审的样例(两件共用一份:并集覆盖各家入参) */ export const W05_TOOL_SAMPLE = { input: { candidates: [ { candidateInternalPn: 'XL2596S-5.0', pairs: [{ parameterName: '输出电压', originalValue: '5.0', candidateValue: '5.0', unit: 'V', constraintType: 'hard', evidenceRef: 'P-08' }], counts: { match: 1, mismatch: 0, unknown: 0, conditional: 0, hardMismatch: 0, softMismatch: 0, configurable: 0 }, }, ], }, expectShape: { type: 'object' }, }; ``` 然后在 `seed-mvp.ts`:顶部加 `import { W05_TOOLS } from './tools-w05.ts';`,把 `for (const contract of [HISTORY_TOOL, MAIL_TOOL, CLASSIFY_TOOL, PLACEHOLDER_TOOL, COVERAGE_SCORE_TOOL, RANK_KEY_PERSONS_TOOL])` 改为 `for (const contract of [HISTORY_TOOL, MAIL_TOOL, CLASSIFY_TOOL, PLACEHOLDER_TOOL, COVERAGE_SCORE_TOOL, RANK_KEY_PERSONS_TOOL, ...W05_TOOLS])`(那条「读类 1 · 写类 1 · 纯工具 4」的日志改成「纯工具 6」)。 - [ ] **Step 6: 门禁子集 + 提交** Run: `pnpm -s typecheck && pnpm -s lint && pnpm vitest run apps/api/src/app/__tests__` Expected: 全绿。 ```bash git add apps/api/src/app/tools/first-party.ts apps/api/src/app/seed-mvp.ts apps/api/src/app/tools-w05.ts apps/api/src/app/__tests__/first-party-substitution.test.ts git commit -m "feat(w05): 差异评估两枚纯工具 —— 逐参数四态比对与推荐定级(R-2026.07) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh" ``` --- ### Task 3: 活动包骨架 + 清单(manifest.ts)+ 结构自测 **Files:** - Create: `activities/part-substitution-assessment/package.json` · `src/index.ts` · `src/constants.ts` · `src/skills.ts` · `src/views/prd-style.ts` · `src/manifest.ts` - Create: `activities/part-substitution-assessment/__tests__/activity.test.ts` - 依赖(本任务先以占位存在,Task 4/5 替换):`src/case-model.ts`、`src/views/report.ts` **Interfaces:** - Produces:`manifest`(ActivityManifest)、常量 `RULE_VERSION = 'R-2026.07'`、`D = 'sub_eval'`;台账地址与列名(Task 4 剧本 / Task 5 模版 / Task 7 e2e 全部照这里抄): - `customer.profile`(customer_code PK;name required;industry/region expect) - `parts.profile`(internal_pn PK;brand_name★ specification_description★ normalized_parameter_set★ package★ lifecycle_status★ tally `active/nrnd/eol/discontinued/risk`;manufacturer_name / process_technology / eol_pcn_status / datasheet_ref / datasheet_version / application_scenarios expect) - `parts.document`(internal_pn + doc_seq PK;document_type★ tally `datasheet/app_note/reference_design/white_paper/sample_doc/commitment_letter/other`;document_title★ document_version★ document_ref★ document_status★ tally `valid/expired/pending/void`;citation_locator / brand_name expect) - `demand.request`(request_id PK;customer_code★ refersTo customer.profile.customer_code;requirement_version★ request_type★ request_description★ input_snapshot_ref★ original_internal_pn★ request_status★ tally `pending/responded/won/abandoned/closed`;application_scenario / bom_position expect;risk_level tally `low/medium/high/critical`、verification_requirement、output_ref、next_action、current_activity 均 `requiredWhen {current_stage:'gate03'}`;current_stage 自由文本) - `substitution.relation`(substitution_relation_id PK;request_id★;original_internal_pn★ / candidate_internal_pn★ 均 refersTo parts.profile.internal_pn;substitution_type★ tally `pin_to_pin/functional/parametric/other`;recommendation_level★ tally `recommended/conditional/alternate/not_recommended`;recommendation_reason★ difference_summary★;risk_level★ tally `low/medium/high/critical`;verification_requirement requiredWhen {recommendation_level:'conditional'};validation_id;analysis_status★ tally `ai_draft/fae_confirmed/pending_customer/customer_approved/rejected/void`;bom_position / analysis_report_ref / confidence_score expect;rule_version★;eval_ref★) - `substitution.difference_item`(substitution_relation_id PK distinct refersTo substitution.relation.substitution_relation_id;difference_item_id PK;parameter_name★ original_value★ candidate_value★;parameter_unit expect;constraint_type★ tally `hard/soft`;compare_result expect;difference_status★ tally `match/mismatch/unknown/conditional`;impact_statement★;unknown_disposition requiredWhen {difference_status:'unknown'};risk_level★ tally `low/medium/high`;evidence_ref★;confidence expect;rule_version★) - `validation.record`(validation_id PK;request_id★;validation_internal_pn★ refersTo parts.profile.internal_pn;validation_type★ tally `pin_to_pin/functional/reliability/certification/retest`;validation_status★ tally `not_started/in_progress/passed/failed/conditional/pending_evidence`;next_validation_action★;test_condition / evidence_ref expect) - `sub_eval.pn_check`(record,caseKey eval_ref;eval_ref + internal_pn PK;role★ tally `original/candidate`;exists_status★ tally `found/absent/created/abandoned`;abandon_reason requiredWhen {exists_status:'abandoned'};note) - `sub_eval.input_gap`(record;eval_ref + item_key PK;input_item★;source_ledger expect;status★ tally `gap/default/ok`;detail expect;supplement requiredWhen {status:'gap'};origin) - `sub_eval.verdict`(record;eval_ref PK;request_id、rule_version、original_internal_pn、candidate_total integer、input_confirmed_total、review_approved_total、landing_prepared_total、relation_total、recommended_total、conditional_total、alternate_total、not_recommended_total、summary) - Produces 出口 `assessment-released`,载荷 `{ evalRef, requestId, relationTotal, recommendedTotal, conditionalTotal, alternateTotal, notRecommendedTotal }`(全部由 `sub_eval.verdict` 同名列承载)。 - Produces 视图名:`p08-master-create` / `p10-document-create` / `eval-input-list` / `difference-review` / `landing-confirm`(Task 5 必须导出同名常量 `P08_MASTER_CREATE` / `P10_DOCUMENT_CREATE` / `EVAL_INPUT_LIST` / `DIFFERENCE_REVIEW` / `LANDING_CONFIRM`)。 - Produces 计划项 id(Task 4 的 CMMN / STEPS 必须一字不差):`S0_bind` `S0d_master_data` `S1_input_confirm` `S2_normalize` `S3_assess` `S4_review` `S5_prepare_landing` `T_release`。 - [ ] **Step 1: 建包骨架** ```bash mkdir -p activities/part-substitution-assessment/src/views activities/part-substitution-assessment/__tests__ cat > activities/part-substitution-assessment/package.json <<'EOF' { "name": "@activities/part-substitution-assessment", "version": "0.1.0", "private": true, "type": "module", "exports": { ".": "./src/index.ts" } } EOF cat > activities/part-substitution-assessment/src/index.ts <<'EOF' /** * ACT-WORK-05 · 差异评估 —— 活动包代码入口(ADR-004:默认导出 defineActivity 的返回值)。 * * 这个活动没有 `run`。案例活动的推进由 Loop 驱动,行为全部写在清单里: * 案例模型 + 说明书 + 步骤剧本 + 三条技能 + 台账声明 + 五张 HTML 报告模版。 * 这个文件之所以还存在,是因为清单的 `entry` 指着它 —— * 它是**被静态审扫描的产物文本**(查 import 与全局引用,证明活动包零环境能力)。 */ import { defineActivity } from '@sdk'; export default defineActivity({ name: 'part-substitution-assessment' }); EOF # 共享常量单独一个文件:manifest.ts 与 case-model.ts 互相引用会撞 TDZ(E04 的 case-model 不 import manifest,这里两边都要用同一批常量) cat > activities/part-substitution-assessment/src/constants.ts <<'EOF' /** 清单与案例模型共用的常量 —— 单独成文件,免得 manifest ↔ case-model 循环 import 撞 TDZ。 */ export const ACTIVITY_ID_TEXT = 'part-substitution-assessment'; /** 活动自持单据的域名 */ export const D = 'sub_eval'; /** GOV 差异规则版本 —— 随包冻结,随每条结论落账(rule_version) */ export const RULE_VERSION = 'R-2026.07'; export const TOOL_COMPARE = 'part-substitution-assessment.compare-parameters'; export const TOOL_RECOMMEND = 'part-substitution-assessment.recommend'; EOF # 视觉底逐字节抄 I01(两包之间没有代码通道,只能各持一份),只换头注 cp activities/brand-content-toolkit/src/views/prd-style.ts activities/part-substitution-assessment/src/views/prd-style.ts python3 - <<'PY' p='activities/part-substitution-assessment/src/views/prd-style.ts' s=open(p,encoding='utf-8').read() head_end=s.index('export const PRD_CSS') new_head='''/** * ACT-WORK-05 差异评估 · 视觉底 —— 逐字节抄自 I01(`brand-content-toolkit/src/views/prd-style.ts`)。 * * 本活动的四份目标件(`docs/SS02-ACT-WORK-05-html/*.html`)与 E04 / I01 的目标件是同一套 * 设计语言产出的(卡壳 / 卡头渐变 / 判据框 / 表格排版全同);`activities/*` 只准 import * `@sdk` / `@contracts`,包之间没有代码通道,只能各自持有一份。改这份不要改口径: * 数据驱动的开关(三档占位 / 缺项红带 / 状态徽 / 编辑闸)在 `report.ts` 的 `BIND_CSS`。 */ ''' open(p,'w',encoding='utf-8').write(new_head+s[head_end:]) PY ``` - [ ] **Step 2: 写技能 `src/skills.ts`** ```ts /** * ACT-WORK-05 差异评估的技能 —— 载方法,不载权力。 * 剧本写「这一步做什么、写哪几列」,技能写「为什么这么判、拿不准时按哪条走」。 */ import { skillIdSchema } from '@contracts'; import type { Skill } from '@contracts'; export const ASSESSMENT_SKILLS: Skill[] = [ { id: skillIdSchema.parse('substitution.normalize-parameters'), label: '把多来源资料整成可比对的标准参数对照表', applicability: '输入确认之后、逐项比对之前:原 PN 与每个候选的参数集要摆成同名同单位的一对一。', method: [ '参数集在 `parts.profile.normalized_parameter_set` 里,一行一参数:`参数名 | 值 | 单位 | hard/soft | 说明`。', '对照表按**原 PN 的参数名**为骨架:原 PN 有的参数,候选没有对应行 → 候选值写「未披露」(它会成为 UNKNOWN);', '候选多出来的参数不比(比对只回答「能不能替」,多出来的能力不构成差异)。', '单位以原 PN 为准:候选单位不同先换算(mV→V、kHz→MHz),换算不了就不改值、在说明里写明 —— 不许静默改数。', '约束类型以原 PN 那一行的 hard/soft 为准;原 PN 没标的按 GOV 口径:封装 / 工艺 / 电气边界(电压 / 电流 / 温度 / 阈值)一律 hard,其余 soft。', '**缺参数不得伪造**:查不到的写「未披露」,不写估计值、不写「约」。', ].join('\n'), crystallized: [], knowledgeRefs: ['标准化参数集行语法', '硬/软约束口径'], version: '1.0.0', }, { id: skillIdSchema.parse('substitution.difference-judgement'), label: '逐项四态由工具算,证据与影响由你写', applicability: '对照表齐了,要出 MATCH / MISMATCH / UNKNOWN / CONDITIONAL 时。', method: [ '四态**不许自己判**:调 `part-substitution-assessment.compare-parameters`,把每个候选的全部参数对一次性传进去(candidates 数组,一次一批)。', '每对参数都带 `evidenceRef`(值从哪份记录来:「P-08」「P-10 Rev H §6.5」「数据工单 <单号> · 确认人 <人>」)——硬约束没有证据引用,工具不判 MATCH。', '客户在应用场景里给了边界(如「输入上限 40V」)就写进那一对的 `requirement`(`>= 40`),候选不满足直接 MISMATCH。', '工具出参照抄进 `substitution.difference_item`:difference_status / compare_result / risk_level 一字不改;', '`impact_statement` 你写:MATCH 写「无」,其余说清对应用 / 验证 / 报价的影响;UNKNOWN 必须写 `unknown_disposition`(挂验证任务号或补来源动作),不许悬空。', '推荐等级同样由工具定(`part-substitution-assessment.recommend`,传每个候选的 counts),`recommendation_reason` 照工具的 reason 扩写,不许把不推荐改成推荐。', ].join('\n'), crystallized: [], knowledgeRefs: ['四态口径', '推荐等级四档'], version: '1.0.0', }, { id: skillIdSchema.parse('substitution.landing'), label: '落账只写分析层,客户批准不碰', applicability: '审核通过之后拟 P-11 / 回写 P-01 时。', method: [ '`analysis_status` 从 `ai_draft` 推到 `fae_confirmed`;`customer_approval_status` 由独立审批流维护 —— 本活动**没有这一列的写权**,写了会被拒。', 'P-01 回写五项:risk_level(取各候选里最高的)、verification_requirement(把所有验证任务号列全)、output_ref(替代分析报告号)、next_action(「提交 ACT-GATE-03 评审放行」)、current_stage 写 `gate03`。', '规则版本随结论落账:每条 P-11 主记录与差异明细都写 rule_version(工具出参的 ruleVersion),中途规则更新不影响进行中的评估。', '被打回重做时改的是**台账本身**(理由 / 依据 / 去向),不是把同一份原样再递一遍。', ].join('\n'), crystallized: [], knowledgeRefs: ['analysis_status 与 customer_approval_status 分离'], version: '1.0.0', }, ]; ``` - [ ] **Step 3: 写清单 `src/manifest.ts`** ```ts /** * 活动包清单 —— ACT-WORK-05 差异评估(案例活动)。 * * 业务出处:`docs/SS02-ACT-WORK-05-html/`(活动流程与工单说明 / 一键启动入参说明 / * 《SS02-SS03-SS04_Demo台账字段细化与统一方案》/ 四份目标件 / 全流程分支图)。 * 骨架照 E04 / I01 已验证的样子:双级入参 → 活动自持单据 + 共享主数据 → decide 循环做中间审核 → * 终局放行。三处与前两个活动刻意不同,写在这里免得下一个读者当成疏漏: * ① **主数据是六张(P-08/P-10/P-01/P-11 主+子/P-06)**,物理表由平台基线 * (`apps/api/src/app/seed-mvp-data.ts`)物化,这里只**追加**本活动要用的列与标记 * (required / tally / refersTo / requiredWhen 是活动的授权面;声明合并是纯 add-column)。 * ② **数据工单**(PN 不在库时让人从零建整条主数据)= 零输入执行工单 + 工作区的件 * (`mine:parts.profile` / `mine:parts.document` 可加行),确认落草稿层、放行并入正式表 * (守铁律②;文档里「确认即入库」在本平台落成「无审核环节、放行并入」)。一张表一张单。 * ③ 逐项四态与推荐等级**由两枚纯工具算**(规则同时以 D01/D02 两张 DMN 随包冻结当文档), * 模型只负责对照表、证据与影响说明 —— 硬约束 MISMATCH 不可能被措辞改判。 */ import type { ActivityManifest, DmnModel } from '@contracts'; import { activityIdSchema } from '@contracts'; import { ASSESSMENT_SKILLS } from './skills.ts'; import { CMMN_XML, NARRATIVE, ASSESSMENT_PHASES, ASSESSMENT_STEPS } from './case-model.ts'; import { P08_MASTER_CREATE, P10_DOCUMENT_CREATE, EVAL_INPUT_LIST, DIFFERENCE_REVIEW, LANDING_CONFIRM } from './views/report.ts'; import { ACTIVITY_ID_TEXT, D, RULE_VERSION, TOOL_COMPARE, TOOL_RECOMMEND } from './constants.ts'; export { D, RULE_VERSION, TOOL_COMPARE, TOOL_RECOMMEND } from './constants.ts'; const ACTIVITY_ID = activityIdSchema.parse(ACTIVITY_ID_TEXT); /** * 两张判定表 —— **文档形态**(求值器在 `apps/api/src/app/tools/first-party.ts`,与这里同一份规则): * D01 逐参数四态、D02 推荐等级。都是 FIRST:规则从上往下第一条命中即出结论,顺序就是优先级。 */ export const D01_DMN: DmnModel['tables'][number] = { id: 'd01-difference-status', label: '差异四态 D01(R-2026.07)', hitPolicy: 'FIRST', inputs: [ { name: 'compare_result', expression: 'compare_result' }, { name: 'constraint_type', expression: 'constraint_type' }, { name: 'evidence_present', expression: 'evidence_present' }, ], outputs: [{ name: 'difference_status' }, { name: 'risk_level' }], rules: [ { id: 'd01-u', when: ['"unknown"', '-', '-'], then: ['unknown', 'medium'], annotation: '一侧未披露 → UNKNOWN,挂验证任务' }, { id: 'd01-m0', when: ['"equal","covers"', '"hard"', 'false'], then: ['conditional', 'medium'], annotation: '硬约束缺证据不判 MATCH' }, { id: 'd01-m', when: ['"equal","covers"', '-', '-'], then: ['match', 'low'], annotation: '一致或候选范围盖住原范围' }, { id: 'd01-c', when: ['"configurable","partial"', '-', '-'], then: ['conditional', 'medium'], annotation: '可调配成 / 部分重叠 → 条件' }, { id: 'd01-x', when: ['"narrower","differs"', '"hard"', '-'], then: ['mismatch', 'high'], annotation: '硬约束不一致' }, { id: 'd01-s', when: ['"narrower","differs"', '"soft"', '-'], then: ['conditional', 'medium'], annotation: '软约束不一致 → 评估影响' }, ], }; export const D02_DMN: DmnModel['tables'][number] = { id: 'd02-recommendation', label: '推荐等级 D02(R-2026.07)', hitPolicy: 'FIRST', inputs: [ { name: 'hard_mismatch_total', expression: 'hard_mismatch_total' }, { name: 'unknown_total', expression: 'unknown_total' }, { name: 'configurable_total', expression: 'configurable_total' }, { name: 'conditional_total', expression: 'conditional_total' }, ], outputs: [{ name: 'recommendation_level' }], rules: [ { id: 'd02-n', when: ['> 0', '-', '-', '-'], then: ['not_recommended'], annotation: '硬约束失败直接不推荐' }, { id: 'd02-u', when: ['0', '> 0', '-', '-'], then: ['conditional'], annotation: '有 UNKNOWN → 等验证闭环' }, { id: 'd02-a', when: ['0', '0', '> 0', '-'], then: ['alternate'], annotation: '只靠改外围配得成 → 备选' }, { id: 'd02-c', when: ['0', '0', '0', '> 0'], then: ['conditional'], annotation: '其余条件项 → 条件推荐' }, { id: 'd02-r', when: ['0', '0', '0', '0'], then: ['recommended'], annotation: '全 MATCH → 推荐' }, ], }; export const ASSESSMENT_DMN: DmnModel = { drd: [ { decision: 'd01-difference-status', requires: [], inputs: ['compare_result', 'constraint_type', 'evidence_present'] }, { decision: 'd02-recommendation', requires: [], inputs: ['hard_mismatch_total', 'unknown_total', 'configurable_total', 'conditional_total'] }, ], tables: [D01_DMN, D02_DMN], }; /** 字段声明的短写(与 E04 同一套写法:显式 primaryKey / masked,其余按需) */ type F = { name: string; type: 'text' | 'number' | 'integer' | 'boolean' | 'date' | 'timestamp' | 'json'; label: string; primaryKey?: boolean; required?: boolean; expect?: boolean; tally?: string[]; auto?: boolean; distinct?: boolean; refersTo?: string; requiredWhen?: { column: string; value: string } }; const col = (f: F) => ({ primaryKey: false, masked: false, ...f }); export const manifest: ActivityManifest = { identity: { activityId: ACTIVITY_ID, name: 'ACT-WORK-05 · 差异评估', packageVersion: '1.0.0', category: '需求分析与方案匹配', sdkVersion: '0.1.0-m5', checksum: 'dev-checksum-part-substitution-assessment', }, /* ── 契约七件 ────────────────────────────────────────────────── */ contract: { activityId: ACTIVITY_ID, version: '1.0.0', input: { /* * 一键启动只收 4 项(文档《一键启动入参说明》极简原则):客户 / 客户需求 / 原 PN / 候选 PN 数组。 * 应用场景、BOM 位、替代类型、规则版本、负责人期限**不是入参** —— 启动后在执行工单 01 里补。 * datasheet 选填文件:传了 AI 在数据工单件上预填,没传人全填(文档 0c:同为合法路径)。 * `customerName` 只在客户不在库(分支 0a)时用:S0 发数据录入卡建档,名字先摆上去让人核。 */ data: { type: 'object', required: ['customerCode', 'requestDescription', 'originalInternalPn', 'candidateInternalPns'], properties: { customerCode: { type: 'string', title: '客户编码(L-02 客户主台账)', examples: ['CU-0142'] }, customerName: { type: 'string', title: '客户名称(仅客户不在库时填,启动后先建档)', examples: ['深圳恒星电源有限公司'] }, requestDescription: { type: 'string', title: '客户需求(原始描述 —— 据此新建 P-01 需求主记录 · 版本 V1)', examples: ['工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND,客户要求验证替代料'], }, originalInternalPn: { type: 'string', title: '原内部料号(P-08)', examples: ['LM2596S-5.0'] }, candidateInternalPns: { type: 'array', title: '候选内部料号(数组,≥1;不在库的启动后经数据工单创建)', items: { type: 'string' }, examples: [['XL2596S-5.0', 'LM2596S-ADJ']], }, datasheets: { type: 'array', format: 'file', title: '候选 datasheet(选填;传了 AI 在数据工单里预填,没传人全填)', items: { type: 'object', properties: { name: { type: 'string', title: '文件名' }, contentType: { type: 'string', title: '类型' }, size: { type: 'integer', title: '字节数' }, ref: { type: 'string', title: '存档号' }, text: { type: 'string', title: '读出来的文本' }, note: { type: 'string', title: '读取说明' }, }, }, }, }, }, messageAllowed: true, messageHint: 'FAE 可在此说明客户的应用边界、验证条件或本轮评估的特殊要求', }, output: { /* 单一必发口:本活动终点是 ACT-GATE-03 评审放行;下游 ACT-WORK-08 尚未实现,按「契约先立」声明 */ mandatory: [ { name: 'assessment-released', description: '差异评估结论已落账(P-11 替代关系主记录 + 差异明细、P-01 回写)—— 供 ACT-WORK-08 证据归集消费;当前无消费者,载荷 = 已落地事实', payload: { type: 'object', required: ['evalRef', 'requestId', 'relationTotal'], properties: { evalRef: { type: 'string', title: '评估案号' }, requestId: { type: 'string', title: '需求编号(P-01)' }, relationTotal: { type: 'integer', title: '落账的替代关系条数' }, recommendedTotal: { type: 'integer', title: '推荐' }, conditionalTotal: { type: 'integer', title: '条件推荐' }, alternateTotal: { type: 'integer', title: '备选' }, notRecommendedTotal: { type: 'integer', title: '不推荐' }, }, }, }, ], selective: [], }, requiredDeliverables: [ { key: 'eval-input', label: '评估输入清单', ledger: `${D}.input_gap`, ledgers: [`${D}.input_gap`, `${D}.pn_check`, `${D}.verdict`], editableAtRelease: [] }, { key: 'difference-items', label: '差异评估结论(P-11 差异明细)', ledger: 'substitution.difference_item', ledgers: ['substitution.difference_item', 'substitution.relation'], editableAtRelease: [] }, { key: 'landing', label: '拟落账内容(P-11 主记录 + P-01 回写)', ledger: 'substitution.relation', ledgers: ['substitution.relation', 'demand.request'], editableAtRelease: [] }, ], /* * 验收判据 —— L1/L2 只读基座读数;L4 三条是文档验收工单 01/02 里留给人的裁量; * 「推荐等级与明细一致」「硬约束不判 MATCH」机器已由工具守住,这里仍留 L4 让签字的人过目。 */ acceptanceCriteria: [ { key: 'input-confirmed', label: '评估输入已经 PW 补齐确认:执行工单 01 已交回,缺口行的补充内容都有,补不了的留了 UNKNOWN', level: 'L1', predicate: `input_confirmed_total > 0 and ${D}_input_gap_rows > 0 and ${D}_input_gap_missing = 0`, }, { key: 'review-approved', label: '差异评估结论已经 FAE/RI 审核通过:验收工单 01 已过,不处在驳回待改状态', level: 'L1', predicate: 'review_approved_total > 0 and substitution_difference_item_rows > 0 and substitution_difference_item_missing = 0', }, { key: 'every-candidate-assessed', label: '每个未放弃的候选都有一条替代关系记录(N 候选 = N 条 P-11,并行独立落账)', level: 'L2', predicate: `substitution_relation_rows > 0 and substitution_relation_rows = count(candidateInternalPns) - ${D}_pn_check_exists_status_abandoned`, }, { key: 'every-item-has-status', label: '关键参数差异均有明确状态:每条明细都是 MATCH / MISMATCH / UNKNOWN / CONDITIONAL 之一', level: 'L2', predicate: 'substitution_difference_item_rows > 0 and substitution_difference_item_difference_status_match + substitution_difference_item_difference_status_mismatch + ' + 'substitution_difference_item_difference_status_unknown + substitution_difference_item_difference_status_conditional = substitution_difference_item_rows', }, { key: 'unknown-has-disposition', label: 'UNKNOWN 项均有去向:每个 UNKNOWN 都挂了验证任务或补来源动作,影响说明与证据引用逐条齐全', level: 'L2', predicate: 'substitution_difference_item_missing = 0', }, { key: 'judged-by-rule', label: '四态与推荐等级由规则算出:逐参数比对工具与推荐定级工具都真调过', level: 'L2', predicate: `tool_part_substitution_assessment_compare_parameters_calls > 0 and tool_part_substitution_assessment_recommend_calls > 0`, }, { key: 'analysis-status-confirmed', label: '分析状态与客户批准状态分离:每条 P-11 只写分析层 analysis_status = FAE 确认,规则版本已锁定随结论落账', level: 'L2', predicate: 'substitution_relation_analysis_status_fae_confirmed = substitution_relation_rows and substitution_relation_missing = 0', }, { key: 'p01-writeback-complete', label: '回写 P-01 字段完整:风险等级、验证要求、输出链接、下一步动作、当前阶段五项齐全', level: 'L2', predicate: 'demand_request_rows > 0 and demand_request_missing = 0', }, { key: 'evidence-credible', label: '证据链可信:每条结论都能追溯到 P-08 / P-10 / P-06 的具体记录,或本单数据工单的创建记录(人工录入)', level: 'L4' }, { key: 'hard-not-auto-match', label: '硬约束项未被自动判 MATCH:封装 / 工艺 / 电气边界等硬约束的 MATCH 都有证据支持', level: 'L4' }, { key: 'recommendation-consistent', label: '推荐等级与差异明细一致:存在 UNKNOWN / CONDITIONAL 的候选没有给「推荐」', level: 'L4' }, /* L5 是责任关:那张卡本身就是这道关,卡面上不摆它(基座统一滤) */ { key: 'gate03-release', label: 'ACT-GATE-03 评审放行:签下去 P-11 落账、P-01 回写,结论供下游消费', level: 'L5' }, ], revisableAtRelease: [], prohibitions: [ { key: 'no-fabricated-parameter', statement: 'UNKNOWN 是一等公民:查不到的参数值必须显式写「未披露」并记去向,不得伪造、不得用估计值或空串冒充' }, { key: 'no-override-hard-mismatch', statement: '硬约束 MISMATCH 不得被任何措辞改判 MATCH;要改只能补证据后重比' }, { key: 'no-customer-approval-write', statement: '客户批准状态由独立审批流维护,本活动不写 customer_approval_status(没有这一列的写权)' }, { key: 'no-commercial-judgement', statement: '本活动只答技术可替代性:不做数量、价格、交期、采购判断' }, { key: 'no-rebind', statement: '启动绑定的 4 项(客户 / 需求 / 原 PN / 候选数组)工单内不可改;绑错只能退回触发方重新启动' }, { key: 'no-external-write', statement: '放行前不得触达外部系统 —— 触达世界的动作集中在放行那一刻' }, ], authorization: { discretion: [], preauthorizedActions: [], /* * 基线:数据工单 ≤2 + 候选放弃/创建决策 ≤2 + 新客户录入卡 1 + 执行单 01 + 审核决策 1 + 放行 1 = 8; * 审核循环预留两轮(+4)= 12。cap 必须严格大于最后一次拍板时的实际次数(E04 实测栽过),留到 16。 */ humanCallBudget: 16, }, resources: { tools: [{ name: TOOL_COMPARE }, { name: TOOL_RECOMMEND }], skills: ASSESSMENT_SKILLS.map((s) => s.id), models: ['deepseek-v4-flash'], roles: ['FAE/RI'], budget: { steps: 90, tokens: 600_000, activeMinutes: 120, humanCalls: 16 }, }, }, /* ── 声明 ────────────────────────────────────────────────────── */ declarations: { models: ['deepseek-v4-flash'], tools: [{ name: TOOL_COMPARE }, { name: TOOL_RECOMMEND }], ledger: [ /* L-02 客户主台账:只在分支 0a(客户不在库)由数据录入卡建一行 —— 录入卡写的列都得声明 */ { domain: 'customer', object: 'profile', kind: 'master', label: '客户档案', fields: [ col({ name: 'customer_code', type: 'text', label: '客户编码', primaryKey: true }), col({ name: 'name', type: 'text', label: '客户名称', required: true }), col({ name: 'industry', type: 'text', label: '所属行业', expect: true }), col({ name: 'region', type: 'text', label: '区域', expect: true }), ], }, /* P-08 芯片主数据:数据工单建整条;参数集是它的一个字段(一张表,不建子表) */ { domain: 'parts', object: 'profile', kind: 'master', label: '芯片主数据(P-08)', fields: [ col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }), col({ name: 'brand_name', type: 'text', label: '品牌名称', required: true }), col({ name: 'manufacturer_name', type: 'text', label: '制造商名称', expect: true }), col({ name: 'specification_description', type: 'text', label: '规格描述', required: true }), col({ name: 'normalized_parameter_set', type: 'text', label: '标准化参数集(一行一参数:参数名 | 值 | 单位 | hard/soft | 说明)', required: true }), col({ name: 'package', type: 'text', label: '封装', required: true }), col({ name: 'process_technology', type: 'text', label: '工艺', expect: true }), col({ name: 'lifecycle_status', type: 'text', label: '生命周期状态', required: true, tally: ['active', 'nrnd', 'eol', 'discontinued', 'risk'] }), col({ name: 'eol_pcn_status', type: 'text', label: 'EOL/PCN 状态', expect: true }), col({ name: 'datasheet_ref', type: 'text', label: 'Datasheet 引用', expect: true }), col({ name: 'datasheet_version', type: 'text', label: 'Datasheet 版本', expect: true }), col({ name: 'application_scenarios', type: 'text', label: '适配应用场景', expect: true }), ], }, /* P-10 原厂技术资料库:数据工单建;评估时只读做证据回链 */ { domain: 'parts', object: 'document', kind: 'master', label: '原厂技术资料库(P-10)', fields: [ col({ name: 'internal_pn', type: 'text', label: '关联内部料号', primaryKey: true }), col({ name: 'doc_seq', type: 'text', label: '资料序号(同一料号下 01/02…)', primaryKey: true }), col({ name: 'document_type', type: 'text', label: '资料类型', required: true, tally: ['datasheet', 'app_note', 'reference_design', 'white_paper', 'sample_doc', 'commitment_letter', 'other'] }), col({ name: 'brand_name', type: 'text', label: '品牌名称', expect: true }), col({ name: 'document_title', type: 'text', label: '资料标题', required: true }), col({ name: 'document_version', type: 'text', label: '资料版本', required: true }), col({ name: 'document_ref', type: 'text', label: '资料文件链接', required: true }), col({ name: 'citation_locator', type: 'text', label: '证据定位(页码/章节/表格)', expect: true }), col({ name: 'document_status', type: 'text', label: '资料状态', required: true, tally: ['valid', 'expired', 'pending', 'void'] }), ], }, /* P-01 需求主记录:S0 新建 V1,放行时回写五项(current_stage = gate03 那一刻五项必填) */ { domain: 'demand', object: 'request', kind: 'master', label: '客户需求主记录(P-01)', fields: [ col({ name: 'request_id', type: 'text', label: '需求编号', primaryKey: true }), col({ name: 'customer_code', type: 'text', label: '客户编码', required: true, refersTo: 'customer.profile.customer_code' }), col({ name: 'requirement_version', type: 'text', label: '需求版本', required: true }), col({ name: 'request_type', type: 'text', label: '需求类型', required: true }), col({ name: 'request_description', type: 'text', label: '需求描述', required: true }), col({ name: 'input_snapshot_ref', type: 'text', label: '输入快照', required: true }), col({ name: 'original_internal_pn', type: 'text', label: '原内部料号', required: true }), col({ name: 'bom_position', type: 'text', label: 'BOM 位', expect: true }), col({ name: 'application_scenario', type: 'text', label: '应用场景', expect: true }), col({ name: 'technical_constraints', type: 'text', label: '技术约束' }), col({ name: 'risk_level', type: 'text', label: '风险等级', tally: ['low', 'medium', 'high', 'critical'], requiredWhen: { column: 'current_stage', value: 'gate03' } }), col({ name: 'verification_requirement', type: 'text', label: '验证/复测/确认要求', requiredWhen: { column: 'current_stage', value: 'gate03' } }), col({ name: 'request_status', type: 'text', label: '需求状态', required: true, tally: ['pending', 'responded', 'won', 'abandoned', 'closed'] }), col({ name: 'current_activity', type: 'text', label: '当前 Activity', requiredWhen: { column: 'current_stage', value: 'gate03' } }), col({ name: 'current_stage', type: 'text', label: '当前阶段' }), col({ name: 'next_action', type: 'text', label: '下一步动作', requiredWhen: { column: 'current_stage', value: 'gate03' } }), col({ name: 'output_ref', type: 'text', label: '标准输出链接', requiredWhen: { column: 'current_stage', value: 'gate03' } }), ], }, /* P-11 替代关系主记录:一候选一条;两枚 PN 都是引用墙(查无此料当场拒 —— 存在性检查的硬门) */ { domain: 'substitution', object: 'relation', kind: 'master', label: '替代关系主记录(P-11)', fields: [ col({ name: 'substitution_relation_id', type: 'text', label: '替代关系ID', primaryKey: true }), col({ name: 'eval_ref', type: 'text', label: '评估案号', required: true }), col({ name: 'request_id', type: 'text', label: '关联需求编号', required: true }), col({ name: 'bom_position', type: 'text', label: '关联 BOM 位', expect: true }), col({ name: 'original_internal_pn', type: 'text', label: '原内部料号', required: true, refersTo: 'parts.profile.internal_pn' }), col({ name: 'candidate_internal_pn', type: 'text', label: '候选内部料号', required: true, refersTo: 'parts.profile.internal_pn' }), col({ name: 'substitution_type', type: 'text', label: '替代类型', required: true, tally: ['pin_to_pin', 'functional', 'parametric', 'other'] }), col({ name: 'recommendation_level', type: 'text', label: '推荐等级', required: true, tally: ['recommended', 'conditional', 'alternate', 'not_recommended'] }), col({ name: 'recommendation_reason', type: 'text', label: '推荐理由', required: true }), col({ name: 'difference_summary', type: 'text', label: '差异摘要', required: true }), col({ name: 'risk_level', type: 'text', label: '替代风险等级', required: true, tally: ['low', 'medium', 'high', 'critical'] }), col({ name: 'verification_requirement', type: 'text', label: '验证要求', requiredWhen: { column: 'recommendation_level', value: 'conditional' } }), col({ name: 'validation_id', type: 'text', label: '关联验证ID' }), col({ name: 'analysis_status', type: 'text', label: '分析状态', required: true, tally: ['ai_draft', 'fae_confirmed', 'pending_customer', 'customer_approved', 'rejected', 'void'] }), col({ name: 'analysis_report_ref', type: 'text', label: '替代分析报告', expect: true }), col({ name: 'confidence_score', type: 'number', label: '证据可信度', expect: true }), col({ name: 'rule_version', type: 'text', label: '差异规则版本', required: true }), ], }, /* P-11 差异明细子表:一条参数一条记录;relation id 上 distinct 让基座数「几条关系有明细」 */ { domain: 'substitution', object: 'difference_item', kind: 'master', label: '差异明细(P-11 子表)', fields: [ col({ name: 'substitution_relation_id', type: 'text', label: '替代关系ID', primaryKey: true, distinct: true, refersTo: 'substitution.relation.substitution_relation_id' }), col({ name: 'difference_item_id', type: 'text', label: '差异明细ID', primaryKey: true }), col({ name: 'parameter_name', type: 'text', label: '标准参数名', required: true }), col({ name: 'original_value', type: 'text', label: '原物料参数值', required: true }), col({ name: 'candidate_value', type: 'text', label: '候选物料参数值', required: true }), col({ name: 'parameter_unit', type: 'text', label: '参数单位', expect: true }), col({ name: 'constraint_type', type: 'text', label: '约束类型', required: true, tally: ['hard', 'soft'] }), col({ name: 'compare_result', type: 'text', label: '比对结果(工具出参)', expect: true }), col({ name: 'difference_status', type: 'text', label: '差异状态', required: true, tally: ['match', 'mismatch', 'unknown', 'conditional'] }), col({ name: 'impact_statement', type: 'text', label: '影响说明(MATCH 写「无」)', required: true }), col({ name: 'unknown_disposition', type: 'text', label: 'UNKNOWN 去向(验证任务号 / 补来源动作)', requiredWhen: { column: 'difference_status', value: 'unknown' } }), col({ name: 'risk_level', type: 'text', label: '风险等级', required: true, tally: ['low', 'medium', 'high'] }), col({ name: 'evidence_ref', type: 'text', label: '差异证据引用', required: true }), col({ name: 'confidence', type: 'number', label: 'AI 置信度', expect: true }), col({ name: 'rule_version', type: 'text', label: '差异规则版本', required: true }), ], }, /* P-06 验证记录:事实类 —— 本活动只写「验证要求」(状态 not_started),不经数据工单建 */ { domain: 'validation', object: 'record', kind: 'master', label: '芯片验证记录(P-06)', fields: [ col({ name: 'validation_id', type: 'text', label: '验证ID', primaryKey: true }), col({ name: 'request_id', type: 'text', label: '关联需求编号', required: true }), col({ name: 'validation_internal_pn', type: 'text', label: '验证内部料号', required: true, refersTo: 'parts.profile.internal_pn' }), col({ name: 'validation_type', type: 'text', label: '验证类型', required: true, tally: ['pin_to_pin', 'functional', 'reliability', 'certification', 'retest'] }), col({ name: 'validation_status', type: 'text', label: '验证状态', required: true, tally: ['not_started', 'in_progress', 'passed', 'failed', 'conditional', 'pending_evidence'] }), col({ name: 'test_condition', type: 'text', label: '测试条件', expect: true }), col({ name: 'next_validation_action', type: 'text', label: '后续验证动作', required: true }), col({ name: 'evidence_ref', type: 'text', label: '验证证据引用', expect: true }), ], }, /* ── 活动自持单据(record,caseKey eval_ref)── */ { domain: D, object: 'pn_check', kind: 'record', caseKey: 'eval_ref', label: '料号存在性核对', fields: [ col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }), col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }), col({ name: 'role', type: 'text', label: '角色', required: true, tally: ['original', 'candidate'] }), col({ name: 'exists_status', type: 'text', label: '在库状态', required: true, tally: ['found', 'absent', 'created', 'abandoned'] }), col({ name: 'abandon_reason', type: 'text', label: '放弃原因', requiredWhen: { column: 'exists_status', value: 'abandoned' } }), col({ name: 'note', type: 'text', label: '备注' }), ], }, { domain: D, object: 'input_gap', kind: 'record', caseKey: 'eval_ref', label: '评估输入清单', fields: [ col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }), col({ name: 'item_key', type: 'text', label: '输入项键', primaryKey: true }), col({ name: 'input_item', type: 'text', label: '输入项', required: true }), col({ name: 'source_ledger', type: 'text', label: '来源台账', expect: true }), col({ name: 'status', type: 'text', label: '状态', required: true, tally: ['gap', 'default', 'ok'] }), col({ name: 'detail', type: 'text', label: '绑定值 / 缺口说明', expect: true }), col({ name: 'supplement', type: 'text', label: '补充内容', requiredWhen: { column: 'status', value: 'gap' } }), col({ name: 'origin', type: 'text', label: '来源(ai/human-added)' }), ], }, { domain: D, object: 'verdict', kind: 'record', caseKey: 'eval_ref', label: '本次评估结论', fields: [ col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }), col({ name: 'request_id', type: 'text', label: '需求编号' }), col({ name: 'rule_version', type: 'text', label: '规则版本' }), col({ name: 'original_internal_pn', type: 'text', label: '原内部料号' }), col({ name: 'candidate_total', type: 'integer', label: '候选数' }), col({ name: 'input_confirmed_total', type: 'integer', label: '评估输入已确认' }), col({ name: 'review_approved_total', type: 'integer', label: '差异结论已审核通过' }), col({ name: 'landing_prepared_total', type: 'integer', label: '拟落账已备好' }), col({ name: 'relation_total', type: 'integer', label: '替代关系条数' }), col({ name: 'recommended_total', type: 'integer', label: '推荐' }), col({ name: 'conditional_total', type: 'integer', label: '条件推荐' }), col({ name: 'alternate_total', type: 'integer', label: '备选' }), col({ name: 'not_recommended_total', type: 'integer', label: '不推荐' }), col({ name: 'summary', type: 'text', label: '本次评估结论' }), ], }, ], listeners: [], harness: { memoryWindow: 24, stallAfter: 'PT4H', nudge: [{ after: 'PT2H', action: 'nudge' }], retry: { times: 2, backoff: 'PT30S' }, }, }, /* ── 资产初始版 ──────────────────────────────────────────────── */ assets: { businessModel: { cmmnXml: CMMN_XML, narrative: NARRATIVE, dmn: ASSESSMENT_DMN, steps: ASSESSMENT_STEPS, phases: ASSESSMENT_PHASES }, skills: ASSESSMENT_SKILLS, /* * 五个局面各一张: * · `p08-master-create` —— S0d 摆,数据工单(P-08),件上可加行、可编辑; * · `p10-document-create` —— S0d 摆,数据工单(P-10),同上; * · `eval-input-list` —— S1 摆,评估输入清单:绑定区只读 + 缺口表可编辑; * · `difference-review` —— S2/S3/S4 摆,差异明细 + 各候选推荐等级,只读; * · `landing-confirm` —— S5/T_release 摆,拟写 P-11 / 拟回写 P-01,只读;放行后即归档件。 */ viewTemplates: [ { name: 'p08-master-create', html: P08_MASTER_CREATE }, { name: 'p10-document-create', html: P10_DOCUMENT_CREATE }, { name: 'eval-input-list', html: EVAL_INPUT_LIST }, { name: 'difference-review', html: DIFFERENCE_REVIEW }, { name: 'landing-confirm', html: LANDING_CONFIRM }, ], cardSchemas: {}, distillHooks: [{ on: 'terminal', prompt: '归纳本次差异评估:哪些候选、各自推荐等级与关键依据、UNKNOWN 挂了什么验证任务、审核是否被驳回过(为什么)、数据工单建了哪些主数据。' }], }, /* * ── 样例集:五条形状真不同(常备案例即验收案例)── * 第一条满载且走「候选缺库 → 数据工单」那条最长的路(测试台落地即预填的是它);样例名 ≤14 字。 * 库里只播了 LM2596S-5.0 / LM2596S-ADJ 两颗(见 seed-mvp-data.ts),XL2596S-5.0 / CN-DCDC-32V / LM2596S-3.3 刻意不在库。 */ samples: [ { name: '候选缺库·派数据工单', input: { customerCode: 'CU-0142', requestDescription: '工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND,客户要求验证替代料;应用 12V→5V、工业温域、输入上限 40V', originalInternalPn: 'LM2596S-5.0', candidateInternalPns: ['XL2596S-5.0', 'LM2596S-ADJ'], }, expect: { ports: ['assessment-released'] }, }, { name: '都在库·常路', input: { customerCode: 'CU-0142', requestDescription: 'LM2596S-5.0 转 NRND,客户先评估同族可调版 LM2596S-ADJ 能否替代(愿意改外围);应用 12V→5V、工业温域', originalInternalPn: 'LM2596S-5.0', candidateInternalPns: ['LM2596S-ADJ'], }, expect: { ports: ['assessment-released'] }, }, { name: '原PN缺库·硬阻塞', input: { customerCode: 'CU-0142', requestDescription: '客户另一块板在用 LM2596S-3.3(3.3V 固定版),同批 PCN,要求评估 LM2596S-ADJ 替代;应用 12V→3.3V', originalInternalPn: 'LM2596S-3.3', candidateInternalPns: ['LM2596S-ADJ'], }, expect: { ports: ['assessment-released'] }, }, { name: '三候选·含硬约束不推荐', input: { customerCode: 'CU-0142', requestDescription: '工业电源 12V→5V,输入上限 40V,工业温域;三个候选:XL2596S-5.0、LM2596S-ADJ、国产 CN-DCDC-32V(输入上限只有 32V)', originalInternalPn: 'LM2596S-5.0', candidateInternalPns: ['XL2596S-5.0', 'LM2596S-ADJ', 'CN-DCDC-32V'], }, expect: { ports: ['assessment-released'] }, }, { name: '新客户·先建档', input: { customerCode: 'CU-9001', customerName: '深圳恒星电源有限公司', requestDescription: '新客户口述:工业电源 12V→5V 在用 LM2596S-5.0,想评估 LM2596S-ADJ', originalInternalPn: 'LM2596S-5.0', candidateInternalPns: ['LM2596S-ADJ'], }, expect: { ports: ['assessment-released'] }, }, ], entry: { file: 'index.js', checksum: 'dev-entry-part-substitution-assessment' }, }; ``` - [ ] **Step 4: 先放两份占位(让 Task 3 的测试能跑;Task 4/5 整份替换)** `src/case-model.ts` 占位: ```ts export const CMMN_XML = ''; export const NARRATIVE = ''; export const ASSESSMENT_STEPS: ReadonlyArray<{ item: string; intent: string; reads: string[]; writes: string[]; tools: string[]; view?: string; doneWhen?: string; guide: string }> = []; export const ASSESSMENT_PHASES: ReadonlyArray<{ name: string; items: string[] }> = []; ``` `src/views/report.ts` 占位: ```ts export const P08_MASTER_CREATE = '
'; export const P10_DOCUMENT_CREATE = '
'; export const EVAL_INPUT_LIST = '
'; export const DIFFERENCE_REVIEW = '
'; export const LANDING_CONFIRM = '
'; ``` - [ ] **Step 5: 写结构自测 `__tests__/activity.test.ts`**(Task 4/5 落地前,案例模型与模版相关的用例会红 —— 这是预期,Task 5 收尾时全绿) ```ts /** * ACT-WORK-05 差异评估的包内自测 —— **只准 import @sdk / @contracts / @sfeel**。 * 案例活动没有 `run`,自测不跑回合;测的是清单本身与四处对齐 * (判据谓词 / 台账声明的授权面 / 决策卡循环骨架 / 报告模版的读写面),结构照 E04 / I01。 */ import { describe, expect, it } from 'vitest'; import { activityManifestSchema } from '@contracts'; import { createMockHost } from '@sdk'; import { manifest, D, RULE_VERSION } from '../src/manifest.ts'; import activity from '../src/index.ts'; const ITEMS = ['S0_bind', 'S0d_master_data', 'S1_input_confirm', 'S2_normalize', 'S3_assess', 'S4_review', 'S5_prepare_landing', 'T_release']; const NO_DONE_WHEN = new Set(['S0d_master_data', 'S1_input_confirm', 'S4_review', 'T_release']); const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'difference-review', 'landing-confirm']; const byAddress = (domain: string, object: string) => manifest.declarations.ledger.find((l) => l.domain === domain && l.object === object); const viewHtml = (name: string) => manifest.assets.viewTemplates.find((t) => t.name === name)?.html ?? ''; describe('清单本身合规', () => { it('过 @contracts 的 schema', () => { expect(() => activityManifestSchema.parse(manifest)).not.toThrow(); }); it('没有静态监听(等外部回音一律落在执行工单上,listen 在本仓不可用)', () => { expect(manifest.declarations.listeners).toEqual([]); }); it('L1/L2 判据都给了谓词;L4 三条不给(裁量项);L5 恰一条', () => { for (const c of manifest.contract.acceptanceCriteria) { if (c.level === 'L1' || c.level === 'L2') expect(c.predicate, c.key).toBeDefined(); if (c.level === 'L4') expect(c.predicate, c.key).toBeUndefined(); } expect(manifest.contract.acceptanceCriteria.filter((c) => c.level === 'L4')).toHaveLength(3); expect(manifest.contract.acceptanceCriteria.filter((c) => c.level === 'L5')).toHaveLength(1); }); it('机检谓词只读基座读数与记账旗:不出现活动自报的汇总列名', () => { const forbidden = ['relation_total', 'candidate_total', 'recommended_total', 'not_recommended_total']; for (const c of manifest.contract.acceptanceCriteria) { for (const name of forbidden) expect(c.predicate ?? '', `${c.key} 读了自报数 ${name}`).not.toContain(name); } }); it('三件必交付物都有台账地址;人环预算严格大于「基线 8 + 两轮审核循环 4」', () => { expect(manifest.contract.requiredDeliverables).toHaveLength(3); for (const d of manifest.contract.requiredDeliverables) expect(d.ledger, d.key).toBeDefined(); expect(manifest.contract.authorization.humanCallBudget).toBeGreaterThan(12); expect(manifest.contract.resources.budget.humanCalls).toBeGreaterThan(12); }); it('起单只必填 4 项(客户 / 需求 / 原 PN / 候选数组);datasheet 是选填文件', () => { expect(manifest.contract.input.data['required']).toEqual(['customerCode', 'requestDescription', 'originalInternalPn', 'candidateInternalPns']); const properties = manifest.contract.input.data['properties'] as Record; expect(properties['candidateInternalPns']?.type).toBe('array'); expect(properties['datasheets']?.format).toBe('file'); }); it('五条样例:第一条满载走数据工单那条路,名字 ≤14 字,形状各不相同', () => { expect(manifest.samples).toHaveLength(5); for (const s of manifest.samples) expect(s.name.length, s.name).toBeLessThanOrEqual(14); expect(new Set(manifest.samples.map((s) => JSON.stringify(s.input))).size).toBe(5); const first = manifest.samples[0]?.input as { candidateInternalPns?: string[] }; expect(first.candidateInternalPns).toContain('XL2596S-5.0'); const newCustomer = manifest.samples.find((s) => (s.input as { customerName?: string }).customerName !== undefined); expect(newCustomer).toBeDefined(); }); it('五张视图模版都声明了,剧本每一步引用的 view 都在其中', () => { expect(new Set(manifest.assets.viewTemplates.map((v) => v.name))).toEqual(new Set(VIEWS)); for (const step of manifest.assets.businessModel.steps as ReadonlyArray<{ view?: string }>) { if (step.view !== undefined) expect(VIEWS, step.view).toContain(step.view); } }); }); describe('台账声明:授权面与《统一方案》口径', () => { it('P-08 一张表:主键 internal_pn,参数集是它的一个必填字段,没有 parts.parameter 子表', () => { const p08 = byAddress('parts', 'profile'); expect(p08?.fields.filter((f) => f.primaryKey).map((f) => f.name)).toEqual(['internal_pn']); expect(p08?.fields.find((f) => f.name === 'normalized_parameter_set')?.required).toBe(true); expect(byAddress('parts', 'parameter')).toBeUndefined(); }); it('P-11 主记录的原 PN / 候选 PN 都是引用墙(查无此料当场拒 —— 存在性检查的硬门)', () => { const rel = byAddress('substitution', 'relation'); for (const name of ['original_internal_pn', 'candidate_internal_pn']) { expect(rel?.fields.find((f) => f.name === name)?.refersTo, name).toBe('parts.profile.internal_pn'); } }); it('客户批准状态**不在声明里**:本活动没有它的写权(只读边界)', () => { const rel = byAddress('substitution', 'relation'); expect(rel?.fields.map((f) => f.name)).not.toContain('customer_approval_status'); }); it('UNKNOWN 必有去向:unknown_disposition 钉在 difference_status = unknown 的行上;四态是 ASCII tally', () => { const item = byAddress('substitution', 'difference_item'); expect(item?.fields.find((f) => f.name === 'unknown_disposition')?.requiredWhen).toEqual({ column: 'difference_status', value: 'unknown' }); expect(item?.fields.find((f) => f.name === 'difference_status')?.tally).toEqual(['match', 'mismatch', 'unknown', 'conditional']); expect(item?.fields.find((f) => f.name === 'substitution_relation_id')?.distinct).toBe(true); }); it('P-01 回写五项钉在 current_stage = gate03 那一刻(放行前不拦,放行门 L2 数得出来)', () => { const p01 = byAddress('demand', 'request'); for (const name of ['risk_level', 'verification_requirement', 'output_ref', 'next_action', 'current_activity']) { expect(p01?.fields.find((f) => f.name === name)?.requiredWhen, name).toEqual({ column: 'current_stage', value: 'gate03' }); } }); it('自持单据都带 caseKey eval_ref 且在主键里;放弃候选必须写原因', () => { for (const object of ['pn_check', 'input_gap', 'verdict']) { const t = byAddress(D, object); expect(t?.kind, object).toBe('record'); expect(t?.caseKey, object).toBe('eval_ref'); expect(t?.fields.find((f) => f.name === 'eval_ref')?.primaryKey, object).toBe(true); } expect(byAddress(D, 'pn_check')?.fields.find((f) => f.name === 'abandon_reason')?.requiredWhen).toEqual({ column: 'exists_status', value: 'abandoned' }); }); it('规则版本常量随包冻结', () => { expect(RULE_VERSION).toBe('R-2026.07'); expect(manifest.assets.businessModel.dmn.tables.map((t) => t.id)).toEqual(['d01-difference-status', 'd02-recommendation']); }); }); describe('案例模型与清单的对齐', () => { it('八个计划项,剧本 item 与 CMMN humanTask id 一一对应', () => { const cmmnIds = [...manifest.assets.businessModel.cmmnXml.matchAll(/ m[1] ?? ''); expect(cmmnIds).toEqual(ITEMS); expect((manifest.assets.businessModel.steps as ReadonlyArray<{ item: string }>).map((s) => s.item)).toEqual(ITEMS); }); it('四处不给 doneWhen(数据工单 / 输入确认 / 审核 / 放行),其余四处都给', () => { for (const s of manifest.assets.businessModel.steps as ReadonlyArray<{ item: string; doneWhen?: string }>) { if (NO_DONE_WHEN.has(s.item)) expect(s.doneWhen, s.item).toBeUndefined(); else expect(s.doneWhen, s.item).toBeDefined(); } }); it('那四项在 CMMN 里都标了人工激活', () => { for (const id of NO_DONE_WHEN) { const block = new RegExp(``).exec(manifest.assets.businessModel.cmmnXml)?.[0] ?? ''; expect(block, id).toContain('manualActivationRule'); } }); it('除放行外每一项恰好属于一个阶段;5 段 + 放行 = x/6,与文档 0.5/1/2/3/4/5 六步对齐', () => { const phases = manifest.assets.businessModel.phases ?? []; expect(phases).toHaveLength(5); const phaseItems = phases.flatMap((p) => p.items); expect([...phaseItems].sort()).toEqual([...ITEMS.filter((i) => i !== 'T_release')].sort()); expect(new Set(phaseItems).size).toBe(phaseItems.length); }); it('说明书点名的计划项都真实存在', () => { const mentioned = [...manifest.assets.businessModel.narrative.matchAll(/\b(S\d[a-z]?_[a-zA-Z_]+|T_release)\b/g)].map((m) => m[1] ?? ''); for (const name of mentioned) expect(ITEMS, `说明书提到但不存在的计划项: ${name}`).toContain(name); }); it('两种决策卡骨架都在说明书里:候选放弃/创建(create/abandon)与审核(approve/reject/custom:)', () => { const n = manifest.assets.businessModel.narrative; expect(n).toContain('"kind":"decide","itemId":"S0d_master_data"'); 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:"'); }); it('T_release 剧本里 judge criterion 的原文逐字对得上 acceptanceCriteria 的 label', () => { const tRelease = (manifest.assets.businessModel.steps as ReadonlyArray<{ item: string; guide?: string }>).find((s) => s.item === 'T_release'); const quoted = [...(tRelease?.guide ?? '').matchAll(/judge criterion="([^"]+)"/g)].map((m) => m[1] ?? ''); expect(quoted).toHaveLength(3); const labels = new Set(manifest.contract.acceptanceCriteria.map((c) => c.label)); for (const q of quoted) expect(labels.has(q), q).toBe(true); }); }); describe('报告模版:编辑面与只读面各就各位', () => { it('两张数据工单件可加行可编辑:data-edit / data-edit-add 与主键 data-col 都在', () => { const p08 = viewHtml('p08-master-create'); expect(p08).toContain('data-edit="parts.profile"'); expect(p08).toContain('data-edit-add="parts.profile"'); expect(p08).toContain('data-col="internal_pn"'); expect(p08).toContain('data-col="normalized_parameter_set"'); const p10 = viewHtml('p10-document-create'); expect(p10).toContain('data-edit="parts.document"'); expect(p10).toContain('data-col="doc_seq"'); }); it('评估输入清单:缺口表可编辑(supplement),绑定区只读', () => { const html = viewHtml('eval-input-list'); expect(html).toContain(`data-edit="${D}.input_gap"`); expect(html).toContain('data-col="supplement"'); 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='); } }); it('每个可编辑格子的 data-col 都在对应台账对象的声明里', () => { const cases: Array<[string, string, string]> = [ ['p08-master-create', 'parts', 'profile'], ['p10-document-create', 'parts', 'document'], ['eval-input-list', D, 'input_gap'], ]; for (const [view, domain, object] of cases) { const cols = new Set([...viewHtml(view).matchAll(/data-col="([^"]+)"/g)].map((m) => m[1] ?? '')); const declared = new Set(byAddress(domain, object)?.fields.map((f) => f.name)); for (const c of cols) expect(declared.has(c), `${view}: ${c}`).toBe(true); } }); it('枚举列只用 data-class-of 投影,不摆 ASCII 原值', () => { expect(viewHtml('difference-review')).toContain('data-class-of="difference_status"'); expect(viewHtml('difference-review')).toContain('data-class-of="recommendation_level"'); expect(viewHtml('difference-review')).not.toContain('data-slot="difference_status"'); }); it('宿主拒推含脚本的模版', async () => { const host = createMockHost({ contract: CONTRACT }); await expect(host.base.interaction.pushHtml('
')).rejects.toThrow(/无脚本执行权/); }); }); describe('活动入口', () => { it('没有 run —— 行为全在清单里', () => { expect(activity.run).toBeUndefined(); }); }); const CONTRACT = { activityId: 'part-substitution-assessment', version: '1.0.0', data: {}, message: undefined, outputPorts: { mandatory: ['assessment-released'], selective: [] }, prohibitions: [], discretion: [], } as const; ``` - [ ] **Step 6: 跑一遍,记下哪些红(只许是案例模型 / 模版相关的那几条)** Run: `pnpm vitest run activities/part-substitution-assessment` Expected:「清单本身合规」「台账声明」两组绿;「案例模型」「报告模版」组红(占位为空)。若「过 @contracts 的 schema」红,先修清单再往下。 - [ ] **Step 7: 提交骨架** ```bash git add activities/part-substitution-assessment git commit -m "feat(w05): 差异评估活动包骨架与清单 —— 六张主数据追加声明、三张自持单据、两张 DMN、五条样例 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh" ``` --- ### Task 4: 案例模型 —— CMMN 八项、说明书、逐步剧本、五段进度 + 剧本自测 **Files:** - Replace: `activities/part-substitution-assessment/src/case-model.ts`(整份替换 Task 3 的占位) - Create: `activities/part-substitution-assessment/__tests__/steps.test.ts` **Interfaces:** - Consumes:Task 3 的台账地址 / 列名 / tally 值、工具名 `TOOL_COMPARE` / `TOOL_RECOMMEND`、视图名。 - Produces:`CMMN_XML`、`NARRATIVE`、`ASSESSMENT_STEPS`(8 条,`item` 顺序 = CMMN humanTask 顺序)、`ASSESSMENT_PHASES`(5 段)。 - 案号约定(剧本与 e2e 共用):`eval_ref = 'W05-' + case:openedAt 去分隔符`;`request_id = 'REQ-' + 同一串数字`;`input_snapshot_ref = 'SNAP-' + eval_ref`;关系 id `REL-<数字串>-01/02…`(按候选数组顺序);明细 id `<关系id>-P01/P02…`;验证 id `VAL-<数字串>-01/02…`;报告号 `RPT-<数字串>`。 - 卡标题约定(e2e 按标题对号):数据工单 P-08 以「数据工单 · 芯片主数据创建」开头;P-10 以「数据工单 · 原厂技术资料创建」开头;执行单以「评估输入与资料补齐确认」开头。 - [ ] **Step 1: 写 `src/case-model.ts`** ```ts /** * ACT-WORK-05 差异评估的案例模型 —— **双表示同版本**:CMMN XML(机器面)与案例说明书(叙事面)。 * * 业务出处:`docs/SS02-ACT-WORK-05-html/ACT-WORK-05-活动流程与工单说明.md`(步骤 0.5 + 5 步, * 1 数据(条件)+ 1 执行 + 2 验收)与《SS02-SS03-SS04_Demo台账字段细化与统一方案》(字段口径)。 * * 文档六步收成 8 个案例模型项: * 启动期 → S0_bind(AI:铸案号、建 P-01 V1、锁规则版本、逐个 PN 查库、写存在性核对表) * 0.5 等待数据 → S0d_master_data(人:数据工单 —— P-08 一张、P-10 一张;候选 ≥2 缺库先决策卡; * 库里都有就 disable 这一项) * 1 建立评估上下文 → S1_input_confirm(人:执行工单 01,评估输入清单,件上补缺口) * 2 读取并标准化 → S2_normalize(AI:P-11 主记录骨架 + 差异明细对照行) * 3 逐项差异评估 → S3_assess(AI + 两枚工具:四态 / 风险 / 证据 / 影响 / 推荐等级 / 验证任务) * 4 FAE/RI 审核 → S4_review(人:验收工单 01 = 决策卡循环,驳回则 AI 改台账再发) * 5 提交评审放行 → S5_prepare_landing(AI:analysis_status 推 fae_confirmed、回写 P-01)+ T_release(人:验收工单 02,终局放行) * * 四处不给 doneWhen:S0d(触达人)、S1(交人补缺口)、S4(拍板且可循环)、T_release(签下去)。 */ import { D, RULE_VERSION, TOOL_COMPARE, TOOL_RECOMMEND } from './constants.ts'; export const CMMN_XML = ` update demand_request_rows > 0 and sub_eval_pn_check_rows > 0 update sub_eval_pn_check_rows = count(candidateInternalPns) + 1 and sub_eval_pn_check_missing = 0 and sub_eval_pn_check_exists_status_absent = 0 and sub_eval_pn_check_exists_status_abandoned < count(candidateInternalPns) and parts_profile_missing = 0 and parts_document_missing = 0 update input_confirmed_total > 0 and sub_eval_input_gap_rows > 0 and sub_eval_input_gap_missing = 0 update substitution_relation_rows > 0 and substitution_relation_rows = count(candidateInternalPns) - sub_eval_pn_check_exists_status_abandoned and substitution_difference_item_rows > 0 and substitution_difference_item_distinct_substitution_relation_id = substitution_relation_rows update substitution_difference_item_missing = 0 and substitution_relation_missing = 0 and substitution_difference_item_difference_status_match + substitution_difference_item_difference_status_mismatch + substitution_difference_item_difference_status_unknown + substitution_difference_item_difference_status_conditional = substitution_difference_item_rows and tool_part_substitution_assessment_compare_parameters_calls > 0 and tool_part_substitution_assessment_recommend_calls > 0 update review_approved_total > 0 and substitution_difference_item_missing = 0 update landing_prepared_total > 0 and substitution_relation_analysis_status_fae_confirmed = substitution_relation_rows and substitution_relation_missing = 0 and demand_request_missing = 0 `; /** * 案例说明书 —— **只写整单一以贯之的事**(逐步做法归剧本)。 * 卡骨架住在这里而不是剧本里(模型会压拍,压过之后只有说明书还在 —— A1/A6/I01 同一课)。 */ export const NARRATIVE = `§0 开案:**判断候选料号在技术上能不能替代原料号,逐参数给出可复核的差异结论,审核后落账**。 入参: · \`customerCode\` —— 客户编码(L-02);\`customerName\` 只在客户不在库时给。 · \`requestDescription\` —— 客户需求原话,据它新建 P-01 需求主记录(版本 V1)。 · \`originalInternalPn\` —— 原内部料号;\`candidateInternalPns\` —— 候选内部料号数组(≥1,逐个评估、各一条 P-11)。 · \`datasheets\` —— 选填的真文件,基座已读成文本(每件 \`{name, text?, note}\`);有就拿它在数据工单里预填,没有就人全填。 **本活动只回答一件事:技术上哪种料能替**。不做数量 / 价格 / 交期 / 采购判断;客户批准状态由审批流维护,你没有那一列的写权。 **四项绑定后不可改**(客户 / 需求 / 原 PN / 候选数组):绑错只能退回启动人重新启动。 **能算的不问人也不自己判**:四态(MATCH / MISMATCH / UNKNOWN / CONDITIONAL)与推荐等级 (推荐 / 条件推荐 / 备选 / 不推荐)全由两枚工具算 —— \`${TOOL_COMPARE}\`、\`${TOOL_RECOMMEND}\`, 一批一次。你负责对照表、证据引用、影响说明、UNKNOWN 的去向。**规则版本 \`${RULE_VERSION}\` 随每条结论落账**。 **UNKNOWN 是一等公民**:查不到的参数值写「未披露」,不许估、不许留空串冒充;每个 UNKNOWN 必须写去向 (挂验证任务号或补来源动作)。硬约束 MISMATCH 不许改判,要改只能补证据后重比。 **存在性检查与数据工单**:启动时逐个料号在 \`parts.profile\` 里查(原 PN + 每个候选)。查无记录 = 「完全没有」, 要人从零建整条主数据 —— 发**数据工单**(一张表一张单:P-08 一张、P-10 一张);PN 在库但个别字段缺 = 「有而不全」, 归执行工单 01 补。原 PN 缺 = 硬阻塞必须建;候选缺:候选 ≥2 时先问人放弃还是创建,候选 = 1 时必须建。 **库里都有就把 S0d_master_data disable 掉**(此单不走这条),别发空单。 **六步走**:绑定与存在性核对 → (等待数据)→ 人补评估输入 → 标准化对照表 → 逐项评估 → 人审结论(可循环)→ 拟落账 → 人放行。 【本单会用到的卡,全部照下面的骨架发,别自己另编】 ① 新客户建档(只在 \`customerCode\` 在 \`customer.profile\` 里查无时,S0_bind 里发;编码用入参给的那个,人核名): {"kind":"entry","itemId":"S0_bind","ask":"客户 不在客户档案里,请核对建档信息后确认", "ledger":"customer.profile","key":{"customer_code":""}, "fields":[ {"key":"name","label":"客户名称","value":"","required":true}, {"key":"industry","label":"所属行业"}, {"key":"region","label":"区域"} ]} 人确认那一下基座已把这一行落进草稿层,**你不许再 write 一遍 customer.profile**。 ② 候选不在库、候选 ≥2 时,一颗一张决策卡(按候选数组顺序,一次只发一张;候选只有一颗时不问、直接建): {"kind":"decide","itemId":"S0d_master_data", "ask":"候选 在芯片主数据里查无记录。是从零建它的主数据(派数据工单),还是本轮放弃这颗候选?", "options":[{"value":"create","label":"创建主数据(派数据工单)","recommended":true}, {"value":"abandon","label":"放弃这颗候选"}],"allowCustom":true} \`value\` 只有 \`"create"\` / \`"abandon"\` / \`"custom:"\` 开头三种:\`"custom:"\` 开头按放弃处理、\`note\` 就是放弃原因; 点了 \`"abandon"\` 没写原因,\`abandon_reason\` 写「负责人本轮放弃(未注明原因)」。 ③ 数据工单 · P-08(所有要建的料号一起,一张单;零输入,填报全在工作区的件上): {"kind":"human","itemId":"S0d_master_data","workOrder":"execution","card":{ "title":"数据工单 · 芯片主数据创建(P-08):<料号列表,顿号分隔>", "ask":"这些料号在芯片主数据里查无记录,请在工作区的件上把整条记录建齐(一颗一行,可加行)。", "steps":["每一行:品牌、规格描述、封装、生命周期状态、标准化参数集为必填,缺一格确认不了", "标准化参数集一行一参数:参数名 | 值 | 单位 | hard/soft | 说明;原厂未披露的值写「未披露」,它会进入 UNKNOWN 并挂验证任务", "有 datasheet 的把引用与版本一起填(成对);制造商与品牌不一致时分开写", "改完保存,回这张卡确认 —— 确认后这批主数据即入库,活动从等待数据唤醒"], "standard":"每颗料号一行,必填列全齐;参数集至少含输入电压范围 / 输出电压 / 输出电流 / 工作温度 / 封装相关的硬约束项。", "basis":[{"k":"触发原因","v":"按 internal_pn 查 P-08 查无记录(完全没有,不是字段缺失)"}, {"k":"AI 预填","v":"<有 datasheet 就写「已按 datasheet 预填 N 项,请核对」,没有写「未上传 datasheet,请全填」>"}]}} ④ 数据工单 · P-10(同一批料号的资料记录,一张单;在 P-08 那张确认之后再发): {"kind":"human","itemId":"S0d_master_data","workOrder":"execution","card":{ "title":"数据工单 · 原厂技术资料创建(P-10):<料号列表>", "ask":"请把这些料号的 datasheet / 承诺函等资料记录补进原厂技术资料库(一份资料一行,可加行)。", "steps":["资料类型、标题、版本、文件链接、资料状态为必填;证据定位写页码或章节,差异评估的证据引用会指向它", "同一料号多份资料就多加几行,序号 01/02… 自己续", "改完保存,回这张卡确认"], "standard":"每颗料号至少一行 datasheet 记录,必填列全齐。", "basis":[{"k":"触发原因","v":"按 internal_pn 查 P-10 查无记录"}]}} ⑤ 执行工单 01 · 评估输入与资料补齐确认(S1_input_confirm;零输入): {"kind":"human","itemId":"S1_input_confirm","workOrder":"execution","card":{ "title":"评估输入与资料补齐确认:<原 PN> → 个候选", "ask":"启动绑定的四项已锁定。请在工作区的件上补齐「待补 / 待确认」行的补充内容,默认值可改可确认。", "steps":["应用场景、BOM 位是启动时未收的:必须补(写在「补充内容」列)", "原厂未披露的参数(如使能阈值)补不到就写「挂验证任务」或「留 UNKNOWN」—— 补不了也是合法答案,不拦流程", "替代类型 / 规则版本 / 负责人期限三行是系统默认,可改可不改", "加一行 = 补充 AI 没列出的资料项;删一行 = 该项不适用;已绑定的四项不可改", "改完保存,回这张卡提交"], "standard":"每一条「待补」行的补充内容都有;补不了的写明理由。", "basis":[{"k":"已绑定","v":"客户 · 需求 V1 · 原 PN <原 PN> · 候选 颗"}, {"k":"缺口","v":" 项待补 / 待确认"}]}} ⑥ 验收工单 01 · 差异评估结论审核(S4_review;决策卡循环,不是终局验收)—— 发之前先在轨迹里找有没有已答过、还没处理的 \`[card-reply]\`: · \`value\` 是 \`"approve"\` → 写 \`${D}.verdict.review_approved_total = 1\`,finish 这一项,别再发卡; · \`value\` 是 \`"reject"\` 或以 \`"custom:"\` 开头 → 都算驳回:\`"reject"\` 没写意见,**不许再发卡追问**;\`"custom:"\` 的 \`note\` 就是他的意见, 按意见改 \`substitution.difference_item\` / \`substitution.relation\`(同 PK 覆盖写,不开新行),意见要挂验证的写一行 \`validation.record\`, 然后**换一种问法**再发一张(措辞不换会被当成同一张卡静默吞掉); · 没有 → 才发第一张: {"kind":"decide","itemId":"S4_review","allowCustom":true, "ask":"差异评估结论已算出: 个候选、共 条参数比对(MATCH a / MISMATCH b / UNKNOWN c / CONDITIONAL d),推荐等级 <逐候选一句>。请审核。", "options":[{"value":"approve","label":"通过","recommended":true}, {"value":"reject","label":"驳回(不写具体意见)"}]} 三个 \`value\` 只有 \`"approve"\` / \`"reject"\` / \`"custom:"\` 开头三种,别造第四种。 【一路上要说话】每一拍带一句 say,说业务的话:查了几颗料、缺几颗、对照了几项、四态各几条、推荐等级各是什么。到了要找人的那一拍,先说清这一轮办完了什么、为什么现在需要他。用件上那三个词(必填 / 建议 / 选填)说缺口,只说事实。 【怎么把一项干完 · 一拍连做】write 和 tool 都不会自动收步 —— 做完用 finish 宣告(有 doneWhen 的项引擎自己收)。 动作排进同一拍的 actions 串一口气给;只有发卡 / 提交(挂起)必须停拍;tool 之后同拍其余动作作废。 **常路 9–12 拍收口**(候选缺库那条路多两张数据工单、多两拍)。 §Z 结案:FAE/RI 在验收单上签字,系统把草稿合并进 P-08 / P-10 / P-01 / P-11 / P-06 正式版并发射 \`assessment-released\` 口。`; /** * 步骤剧本 —— 一个计划项一条;`doneWhen` 是「成果已存在」的判据,不是「该不该做」。 * 四处不给 doneWhen:S0d(发数据工单触达人)、S1(交人补缺口)、S4(拍板且循环)、T_release(签下去)。 */ export const ASSESSMENT_STEPS = [ { item: 'S0_bind', intent: '铸案号、建需求 V1、锁规则版本、逐个料号查库、写存在性核对表', writes: ['demand.request', `${D}.pn_check`, `${D}.verdict`, 'customer.profile'], reads: ['customerCode', 'customerName', 'requestDescription', 'originalInternalPn', 'candidateInternalPns', 'datasheets', 'case:openedAt', 'customer.profile', 'parts.profile'], view: 'eval-input-list', tools: [], /* * 「该有几行」也进判据:原 PN + 每个候选各一行核对(count(candidateInternalPns) + 1); * 少一行 = 一颗料从这一单里凭空消失。count() 对未绑定名回 1 —— candidateInternalPns 是必填入参,恒绑定。 */ doneWhen: `demand_request_rows > 0 and demand_request_missing = 0 and ${D}_verdict_rows > 0 and ` + `${D}_pn_check_rows = count(candidateInternalPns) + 1 and ${D}_pn_check_missing = 0`, guide: `第一件事:**把这一单的上下文立起来**,一拍连做(客户不在库时多一张录入卡、多一拍)。 【先铸案号】\`eval_ref = 'W05-' + case:openedAt 按 YYYYMMDDHHmmss 格式化\`(\`case:openedAt\` 形如 "2026-08-26 21:03:15",去掉分隔符即得); \`request_id = 'REQ-' + 同一串数字\`;\`input_snapshot_ref = 'SNAP-' + eval_ref\`。**不许从正式表里别单的案号里挑**,整单只有这一个 eval_ref。 【客户在不在库】在 \`customer.profile\` 里找 \`customerCode\`:在 → 直接用;不在 → 照说明书①发数据录入卡(名字用 \`customerName\`), 发完这一拍结束;人确认后这家客户就在草稿层里了,下一拍接着做。**客户不在库也不许自己 write customer.profile**。 【写 \`demand.request\` 一行(V1)】request_id、customer_code(= customerCode)、requirement_version = "V1"、request_type = "技术需求"、 request_description(入参原话,不改写)、input_snapshot_ref、original_internal_pn、request_status = "pending"、current_activity = "ACT-WORK-05"、 current_stage = "assessing"。application_scenario / bom_position **这一步不写**(启动时未收,S1 由人补)。 【逐个料号查库,写 \`${D}.pn_check\`】\`parts.profile\` 整表在你的作用域里,按 \`internal_pn\` 逐个核对:原 PN 一行(role = "original"), 每个候选一行(role = "candidate");在 → exists_status = "found";不在 → exists_status = "absent"(**这一格先写 absent,等数据工单确认后再改**)。 一颗都不能少:数组里点了几颗候选,就写几行 + 原 PN 一行。note 选填(如「PCN 转 NRND」)。 【写 \`${D}.verdict\` 一行】eval_ref、request_id、rule_version = "${RULE_VERSION}"、original_internal_pn、candidate_total = 候选数组长度。 写完引擎自己收这一项。收完先看 \`${D}.pn_check\`:有 absent 的 → 下一项是 S0d_master_data(数据工单);一个 absent 都没有 → **disable S0d_master_data**(reason 写「原 PN 与全部候选都在库,无需建主数据」),然后等 S1 那一项 Enabled。`, }, { item: 'S0d_master_data', intent: '料号不在库:派数据工单让人从零建整条主数据(P-08 一张、P-10 一张);候选 ≥2 先问放弃还是创建', writes: ['parts.profile', 'parts.document', `${D}.pn_check`], reads: [`${D}.pn_check`, 'candidateInternalPns', 'originalInternalPn', 'datasheets', 'parts.profile', 'parts.document'], view: 'p08-master-create', tools: [], /* 触达人,不给 doneWhen;人工激活:发卡就算启动 */ guide: `这一项只在 \`${D}.pn_check\` 里有 absent 行时走;都在库就 disable 它(见 S0 剧本)。**先分岔,再动作**: 【先看轨迹】这一项里已经发过哪些卡、人答了什么(【当前这一步】下面那行引擎事实说得清楚): · 原 PN absent → **硬阻塞**,不问人,直接进入「建」的名单; · 候选 absent 且候选数 ≥2 → 每颗 absent 的候选照说明书②各发一张决策卡(一次一张,按数组顺序);人选 "abandon"/"custom:" 的, 把那一行 \`${D}.pn_check\` 改成 exists_status = "abandoned" + abandon_reason(同 PK 覆盖写),这颗不再评估;选 "create" 的进「建」的名单; · 候选 absent 且候选只有 1 颗 → 不问,必须建。 **别把同一颗候选问两遍**:决策卡答过就按答案办。 【「建」的名单定了 → 先写骨架行,再发 P-08 数据工单】 1. 给名单里每颗料写一行 \`parts.profile\` 骨架:key {internal_pn},patch 只带你确有依据的列 —— 有 \`datasheets[].text\` 就从里面读 brand_name / specification_description / package / lifecycle_status(active/nrnd/eol/discontinued/risk 之一)/ normalized_parameter_set (一行一参数:参数名 | 值 | 单位 | hard/soft | 说明,原厂没写的值写「未披露」)/ datasheet_ref + datasheet_version;没有 datasheet 就只写 internal_pn 这一列(其余留给人填,**别编**)。 2. 同一拍给每颗料写一行 \`parts.document\` 骨架:key {internal_pn, doc_seq: "01"},patch {document_type: "datasheet", document_status: "pending", brand_name 有就写}。 3. 同一拍发说明书③那张 P-08 数据工单(title 里列全料号),发完这一拍结束(挂起)。 【P-08 那张确认回来了】看轨迹里的 [card-reply]:再发说明书④那张 P-10 数据工单(只发一次),发完这一拍结束。 【P-10 那张也确认回来了】把名单里每颗料的 \`${D}.pn_check\` 改成 exists_status = "created"(同 PK 覆盖写),然后 finish 这一项。 这时 S1_input_confirm 的入场前提(全部料号落定、本单建的主数据不缺必填)自己就成立了 —— 缺必填由提交闸兜着,卡回来时一定是齐的。 【这两张卡一单各只发一次】人交回来之后不许再发;答复里没有值(空 values)也算答过 —— 人在件上填的东西已经落草稿层。`, }, { item: 'S1_input_confirm', intent: '评估输入清单:绑定四项只读核对,缺口行交 PW 在件上补', writes: [`${D}.input_gap`, `${D}.verdict`, 'demand.request'], reads: [`${D}.input_gap`, `${D}.pn_check`, 'demand.request', 'parts.profile', 'parts.document', 'candidateInternalPns', 'originalInternalPn'], view: 'eval-input-list', tools: [], /* 交人补缺口,不给 doneWhen;人工激活:发卡就算启动 */ guide: `料号全部落定了,**该 PW 补评估输入了** —— 这是本活动唯一的执行工单,零输入。**先分岔**:【当前这一步】那行引擎事实说 「从没给它发过卡」→ 走【发卡】;说「人已经被问过了」→ 走【收口】。 【发卡】先把评估输入清单写下来(\`${D}.input_gap\`,一行一项,item_key 用英文短键,input_item 用人读名),一拍连做: · 已绑定四项(status = "ok",detail 写绑定值,origin = "ai"):item_key = customer / request / original_pn / candidates (候选多颗就 candidate_1、candidate_2… 一颗一行,detail 写料号与在库状态); · 待补两项(status = "gap",detail 写「启动时未收」):application_scenario「应用场景」(source_ledger = "P-01")、bom_position「BOM 位」(source_ledger = "P-09"); · 默认可改三项(status = "default",detail 写默认值):substitution_type「替代类型」= "Pin-to-Pin"、rule_version「规则版本」= "${RULE_VERSION}"、owner「负责人 / 期限」= "按客户线默认 · 1 个工作日"; · 待确认(status = "gap"):逐颗候选看 \`parts.profile.normalized_parameter_set\`,值是「未披露」的参数各一行(item_key = unknown_<料号>_<序号>, input_item 如「XL2596S-5.0 使能阈值」,source_ledger = "P-08",detail 写「原厂未披露(有而不全)」); · 齐备项(status = "ok"):原 PN 资料版本(source_ledger = "P-10",detail 写 datasheet 版本)。 写完同一拍照说明书⑤发执行工单,发完这一拍结束(挂起)。 【收口】人交回之后只做三件事,然后 finish: ① 读 \`${D}.input_gap\` 现值:把 application_scenario / bom_position 两行的 supplement 抄进 \`demand.request\`(同 PK 覆盖写这两列; supplement 写「补不了」「UNKNOWN」之类的,这两列不写);人加的行(origin = "human-added")同等作数,写进 technical_constraints 一句汇总。 ② 写 \`${D}.verdict.input_confirmed_total = 1\`。 ③ finish 这一项。**别在这一拍做对照表** —— 那是下一步的活。 【这张卡一单只发一次】人交回来之后不许再发;缺必填有提交闸兜着。`, }, { item: 'S2_normalize', intent: '读取并标准化:每个未放弃的候选一条 P-11 主记录骨架 + 逐参数对照行', writes: ['substitution.relation', 'substitution.difference_item'], reads: ['parts.profile', 'parts.document', 'demand.request', `${D}.pn_check`, `${D}.input_gap`, `${D}.verdict`, 'candidateInternalPns', 'originalInternalPn'], view: 'difference-review', tools: [], doneWhen: `substitution_relation_rows > 0 and substitution_relation_rows = count(candidateInternalPns) - ${D}_pn_check_exists_status_abandoned and ` + 'substitution_difference_item_rows > 0 and substitution_difference_item_distinct_substitution_relation_id = substitution_relation_rows', guide: `输入齐了,**把多来源资料整成可比对的对照表**,一拍连做(先写关系,再写明细 —— 明细引用关系 id,顺序反了会被拒)。 【每个未放弃的候选一条 \`substitution.relation\` 骨架】(\`${D}.pn_check\` 里 exists_status 是 found / created 的候选;abandoned 的不写) substitution_relation_id = "REL-<案号数字串>-01/02…"(按候选数组顺序)、eval_ref、request_id、original_internal_pn、candidate_internal_pn、 substitution_type = "pin_to_pin"(人在评估输入清单里改了替代类型就照改:功能替代 → functional、参数替代 → parametric)、 analysis_status = "ai_draft"、rule_version = "${RULE_VERSION}"、bom_position(照 demand.request 现值抄,没有不写)。 recommendation_level / recommendation_reason / difference_summary / risk_level **这一步一格都不写**(S3 的活)。 【每条关系逐参数写 \`substitution.difference_item\` 对照行】骨架按**原 PN 的参数集**(\`parts.profile.normalized_parameter_set\`,一行一参数 「参数名 | 值 | 单位 | hard/soft | 说明」):原 PN 有的每个参数一行 —— difference_item_id = "<关系id>-P01/P02…"、parameter_name、 original_value、candidate_value(候选参数集里同名那一行的值;候选没有这个参数 → 写「未披露」)、parameter_unit(以原 PN 为准;候选单位不同先换算, 换算不了原值照写、evidence_ref 里注明)、constraint_type(原 PN 那一行的 hard/soft;没标的按口径:封装 / 工艺 / 电压 / 电流 / 温度 / 阈值 hard,其余 soft)、 rule_version = "${RULE_VERSION}"、evidence_ref(值从哪来:「P-08 · P-10 <版本>」;本单数据工单建的料号写「人工录入 · 数据工单 · P-08」)。 另加两行**封装**与**生命周期**(constraint_type = hard / soft,值分别取 \`package\` 与 \`lifecycle_status\`)。 difference_status / compare_result / risk_level / impact_statement / unknown_disposition **这一步不写**。 **缺参数不得伪造**:候选那一侧查不到就写「未披露」。写完引擎自己收这一项(每条关系都有明细行才算)。`, }, { item: 'S3_assess', intent: '逐项差异评估:四态与推荐等级由工具算,证据 / 影响 / UNKNOWN 去向由你写', writes: ['substitution.relation', 'substitution.difference_item', 'validation.record', `${D}.verdict`], reads: ['substitution.relation', 'substitution.difference_item', 'demand.request', `${D}.input_gap`, `${D}.verdict`], view: 'difference-review', tools: [TOOL_COMPARE, TOOL_RECOMMEND], doneWhen: 'substitution_difference_item_missing = 0 and substitution_relation_missing = 0 and ' + 'substitution_difference_item_difference_status_match + substitution_difference_item_difference_status_mismatch + ' + 'substitution_difference_item_difference_status_unknown + substitution_difference_item_difference_status_conditional = substitution_difference_item_rows and ' + 'tool_part_substitution_assessment_compare_parameters_calls > 0 and tool_part_substitution_assessment_recommend_calls > 0', guide: `对照表铺好了,**固定三拍**:比对 → 定级 → 写盘。**工具调用后同拍其余动作作废**,别把工具和写台账串在一起;两枚工具各只调一次。 【第一拍:只调 \`${TOOL_COMPARE}\`,别的什么都不写】把**全部**关系一次打包: {"candidates":[{"candidateInternalPn":"<候选>","pairs":[{"parameterName":"输入电压范围","originalValue":"4.5~40","candidateValue":"4.5~32", "unit":"V","constraintType":"hard","requirement":">= 40","evidenceRef":"P-08 · P-10 Rev H"}, …]}, …]} · pairs 逐条照 \`substitution.difference_item\` 抄(parameterName / originalValue / candidateValue / unit / constraintType / evidenceRef); · **客户在应用场景里给了边界的参数才写 requirement**(如「输入上限 40V」→ 输入电压范围那一对写 ">= 40";「工业温域」→ 工作温度写 "-40~85"); · 每一对都带 evidenceRef —— 硬约束没有证据引用工具不判 MATCH,那正是要的。 调完这一拍结束。 【第二拍:只调 \`${TOOL_RECOMMEND}\`】把第一拍出参里每个候选的 counts 原样传:{"candidates":[{"candidateInternalPn":"…","counts":{…}}, …]}。调完结束。 【第三拍:写盘,一拍连做,一次都不再调工具】 ① 逐条 \`substitution.difference_item\` 同 PK 覆盖写:difference_status / compare_result / risk_level 照第一拍出参**一字不改**; impact_statement(MATCH 写「无」;其余说清对应用 / 验证 / 报价的影响,如「输入上限 32V < 客户 40V,不能上板」); difference_status = unknown 的行**必须**写 unknown_disposition(「挂验证任务 VAL-<案号数字串>-01:实测使能阈值」或「向原厂补来源」);confidence(0~1)。 ② 每个 UNKNOWN 写一行 \`validation.record\`:validation_id = "VAL-<案号数字串>-01/02…"、request_id、validation_internal_pn(候选)、 validation_type = "pin_to_pin"、validation_status = "not_started"、next_validation_action(要测什么、什么条件)、test_condition。 ③ 逐条 \`substitution.relation\` 同 PK 覆盖写:recommendation_level / risk_level 照第二拍出参、recommendation_reason(工具 reason 扩写成人话, 引具体参数)、difference_summary(「7 项比对:4 MATCH / 1 MISMATCH / 1 UNKNOWN / 1 CONDITIONAL」)、analysis_report_ref = "RPT-<案号数字串>"、 confidence_score(取该候选各行 confidence 的最小值);recommendation_level = conditional 的行**必须**写 verification_requirement(列出 VAL 号)、validation_id。 ④ \`${D}.verdict\` 同 PK 覆盖写:relation_total、recommended_total / conditional_total / alternate_total / not_recommended_total(数关系表)。 写完引擎自己收这一项;下一项是审核(S4),**这一拍不要发卡**。 **不许把工具的结论改成别的**:不推荐就是不推荐;要改只能补证据后重比。`, }, { item: 'S4_review', intent: '差异评估结论审核:决策卡循环,通过或驳回 + AI 按意见改台账重发', writes: ['substitution.relation', 'substitution.difference_item', 'validation.record', `${D}.verdict`], reads: ['substitution.relation', 'substitution.difference_item', 'validation.record', `${D}.verdict`], view: 'difference-review', tools: [], /* 要人拍板,且这一项内部可能循环 —— 不给 doneWhen */ guide: `结论算出来了,**该 FAE/RI 审核了** —— 这是验收工单 01,走决策卡(不是终局验收)。**先分岔,再动作**: 轨迹里找这一项已答过、还没处理的 [card-reply]: · 没有 → 照说明书⑥发第一张决策卡(ask 里把候选数、比对条数、四态各几条、逐候选推荐等级说全),发完这一拍结束。 · "approve" → 写 \`${D}.verdict.review_approved_total = 1\`,finish 这一项。别再发卡。 · "reject" / "custom:…" → **驳回**。按意见改台账(同 PK 覆盖写,不开新行、不改行数): · 意见点名某个参数 → 改那一行 \`substitution.difference_item\` 的 impact_statement / unknown_disposition / evidence_ref;要挂验证的补一行 \`validation.record\`; · 意见说推荐理由站不住 → 改 \`substitution.relation.recommendation_reason\`(等级不许改 —— 那是工具算的;要改先回 S3 补证据重比,本活动不回头,写清楚为什么维持); · 没写具体意见("reject")→ 自己挑最站不住的一处改实(证据最含糊的那条 evidence_ref、去向最空的那条 unknown_disposition),**至少改对一处**; 然后**换一种问法**再发一张决策卡(把改了什么写进 ask),发完结束。 **基座对着字面比对意见与草稿**:观察里点名了哪一行,就改那一行,不挪到别的行头上。`, }, { item: 'S5_prepare_landing', intent: '拟落账:P-11 分析状态推到 FAE 确认,回写 P-01 五项', writes: ['substitution.relation', 'demand.request', `${D}.verdict`], reads: ['substitution.relation', 'substitution.difference_item', 'validation.record', 'demand.request', `${D}.verdict`], view: 'landing-confirm', tools: [], doneWhen: 'landing_prepared_total > 0 and substitution_relation_analysis_status_fae_confirmed = substitution_relation_rows and ' + 'substitution_relation_missing = 0 and demand_request_missing = 0', guide: `审核通过了,**把拟落账的内容备好**,一拍连做: ① 每条 \`substitution.relation\` 同 PK 覆盖写 analysis_status = "fae_confirmed"(只推分析层;客户批准状态你没有写权,别碰)。 ② \`demand.request\` 同 PK 覆盖写五项 + 状态:risk_level(取各候选 risk_level 里最高的一档)、verification_requirement(把所有 VAL 号与要测什么列全; 没有 UNKNOWN 写「无需验证」)、output_ref = "RPT-<案号数字串>"、next_action = "提交 ACT-GATE-03 评审放行"、current_activity = "ACT-WORK-05"、 current_stage = "gate03"、request_status = "responded"。 ③ \`${D}.verdict\` 同 PK 覆盖写:landing_prepared_total = 1、summary 一句人话(几个候选各什么等级、挂了几个验证任务)。 写完引擎自己收这一项,下一项是放行。**这一拍不要 submit**。`, }, { item: 'T_release', intent: '拟落账内容已备好,提交 ACT-GATE-03 交 FAE/RI 放行;被打回就改台账再交', /* 被打回重做时要改的就是这几张 —— 不声明就写不进去 */ writes: ['substitution.relation', 'substitution.difference_item', 'validation.record', 'demand.request', `${D}.verdict`], reads: ['substitution.relation', 'substitution.difference_item', 'validation.record', 'demand.request', `${D}.verdict`], view: 'landing-confirm', tools: [], /* 放行触达世界,必须人按一下 —— 引擎不自动收它 */ guide: `落账确认那一屏摆着拟写 P-11 主记录与拟回写 P-01。**先看轨迹里有没有「验收被打回重做」**: 有 → 这一拍不是再提交一遍。照意见改**台账本身**(同 PK 覆盖写,不开新行): · 他说推荐等级与明细不符 → 核对 \`substitution.relation.recommendation_level\` 与该关系明细的四态计数,不符的把 recommendation_reason 改实、把漏挂的验证要求补上; · 他说 P-01 回写不全 → 补 \`demand.request\` 那五项; · 他说证据不可信 → 改那几行 \`substitution.difference_item.evidence_ref\`(指到具体资料版本与页码); · 没写具体意见 → 自己挑最站不住的一处改实,至少改对一处; 改完这一拍再 judge + submit。**基座那道闸:打回之后台账一个字没改就不许再提交。** 没被打回(或已改过)→ 先裁三条裁量判据,然后 submit,同一拍连做: · judge criterion="证据链可信:每条结论都能追溯到 P-08 / P-10 / P-06 的具体记录,或本单数据工单的创建记录(人工录入)" —— 逐条看 evidence_ref 指不指得到具体记录; · judge criterion="硬约束项未被自动判 MATCH:封装 / 工艺 / 电气边界等硬约束的 MATCH 都有证据支持" —— 看 hard 且 match 的行 evidence_ref 是否非空且具体; · judge criterion="推荐等级与差异明细一致:存在 UNKNOWN / CONDITIONAL 的候选没有给「推荐」" —— 逐候选核对; **裁成不满足照样能提交** —— 理由连着卡一起交给签字的人。criterion 照判据名整句原样抄。 这一单没有分支去向,只发一口 \`assessment-released\`,不必你选出口。终局唯放行:你不能自己宣布完成。`, }, ]; /** * 阶段 —— 进度条的刻度,只给人看。除放行外每个计划项恰属一个阶段; * 放行那一格由基座统一加,人看到的是 **x/6** —— 与文档的步骤 0.5 / 1 / 2 / 3 / 4 / 5 六步逐格对齐: * 0.5 启动与存在性检查(等待数据)· 1 建立评估上下文 · 2 读取并标准化 · 3 逐项差异评估 · 4 FAE 审核与补证(含拟落账)· 5 提交评审放行(基座那格) */ export const ASSESSMENT_PHASES = [ { name: '启动与存在性检查', items: ['S0_bind', 'S0d_master_data'] }, { name: '建立评估上下文', items: ['S1_input_confirm'] }, { name: '读取并标准化资料', items: ['S2_normalize'] }, { name: '逐项差异评估', items: ['S3_assess'] }, { name: 'FAE 审核与补证', items: ['S4_review', 'S5_prepare_landing'] }, ]; ``` - [ ] **Step 2: 写剧本自测 `__tests__/steps.test.ts`** ```ts /** * 步骤剧本与呈现面 —— 这段错了不报错,只是活动开始转圈 / 界面开始撒谎(与 E04 / I01 同一套惯例)。 */ import { describe, expect, it } from 'vitest'; import { evaluateCondition } from '@sfeel'; import { ASSESSMENT_PHASES, ASSESSMENT_STEPS, CMMN_XML, NARRATIVE } from '../src/case-model.ts'; import { manifest, D } from '../src/manifest.ts'; const ITEM_IDS = [...CMMN_XML.matchAll(/<(?:humanTask|decisionTask|stage|milestone)\s+id="([^"]+)"/g)].map((m) => m[1] ?? ''); function doneOf(item: string): string { const found = ASSESSMENT_STEPS.find((s) => s.item === item)?.doneWhen; if (found === undefined) throw new Error(`${item} 没有 doneWhen`); return found; } /** 谓词写坏了引擎当作「没算出来」——测里也照这个口径 */ function safeEval(expression: string, scope: Record): boolean { try { return evaluateCondition(expression, scope); } catch { return false; } } /** 开局局面:入参 + 所有基座读数归零 */ const fresh = (input: Record): Record => ({ ...input, demand_request_rows: 0, demand_request_missing: 0, [`${D}_verdict_rows`]: 0, [`${D}_pn_check_rows`]: 0, [`${D}_pn_check_missing`]: 0, [`${D}_pn_check_exists_status_absent`]: 0, [`${D}_pn_check_exists_status_abandoned`]: 0, [`${D}_input_gap_rows`]: 0, [`${D}_input_gap_missing`]: 0, substitution_relation_rows: 0, substitution_relation_missing: 0, substitution_relation_analysis_status_fae_confirmed: 0, substitution_difference_item_rows: 0, substitution_difference_item_missing: 0, substitution_difference_item_distinct_substitution_relation_id: 0, substitution_difference_item_difference_status_match: 0, substitution_difference_item_difference_status_mismatch: 0, substitution_difference_item_difference_status_unknown: 0, substitution_difference_item_difference_status_conditional: 0, tool_part_substitution_assessment_compare_parameters_calls: 0, tool_part_substitution_assessment_recommend_calls: 0, }); describe('剧本与案例模型对得上', () => { it('每一条剧本都指向一个真实存在的计划项,图上每一步也都有剧本', () => { for (const step of ASSESSMENT_STEPS) expect(ITEM_IDS, `剧本里的 ${step.item} 在 CMMN 里不存在`).toContain(step.item); for (const id of ITEM_IDS) expect(ASSESSMENT_STEPS.map((s) => s.item), `计划项 ${id} 没有剧本`).toContain(id); }); it('清单里挂的就是这份剧本与这份阶段', () => { expect(manifest.assets.businessModel.steps).toBe(ASSESSMENT_STEPS); expect(manifest.assets.businessModel.phases).toBe(ASSESSMENT_PHASES); }); }); describe('剧本声明的读写面 ⊆ 清单声明', () => { const declaredAddresses = new Set(manifest.declarations.ledger.map((d) => `${d.domain}.${d.object}`)); const declaredTools = new Set(manifest.declarations.tools.map((t) => t.name)); it('steps[].writes / reads(台账那几个)全落在声明过的对象上', () => { for (const step of ASSESSMENT_STEPS) { for (const address of step.writes) expect(declaredAddresses.has(address), `${step.item} 要写 ${address}`).toBe(true); for (const address of step.reads.filter((r) => r.includes('.') && !r.startsWith('case:'))) { expect(declaredAddresses.has(address), `${step.item} 要读 ${address}`).toBe(true); } } }); it('steps[].tools 全部是声明过的工具;只有 S3 调工具', () => { for (const step of ASSESSMENT_STEPS) { for (const tool of step.tools) expect(declaredTools.has(tool), `${step.item} 要调 ${tool}`).toBe(true); if (step.item !== 'S3_assess') expect(step.tools, step.item).toEqual([]); } expect(ASSESSMENT_STEPS.find((s) => s.item === 'S3_assess')?.tools).toHaveLength(2); }); it('customer_approval_status 在任何剧本里都不许写(只读边界)', () => { for (const step of ASSESSMENT_STEPS) expect(step.guide, step.item).not.toMatch(/customer_approval_status\s*=/); }); }); describe('doneWhen —— 开局不许真,干完必须真', () => { it('**开局一步都不该被自动收掉** —— 五条样例的入参形状逐一试', () => { for (const sample of manifest.samples) { for (const step of ASSESSMENT_STEPS) { if (step.doneWhen === undefined) continue; expect(safeEval(step.doneWhen, fresh(sample.input as Record)), `样例「${sample.name}」下 ${step.item} 开局就为真`).toBe(false); } } }); it('S0:两颗候选 + 原 PN = 3 行核对齐、需求与结论都写了 → 收得掉;少写一颗 → 收不掉', () => { const base = { candidateInternalPns: ['XL2596S-5.0', 'LM2596S-ADJ'], demand_request_rows: 1, demand_request_missing: 0, [`${D}_verdict_rows`]: 1, [`${D}_pn_check_missing`]: 0 }; expect(safeEval(doneOf('S0_bind'), { ...base, [`${D}_pn_check_rows`]: 3 })).toBe(true); expect(safeEval(doneOf('S0_bind'), { ...base, [`${D}_pn_check_rows`]: 2 })).toBe(false); }); it('S2:未放弃的候选各一条关系且每条都有明细 → 收得掉;放弃一颗后关系数随之减一', () => { const two = { candidateInternalPns: ['A', 'B'], [`${D}_pn_check_exists_status_abandoned`]: 0, substitution_difference_item_rows: 14 }; expect(safeEval(doneOf('S2_normalize'), { ...two, substitution_relation_rows: 2, substitution_difference_item_distinct_substitution_relation_id: 2 })).toBe(true); expect(safeEval(doneOf('S2_normalize'), { ...two, substitution_relation_rows: 2, substitution_difference_item_distinct_substitution_relation_id: 1 })).toBe(false); expect(safeEval(doneOf('S2_normalize'), { ...two, [`${D}_pn_check_exists_status_abandoned`]: 1, substitution_relation_rows: 1, substitution_difference_item_distinct_substitution_relation_id: 1 })).toBe(true); }); it('S3:四态之和 = 明细行数、必填齐、两枚工具都调过 → 收得掉;工具没调过 → 收不掉', () => { const ok = { substitution_difference_item_missing: 0, substitution_relation_missing: 0, substitution_difference_item_rows: 7, substitution_difference_item_difference_status_match: 4, substitution_difference_item_difference_status_mismatch: 1, substitution_difference_item_difference_status_unknown: 1, substitution_difference_item_difference_status_conditional: 1, tool_part_substitution_assessment_compare_parameters_calls: 1, tool_part_substitution_assessment_recommend_calls: 1, }; expect(safeEval(doneOf('S3_assess'), ok)).toBe(true); expect(safeEval(doneOf('S3_assess'), { ...ok, tool_part_substitution_assessment_recommend_calls: 0 })).toBe(false); expect(safeEval(doneOf('S3_assess'), { ...ok, substitution_difference_item_difference_status_match: 3 })).toBe(false); }); it('S5:旗 + 全部关系 FAE 确认 + P-01 回写齐 → 收得掉;缺一头都收不掉', () => { const ok = { landing_prepared_total: 1, substitution_relation_analysis_status_fae_confirmed: 2, substitution_relation_rows: 2, substitution_relation_missing: 0, demand_request_missing: 0 }; expect(safeEval(doneOf('S5_prepare_landing'), ok)).toBe(true); expect(safeEval(doneOf('S5_prepare_landing'), { ...ok, substitution_relation_analysis_status_fae_confirmed: 1 })).toBe(false); expect(safeEval(doneOf('S5_prepare_landing'), { ...ok, demand_request_missing: 1 })).toBe(false); }); }); describe('说明书与剧本的骨架', () => { it('五张卡的骨架都住在说明书里(压拍之后只有说明书还在)', () => { expect(NARRATIVE).toContain('"kind":"entry","itemId":"S0_bind"'); expect(NARRATIVE).toContain('"kind":"decide","itemId":"S0d_master_data"'); expect(NARRATIVE).toContain('"title":"数据工单 · 芯片主数据创建(P-08)'); expect(NARRATIVE).toContain('"title":"数据工单 · 原厂技术资料创建(P-10)'); expect(NARRATIVE).toContain('"title":"评估输入与资料补齐确认'); expect(NARRATIVE).toContain('"kind":"decide","itemId":"S4_review"'); }); it('剧本里不引反例原话;S3 固定三拍且写明「工具后同拍作废」', () => { const s3 = ASSESSMENT_STEPS.find((s) => s.item === 'S3_assess')?.guide ?? ''; expect(s3).toContain('第一拍'); expect(s3).toContain('第二拍'); expect(s3).toContain('第三拍'); expect(s3).toContain('工具调用后同拍其余动作作废'); }); it('S0 剧本写明库里都有就 disable S0d;S0d 剧本写明候选 ≥2 才问、=1 必建、原 PN 硬阻塞', () => { const s0 = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? ''; expect(s0).toContain('disable S0d_master_data'); const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_master_data')?.guide ?? ''; expect(s0d).toContain('硬阻塞'); expect(s0d).toContain('候选只有 1 颗'); }); it('五段进度,每段名 ≤10 字;除放行外每项恰属一段', () => { expect(ASSESSMENT_PHASES).toHaveLength(5); for (const p of ASSESSMENT_PHASES) expect(p.name.length, p.name).toBeLessThanOrEqual(10); const all = ASSESSMENT_PHASES.flatMap((p) => p.items); expect(new Set(all).size).toBe(all.length); expect([...all].sort()).toEqual([...ITEM_IDS.filter((i) => i !== 'T_release')].sort()); }); }); ``` - [ ] **Step 3: 跑包内测试** Run: `pnpm vitest run activities/part-substitution-assessment` Expected:steps.test 全绿;activity.test 里除「报告模版」组外全绿(模版仍是占位)。 - [ ] **Step 4: 提交** ```bash git add activities/part-substitution-assessment git commit -m "feat(w05): 差异评估案例模型 —— CMMN 八项七哨兵、说明书五卡骨架、八条剧本、五段进度 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh" ``` --- ### Task 5: 五张 HTML 模版(目标件平移) **Files:** - Replace: `activities/part-substitution-assessment/src/views/report.ts`(整份替换 Task 3 的占位) - Test: `activities/part-substitution-assessment/__tests__/activity.test.ts`(Task 3 已写好的「报告模版」组此时转绿) **Interfaces:** - Consumes:Task 3 的台账列名 / tally 值;基座读数名 `parts_profile_rows / parts_profile_missing / parts_profile_missing_keys`、`parts_document_*`、`sub_eval_input_gap_*`、`substitution_relation_rows`、`substitution_difference_item_rows`、四态计数;引擎注入 `case:owner` / `case:taskId`;摊平列 `eval_ref` / `request_id` / `rule_version` / `original_internal_pn`;入参 `customerCode` / `requestDescription` / `originalInternalPn`。 - Produces:`P08_MASTER_CREATE` / `P10_DOCUMENT_CREATE` / `EVAL_INPUT_LIST` / `DIFFERENCE_REVIEW` / `LANDING_CONFIRM`(字符串)。 - 平移法(《HTML报告生成规范》§7):目标件 `docs/SS02-ACT-WORK-05-html/*.html` 的版面语言原样搬(它与 E04/I01 目标件同一套壳,`prd-style.ts` 已逐字节持有);把设计稿记号换成平台绑定:`data-rows="in.*/out.*"` → `data-rows="mine:域.对象"`;`data-field` → `data-slot` + 可改格加 `data-col`;`meta.task.owner/id` → `case:owner/case:taskId`;`sd.*` 读数 → 基座读数;`card.deadline`(本仓没有工单期限这个事实)与 `card.id` 单号**不摆**;目标件里 JS 造的行 / 算的计数一律换成 `data-rows` + 基座读数。 - [ ] **Step 1: 写 `src/views/report.ts`** 先把 I01 `activities/brand-content-toolkit/src/views/report.ts` 里 `const BIND_CSS = \`` 到它的闭合反引号(`/** 三层壳照目标件 */` 之前)整段**原样复制**过来作为本文件的 `BIND_CSS`(数据驱动的开关全在那里:三档占位 / 缺项红带 / 编辑闸 / 增删行 / 状态徽基座);然后按下面写其余内容: ```ts /** * ACT-WORK-05 差异评估的 HTML 报告模版 —— **活动自己持有的资产**,随包版本冻结。 * * 五个局面各一张: * · `p08-master-create` —— S0d 摆,数据工单(P-08 芯片主数据),可加行、可编辑; * · `p10-document-create` —— S0d 摆,数据工单(P-10 原厂技术资料),可加行、可编辑; * · `eval-input-list` —— S0/S1 摆,评估输入清单:绑定区只读 + 缺口表可编辑; * · `difference-review` —— S2/S3/S4 摆,各候选推荐等级 + 差异明细,**只读**(通过/驳回在对话区决策卡); * · `landing-confirm` —— S5/T_release 摆,拟写 P-11 / 拟回写 P-01,**只读**,放行后即归档件。 * * 版面语言逐字量自 `docs/SS02-ACT-WORK-05-html/` 四份目标件;取值三纪律:顶层只取汇总列与单值列, * 逐条走 `mine:域.对象`,列名逐字照 `declarations.ledger` 抄。**模版不算不判**。 */ import { PRD_CSS } from './prd-style.ts'; import { D } from '../constants.ts'; /* ==================== 绑定期补的样式(从 I01 report.ts 的 BIND_CSS 原样复制)==================== */ const BIND_CSS = `…(I01 那一段,原样)…`; /* ==================== 本活动的枚举徽标与参数集格子 ==================== */ const EXTRA_CSS = ` /* 参数集一行一参数:只读那一份按行换行显示 */ .proto-root .jc-pre { white-space: pre-line; font-family: var(--mono); font-size: 12px; } .proto-root textarea.jc-cell { min-height: 120px; font-family: var(--mono); font-size: 12px; } /* 徽标按列作用域取人读名(同一个 v-conditional 在推荐等级与四态里是两个词) */ .proto-root .jc-tag.t-role.v-original::after { content: "原 PN"; } .proto-root .jc-tag.t-role.v-candidate::after { content: "候选"; } .proto-root .jc-tag.t-exist.v-found::after { content: "在库"; } .proto-root .jc-tag.t-exist.v-absent::after { content: "不在库"; } .proto-root .jc-tag.t-exist.v-created::after { content: "本单新建"; } .proto-root .jc-tag.t-exist.v-abandoned::after { content: "已放弃"; } .proto-root .jc-tag.t-exist.v-absent { color: var(--danger); background: var(--danger-soft); } .proto-root .jc-tag.t-exist.v-created { color: var(--ok); background: var(--ok-soft); } .proto-root .jc-tag.t-gap.v-gap::after { content: "待补*"; } .proto-root .jc-tag.t-gap.v-default::after { content: "默认·可改"; } .proto-root .jc-tag.t-gap.v-ok::after { content: "齐备"; } .proto-root .jc-tag.t-gap.v-gap { color: var(--danger); background: var(--danger-soft); } .proto-root .jc-tag.t-gap.v-ok { color: var(--ok); background: var(--ok-soft); } .proto-root .jc-tag.t-life.v-active::after { content: "Active"; } .proto-root .jc-tag.t-life.v-nrnd::after { content: "NRND"; } .proto-root .jc-tag.t-life.v-eol::after { content: "EOL"; } .proto-root .jc-tag.t-life.v-discontinued::after { content: "停产"; } .proto-root .jc-tag.t-life.v-risk::after { content: "风险"; } .proto-root .jc-tag.t-doc.v-datasheet::after { content: "Datasheet"; } .proto-root .jc-tag.t-doc.v-app_note::after { content: "应用笔记"; } .proto-root .jc-tag.t-doc.v-reference_design::after { content: "参考设计"; } .proto-root .jc-tag.t-doc.v-white_paper::after { content: "白皮书"; } .proto-root .jc-tag.t-doc.v-sample_doc::after { content: "样片资料"; } .proto-root .jc-tag.t-doc.v-commitment_letter::after { content: "承诺函"; } .proto-root .jc-tag.t-doc.v-other::after { content: "其他"; } .proto-root .jc-tag.t-docst.v-valid::after { content: "有效"; } .proto-root .jc-tag.t-docst.v-expired::after { content: "过期"; } .proto-root .jc-tag.t-docst.v-pending::after { content: "待核实"; } .proto-root .jc-tag.t-docst.v-void::after { content: "作废"; } .proto-root .jc-tag.t-sub.v-pin_to_pin::after { content: "Pin-to-Pin"; } .proto-root .jc-tag.t-sub.v-functional::after { content: "功能替代"; } .proto-root .jc-tag.t-sub.v-parametric::after { content: "参数替代"; } .proto-root .jc-tag.t-sub.v-other::after { content: "其他"; } .proto-root .jc-tag.t-rec.v-recommended::after { content: "推荐"; } .proto-root .jc-tag.t-rec.v-conditional::after { content: "条件推荐"; } .proto-root .jc-tag.t-rec.v-alternate::after { content: "备选"; } .proto-root .jc-tag.t-rec.v-not_recommended::after { content: "不推荐"; } .proto-root .jc-tag.t-rec.v-recommended { color: var(--ok); background: var(--ok-soft); } .proto-root .jc-tag.t-rec.v-conditional { color: var(--warn-accent); background: var(--warn-soft); } .proto-root .jc-tag.t-rec.v-not_recommended { color: var(--danger); background: var(--danger-soft); } .proto-root .jc-tag.t-anal.v-ai_draft::after { content: "AI 草稿"; } .proto-root .jc-tag.t-anal.v-fae_confirmed::after { content: "FAE 确认"; } .proto-root .jc-tag.t-anal.v-pending_customer::after { content: "待客户验证"; } .proto-root .jc-tag.t-anal.v-customer_approved::after { content: "客户已批准"; } .proto-root .jc-tag.t-anal.v-rejected::after { content: "不批准"; } .proto-root .jc-tag.t-anal.v-void::after { content: "作废"; } .proto-root .jc-tag.t-anal.v-fae_confirmed { color: var(--ok); background: var(--ok-soft); } .proto-root .jc-tag.t-diff.v-match::after { content: "MATCH"; } .proto-root .jc-tag.t-diff.v-mismatch::after { content: "MISMATCH"; } .proto-root .jc-tag.t-diff.v-unknown::after { content: "UNKNOWN"; } .proto-root .jc-tag.t-diff.v-conditional::after { content: "CONDITIONAL"; } .proto-root .jc-tag.t-diff.v-match { color: var(--ok); background: var(--ok-soft); } .proto-root .jc-tag.t-diff.v-mismatch { color: var(--danger); background: var(--danger-soft); } .proto-root .jc-tag.t-diff.v-unknown { color: var(--ink-3); background: var(--soft); } .proto-root .jc-tag.t-diff.v-conditional { color: var(--warn-accent); background: var(--warn-soft); } .proto-root .jc-tag.t-con.v-hard::after { content: "硬约束"; } .proto-root .jc-tag.t-con.v-soft::after { content: "软约束"; } .proto-root .jc-tag.t-con.v-hard { color: var(--danger); background: var(--danger-soft); } .proto-root .jc-tag.t-risk.v-low::after { content: "低"; } .proto-root .jc-tag.t-risk.v-medium::after { content: "中"; } .proto-root .jc-tag.t-risk.v-high::after { content: "高"; } .proto-root .jc-tag.t-risk.v-critical::after { content: "关键"; } .proto-root .jc-tag.t-risk.v-low { color: var(--ok); background: var(--ok-soft); } .proto-root .jc-tag.t-risk.v-medium { color: var(--warn-accent); background: var(--warn-soft); } .proto-root .jc-tag.t-risk.v-high, .proto-root .jc-tag.t-risk.v-critical { color: var(--danger); background: var(--danger-soft); } /* 绑定区:四项锁定的灰底只读行 */ .proto-root .jc-bound { display: grid; grid-template-columns: 140px 1fr; gap: 6px 14px; padding: 10px 14px; margin: 8px 0 14px; background: var(--soft); border: 1px solid var(--line-soft); border-radius: 9px; font-size: 13px; } .proto-root .jc-bound b { color: var(--ink-3); font-weight: 600; } `; /** 三层壳照目标件(收件人 / 关联任务两格由引擎注入;目标件的「最迟交回」与单号本仓没有真数据源,不摆) */ const doc = (input: { tone: 'human' | 'ledger'; badge: string; tag: string; kicker: string; title: string; step: string; state: string; body: string; foot: string }): string => `
${input.badge}
${input.kicker}

${input.title}

收件人: · 关联任务:
${input.tag} ${input.step} ${input.state}
${input.body}
${input.foot}
`; const part = (label: string, aside = ''): string => `

【${label}】${aside === '' ? '' : ` —— ${aside}`}

`; const criteria = (title: string, items: readonly string[]): string => `
${title}
    ${items.map((i) => `
  • ${i}
  • `).join('')}
`; const flow = (text: string): string => `
${text}
`; /** 页脚右侧「工单类型:× · <案号>」 */ const sign = (note: string, tag: string): string => `${note}${tag} · `; const editGate = (address: string, label: string): string => `
${label}
`; const gapbar = (object: string, what: string): string => `
还差 行${what}没填 —— 。按「编辑」开表,红框那几格就是要填的。补齐了才确认得了。
`; const tag = (scope: string, col: string): string => ``; const cell = (col: string, cls = ''): string => ``; const ro = (col: string, cls = ''): string => ``; const select = (col: string, options: ReadonlyArray, required = false): string => ``; /* ==================== 局面 0 · 数据工单 P-08 ==================== */ const P08_TABLE = `
内部料号 品牌 制造商 规格描述 封装 工艺 生命周期 EOL / PCN Datasheet 引用 Datasheet 版本 适配场景 标准化参数集(一行一参数:参数名 | 值 | 单位 | hard/soft | 说明)
${cell('internal_pn', 'mono')} ${cell('brand_name')} ${cell('manufacturer_name')} ${cell('specification_description')} ${cell('package', 'mono')} ${cell('process_technology')} ${select('lifecycle_status', [['active', 'Active'], ['nrnd', 'NRND'], ['eol', 'EOL'], ['discontinued', '停产'], ['risk', '风险']], true)}${tag('t-life', 'lifecycle_status')} ${cell('eol_pcn_status')} ${cell('datasheet_ref', 'mono')} ${cell('datasheet_version')} ${cell('application_scenarios')}
`; export const P08_MASTER_CREATE = doc({ tone: 'human', badge: '数据', tag: '数据工单', kicker: 'SS02 · ACT-WORK-05 差异评估 · 数据工单(P-08)', title: '芯片主数据创建', step: '步骤 0.5 · 等待数据', state: `待建 / 行`, body: part('做什么', '批量创建 P-08 主数据记录') + `

模板定位:本单是「数据工单」的实例 —— 一单一类(可多条):目标台账 = 芯片主数据(P-08); 字段即表头、每行一条新记录;标准化参数集是这条记录的一个字段(一行一参数)。

触发原因:系统按内部料号查芯片主数据查无记录 —— 是「完全没有」,不是字段缺失(那归执行工单)。

AI 预填:上传了 datasheet 的料号 AI 已按资料预填,标着值的格子请核对;没上传的料号全部手填,同为合法路径。

纪律:必填列全齐才允许确认;确认后这批记录进本单草稿,放行时并入正式库并留痕(确认人 = 数据责任人);只建主数据,验证记录等事实类不在此建。

` + part('创建什么', '主数据记录(每行一条,*为必填列)') + gapbar('parts_profile', '必填') + editGate('parts.profile', `待建 条记录,点「编辑」核对预填、补齐空缺;「+ 加一行」= 再建一条`) + `
必填列(*)全齐才允许确认;原厂未披露的参数值请直接写「未披露」,它会进入 UNKNOWN 并挂验证任务;删一行 = 该记录不建。改动只落任务草稿层,保存不等于放行。
` + P08_TABLE + `
新行的内部料号必须写(它是这一行的钥匙);其余必填列全部填写后才允许确认。
` + flow('提交 / 确认:本工单只承载内容;确认请在对话区的【执行卡】中进行。确认后活动自动从「等待数据」唤醒。'), foot: sign('SS02 · ACT-WORK-05 差异评估 · 数据工单 · 芯片主数据创建(P-08)', '工单类型:数据'), }); /* ==================== 局面 0′ · 数据工单 P-10 ==================== */ const P10_TABLE = `
关联料号 序号 资料类型 品牌 资料标题 版本 文件链接 证据定位 状态
${cell('internal_pn', 'mono')} ${cell('doc_seq', 'mono')} ${select('document_type', [['datasheet', 'Datasheet'], ['app_note', '应用笔记'], ['reference_design', '参考设计'], ['white_paper', '白皮书'], ['sample_doc', '样片资料'], ['commitment_letter', '承诺函'], ['other', '其他']], true)}${tag('t-doc', 'document_type')} ${cell('brand_name')} ${cell('document_title')} ${cell('document_version')} ${cell('document_ref', 'mono')} ${cell('citation_locator')} ${select('document_status', [['valid', '有效'], ['expired', '过期'], ['pending', '待核实'], ['void', '作废']], true)}${tag('t-docst', 'document_status')}
`; export const P10_DOCUMENT_CREATE = doc({ tone: 'human', badge: '数据', tag: '数据工单', kicker: 'SS02 · ACT-WORK-05 差异评估 · 数据工单(P-10)', title: '原厂技术资料创建', step: '步骤 0.5 · 等待数据', state: `待建 / 行`, body: part('做什么', '为本单新建的料号登记原厂资料(P-10)') + `

触发原因:按内部料号查原厂技术资料库查无记录。一份资料一行;同一料号多份资料序号 01/02… 自己续。

为什么要它:差异明细的证据引用要指到具体资料的版本与页码 —— 没有这一行,结论就追溯不到来源。

` + part('创建什么', '资料记录(每行一条,*为必填列)') + gapbar('parts_document', '必填') + editGate('parts.document', `待建 条资料记录,点「编辑」补齐`) + `
资料类型、标题、版本、文件链接、状态为必填;证据定位写页码或章节。改动只落任务草稿层,保存不等于放行。
` + P10_TABLE + `
关联料号与序号是这一行的钥匙,新行都要写;必填列全齐才允许确认。
` + flow('提交 / 确认:本工单只承载内容;确认请在对话区的【执行卡】中进行。'), foot: sign('SS02 · ACT-WORK-05 差异评估 · 数据工单 · 原厂技术资料创建(P-10)', '工单类型:数据'), }); /* ==================== 局面 1 · 评估输入清单(S0/S1)==================== */ const PN_CHECK_TABLE = `
料号 角色 在库状态 备注 / 放弃原因
${ro('internal_pn', 'mono')} ${tag('t-role', 'role')} ${tag('t-exist', 'exists_status')}
`; const GAP_TABLE = `
键 输入项 来源 状态 绑定值 / 缺口说明 补充内容
${cell('item_key', 'mono')} ${cell('input_item')} ${cell('source_ledger', 'mono')} ${select('status', [['gap', '待补'], ['default', '默认·可改'], ['ok', '齐备']])}${tag('t-gap', 'status')} ${cell('detail')}
`; export const EVAL_INPUT_LIST = doc({ tone: 'human', badge: '执行', tag: '执行工单', kicker: 'SS02 · ACT-WORK-05 差异评估 · 执行工单 1/1', title: '评估输入与资料补齐确认', step: '步骤 1/5 · 建立评估上下文', state: `待补 项`, body: part('做什么', '启动 4 项已绑定,其余信息在工单里补齐') + `

AI 已完成:系统已按启动传入的 4 项完成绑定 —— 客户、客户需求(新建 P-01 · 版本 V1)、原 PN、候选 PN(数组,逐个评估)—— 并生成输入快照。

需要你补齐:启动时未收的信息(应用场景、BOM 位)直接在下表补;默认值(替代类型 / 规则版本 / 负责人期限)可确认可改。本单缺口只覆盖「有而不全」;「完全没有」(料号不在库)已由数据工单创建。未补齐的缺口项将按 UNKNOWN 进入差异评估。

` + part('已绑定', '启动时冻结,工单内不可改 —— 绑错请退回启动人重新启动') + `
客户 客户需求 · V1 · 原内部料号 规则版本
` + PN_CHECK_TABLE + part('交回什么', '评估输入清单(已绑定项只读核对,缺口项补齐,*为必填)') + gapbar(`${D}_input_gap`, '补充内容') + editGate(`${D}.input_gap`, `还有 项待补 / 待确认,点「编辑」在「补充内容」列填入`) + `
加一行 = 补充 AI 未列出的资料项(新行的「输入项」与「补充内容」必填);删一行 = 该输入项不适用;补不了的写「留 UNKNOWN」或「挂验证任务」—— 那也是一个答案。改动只落任务草稿层,保存不等于放行。
` + GAP_TABLE + `
键写一个英文短名(如 thermal_note);「绑定值 / 缺口说明」栏写清这项资料是什么、AI 为什么拿不到。
` + flow('审批 / 提交:本工单只承载内容;「提交」请在对话区的【执行卡】中进行。提交后去向:进入步骤 2 · AI 读取与标准化,仍缺失的项将按 UNKNOWN 进入差异评估。'), foot: sign('SS02 · ACT-WORK-05 差异评估 · 执行工单 1/1 · 评估输入与资料补齐确认', '工单类型:执行'), }); /* ==================== 局面 2 · 差异评估结论审核(S2/S3/S4,只读)==================== */ const RELATION_TABLE = `
候选料号 替代类型 推荐等级 推荐理由 差异摘要 风险 验证要求 置信度
${ro('candidate_internal_pn', 'mono')} ${tag('t-sub', 'substitution_type')} ${tag('t-rec', 'recommendation_level')} ${ro('recommendation_reason')} ${ro('difference_summary')} ${tag('t-risk', 'risk_level')} ${ro('verification_requirement')} ${ro('confidence_score', 'mono')}
`; const ITEM_TABLE = `
关系 参数项 原 PN 值 候选 PN 值 单位 约束类型 结论 风险 证据引用 影响说明 UNKNOWN 去向 置信度
${ro('substitution_relation_id', 'mono')} ${ro('original_value', 'mono')} ${ro('candidate_value', 'mono')} ${ro('parameter_unit')} ${tag('t-con', 'constraint_type')} ${tag('t-diff', 'difference_status')} ${tag('t-risk', 'risk_level')} ${ro('evidence_ref', 'mono')} ${ro('impact_statement')} ${ro('unknown_disposition')} ${ro('confidence', 'mono')}
`; export const DIFFERENCE_REVIEW = doc({ tone: 'ledger', badge: '验收', tag: '验收工单', kicker: 'SS02 · ACT-WORK-05 差异评估 · 验收工单 1/2', title: '差异评估结论审核', step: '步骤 3~4/5 · 逐项差异评估 + FAE/RI 审核', state: `共 个候选 · 条比对`, body: part('AI 结论说明', '结论是怎么算出来的') + `

AI 已完成的计算:按 GOV 评估规则(锁定版本 ),对原 PN 与每个候选逐项比对,四态结论 —— MATCH / MISMATCH / UNKNOWN / CONDITIONAL ,并给出风险等级、证据引用与置信度。 差异明细 = P-11 差异明细子表(一条参数一条记录);当前为 AI 草稿,审核通过前不落账。本单数据工单创建的主数据,其参数证据标注「人工录入」。

` + part('验收判据', '你据此判断「通过 / 驳回」') + criteria('判断这份差异评估结论是否可信,请逐项核对', [ '关键参数差异均有明确状态:逐项结论无漏项,每个参数都有四态之一的判定;', 'UNKNOWN 项均有去向:每个 UNKNOWN 都挂了验证任务或补来源动作,不允许悬空;', '证据链完整:每项结论可追溯至 P-08 / P-10 / P-06 的具体记录,或本单数据工单的创建记录(人工录入);', '硬约束项未被自动判 MATCH:封装 / 工艺等硬约束未经证据支持不得判 MATCH。', ]) + part('各候选推荐等级', '只读展示 · 每个候选一条 P-11 主记录草稿') + RELATION_TABLE + part('AI 产出的差异明细', '只读展示 · P-11 差异明细子表草稿') + ITEM_TABLE + flow('审批:本工单只承载内容;「通过 / 驳回」请在对话区的【验收卡】中进行。审核意见与补充条件写入 P-06(验证要求);通过后去向:进入步骤 5 · 评审放行与落账确认。'), foot: sign('SS02 · ACT-WORK-05 差异评估 · 验收工单 1/2 · 差异评估结论审核', '工单类型:验收'), }); /* ==================== 局面 3 · 评审放行与落账确认(S5/T_release,只读)==================== */ const P11_TABLE = `
替代关系ID 替代类型 原内部料号 候选内部料号 推荐等级 推荐理由 差异摘要 替代分析报告 分析状态 客户批准状态 风险 验证要求 证据可信度 规则版本
${ro('substitution_relation_id', 'mono')} ${tag('t-sub', 'substitution_type')} ${ro('original_internal_pn', 'mono')} ${ro('candidate_internal_pn', 'mono')} ${tag('t-rec', 'recommendation_level')} ${ro('recommendation_reason')} ${ro('difference_summary')} ${ro('analysis_report_ref', 'mono')} ${tag('t-anal', 'analysis_status')} 未提交 由审批流程维护,本活动不产生 ${tag('t-risk', 'risk_level')} ${ro('verification_requirement')} ${ro('confidence_score', 'mono')} ${ro('rule_version', 'mono')}
`; const P01_TABLE = `
需求编号 风险等级 验证要求 输出链接 下一步动作 当前阶段 需求状态
${ro('request_id', 'mono')} ${tag('t-risk', 'risk_level')} ${ro('verification_requirement')} ${ro('output_ref', 'mono')} ${ro('next_action')} ${ro('current_stage', 'mono')} ${ro('request_status', 'mono')}
`; export const LANDING_CONFIRM = doc({ tone: 'ledger', badge: '验收', tag: '验收工单', kicker: 'SS02 · ACT-WORK-05 差异评估 · 验收工单 2/2', title: '评审放行与落账确认', step: '步骤 5/5 · 提交 ACT-GATE-03', state: `拟落账 条 P-11`, body: part('AI 结论说明', '本单确认什么') + `

本活动以 ACT-GATE-03 评审放行为终点。本单确认拟落账内容:评估结论层写 P-11 替代关系主记录(每个候选一条)与差异明细子表,需求状态回写 P-01(风险等级 / 验证要求 / 输出链接 / 下一步动作 / 当前阶段)。 客户批准不属于本活动 —— 客户批准状态由审批流程维护,本单只写分析层(分析状态 = FAE 确认)。

` + part('验收判据', '你据此判断「通过 / 驳回」') + criteria('放行与落账前,请逐项核对', [ '推荐等级与差异明细一致:存在 UNKNOWN / CONDITIONAL 的候选,推荐等级不得给「推荐」;', '分析状态与客户批准状态分离:本单只写分析层(FAE 确认),客户批准状态保持「未提交」;', 'GOV 规则版本已锁定:规则版本随结论一并落账;', '回写 P-01 字段完整:风险等级、验证要求、输出链接、下一步动作、当前阶段五项齐全。', ]) + part('拟写 P-11 · 替代关系主记录', '只读展示') + P11_TABLE + part('拟回写 P-01 · 需求状态', '只读展示') + P01_TABLE + flow('审批:本工单只承载内容;「通过 / 驳回」请在对话区的【验收卡】中进行。通过后去向:P-11 落账、P-01 回写、放行;结论供 ACT-WORK-08 证据归集消费。'), foot: sign('SS02 · ACT-WORK-05 差异评估 · 验收工单 2/2 · 评审放行与落账确认', '工单类型:验收'), }); ``` - [ ] **Step 2: 跑包内测试,全绿** Run: `pnpm vitest run activities/part-substitution-assessment` Expected: activity.test + steps.test 全部 PASS(含「报告模版」组 7 条)。 - [ ] **Step 3: 全仓门禁子集 + 提交** Run: `pnpm -s typecheck && pnpm -s lint && pnpm -s depcruise` Expected: 全绿(活动包只 import `@sdk`/`@contracts`/`@sfeel` 与包内文件)。 ```bash git add activities/part-substitution-assessment git commit -m "feat(w05): 五张报告模版 —— 两张数据工单件、评估输入清单、差异审核件、落账确认件(目标件平移) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh" ``` --- ### Task 6: 登记进构建与安装通道、装进库、库内验收、测试台冒烟 **Files:** - Modify: `tools/build-activities.ts`(import + `PACKAGES` 加一条) - Modify: `apps/api/src/app/seed-activities.ts`(`REAL_PACKAGES` 加一条;装包之前注册 `W05_TOOLS`) - Test: `apps/api/src/app/__tests__/seed-mvp-data.test.ts`(已过);`docker exec … psql` 验收 SQL **Interfaces:** - Consumes:Task 2 的 `W05_TOOLS` / `W05_TOOL_SAMPLE`;Task 3–5 的活动包。 - Produces:库里装好的 `part-substitution-assessment@1.0.0`、六张主数据表 + 三张自持表、样例料号;测试台「活动」页签里能选到它并起单。 - [ ] **Step 1: 登记到构建脚本** 在 `tools/build-activities.ts` 的 import 区加: ```ts import { manifest as partSubstitutionAssessment } from '../activities/part-substitution-assessment/src/manifest.ts'; ``` `PACKAGES` 数组末尾加: ```ts { dir: 'part-substitution-assessment', manifest: partSubstitutionAssessment }, ``` - [ ] **Step 2: 登记到安装通道并顺带注册纯工具** `apps/api/src/app/seed-activities.ts`: ```ts const REAL_PACKAGES = ['crm-relationship-coverage', 'brand-content-toolkit', 'part-substitution-assessment'] as const; ``` 顶部加 `import { W05_TOOLS, W05_TOOL_SAMPLE } from './tools-w05.ts';`,在 `installRealActivities` 里 `const root = activitiesRoot();` 之前插入: ```ts /* * **纯工具随活动一起注册**(2026-08-26):从前工具契约只在 db:seed 里注册,只跑 activities:install 的 * 开发机上,活动清单声明了工具、白名单也发了,`sys.tool` 里却没有那件工具 —— 第一次跑就撞「工具不存在」。 * 「安装即用」包含它声明过的纯工具能调得动。register 按 name+version 幂等,重复装是 no-op。 */ for (const contract of W05_TOOLS) { const verdict = await container.toolbox.register({ contract, samples: [W05_TOOL_SAMPLE] }); if (!verdict.accepted) log(` 工具 ${contract.name} 未过两审:${verdict.staticFindings.map((f) => f.message).join('; ')}`); } ``` 同文件那句 `主数据四域已物化:${MASTER_OBJECTS.length} 张表` 改成 `主数据 ${MASTER_DOMAINS.length} 域已物化:${MASTER_OBJECTS.length} 张表`(import 里把 `MASTER_DOMAINS` 一并从 `./seed-mvp-data.ts` 引进来)—— 日志说「四域」而库里八域,就是一处会撒谎的话。 - [ ] **Step 3: 构建 + 装包(这一步同时物化六张主数据表、三张自持表,并播样例料号)** ```bash pnpm activities:install 2>&1 | tail -20 ``` Expected 日志里有:`主数据 8 域已物化:17 张表`(原 11 张 + 6)、`工具 …` 无「未过两审」、`活动包已装:ACT-WORK-05 · 差异评估(part-substitution-assessment@1.0.0) · 已授权工具 2 件`、静态审**无 error**。若出现 `静态审提示:[error] …`: - `predicate-unbound` 指名 `candidateInternalPns` → 把该谓词里的 `count(candidateInternalPns)` 换成读 `sub_eval.verdict.candidate_total`(模型写)并在 doneWhen/哨兵里保留 `_rows` 那一半;升版本重装; - `reference-target-unbound` → 检查 `refersTo` 的三个目标列是否与清单里声明的列名一字不差; - `ledger-kind-invalid` → 自持表的 `caseKey` 必须在主键里。 - [ ] **Step 4: 库内验收(表、主键、性质、注释、样例行)** ```bash docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select domain||'.'||object||' | '||kind||' | '||coalesce(case_key,'-') from ledger.__object_meta where domain in ('parts','demand','substitution','validation','sub_eval') order by 1" docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select conrelid::regclass||' '||pg_get_constraintdef(oid) from pg_constraint where contype='p' and conrelid::regclass::text ~ '^ledger\\.(parts|demand|substitution|validation|sub_eval)__' order by 1" docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select string_agg(column_name||':'||data_type, ', ' order by ordinal_position) from information_schema.columns where table_schema='ledger' and table_name='substitution__difference_item'" docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select obj_description('ledger.parts__profile'::regclass)||' / '||col_description('ledger.parts__profile'::regclass, 6)" docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select internal_pn||' | '||brand_name||' | '||lifecycle_status||' | '||left(normalized_parameter_set, 40) from ledger.parts__profile order by 1" docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select internal_pn||'/'||doc_seq||' '||document_type||' '||document_version from ledger.parts__document order by 1" docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select activity_id||' @'||version||' '||state from sys.activity_package where activity_id='part-substitution-assessment'" docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select name||' v'||version from sys.tool where name like 'part-substitution-assessment.%'" ``` Expected: - `__object_meta` 9 行:`parts.profile master`、`parts.document master`、`demand.request master`、`substitution.relation master`、`substitution.difference_item master`、`validation.record master`、`sub_eval.pn_check record eval_ref`、`sub_eval.input_gap record eval_ref`、`sub_eval.verdict record eval_ref`; - 主键:`parts__profile (internal_pn)`、`parts__document (internal_pn, doc_seq)`、`substitution__relation (substitution_relation_id)`、`substitution__difference_item (substitution_relation_id, difference_item_id)`、`sub_eval__pn_check (eval_ref, internal_pn)`、`sub_eval__input_gap (eval_ref, item_key)`、`sub_eval__verdict (eval_ref)`; - `substitution__difference_item` 的列里含 `compare_result:text` 与 `unknown_disposition:text`(清单追加的两列真长出来了); - 表注释 `芯片主数据(P-08)`,第 6 列注释是参数集的人读名; - `parts__profile` 两行(`LM2596S-5.0 | TI | nrnd | 输入电压范围 | 4.5~40 …`、`LM2596S-ADJ | TI | active | …`);`parts__document` 两行; - `sys.activity_package` 有 `part-substitution-assessment @1.0.0 installed`(state 值以库里 E04 那一行为准,同样即可);`sys.tool` 两件 `v1.0.0`。 把这几条输出原样贴进 `docs/ACT-WORK-05-端到端测试记录.md` 的「0. 库内验收」节(Task 8 Step 1 建文件时一起补)。 - [ ] **Step 5: 测试台冒烟(不起单,只看装没装对)** 用 claude-in-chrome 打开 `http://localhost:5173/design#/design/testbed/act`:活动下拉里有「ACT-WORK-05 · 差异评估」;选中后表单预填的是样例 0(客户 CU-0142、候选 `["XL2596S-5.0","LM2596S-ADJ"]`),样例钮五枚都显示名字(没有退成「样例 N」),「落真库」勾着。再打开 `http://localhost:5173/design#/design/ledgers` 数据域那一屏,按活动分组里能看到 ACT-WORK-05 名下 9 张表、`parts.profile` 正式 2 行。截图存 `docs/img/w05-testbed-smoke.png`(用 computer 工具 screenshot 后保存)。 - [ ] **Step 6: 门禁子集 + 提交** Run: `pnpm -s typecheck && pnpm -s lint && pnpm -s depcruise && pnpm vitest run apps/api/src/app/__tests__ activities/part-substitution-assessment` Expected: 全绿。 ```bash git add tools/build-activities.ts apps/api/src/app/seed-activities.ts activities/part-substitution-assessment/dist/manifest.json activities/part-substitution-assessment/dist/index.js git commit -m "feat(w05): 差异评估活动登记进构建与安装通道;装包时顺带注册纯工具 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh" ``` (`dist/` 是否入库以 E04 现状为准:`git ls-files activities/crm-relationship-coverage/dist` 有输出就一起 add,没有就不 add。) --- ### Task 7: 脚本 e2e 套件 `w05`(五案例) **Files:** - Modify: `tools/e2e-a2.mjs`(`SUITES` 加 `w05`;`buildEdits` 加 `stageEdits` / `fillWhere`;`expectGateBlocks` 认 `fillWhere`;`main()` 套件名单加 `w05`) **Interfaces:** - Consumes:Task 4 的卡标题约定(P-08 单 `数据工单 · 芯片主数据创建`、P-10 单 `数据工单 · 原厂技术资料创建`、执行单 `评估输入与资料补齐确认`)、决策卡 value(`create` / `abandon` / `approve` / `reject` / `custom:`)、样例序号 0–4。 - Produces:`node tools/e2e-a2.mjs w05` 5/5 通过;每案例 `released`、被拒 0。 - [ ] **Step 1: 扩 `buildEdits` —— 案例级按表的编辑(`stageEdits`)与按条件整行扫空(`fillWhere`)** 把 `function buildEdits(stage, drafts) {` 改为 `function buildEdits(stage, drafts, kase = {}) {`,并把里面 `for (const [i, edit] of (stage.edits ?? []).entries()) {` 改为 ```js // 案例级按表的编辑叠在套件级之上(同一套件不同案例要建的料号不同 —— 参数集不能是同一份) const edits = [...(stage.edits ?? []), ...((kase.stageEdits ?? {})[stage.address] ?? [])]; for (const [i, edit] of edits.entries()) { ``` 在 `if (stage.fillAllBlank) { … }` 之后加: ```js /* * 按条件整行扫空:只给某一档状态的行填(评估输入清单里只有 status = gap 的行要补充内容, * default / ok 的行本就不该填 —— fillAllBlank 会把它们也填上,那是替人说了他没说的话)。 */ if (stage.fillWhere) { const { column, equals, values } = stage.fillWhere; for (const r of rows) { if (String(r[column] ?? '') === equals && !taken.has(String(r[rowKey]))) { lines.push({ [rowKey]: String(r[rowKey]), ...values }); } } } ``` `runCase` 里 `const lines = buildEdits(stage, body.drafts ?? []);` 改为 `const lines = buildEdits(stage, body.drafts ?? [], kase);`。 `expectGateBlocks` 里 ```js const rows = draftRows(body.drafts ?? [], stage.address); const blank = rows.filter((r) => String(r[column] ?? '').trim() === '').length; ``` 改为 ```js const rows = draftRows(body.drafts ?? [], stage.address); // 有 fillWhere 的段只数该档的行:default / ok 行空着是本分,不是「闸没拦」 const scoped = stage.fillWhere ? rows.filter((r) => String(r[stage.fillWhere.column] ?? '') === stage.fillWhere.equals) : rows; const blank = scoped.filter((r) => String(r[column] ?? '').trim() === '').length; ``` - [ ] **Step 2: 加套件(放在 `i01: {…}` 之后、`};` 之前)** ```js /* * ACT-WORK-05 差异评估 —— 1 数据(条件,P-08 / P-10 各一张)+ 1 执行 + 2 验收(审核走决策卡循环、放行终局)。 * 库里只播了 LM2596S-5.0 / LM2596S-ADJ:XL2596S-5.0、CN-DCDC-32V、LM2596S-3.3 刻意不在, * 「候选缺库 / 原 PN 缺库 / 硬约束不推荐」三条路靠它们。e2e 关落真库,主数据不落正式表,案例可反复跑。 */ w05: { activityId: 'part-substitution-assessment', address: 'sub_eval.input_gap', blankCol: 'supplement', stages: [ { match: /数据工单 · 芯片主数据创建/, address: 'parts.profile', rowKey: 'internal_pn', blankCol: 'brand_name', expectGateBlock: true, edits: [], // 案例没点名的料号兜底(不该走到这里 —— 每个案例都在 stageEdits 里逐颗给了) fillAllBlank: { brand_name: '兜底品牌', specification_description: '兜底规格(案例没给参数集)', package: 'TO-263-5', lifecycle_status: 'active', normalized_parameter_set: '输入电压范围 | 4.5~40 | V | hard | 兜底', }, }, { match: /数据工单 · 原厂技术资料创建/, address: 'parts.document', rowKey: 'internal_pn', blankCol: 'document_title', expectGateBlock: true, edits: [], fillAllBlank: { doc_seq: '01', document_type: 'datasheet', document_title: 'Datasheet(e2e 兜底)', document_version: 'Rev A', document_ref: 'https://example.com/datasheet.pdf', citation_locator: 'p.1', document_status: 'valid', }, }, { match: /评估输入与资料补齐确认/, address: 'sub_eval.input_gap', rowKey: 'item_key', blankCol: 'supplement', expectGateBlock: true, edits: [ { item_key: 'application_scenario', supplement: '12V→5V、工业温域(-40~85℃)、输入上限 40V' }, { item_key: 'bom_position', supplement: 'U3 电源位' }, ], // 其余 gap 行(原厂未披露的参数)统一「留 UNKNOWN」—— 补不了也是合法答案 fillWhere: { column: 'status', equals: 'gap', values: { supplement: '原厂未披露,留 UNKNOWN 并挂验证任务' } }, }, ], cases: [ { sample: 0, name: '候选缺库 · XL2596S-5.0 派数据工单(P-08 + P-10)· 审核先点名驳回再通过', // 决策卡序:候选 ≥2 缺库先问 → create;审核第一张点名意见驳回;第二张通过 decideSeq: ['create', 'custom:使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件', 'approve'], expectDecision: true, expectPort: 'assessment-released', stageEdits: { 'parts.profile': [ { internal_pn: 'XL2596S-5.0', brand_name: 'XLSEMI', manufacturer_name: 'XLSEMI', specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz', package: 'TO-263-5', process_technology: 'BCD', lifecycle_status: 'active', datasheet_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf', datasheet_version: 'Rev C', application_scenarios: '工业电源;消费电源', normalized_parameter_set: [ '输入电压范围 | 4.5~40 | V | hard | 客户上限 40V', '输出电压 | 5.0 | V | hard | 固定版', '输出电流 | 3 | A | hard | 满载能力', '开关频率 | 150 | kHz | hard | 影响外围', '静态电流 | 4.2 | mA | soft | 待机功耗', '工作温度 | -40~+125 | ℃ | hard | 工业温域', '使能阈值 | 未披露 | V | hard | 原厂未给', ].join('\n'), }, ], 'parts.document': [ { internal_pn: 'XL2596S-5.0', doc_seq: '01', document_type: 'datasheet', brand_name: 'XLSEMI', document_title: 'XL2596S 150KHz 3A Buck DC/DC Converter', document_version: 'Rev C', document_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf', citation_locator: 'p.3 Electrical Characteristics', document_status: 'valid', }, ], }, // 使能阈值未披露 → UNKNOWN → 条件推荐(工具算的,不是模型说的) expectRevised: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'XL2596S-5.0', column: 'recommendation_level', contains: 'conditional' }, }, { sample: 1, name: '都在库 · 常路 · 可调版 → 备选 · 审核一次通过', decide: 'approve', expectPort: 'assessment-released', expectRevised: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'LM2596S-ADJ', column: 'recommendation_level', contains: 'alternate' }, }, { sample: 2, name: '原 PN 缺库 · 硬阻塞建档(不问人)· 审核通过', decide: 'approve', expectPort: 'assessment-released', stageEdits: { 'parts.profile': [ { internal_pn: 'LM2596S-3.3', brand_name: 'TI', manufacturer_name: 'Texas Instruments', specification_description: '降压型 DCDC 稳压器,3A,固定 3.3V,150kHz', package: 'TO-263-5', process_technology: 'BCD', lifecycle_status: 'nrnd', datasheet_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf', datasheet_version: 'Rev H (2023-09)', normalized_parameter_set: [ '输入电压范围 | 4.5~40 | V | hard | 客户上限 40V', '输出电压 | 3.3 | V | hard | 固定版', '输出电流 | 3 | A | hard | 满载能力', '开关频率 | 150 | kHz | hard | 影响外围', '静态电流 | 5 | mA | soft | 待机功耗', '工作温度 | -40~+85 | ℃ | hard | 工业温域', '使能阈值 | 1.4 | V | hard | ON/OFF 阈值', ].join('\n'), }, ], 'parts.document': [ { internal_pn: 'LM2596S-3.3', doc_seq: '01', document_type: 'datasheet', brand_name: 'TI', document_title: 'LM2596 SIMPLE SWITCHER Power Converter', document_version: 'Rev H (2023-09)', document_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf', citation_locator: '§6.5 Electrical Characteristics(3.3V 列)', document_status: 'valid', }, ], }, }, { sample: 3, name: '三候选 · 两颗缺库各问一次 · 国产 32V 硬约束不推荐', decideSeq: ['create', 'create', 'approve'], expectDecision: true, expectPort: 'assessment-released', stageEdits: { 'parts.profile': [ { internal_pn: 'XL2596S-5.0', brand_name: 'XLSEMI', specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz', package: 'TO-263-5', lifecycle_status: 'active', normalized_parameter_set: [ '输入电压范围 | 4.5~40 | V | hard | 客户上限 40V', '输出电压 | 5.0 | V | hard | 固定版', '输出电流 | 3 | A | hard | 满载能力', '开关频率 | 150 | kHz | hard | 影响外围', '静态电流 | 4.2 | mA | soft | 待机功耗', '工作温度 | -40~+125 | ℃ | hard | 工业温域', '使能阈值 | 未披露 | V | hard | 原厂未给', ].join('\n'), }, { internal_pn: 'CN-DCDC-32V', brand_name: '国产某厂', specification_description: '降压型 DCDC,3A,固定 5.0V,输入上限 32V', package: 'TO-263-5', lifecycle_status: 'active', normalized_parameter_set: [ '输入电压范围 | 4.5~32 | V | hard | 上限 32V', '输出电压 | 5.0 | V | hard | 固定版', '输出电流 | 3 | A | hard | 满载能力', '开关频率 | 150 | kHz | hard | 影响外围', '静态电流 | 6 | mA | soft | 待机功耗', '工作温度 | -40~+85 | ℃ | hard | 工业温域', '使能阈值 | 1.3 | V | hard | ON/OFF 阈值', ].join('\n'), }, ], 'parts.document': [ { internal_pn: 'XL2596S-5.0', doc_seq: '01', document_type: 'datasheet', document_title: 'XL2596S Datasheet', document_version: 'Rev C', document_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf', citation_locator: 'p.3', document_status: 'valid' }, { internal_pn: 'CN-DCDC-32V', doc_seq: '01', document_type: 'datasheet', document_title: 'CN-DCDC-32V Datasheet', document_version: 'V1.0', document_ref: 'https://example.com/cn-dcdc-32v.pdf', citation_locator: 'p.2', document_status: 'valid' }, ], }, expectRevised: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'CN-DCDC-32V', column: 'recommendation_level', contains: 'not_recommended' }, }, { sample: 4, name: '新客户 · 录入卡先建档 · 审核通过', entryValues: { name: '深圳恒星电源有限公司', industry: '工业电源', region: '广东' }, decide: 'approve', expectPort: 'assessment-released', }, ], }, ``` `main()` 里 `['a1', 'a2', 'a3', 'a4', 'a6', 'a7', 'e04', 'i01']` 改为 `['a1', 'a2', 'a3', 'a4', 'a6', 'a7', 'e04', 'i01', 'w05']`。 - [ ] **Step 3: 跑套件(先停稳定版,免得两台后端交替推同一单)** ```bash ./stable.sh stop pnpm --filter @app/api reset-runs node tools/e2e-a2.mjs w05 2>&1 | tee /tmp/w05-e2e-round1.log | tail -60 ``` Expected: `5/5 通过`;每案例一行 `→ released · 被拒 0 · N 拍 / Mk token · 决策卡 K 张 · 发口 [assessment-released]`,案例 0/3 的 `闸拦住空手交卷(400)—— 如设计` 至少各出现两次(P-08 / P-10 / 执行单),案例 4 有 `录入卡代答`。 **红了怎么办**(这是设计欠账不是模型的错,修清单 / 剧本 / 说明书,升 `packageVersion` 重装再跑;每一轮都记进 Task 8 的测试记录): - `熔断交人` / 被拒 > 0 → 看 `轨迹末 12 条`:拒回指向哪一步 — `写了等于没写` 多半是剧本让它重写已有的值;`要先…` 类是墙,照设计律「墙要配路」改;`查无此值`(refersTo)= 存在性核对没做对,看 S0/S0d 剧本; - `需要你判断下一步` 兜底卡 → 模型输出不成形两次,看 agent-runner 重问日志,多半是 S3 第三拍写盘太长(考虑把 ① 明细与 ③ 关系拆成两拍写); - `认不出这张执行卡属于哪一段` → 卡标题没按说明书骨架,改说明书③④⑤的 title 或 `stages[].match`; - `期望口 assessment-released 未在发射名单里` → 放行门某条 L2 没绿,看验收卡判据哪条红(测试台观测面 `cards` 里的判据表)。 - [ ] **Step 4: 再跑一遍确认稳定(两轮都 5/5 才算),然后清运行数据、提交** ```bash node tools/e2e-a2.mjs w05 2>&1 | tee /tmp/w05-e2e-round2.log | tail -20 pnpm --filter @app/api reset-runs git add tools/e2e-a2.mjs git commit -m "test(w05): e2e 套件 —— 五案例(候选缺库 / 常路 / 原 PN 硬阻塞 / 三候选含不推荐 / 新客户建档) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh" ``` (活动包若在本任务里改过版本,`git add activities/part-substitution-assessment` 一并提交。) --- ### Task 8: 浏览器全流程全分支走查 + 测试记录文档 **Files:** - Create: `docs/ACT-WORK-05-端到端测试记录.md`(每一轮追加一节;Task 6 的库内验收与 Task 7 的两轮脚本结果也补进来) - 可能 Modify: `activities/part-substitution-assessment/src/*`(走查逼出的修改,每改必升 `packageVersion` 并 `pnpm activities:install`) **Interfaces:** - Consumes:测试台 `http://localhost:5173/design#/design/testbed/act`(活动页签;「起单」钮;样例钮;「落真库」勾选框默认勾着;「日志」页签看逐拍轨迹)、工作台 `http://localhost:5173/`(左栏任务列表 → 中间工作区的件 → 右侧对话区卡;执行卡钮「提交」,验收卡「重做」/「确认通过」,录入卡「确认」,决策卡按钮 = 选项文案;件上「编辑 / 取消 / 保存 / + 加一行 / ×」)、Chrome 当前已登录会话。 - Produces:五轮走查记录、18 条分支的覆盖表、逼出的修改清单。 - [ ] **Step 1: 建记录文档 + 走查前置** ```bash cat > docs/ACT-WORK-05-端到端测试记录.md <<'EOF' # ACT-WORK-05 差异评估 · 端到端测试记录 > 活动包 `part-substitution-assessment`;实施计划 `docs/superpowers/plans/2026-08-26-act-work-05-part-substitution-assessment.md`。 > 每一轮一节:**日期 · 案例 · 走的路径 · 结果(终态 / 被拒 / 拍数 / token / 人环次数)· 观察到的问题 · 修改(文件 @ 版本)· 复测结论**。 > 脚本 e2e 与浏览器走查都记;「界面不撒谎」:没跑到的分支写「未覆盖」并说原因,不写「通过」。 ## 0. 库内验收(2026-08-26) (Task 6 Step 4 的 SQL 输出原样贴这里) ## 1. 脚本 e2e(`node tools/e2e-a2.mjs w05`) ### 第 1 轮 (贴 tail -60;每案例一行结论;红了写原因与修改) ### 第 2 轮(稳定性复跑) ## 2. 浏览器全流程全分支走查 ### 分支覆盖表 | 分支 | 触发条件 | 覆盖轮次 | 结果 | 备注 | |---|---|---|---|---| | 0a | 启动期:新客户无档案 | R5 | | 数据录入卡建档 | | 0b / 1d | 候选无 P-08 主数据 → 派数据工单 | R2 / R3 | | P-08 + P-10 各一张 | | 0c | 未传 datasheet → 人全填 | R2 / R3 / R4 | | R2b 另试「传了 datasheet → AI 预填」 | | 1a | 绑定参数有误 → 退回重启 | R1 | | 工单内四项不可改(件上没有编辑面) | | 1b | 输入存在缺口(有而不全) | R1 / R2 | | 应用场景 / BOM 位 / 未披露参数 | | 1c | 缺口补不了 → 留 UNKNOWN | R2 | | 补充内容写「留 UNKNOWN」 | | 1d(原 PN) | 原 PN 无数据 → 硬阻塞必须建 | R4 | | 不问人 | | 1d(放弃) | 候选 ≥2 缺库 → 放弃并记原因 | R3 | | | | 2a | 单位冲突 → 阻断回步骤 1 | — | 未覆盖 | 需要冲突单位的夹具料号;记 debt | | 2b | 证据版本冲突 → 人工复核 | — | 未覆盖 | 同上 | | 3a | 硬约束 MISMATCH → 不推荐 | R3 | | CN-DCDC-32V | | 4a | UNKNOWN 无去向 → 驳回补证 | R2 | | 审核卡点名驳回 | | 4b | 证据链不全 → 驳回 | R2 | | 同一张卡的第二种意见 | | 5a | GATE-03 放行 | R1–R5 | | | | 5b | GATE-03 驳回 → REWORK | R4 | | 验收卡「重做」 | | 6a | 至少一个推荐 / 备选 | R1 | | 备选 | | 6b | 全是条件推荐 | R2 | | XL 条件推荐 | | 6c | 全不推荐 | — | 未覆盖 | 需要一颗全硬约束失败的原 PN 组合;记 debt | | 6d | 客户不批准 | — | 范围外 | 独立审批流 | ### R1 · 都在库 · 常路(样例 1) ### R2 · 候选缺库 · 数据工单 · 审核驳回循环(样例 0) ### R2b · 传了 datasheet → AI 预填(样例 0 + 上传) ### R3 · 三候选 · 一颗放弃一颗新建 · 硬约束不推荐(样例 3) ### R4 · 原 PN 缺库 · 硬阻塞 · 放行驳回重做(样例 2) ### R5 · 新客户 · 录入卡建档(样例 4) ## 3. 结论与欠账 EOF ./stable.sh stop # 走查期间只留 dev(8787 / 5173),避免两台交替推同一单 pnpm --filter @app/api reset-runs ``` 用 claude-in-chrome:`ToolSearch "select:mcp__claude-in-chrome__tabs_context_mcp,mcp__claude-in-chrome__navigate,mcp__claude-in-chrome__computer,mcp__claude-in-chrome__read_page,mcp__claude-in-chrome__find,mcp__claude-in-chrome__form_input,mcp__claude-in-chrome__get_page_text,mcp__claude-in-chrome__tabs_create_mcp,mcp__claude-in-chrome__file_upload,mcp__claude-in-chrome__read_console_messages"` → `tabs_context_mcp` → 新开一页 `http://localhost:5173/`。**出现 Keycloak 登录页就停下来请用户在浏览器里登录一次**(凭证只有他有),登录后继续。每轮之间跑「轮间清场」: ```bash pnpm --filter @app/api reset-runs docker exec -i jcai-postgres psql -U jcai -d jcai_wos -c " delete from ledger.substitution__difference_item where substitution_relation_id like 'REL-%'; delete from ledger.substitution__relation where substitution_relation_id like 'REL-%'; delete from ledger.validation__record where validation_id like 'VAL-%'; delete from ledger.demand__request where request_id like 'REQ-%'; delete from ledger.parts__document where internal_pn in ('XL2596S-5.0','CN-DCDC-32V','LM2596S-3.3'); delete from ledger.parts__profile where internal_pn in ('XL2596S-5.0','CN-DCDC-32V','LM2596S-3.3'); delete from ledger.customer__profile where customer_code = 'CU-9001';" ``` (浏览器走查「落真库」默认勾着,放行后主数据真落正式表 —— 不清掉,下一轮「候选缺库」那条路就试不出来。) **每一轮记录的固定项**:起单时刻与 taskId(测试台起单后地址栏 / 观测面顶部);进度条刻度(应是 x/6,每个人环时条上有格);每张卡的文案是否人话、指导书是否显示;件上编辑 → 保存 → 闸 / 红带是否同源(空着必填按「提交」要被拦并点名行列);对话区 say 是否一轮一句;工作成果快照份数(执行单 / 验收单各一份 + 决策卡);终态与 `被拒 / 拍数 / token`(测试台「日志」页签或 `curl -s -H "authorization: Bearer $TOKEN" http://localhost:8787/api/gov/testbed/activity/ | jq '{terminal,used,rejects:[.trail[]|select(.kind=="action-rejected")|.title]}'`,token 用 e2e 的 `login()` 同款 client_credentials 取);落库验证 SQL(见各轮)。发现问题 → 改清单 / 剧本 / 模版 → 升 `packageVersion` → `pnpm activities:install` → 记录「修改」→ 复跑该轮直到 released 且被拒 0。 - [ ] **Step 2: R1 · 都在库 · 常路(样例 1)** 1. 测试台 → 活动页签 → 活动「ACT-WORK-05 · 差异评估」→ 样例「都在库·常路」→「起单」。记 taskId。 2. 工作台:左栏点开这一单。等 AI 跑 S0(对话区应出一句「已绑定…原 PN 与候选都在库,不建主数据」之类的话;进度条 1/6 → 2/6)。**分支 1a**:确认工作区「已绑定」区四项灰底只读、没有编辑钮。 3. 执行卡「评估输入与资料补齐确认」到达:看指导书(五条 steps)与依据;工作区件:料号核对表两行(原 PN 在库 / 候选 在库 徽标),评估输入清单里 `application_scenario` / `bom_position` 两行状态「待补*」、三行「默认·可改」、齐备行。**先按「提交」不填** → 应被闸拦,提示点名缺哪几行(记原文)。 4. 件上「编辑」→ 两行补充内容各填一句(`12V→5V、工业温域、40V 输入上限` / `U3 电源位`)→「保存」→ 红带消失、闸文案变;再「提交」→ 通过;对话区出现「人已交回」痕。 5. AI 跑 S2/S3(三拍:比对 → 定级 → 写盘)。工作区换成差异审核件:关系表 1 行(`LM2596S-ADJ` 推荐等级「备选」),明细表 ≥8 行(输出电压 CONDITIONAL,其余 MATCH)。**分支 6a**。 6. 审核决策卡到达 →「通过」。AI 跑 S5 → 工作区换成落账确认件(拟写 P-11 一行 analysis_status「FAE 确认」,拟回写 P-01 一行 current_stage `gate03`)→ 验收卡到达:判据表 L1/L2 全绿、L4 三条带 AI 裁断与理由。 7.「确认通过」→ 中间态「字已签下,正在落地」→ 落地结果屏(进了哪几张表各几行、转出 `assessment-released`)→ 终态 released。**分支 5a**。 8. 工作成果:定稿两份(执行单快照 / 验收单快照)+ 决策卡快照,翻份翻页正常。 9. 落库验证: ```bash docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select substitution_relation_id||' '||candidate_internal_pn||' '||recommendation_level||' '||analysis_status||' '||rule_version from ledger.substitution__relation" docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select count(*)||' 条明细;UNKNOWN '||sum(case when difference_status='unknown' then 1 else 0 end) from ledger.substitution__difference_item" docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select request_id||' '||request_status||' '||current_stage||' '||coalesce(risk_level,'')||' '||coalesce(output_ref,'') from ledger.demand__request" ``` Expected:一条关系 `alternate fae_confirmed R-2026.07`;P-01 一行 `responded gate03 … RPT-…`。 10. 记进 R1 节;有问题按固定流程修、升版、复跑。 - [ ] **Step 3: R2 · 候选缺库 · 数据工单 · 审核驳回循环(样例 0)** 轮间清场 → 起单样例「候选缺库·派数据工单」。 1. S0 后对话区应说「XL2596S-5.0 不在库」;进度条停在 1/6「启动与存在性检查」。决策卡「候选 XL2596S-5.0 在芯片主数据里查无记录…」→ 点「创建主数据(派数据工单)」。**分支 0b / 1d**。 2. 执行卡「数据工单 · 芯片主数据创建(P-08):XL2596S-5.0」到达;工作区 = P-08 件,一行骨架只有料号(**分支 0c 人全填**)。空手「提交」→ 闸拦并点名(记原文)。「编辑」→ 填品牌 / 规格 / 封装 / 生命周期(下拉 Active)/ 参数集(textarea 七行,使能阈值写「未披露」)/ Datasheet 引用与版本 →「保存」→ 红带收 →「+ 加一行」加一颗假料再「×」撤掉(增删行链)→「保存」→「提交」。 3. 执行卡「数据工单 · 原厂技术资料创建(P-10):XL2596S-5.0」到达;件上一行骨架(类型 Datasheet、状态待核实);填标题 / 版本 / 链接 / 证据定位、状态改「有效」→ 保存 → 提交。进度条进 2/6。 4. 执行卡「评估输入与资料补齐确认」:清单里多一行「XL2596S-5.0 使能阈值」待补(**分支 1b**);应用场景 / BOM 位填值,使能阈值那行补充内容写「原厂未披露,留 UNKNOWN 挂验证任务」(**分支 1c**)→ 保存 → 提交。 5. S2/S3 后审核件:两条关系(XL 条件推荐 / ADJ 备选),明细里「使能阈值 UNKNOWN」那行 UNKNOWN 去向写着 VAL 号。**分支 6b**。 6. 审核决策卡 → 在输入框写意见「使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件」提交(= custom 驳回,**分支 4a**)。看:对话区观察「意见按字面点名对上了…」,AI 改那一行(unknown_disposition / validation.record 的 test_condition),换问法再发一张卡;件上那一行确实变了。再驳回一次,意见「证据引用请指到 datasheet 的页码」(**分支 4b**),看 evidence_ref 变化;第三张卡「通过」。 7. 拟落账 → 验收卡 → 确认通过 → released。落库验证: ```bash docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select internal_pn||' '||brand_name||' '||lifecycle_status from ledger.parts__profile where internal_pn='XL2596S-5.0'" docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select validation_id||' '||validation_internal_pn||' '||validation_status||' '||left(next_validation_action,40) from ledger.validation__record" docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select candidate_internal_pn||' '||recommendation_level||' '||coalesce(verification_requirement,'') from ledger.substitution__relation order by 1" ``` Expected:XL 真落了 P-08;一条 VAL `not_started`;XL `conditional` 且验证要求含 VAL 号;ADJ `alternate`。 8. 记录:人环次数(决策 3 + 数据 2 + 执行 1 + 放行 1 = 7)、拍数、被拒。 - [ ] **Step 4: R2b · 传了 datasheet → AI 预填(样例 0 + 上传)** 轮间清场。先造一份文本 datasheet: ```bash cat > "$TMPDIR/XL2596S-5.0-datasheet.txt" <<'EOF' XL2596S-5.0 XLSEMI 150KHz 3A Buck DC/DC Converter Rev C Package TO-263-5, BCD process, Active Input voltage range 4.5V to 40V Output voltage 5.0V fixed, output current 3A Switching frequency 150kHz, quiescent current 4.2mA Operating temperature -40 to +125 C Enable threshold: not specified EOF ``` 测试台样例 0 → 「候选 datasheet」文件选择器上传这份 txt(claude-in-chrome `file_upload`)→ 起单。看 P-08 数据工单的件:品牌 / 规格 / 封装 / 生命周期 / 参数集**已预填**(参数集里使能阈值是「未披露」),卡上依据写「已按 datasheet 预填 N 项」。核对后直接提交(闸应判「无缺」放行)。其余同 R2,审核直接通过。记录预填了几格、有没有编造(**分支 0c 的另一半**)。 - [ ] **Step 5: R3 · 三候选 · 一颗放弃一颗新建 · 硬约束不推荐(样例 3)** 轮间清场 → 起单样例「三候选·含硬约束不推荐」。 1. 两张决策卡按数组顺序来:XL2596S-5.0 → 点「放弃这颗候选」(不写原因,看 `abandon_reason` 是否落了「负责人本轮放弃(未注明原因)」)(**分支 1d 放弃**);CN-DCDC-32V →「创建主数据」。 2. P-08 数据工单只有 CN 一行 → 填(输入电压范围 `4.5~32`)→ 提交;P-10 同。 3. 执行单 → 补两行 → 提交。审核件:两条关系(ADJ 备选、CN **不推荐**),CN 的「输入电压范围」行 MISMATCH 高风险、影响说明写着 32V < 40V(**分支 3a**);XL 不出现在关系表里、核对表里标「已放弃」。 4. 审核通过 → 放行 → released。落库:`select candidate_internal_pn||' '||recommendation_level from ledger.substitution__relation order by 1` → `CN-DCDC-32V not_recommended` + `LM2596S-ADJ alternate`;`select internal_pn||' '||exists_status from ledger.sub_eval__pn_check` 在放行前查(record 表放行后仍在,reset-runs 才清)→ XL `abandoned`。 - [ ] **Step 6: R4 · 原 PN 缺库 · 硬阻塞 · 放行驳回重做(样例 2)** 轮间清场 → 起单样例「原PN缺库·硬阻塞」。 1. S0 后**不出决策卡**,直接来 P-08 数据工单「LM2596S-3.3」(**分支 1d 原 PN**);填(输出电压 3.3)→ 提交;P-10 → 提交。 2. 执行单 → 审核通过 → 落账确认件 → 验收卡:**点「重做」**,意见写「P-01 的验证要求请把测试条件也写进去」(**分支 5b**)。看:对话区「验收被打回重做:…」,AI 改 `demand.request.verification_requirement`(件上那一格变了),再次提交 → 新验收卡 →「确认通过」→ released。 3. 若 AI 原样重提(基座闸「打回之后台账一个字没改」拒回)→ 这是剧本 T_release 的欠账,改剧本、升版、复跑。 4. 落库:`select original_internal_pn||' → '||candidate_internal_pn||' '||recommendation_level from ledger.substitution__relation`;`select internal_pn from ledger.parts__profile where internal_pn='LM2596S-3.3'`。 - [ ] **Step 7: R5 · 新客户 · 录入卡建档(样例 4)** 轮间清场 → 起单样例「新客户·先建档」。 1. S0 发数据录入卡「客户 CU-9001 不在客户档案里…」:名称预填「深圳恒星电源有限公司」,行业 / 区域空 → 填「工业电源 / 广东」→「确认」(**分支 0a**)。对话区痕「已写入客户档案」;AI 接着写 P-01(customer_code 引用墙这时应过)。 2. 其余同 R1 → released。落库:`select customer_code||' '||name||' '||coalesce(industry,'') from ledger.customer__profile where customer_code='CU-9001'` → 一行。 3. 轮间清场(把 CU-9001 删掉)。 - [ ] **Step 8: 收口记录** 每轮结束把「结果 / 问题 / 修改 / 复测」填进对应节;分支覆盖表逐行填「结果」(通过 / 未覆盖 + 原因);「3. 结论与欠账」写:五轮终态与被拒数汇总、逼出的修改列表(文件 @ 版本)、未覆盖分支(2a / 2b / 6c)与建议夹具、留给 Task 9 的 debt。走查中改过的活动包随本任务提交: ```bash git add activities/part-substitution-assessment docs/ACT-WORK-05-端到端测试记录.md git commit -m "test(w05): 浏览器全流程全分支走查五轮(R1–R5 + R2b)与测试记录;走查逼出的活动包修订 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh" ``` --- ### Task 9: 文档、欠账、全量门禁、收尾 **Files:** - Create: `docs/数据域字段说明-W05.md` - Modify: `CLAUDE.md`(现状快照加一条;文档权威地图加两行)· `docs/debt.md`(追加一节) - [ ] **Step 1: 写 `docs/数据域字段说明-W05.md`**(口径与 `docs/数据域字段说明-E04-I01.md` 同一套:一览表 + 每张表「重点字段(按重要程度)」+「数据结构」;★ = 重点,【人填】= 人在件上 / 录入卡填) ```markdown # 数据域字段说明 —— ACT-WORK-05 差异评估 > 对应设计中心 → 数据域「ACT-WORK-05 · 差异评估」分组。字段以活动清单的台账声明为准 > (`activities/part-substitution-assessment/src/manifest.ts` 的 `declarations.ledger`); > 主数据六张的**物理结构**由平台基线 `apps/api/src/app/seed-mvp-data.ts` 物化(《SS02-SS03-SS04_Demo台账字段细化与统一方案》§4.2 全列), > 活动只追加自己要用的列与标记。约束词表见 `docs/数据域字段说明-E04-I01.md`「怎么读这份文档」。 ### 一览 | 全景 | 台账地址 | 人读名 | 性质 | 本活动用到的列 | 人填列 | |---|---|---|---|---|---| | L-02 | `customer.profile` | 客户档案 | 主数据 · 共享 | 4 | 3(录入卡,仅新客户) | | P-08 | `parts.profile` | 芯片主数据 | 主数据 · 共享 | 12 | 12(数据工单件上) | | P-10 | `parts.document` | 原厂技术资料库 | 主数据 · 共享 | 9 | 9(数据工单件上) | | P-01 | `demand.request` | 客户需求主记录 | 主数据 · 共享 | 17 | 0(应用场景 / BOM 位经执行单由 AI 抄入) | | P-11 | `substitution.relation` | 替代关系主记录 | 主数据 · 共享 | 17 | 0(只审不改) | | P-11 子 | `substitution.difference_item` | 差异明细 | 主数据 · 共享 | 15 | 0 | | P-06 | `validation.record` | 芯片验证记录 | 主数据 · 共享 | 8 | 0(只写验证要求) | | — | `sub_eval.pn_check` | 料号存在性核对 | 本活动工作台账 | 6 | 0(决策卡定放弃) | | — | `sub_eval.input_gap` | 评估输入清单 | 本活动工作台账 | 8 | 3(补充内容 / 加行) | | — | `sub_eval.verdict` | 本次评估结论 | 本活动工作台账 | 14 | 0 | ## 一、`parts.profile` 芯片主数据(P-08) 一张表:料号一行,标准化参数集是它的一个字段(一行一参数 `参数名 | 值 | 单位 | hard/soft | 说明`)。 库里查无记录 → 数据工单,人在件上从零建整条(可加行);确认落草稿层,放行并入正式表。 **重点字段(按重要程度)** 1. `internal_pn` ★【人填】主键 —— 全链路只认它,不可自由文本 2. `normalized_parameter_set` ★【人填】必填 —— 差异评估逐项比对的唯一输入;「未披露」进 UNKNOWN 3. `lifecycle_status` ★【人填】必填 · 取值域 `active/nrnd/eol/discontinued/risk` 4. `package` ★【人填】必填 —— 硬约束 5. `specification_description` ★【人填】必填 6. `brand_name` ★【人填】必填 7. `datasheet_ref` / `datasheet_version` ★【人填】建议填(成对)—— 证据回链 **数据结构** | 列名 | 人读名 | 类型 | 约束 | 写入者 | 说明 | |---|---|---|---|---|---| | `internal_pn` ★ | 内部料号 | text | 主键 | 人(件上)/ 种子 | | | `brand_name` ★ | 品牌名称 | text | 必填 | 人(件上)/ AI 预填 | | | `manufacturer_name` | 制造商名称 | text | 建议填 | 人(件上) | 与品牌不一致时分开记 | | `specification_description` ★ | 规格描述 | text | 必填 | 人(件上)/ AI 预填 | | | `normalized_parameter_set` ★ | 标准化参数集 | text | 必填 | 人(件上)/ AI 预填 | 一行一参数 | | `package` ★ | 封装 | text | 必填 | 人(件上)/ AI 预填 | 硬约束 | | `process_technology` | 工艺 | text | 建议填 | 人(件上) | 硬约束 | | `lifecycle_status` ★ | 生命周期状态 | text | 必填 · 取值域 | 人(件上,下拉) | | | `eol_pcn_status` | EOL/PCN 状态 | text | 建议填 | 人(件上) | | | `datasheet_ref` ★ | Datasheet 引用 | text | 建议填 | 人(件上)/ AI 预填 | | | `datasheet_version` ★ | Datasheet 版本 | text | 建议填 | 人(件上)/ AI 预填 | 有引用就该有版本 | | `application_scenarios` | 适配应用场景 | text | 建议填 | 人(件上) | | ``` (其余九张表照这个格式写全:列名逐字照清单抄,「写入者」按剧本 —— S0 写 P-01 / pn_check / verdict;S0d 人建 P-08 / P-10;S1 人补 input_gap、AI 抄两列进 P-01;S2/S3 写 relation / difference_item / validation;S5 写 relation.analysis_status 与 P-01 五项。`customer_approval_status` 一栏写「审批流维护,本活动无写权」。) - [ ] **Step 2: CLAUDE.md** 「现状快照」末尾加一条(照既有条目的写法:事实 + 账): ```markdown - **ACT-WORK-05 差异评估补齐到验收线**(2026-08-26,`part-substitution-assessment@<最终版本>`):本仓第三个真活动包, 也是第一个把 SS02 主数据(P-08 / P-10 / P-01 / P-11 主+子 / P-06)建进库的活动 —— 六张表进平台基线 `MASTER_OBJECTS`(四域 → 八域),活动只追加列与标记;P-08 一张表(参数集是一个 text 字段,一行一参数)。 **数据工单** = 零输入执行工单 + 件上可加行(一张表一张单:P-08 / P-10),确认落草稿、放行并入(守铁律②)。 四态与推荐等级由两枚纯工具算(`compare-parameters` / `recommend`,规则 R-2026.07 同时以 DMN 随包冻结); 审核走决策卡循环,放行终局。**e2e 五案例 5/5 released、被拒 0**;浏览器五轮(R1–R5 + R2b)走查见 `docs/ACT-WORK-05-端到端测试记录.md`(18 条分支覆盖表:2a / 2b / 6c 未覆盖,记 debt)。 <走查逼出的修改各一句> ``` 「文档权威地图」表里加两行:`docs/数据域字段说明-W05.md`(ACT-WORK-05 十张表字段口径)与 `docs/ACT-WORK-05-端到端测试记录.md`(逐轮测试与修改结论)。 - [ ] **Step 3: `docs/debt.md` 追加一节** ```markdown ## 2026-08-26 · ACT-WORK-05 差异评估落地时留下的欠账 - **数据工单还没有自己的卡皮**:对话区里它是一张标题带「数据工单 ·」的执行卡;文档要的「数据卡」 (`workOrder:'data'`)要走契约四步 + 前端卡皮,MVP 先不做。 - **datasheet 只能在起单时上传**:平台文件入口只有起单 `format:'file'`;文档说「在数据工单里上传」, 件上 / 卡上的上传控件是新基座能力。 - **PW 与 FAE 一人兼**:平台一任务恰一名负责人,执行单与验收单都发给 owner;派给第二个人是新能力。 - **参数集是 text 列的行语法**,不是《统一方案》字面上的 Structure / 子表:件上用 textarea 一行一参数; 要按行编辑就得物理拆 `parts.parameter`(用户定「一张表」,先不拆)。 - **`market.part`(老料号占位表,主键 `pn`)与 `parts.profile`(P-08,主键 `internal_pn`)重叠**: 前者空且无人读写,待并或退役。 - **分支 2a 单位冲突 / 2b 证据版本冲突 / 6c 全不推荐 未覆盖**:需要冲突单位与冲突版本的夹具料号、 以及一颗全硬约束失败的组合;补夹具后加 e2e 案例。 - **存在性检查靠模型读整表 + refersTo 墙**:没有「这颗料在不在库」的基座读数;料号表大了整表进上下文会撑爆 截断阈值,届时要补一枚 `parts.exists` 读工具或基座读数。 - **`customer_approval_status` 不在活动声明里**,所以件上那一格是静态文案「未提交」而不是取值 —— 审批流做起来后改成 data-slot。 ``` - [ ] **Step 4: 全量门禁(退出码)** ```bash pnpm gates; echo "gates exit=$?" ``` Expected: `gates exit=0`。红了逐个修(常见:lint 的 §12.5 禁止项、depcruise 活动包越界 import、api:check —— 本计划没改 `packages/contracts`,api-report 不该变;若变了说明改了契约,补 `pnpm api:check --update && git add api-report`)。 - [ ] **Step 5: 提交、收尾** ```bash git add docs/数据域字段说明-W05.md docs/debt.md CLAUDE.md git commit -m "docs(w05): 数据域字段说明、现状快照、欠账登记 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh" git log --oneline mvp-foundation..HEAD ./stable.sh start # 走查完把稳定版起回来(要它带上新活动就 ./stable.sh update) ``` 合并与推送按用户的规矩由用户在终端做(主支发起合并再推主支;本会话无 TTY 推不了 Gitea)—— 收尾时把分支名 `feat/act-work-05` 与提交列表报给用户。 --- ## 自检清单(写完计划后过一遍) - [x] 规格覆盖:启动 4 项 / 双通道无感知(渠道落 `origin`)/ 存在性检查 / 数据工单 P-08 + P-10 一表一单 / 候选 ≥2 放弃决策 / 原 PN 硬阻塞 / 执行单 01 缺口补齐与 UNKNOWN 合法放行 / 步骤 2–3 纯 AI 不派单 / 四态 + 风险 + 证据 + 置信度 / 推荐四档 / 验收 01 四判据(决策卡循环、意见写 P-06)/ 验收 02 四判据(终局放行)/ P-11 主 + 子落账 / P-01 回写五项 / analysis_status 与 customer_approval_status 分离 / 规则版本锁定 / 多候选并行各一条 P-11 / 台账建进库(基线 + 清单)/ e2e 五案例 / 浏览器五轮 + 分支覆盖表 / 测试记录文档 —— 各有任务落点。 - [x] 未覆盖且已明示:2a / 2b / 6c(夹具缺)、6d(范围外)、数据卡卡皮、件上上传、双负责人。 - [x] 名字一致性:活动 id `part-substitution-assessment`;域 `sub_eval` / `parts` / `demand` / `substitution` / `validation`;视图名五个;计划项八个;工具两个;出口 `assessment-released`;读数名与 tally 值在 Task 3/4/5/7 之间逐字相同(`exists_status: found/absent/created/abandoned`,`status: gap/default/ok`,四态 `match/mismatch/unknown/conditional`,推荐 `recommended/conditional/alternate/not_recommended`)。 - [x] 每个代码步骤给了完整代码;每个验证步骤给了命令与期望输出;没有「TBD / 后补 / 类似 Task N」。