活动包 views/target-style.ts / report.ts 与两份 W05 文档都指着 docs/SS02-ACT-WORK-05-html —— 模版「逐字节搬自目标件」的那份目标件此前没入库。docs/img/cy 三张 png 现无引用,随批入库。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
249 KiB
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();模版含<script即拒装。 - 安装恒新增:改了包内容必升
identity.packageVersion,同版重装静默 no-op。 - 机检(L1/L2、哨兵、doneWhen)只读基座读数(
域_对象_rows / _missing / _<列>_<值> / _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 <noreply@anthropic.com>与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 "<sql>")。
台账怎么物化(不写迁移,声明即建表):
| 声明 | 物理落点 |
|---|---|
{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 用):
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.tsapps/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 开功能分支(工作树里已有的未提交改动跟着走,不碰它们)
cd /Users/xcs/project2/tmp/BPOS-MVP
git checkout -b feat/act-work-05
git status --short | wc -l # 记下这个数:本计划的提交只 add 自己点名的文件
- Step 2: 确认库与两套服务在跑
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 凭证在环境里(值不打印)
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_DOMAINS8 域;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 把 '只有客户 / 项目 / 供应商 / 市场四个域' 那条改成:
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之后、];之前)追加六张表
/* ── 料件: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 改为:
/** 八域的呈现次序 —— 谁在买 → 为什么买 → 从哪买 → 买什么 → 料件 → 需求 → 替代 → 验证 */
export const MASTER_DOMAINS = ['customer', 'project', 'supplier', 'market', 'parts', 'demand', 'substitution', 'validation'] as const;
- Step 4: 在
MASTER_SAMPLE_ROWS数组末尾追加样例料号(现有最后一个元素之后、];之前)
/*
* ── 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] 仍是样例客户,不改也不炸;但下一个人再往前插就错)——改为:
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: 类型检查 + 提交
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 <noreply@anthropic.com>
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:
/**
* 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<string, unknown>) => {
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<string, unknown>;
};
type Item = { parameterName: string; compareResult: string; differenceStatus: string; riskLevel: string };
type Result = { candidateInternalPn: string; items: Item[]; counts: Record<string, number> };
const pair = (parameterName: string, originalValue: string, candidateValue: string, constraintType: 'hard' | 'soft', extra: Record<string, string> = {}) => ({
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之前加实现
/* ── 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<string, number> = { 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<CompareCounts> }>) : [];
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 数组末尾登记:
['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)
/**
* 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: 全绿。
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 <noreply@anthropic.com>
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★ tallyactive/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★ tallydatasheet/app_note/reference_design/white_paper/sample_doc/commitment_letter/other;document_title★ document_version★ document_ref★ document_status★ tallyvalid/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★ tallypending/responded/won/abandoned/closed;application_scenario / bom_position expect;risk_level tallylow/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★ tallypin_to_pin/functional/parametric/other;recommendation_level★ tallyrecommended/conditional/alternate/not_recommended;recommendation_reason★ difference_summary★;risk_level★ tallylow/medium/high/critical;verification_requirement requiredWhen {recommendation_level:'conditional'};validation_id;analysis_status★ tallyai_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★ tallyhard/soft;compare_result expect;difference_status★ tallymatch/mismatch/unknown/conditional;impact_statement★;unknown_disposition requiredWhen {difference_status:'unknown'};risk_level★ tallylow/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★ tallypin_to_pin/functional/reliability/certification/retest;validation_status★ tallynot_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★ tallyoriginal/candidate;exists_status★ tallyfound/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★ tallygap/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_bindS0d_master_dataS1_input_confirmS2_normalizeS3_assessS4_reviewS5_prepare_landingT_release。 -
Step 1: 建包骨架
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
/**
* 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
/**
* 活动包清单 —— 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 占位:
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 占位:
export const P08_MASTER_CREATE = '<div class="proto-root"></div>';
export const P10_DOCUMENT_CREATE = '<div class="proto-root"></div>';
export const EVAL_INPUT_LIST = '<div class="proto-root"></div>';
export const DIFFERENCE_REVIEW = '<div class="proto-root"></div>';
export const LANDING_CONFIRM = '<div class="proto-root"></div>';
- Step 5: 写结构自测
__tests__/activity.test.ts(Task 4/5 落地前,案例模型与模版相关的用例会红 —— 这是预期,Task 5 收尾时全绿)
/**
* 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<string, { type?: string; format?: string }>;
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(/<humanTask id="([^"]+)"/g)].map((m) => 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(`<humanTask id="${id}"[\\s\\S]*?</humanTask>`).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('<div><script>x()</script></div>')).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: 提交骨架
git add activities/part-substitution-assessment
git commit -m "feat(w05): 差异评估活动包骨架与清单 —— 六张主数据追加声明、三张自持单据、两张 DMN、五条样例
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;关系 idREL-<数字串>-01/02…(按候选数组顺序);明细 id<关系id>-P01/P02…;验证 idVAL-<数字串>-01/02…;报告号RPT-<数字串>。 -
卡标题约定(e2e 按标题对号):数据工单 P-08 以「数据工单 · 芯片主数据创建」开头;P-10 以「数据工单 · 原厂技术资料创建」开头;执行单以「评估输入与资料补齐确认」开头。
-
Step 1: 写
src/case-model.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 = `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL">
<case id="part-substitution-assessment">
<!--
案卷项的 id 就是任务视图里的键名。入参顶层字段(customerCode / customerName / requestDescription /
originalInternalPn / candidateInternalPns / datasheets)不进这份 caseFileModel —— 谓词里只用
count(candidateInternalPns)。其余全是基座读数(行数 / 缺必填 / 按值计数 / 工具调用数)与
sub_eval.verdict 里的三面记账旗(每面旗都配着一半基座数)。
-->
<caseFileModel>
<caseFileItem id="demand_request_rows" name="本单写下的需求主记录行数(基座现数)"/>
<caseFileItem id="demand_request_missing" name="需求主记录缺必填的行数(基座现数)"/>
<caseFileItem id="sub_eval_pn_check_rows" name="存在性核对行数(基座现数)"/>
<caseFileItem id="sub_eval_pn_check_missing" name="存在性核对缺必填的行数(基座现数)"/>
<caseFileItem id="sub_eval_pn_check_exists_status_absent" name="仍不在库的料号数(基座现数)"/>
<caseFileItem id="sub_eval_pn_check_exists_status_abandoned" name="已放弃的候选数(基座现数)"/>
<caseFileItem id="sub_eval_verdict_rows" name="结论行数(基座现数)"/>
<caseFileItem id="parts_profile_missing" name="本单建的芯片主数据缺必填的行数(基座现数)"/>
<caseFileItem id="parts_document_missing" name="本单建的资料记录缺必填的行数(基座现数)"/>
<caseFileItem id="input_confirmed_total" name="评估输入已确认(人环凭据)"/>
<caseFileItem id="sub_eval_input_gap_rows" name="评估输入清单行数(基座现数)"/>
<caseFileItem id="sub_eval_input_gap_missing" name="评估输入清单缺必填的行数(基座现数)"/>
<caseFileItem id="substitution_relation_rows" name="替代关系行数(基座现数)"/>
<caseFileItem id="substitution_relation_missing" name="替代关系缺必填的行数(基座现数)"/>
<caseFileItem id="substitution_relation_analysis_status_fae_confirmed" name="分析状态已 FAE 确认的关系数(基座现数)"/>
<caseFileItem id="substitution_difference_item_rows" name="差异明细行数(基座现数)"/>
<caseFileItem id="substitution_difference_item_missing" name="差异明细缺必填的行数(基座现数)"/>
<caseFileItem id="substitution_difference_item_distinct_substitution_relation_id" name="有明细的关系数(基座现数不同值)"/>
<caseFileItem id="substitution_difference_item_difference_status_match" name="MATCH 条数(基座现数)"/>
<caseFileItem id="substitution_difference_item_difference_status_mismatch" name="MISMATCH 条数(基座现数)"/>
<caseFileItem id="substitution_difference_item_difference_status_unknown" name="UNKNOWN 条数(基座现数)"/>
<caseFileItem id="substitution_difference_item_difference_status_conditional" name="CONDITIONAL 条数(基座现数)"/>
<caseFileItem id="tool_part_substitution_assessment_compare_parameters_calls" name="比对工具成功调用次数(基座现数)"/>
<caseFileItem id="tool_part_substitution_assessment_recommend_calls" name="定级工具成功调用次数(基座现数)"/>
<caseFileItem id="review_approved_total" name="差异结论已审核通过(仅通过时置一)"/>
<caseFileItem id="landing_prepared_total" name="拟落账已备好(S5 写完时置一)"/>
</caseFileModel>
<casePlanModel id="plan" name="差异评估" autoComplete="true">
<!-- ① 已绑定:需求主记录与存在性核对都写下了 —— 数据工单那一项的入场前提 -->
<sentry id="s_bound">
<onPart id="op_b" caseFileItemRef="sub_eval_pn_check_rows"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_b"><condition>demand_request_rows > 0 and sub_eval_pn_check_rows > 0</condition></ifPart>
</sentry>
<!--
② 料号全部落定:原 PN + 每个候选各一行核对,没有一颗还「不在库」,放弃的少于候选数,
本单建的主数据 / 资料不缺必填 —— 评估输入确认那一步的入场前提。全是基座现数。
-->
<sentry id="s_pns_resolved">
<onPart id="op_pr" caseFileItemRef="sub_eval_pn_check_exists_status_absent"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_pr"><condition>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</condition></ifPart>
</sentry>
<!-- ③ 评估输入已确认(旗 + 清单有行且必填齐)—— 标准化那一步的入场前提 -->
<sentry id="s_input_confirmed">
<onPart id="op_ic" caseFileItemRef="input_confirmed_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_ic"><condition>input_confirmed_total > 0 and sub_eval_input_gap_rows > 0 and sub_eval_input_gap_missing = 0</condition></ifPart>
</sentry>
<!-- ④ 对照表已铺好:每个未放弃的候选一条关系、每条关系都有明细 —— 逐项评估的入场前提 -->
<sentry id="s_normalized">
<onPart id="op_n" caseFileItemRef="substitution_difference_item_rows"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_n"><condition>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</condition></ifPart>
</sentry>
<!-- ⑤ 逐项评估已完成:明细与关系必填齐、四态之和 = 明细行数、两枚工具都真调过 —— 审核的入场前提 -->
<sentry id="s_assessed">
<onPart id="op_a" caseFileItemRef="substitution_difference_item_missing"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_a"><condition>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</condition></ifPart>
</sentry>
<!-- ⑥ 审核已通过 —— 拟落账的入场前提 -->
<sentry id="s_reviewed">
<onPart id="op_r" caseFileItemRef="review_approved_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_r"><condition>review_approved_total > 0 and substitution_difference_item_missing = 0</condition></ifPart>
</sentry>
<!-- ⑦ 拟落账已备好:每条关系都到 FAE 确认、P-01 回写五项齐 —— 放行门的入场前提 -->
<sentry id="s_landing_ready">
<onPart id="op_l" caseFileItemRef="landing_prepared_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_l"><condition>landing_prepared_total > 0 and substitution_relation_analysis_status_fae_confirmed = substitution_relation_rows and substitution_relation_missing = 0 and demand_request_missing = 0</condition></ifPart>
</sentry>
<!-- 启动期:铸案号、建需求 V1、锁规则版本、逐个料号查库(能算的不问人) -->
<humanTask id="S0_bind" name="建立评估上下文:绑定四项、新建需求、存在性核对"/>
<!-- 0.5 等待数据:PN 不在库 → 数据工单(P-08 / P-10 各一张);触达人,不给 doneWhen;库里都有就 disable -->
<humanTask id="S0d_master_data" name="数据工单:补建芯片主数据与原厂资料">
<entryCriterion id="ec_s0d" sentryRef="s_bound"/>
<itemControl><manualActivationRule id="m_s0d"/></itemControl>
</humanTask>
<!-- 1 建立评估上下文:交 PW 在件上补缺口(执行工单 01)—— 不给 doneWhen -->
<humanTask id="S1_input_confirm" name="评估输入与资料补齐确认">
<entryCriterion id="ec_s1" sentryRef="s_pns_resolved"/>
<itemControl><manualActivationRule id="m_s1"/></itemControl>
</humanTask>
<!-- 2 读取并标准化资料(纯 AI) -->
<humanTask id="S2_normalize" name="标准化参数对照表:P-11 主记录骨架 + 差异明细对照行">
<entryCriterion id="ec_s2" sentryRef="s_input_confirmed"/>
</humanTask>
<!-- 3 逐项差异评估(AI + 两枚工具) -->
<humanTask id="S3_assess" name="逐项差异评估:四态、风险、证据、推荐等级、验证任务">
<entryCriterion id="ec_s3" sentryRef="s_normalized"/>
</humanTask>
<!-- 4 FAE/RI 审核:决策卡循环,通过或驳回 + AI 改台账重发 —— 不给 doneWhen -->
<humanTask id="S4_review" name="差异评估结论审核:通过或驳回(驳回则 AI 修订重提交)">
<entryCriterion id="ec_s4" sentryRef="s_assessed"/>
<itemControl><manualActivationRule id="m_s4"/></itemControl>
</humanTask>
<!-- 5(前半)拟落账:分析状态推到 FAE 确认、回写 P-01(不触达世界,给 doneWhen) -->
<humanTask id="S5_prepare_landing" name="拟落账:P-11 分析状态 → FAE 确认,回写 P-01">
<entryCriterion id="ec_s5" sentryRef="s_reviewed"/>
</humanTask>
<!-- 5(后半)提交 ACT-GATE-03:终局唯放行,必须人工激活 -->
<humanTask id="T_release" name="评审放行与落账确认(ACT-GATE-03)">
<entryCriterion id="ec_rel" sentryRef="s_landing_ready"/>
<itemControl><manualActivationRule id="m_rl"/></itemControl>
</humanTask>
</casePlanModel>
</case>
</definitions>`;
/**
* 案例说明书 —— **只写整单一以贯之的事**(逐步做法归剧本)。
* 卡骨架住在这里而不是剧本里(模型会压拍,压过之后只有说明书还在 —— 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":"客户 <customerCode> 不在客户档案里,请核对建档信息后确认",
"ledger":"customer.profile","key":{"customer_code":"<customerCode>"},
"fields":[
{"key":"name","label":"客户名称","value":"<customerName,没有就整格不写 value>","required":true},
{"key":"industry","label":"所属行业"},
{"key":"region","label":"区域"}
]}
人确认那一下基座已把这一行落进草稿层,**你不许再 write 一遍 customer.profile**。
② 候选不在库、候选 ≥2 时,一颗一张决策卡(按候选数组顺序,一次只发一张;候选只有一颗时不问、直接建):
{"kind":"decide","itemId":"S0d_master_data",
"ask":"候选 <internal_pn> 在芯片主数据里查无记录。是从零建它的主数据(派数据工单),还是本轮放弃这颗候选?",
"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> → <N> 个候选",
"ask":"启动绑定的四项已锁定。请在工作区的件上补齐「待补 / 待确认」行的补充内容,默认值可改可确认。",
"steps":["应用场景、BOM 位是启动时未收的:必须补(写在「补充内容」列)",
"原厂未披露的参数(如使能阈值)补不到就写「挂验证任务」或「留 UNKNOWN」—— 补不了也是合法答案,不拦流程",
"替代类型 / 规则版本 / 负责人期限三行是系统默认,可改可不改",
"加一行 = 补充 AI 没列出的资料项;删一行 = 该项不适用;已绑定的四项不可改",
"改完保存,回这张卡提交"],
"standard":"每一条「待补」行的补充内容都有;补不了的写明理由。",
"basis":[{"k":"已绑定","v":"客户 <customerCode> · 需求 <request_id> V1 · 原 PN <原 PN> · 候选 <N> 颗"},
{"k":"缺口","v":"<N> 项待补 / 待确认"}]}}
⑥ 验收工单 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":"差异评估结论已算出:<N> 个候选、共 <M> 条参数比对(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
/**
* 步骤剧本与呈现面 —— 这段错了不报错,只是活动开始转圈 / 界面开始撒谎(与 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<string, unknown>): boolean {
try {
return evaluateCondition(expression, scope);
} catch {
return false;
}
}
/** 开局局面:入参 + 所有基座读数归零 */
const fresh = (input: Record<string, unknown>): Record<string, unknown> => ({
...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<string, unknown>)), `样例「${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: 提交
git add activities/part-substitution-assessment
git commit -m "feat(w05): 差异评估案例模型 —— CMMN 八项七哨兵、说明书五卡骨架、八条剧本、五段进度
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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`(数据驱动的开关全在那里:三档占位 / 缺项红带 / 编辑闸 / 增删行 / 状态徽基座);然后按下面写其余内容:
/**
* 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 =>
`<style>${PRD_CSS}${BIND_CSS}${EXTRA_CSS}</style>
<div class="proto-root"><div class="app-shell"><div class="jc-stage">
<article class="jc-report tone-${input.tone}">
<header class="jc-rp-head">
<div class="jc-rp-badge">${input.badge}</div>
<div class="jc-rp-tit">
<em>${input.kicker}</em>
<h3>${input.title}</h3>
<div class="jc-rp-sub">收件人:<span data-slot="case:owner"></span> · 关联任务:<span class="jc-rp-taskid" data-slot="case:taskId" data-blank></span></div>
</div>
<div class="jc-rp-side">
<span class="jc-rp-tag">${input.tag}</span>
<span class="jc-rp-step">${input.step}</span>
${input.state}
</div>
</header>
<div class="jc-rp-bd">${input.body}</div>
<footer class="jc-rp-foot">${input.foot}</footer>
</article>
</div></div></div>`;
const part = (label: string, aside = ''): string => `<p class="jc-rp-part"><b>【${label}】</b>${aside === '' ? '' : ` —— ${aside}`}</p>`;
const criteria = (title: string, items: readonly string[]): string =>
`<div class="jc-criteria"><div class="jc-criteria-t">${title}</div><ul>${items.map((i) => `<li>${i}</li>`).join('')}</ul></div>`;
const flow = (text: string): string => `<div class="jc-flow">${text}</div>`;
/** 页脚右侧「工单类型:× · <案号>」 */
const sign = (note: string, tag: string): string => `<span>${note}</span><span>${tag} · <span data-slot="eval_ref" data-blank></span></span>`;
const editGate = (address: string, label: string): string => `<div class="jc-editgate" data-edit-for="${address}">
<span class="jc-editgate-t">${label}</span>
<button type="button" class="jc-egbtn open" data-edit-open="${address}">编辑</button>
<button type="button" class="jc-egbtn ghost" data-edit-cancel="${address}">取消</button>
<button type="button" class="jc-egbtn save" data-edit-save="${address}">保存</button>
</div>`;
const gapbar = (object: string, what: string): string => `<div class="jc-gapbar" data-class-of="${object}_missing">
<span><b>还差 <span data-slot="${object}_missing"></span> 行${what}没填</b> —— <span data-slot="${object}_missing_keys"></span>。按「编辑」开表,红框那几格就是要填的。补齐了才确认得了。</span>
</div>`;
const tag = (scope: string, col: string): string => `<span class="jc-tag ${scope}" data-class-of="${col}"></span>`;
const cell = (col: string, cls = ''): string => `<span class="jc-vcell${cls === '' ? '' : ` ${cls}`}" data-slot="${col}" data-col="${col}" data-blank></span>`;
const ro = (col: string, cls = ''): string => `<span class="jc-vcell${cls === '' ? '' : ` ${cls}`}" data-slot="${col}" data-blank></span>`;
const select = (col: string, options: ReadonlyArray<readonly [string, string]>, required = false): string =>
`<select class="jc-cell" data-col="${col}"${required ? ' required' : ''}><option value=""></option>${options.map(([v, l]) => `<option value="${v}">${l}</option>`).join('')}</select>`;
/* ==================== 局面 0 · 数据工单 P-08 ==================== */
const P08_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th class="req" style="min-width:140px">内部料号</th>
<th class="req" style="min-width:100px">品牌</th>
<th class="sug" style="min-width:120px">制造商</th>
<th class="req" style="min-width:220px">规格描述</th>
<th class="req" style="min-width:110px">封装</th>
<th class="sug" style="min-width:90px">工艺</th>
<th class="req" style="min-width:120px">生命周期</th>
<th class="sug" style="min-width:150px">EOL / PCN</th>
<th class="sug" style="min-width:200px">Datasheet 引用</th>
<th class="sug" style="min-width:110px">Datasheet 版本</th>
<th class="sug" style="min-width:140px">适配场景</th>
<th class="req" style="min-width:320px">标准化参数集(一行一参数:参数名 | 值 | 单位 | hard/soft | 说明)</th>
</tr></thead>
<tbody data-rows="mine:parts.profile" data-edit="parts.profile"><tr>
<td class="jc-rn"><button type="button" class="jc-rowdel" data-edit-del="parts.profile" title="删一行 = 该记录不建">×</button></td>
<td class="req" data-h="内部料号">${cell('internal_pn', 'mono')}</td>
<td class="req" data-h="品牌">${cell('brand_name')}</td>
<td class="sug" data-h="制造商">${cell('manufacturer_name')}</td>
<td class="req" data-h="规格描述">${cell('specification_description')}</td>
<td class="req" data-h="封装">${cell('package', 'mono')}</td>
<td class="sug" data-h="工艺">${cell('process_technology')}</td>
<td class="req" data-h="生命周期">${select('lifecycle_status', [['active', 'Active'], ['nrnd', 'NRND'], ['eol', 'EOL'], ['discontinued', '停产'], ['risk', '风险']], true)}${tag('t-life', 'lifecycle_status')}</td>
<td class="sug" data-h="EOL / PCN">${cell('eol_pcn_status')}</td>
<td class="sug" data-h="Datasheet 引用">${cell('datasheet_ref', 'mono')}</td>
<td class="sug" data-h="Datasheet 版本">${cell('datasheet_version')}</td>
<td class="sug" data-h="适配场景">${cell('application_scenarios')}</td>
<td class="req" data-h="标准化参数集"><span class="jc-vcell jc-pre" data-slot="normalized_parameter_set" data-blank></span><textarea class="jc-cell" data-col="normalized_parameter_set" placeholder="输入电压范围 | 4.5~40 | V | hard | 客户上限 40V 输出电压 | 5.0 | V | hard | 固定版 使能阈值 | 未披露 | V | hard | 原厂未给"></textarea></td>
</tr></tbody>
</table></div>`;
export const P08_MASTER_CREATE = doc({
tone: 'human',
badge: '数据',
tag: '数据工单',
kicker: 'SS02 · ACT-WORK-05 差异评估 · 数据工单(P-08)',
title: '芯片主数据创建',
step: '步骤 0.5 · 等待数据',
state: `<span class="jc-rp-wait warn">待建 <b data-slot="parts_profile_missing"></b> / <b data-slot="parts_profile_rows"></b> 行</span>`,
body:
part('做什么', '批量创建 P-08 主数据记录') +
`<div class="jc-derive">
<p><span class="need">模板定位</span>:本单是「数据工单」的实例 —— <b>一单一类(可多条)</b>:目标台账 = <b>芯片主数据(P-08)</b>;
<b>字段即表头、每行一条新记录</b>;标准化参数集是这条记录的一个字段(一行一参数)。</p>
<p><span class="need">触发原因</span>:系统按内部料号查芯片主数据<b>查无记录</b> —— 是「完全没有」,不是字段缺失(那归执行工单)。</p>
<p><span class="done">AI 预填</span>:上传了 datasheet 的料号 AI 已按资料预填,标着值的格子请核对;没上传的料号全部手填,同为合法路径。</p>
<p><span class="need">纪律</span>:必填列全齐才允许确认;确认后这批记录进本单草稿,放行时并入正式库并留痕(确认人 = 数据责任人);只建主数据,验证记录等事实类不在此建。</p>
</div>` +
part('创建什么', '主数据记录(每行一条,*为必填列)') +
gapbar('parts_profile', '必填') +
editGate('parts.profile', `<b>待建 <span data-slot="parts_profile_rows"></span> 条记录</b>,点「编辑」核对预填、补齐空缺;「+ 加一行」= 再建一条`) +
`<div class="jc-editbar"><span>必填列(*)全齐才允许确认;原厂未披露的参数值请直接写「未披露」,它会进入 UNKNOWN 并挂验证任务;删一行 = 该记录不建。改动只落任务草稿层,<b>保存不等于放行</b>。</span></div>` +
P08_TABLE +
`<div class="jc-addrow">
<button type="button" class="jc-egbtn ghost" data-edit-add="parts.profile">+ 加一行(再建一条记录)</button>
<em>新行的内部料号必须写(它是这一行的钥匙);其余必填列全部填写后才允许确认。</em>
</div>` +
flow('<b>提交 / 确认:</b>本工单只承载内容;确认请在对话区的【执行卡】中进行。确认后活动自动从「等待数据」唤醒。'),
foot: sign('SS02 · ACT-WORK-05 差异评估 · 数据工单 · 芯片主数据创建(P-08)', '工单类型:数据'),
});
/* ==================== 局面 0′ · 数据工单 P-10 ==================== */
const P10_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th class="req" style="min-width:140px">关联料号</th>
<th class="req" style="width:70px;min-width:70px">序号</th>
<th class="req" style="min-width:120px">资料类型</th>
<th class="sug" style="min-width:90px">品牌</th>
<th class="req" style="min-width:220px">资料标题</th>
<th class="req" style="min-width:110px">版本</th>
<th class="req" style="min-width:220px">文件链接</th>
<th class="sug" style="min-width:180px">证据定位</th>
<th class="req" style="min-width:100px">状态</th>
</tr></thead>
<tbody data-rows="mine:parts.document" data-edit="parts.document"><tr>
<td class="jc-rn"><button type="button" class="jc-rowdel" data-edit-del="parts.document" title="删一行 = 该资料不建">×</button></td>
<td class="req" data-h="关联料号">${cell('internal_pn', 'mono')}</td>
<td class="req" data-h="序号">${cell('doc_seq', 'mono')}</td>
<td class="req" data-h="资料类型">${select('document_type', [['datasheet', 'Datasheet'], ['app_note', '应用笔记'], ['reference_design', '参考设计'], ['white_paper', '白皮书'], ['sample_doc', '样片资料'], ['commitment_letter', '承诺函'], ['other', '其他']], true)}${tag('t-doc', 'document_type')}</td>
<td class="sug" data-h="品牌">${cell('brand_name')}</td>
<td class="req" data-h="资料标题">${cell('document_title')}</td>
<td class="req" data-h="版本">${cell('document_version')}</td>
<td class="req" data-h="文件链接">${cell('document_ref', 'mono')}</td>
<td class="sug" data-h="证据定位">${cell('citation_locator')}</td>
<td class="req" data-h="状态">${select('document_status', [['valid', '有效'], ['expired', '过期'], ['pending', '待核实'], ['void', '作废']], true)}${tag('t-docst', 'document_status')}</td>
</tr></tbody>
</table></div>`;
export const P10_DOCUMENT_CREATE = doc({
tone: 'human',
badge: '数据',
tag: '数据工单',
kicker: 'SS02 · ACT-WORK-05 差异评估 · 数据工单(P-10)',
title: '原厂技术资料创建',
step: '步骤 0.5 · 等待数据',
state: `<span class="jc-rp-wait warn">待建 <b data-slot="parts_document_missing"></b> / <b data-slot="parts_document_rows"></b> 行</span>`,
body:
part('做什么', '为本单新建的料号登记原厂资料(P-10)') +
`<div class="jc-derive">
<p><span class="need">触发原因</span>:按内部料号查原厂技术资料库<b>查无记录</b>。一份资料一行;同一料号多份资料序号 01/02… 自己续。</p>
<p><span class="need">为什么要它</span>:差异明细的证据引用要指到具体资料的版本与页码 —— 没有这一行,结论就追溯不到来源。</p>
</div>` +
part('创建什么', '资料记录(每行一条,*为必填列)') +
gapbar('parts_document', '必填') +
editGate('parts.document', `<b>待建 <span data-slot="parts_document_rows"></span> 条资料记录</b>,点「编辑」补齐`) +
`<div class="jc-editbar"><span>资料类型、标题、版本、文件链接、状态为必填;证据定位写页码或章节。改动只落任务草稿层,<b>保存不等于放行</b>。</span></div>` +
P10_TABLE +
`<div class="jc-addrow">
<button type="button" class="jc-egbtn ghost" data-edit-add="parts.document">+ 加一行(再登记一份资料)</button>
<em>关联料号与序号是这一行的钥匙,新行都要写;必填列全齐才允许确认。</em>
</div>` +
flow('<b>提交 / 确认:</b>本工单只承载内容;确认请在对话区的【执行卡】中进行。'),
foot: sign('SS02 · ACT-WORK-05 差异评估 · 数据工单 · 原厂技术资料创建(P-10)', '工单类型:数据'),
});
/* ==================== 局面 1 · 评估输入清单(S0/S1)==================== */
const PN_CHECK_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th style="min-width:160px">料号</th>
<th style="width:90px;min-width:90px">角色</th>
<th style="width:110px;min-width:110px">在库状态</th>
<th style="min-width:200px">备注 / 放弃原因</th>
</tr></thead>
<tbody data-rows="mine:${D}.pn_check"><tr>
<td class="jc-rn"></td>
<td data-h="料号">${ro('internal_pn', 'mono')}</td>
<td data-h="角色">${tag('t-role', 'role')}</td>
<td data-h="在库状态">${tag('t-exist', 'exists_status')}</td>
<td class="dimempty" data-h="备注"><span class="jc-vcell" data-slot="note" data-blank></span> <span class="jc-vcell" data-slot="abandon_reason" data-blank></span></td>
</tr></tbody>
</table></div>`;
const GAP_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th style="width:120px;min-width:120px">键</th>
<th class="req" style="min-width:170px">输入项</th>
<th class="sug" style="width:80px;min-width:80px">来源</th>
<th style="width:100px;min-width:100px">状态</th>
<th class="sug" style="min-width:220px">绑定值 / 缺口说明</th>
<th class="req" style="min-width:260px">补充内容</th>
</tr></thead>
<tbody data-rows="mine:${D}.input_gap" data-edit="${D}.input_gap"><tr>
<td class="jc-rn"><button type="button" class="jc-rowdel" data-edit-del="${D}.input_gap" title="删一行 = 该输入项不适用">×</button></td>
<td data-h="键">${cell('item_key', 'mono')}</td>
<td class="req" data-h="输入项">${cell('input_item')}</td>
<td class="sug" data-h="来源">${cell('source_ledger', 'mono')}</td>
<td data-h="状态">${select('status', [['gap', '待补'], ['default', '默认·可改'], ['ok', '齐备']])}${tag('t-gap', 'status')}</td>
<td class="sug" data-h="绑定值 / 缺口说明">${cell('detail')}</td>
<td class="req" data-h="补充内容"><span class="jc-vcell" data-slot="supplement" data-blank></span><textarea class="jc-cell" data-col="supplement" placeholder="资料链接 / 任务编号 / 具体值;补不了写「留 UNKNOWN」或「挂验证任务」"></textarea></td>
</tr></tbody>
</table></div>`;
export const EVAL_INPUT_LIST = doc({
tone: 'human',
badge: '执行',
tag: '执行工单',
kicker: 'SS02 · ACT-WORK-05 差异评估 · 执行工单 1/1',
title: '评估输入与资料补齐确认',
step: '步骤 1/5 · 建立评估上下文',
state: `<span class="jc-rp-wait warn">待补 <b data-slot="${D}_input_gap_missing"></b> 项</span>`,
body:
part('做什么', '启动 4 项已绑定,其余信息在工单里补齐') +
`<div class="jc-derive">
<p><span class="done">AI 已完成</span>:系统已按启动传入的 <b>4 项</b>完成绑定 —— 客户、客户需求(新建 P-01 · 版本 V1)、原 PN、候选 PN(数组,逐个评估)—— 并生成输入快照。</p>
<p><span class="need">需要你补齐</span>:启动时未收的信息(应用场景、BOM 位)<b>直接在下表补</b>;默认值(替代类型 / 规则版本 / 负责人期限)可确认可改。本单缺口只覆盖「<b>有而不全</b>」;「完全没有」(料号不在库)已由数据工单创建。未补齐的缺口项将按 <b>UNKNOWN</b> 进入差异评估。</p>
</div>` +
part('已绑定', '启动时冻结,工单内不可改 —— 绑错请退回启动人重新启动') +
`<div class="jc-bound">
<b>客户</b><span data-slot="customerCode" data-blank></span>
<b>客户需求</b><span><span class="mono" data-slot="request_id" data-blank></span> · V1 · <span data-slot="requestDescription" data-blank></span></span>
<b>原内部料号</b><span class="mono" data-slot="originalInternalPn" data-blank></span>
<b>规则版本</b><span class="mono" data-slot="rule_version" data-blank></span>
</div>` +
PN_CHECK_TABLE +
part('交回什么', '评估输入清单(已绑定项只读核对,缺口项补齐,*为必填)') +
gapbar(`${D}_input_gap`, '补充内容') +
editGate(`${D}.input_gap`, `<b>还有 <span data-slot="${D}_input_gap_missing"></span> 项待补 / 待确认</b>,点「编辑」在「补充内容」列填入`) +
`<div class="jc-editbar"><span>加一行 = 补充 AI 未列出的资料项(新行的「输入项」与「补充内容」必填);删一行 = 该输入项不适用;补不了的写「留 UNKNOWN」或「挂验证任务」—— 那也是一个答案。改动只落任务草稿层,<b>保存不等于放行</b>。</span></div>` +
GAP_TABLE +
`<div class="jc-addrow">
<button type="button" class="jc-egbtn ghost" data-edit-add="${D}.input_gap">+ 加一行(补充资料项)</button>
<em>键写一个英文短名(如 thermal_note);「绑定值 / 缺口说明」栏写清这项资料是什么、AI 为什么拿不到。</em>
</div>` +
flow('<b>审批 / 提交:</b>本工单只承载内容;「提交」请在对话区的【执行卡】中进行。<b>提交后去向:</b>进入步骤 2 · AI 读取与标准化,仍缺失的项将按 UNKNOWN 进入差异评估。'),
foot: sign('SS02 · ACT-WORK-05 差异评估 · 执行工单 1/1 · 评估输入与资料补齐确认', '工单类型:执行'),
});
/* ==================== 局面 2 · 差异评估结论审核(S2/S3/S4,只读)==================== */
const RELATION_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th style="min-width:150px">候选料号</th>
<th style="width:100px;min-width:100px">替代类型</th>
<th style="width:100px;min-width:100px">推荐等级</th>
<th style="min-width:260px">推荐理由</th>
<th style="min-width:200px">差异摘要</th>
<th style="width:70px;min-width:70px">风险</th>
<th style="min-width:180px">验证要求</th>
<th style="width:80px;min-width:80px">置信度</th>
</tr></thead>
<tbody data-rows="mine:substitution.relation"><tr>
<td class="jc-rn"></td>
<td data-h="候选料号">${ro('candidate_internal_pn', 'mono')}</td>
<td data-h="替代类型">${tag('t-sub', 'substitution_type')}</td>
<td data-h="推荐等级">${tag('t-rec', 'recommendation_level')}</td>
<td class="dimempty" data-h="推荐理由">${ro('recommendation_reason')}</td>
<td class="dimempty" data-h="差异摘要">${ro('difference_summary')}</td>
<td data-h="风险">${tag('t-risk', 'risk_level')}</td>
<td class="dimempty" data-h="验证要求">${ro('verification_requirement')}</td>
<td class="dimempty" data-h="置信度">${ro('confidence_score', 'mono')}</td>
</tr></tbody>
</table></div>`;
const ITEM_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th style="min-width:130px">关系</th>
<th style="min-width:110px">参数项</th>
<th style="min-width:90px">原 PN 值</th>
<th style="min-width:90px">候选 PN 值</th>
<th style="width:60px;min-width:60px">单位</th>
<th style="width:80px;min-width:80px">约束类型</th>
<th style="width:110px;min-width:110px">结论</th>
<th style="width:60px;min-width:60px">风险</th>
<th style="min-width:160px">证据引用</th>
<th style="min-width:200px">影响说明</th>
<th style="min-width:160px">UNKNOWN 去向</th>
<th style="width:70px;min-width:70px">置信度</th>
</tr></thead>
<tbody data-rows="mine:substitution.difference_item"><tr>
<td class="jc-rn"></td>
<td data-h="关系">${ro('substitution_relation_id', 'mono')}</td>
<td data-h="参数项"><b class="cust" data-slot="parameter_name" data-blank></b></td>
<td data-h="原 PN 值">${ro('original_value', 'mono')}</td>
<td data-h="候选 PN 值">${ro('candidate_value', 'mono')}</td>
<td class="dimempty" data-h="单位">${ro('parameter_unit')}</td>
<td data-h="约束类型">${tag('t-con', 'constraint_type')}</td>
<td data-h="结论">${tag('t-diff', 'difference_status')}</td>
<td data-h="风险">${tag('t-risk', 'risk_level')}</td>
<td class="dimempty" data-h="证据引用">${ro('evidence_ref', 'mono')}</td>
<td class="dimempty" data-h="影响说明">${ro('impact_statement')}</td>
<td class="dimempty" data-h="UNKNOWN 去向">${ro('unknown_disposition')}</td>
<td class="dimempty" data-h="置信度">${ro('confidence', 'mono')}</td>
</tr></tbody>
</table></div>`;
export const DIFFERENCE_REVIEW = doc({
tone: 'ledger',
badge: '验收',
tag: '验收工单',
kicker: 'SS02 · ACT-WORK-05 差异评估 · 验收工单 1/2',
title: '差异评估结论审核',
step: '步骤 3~4/5 · 逐项差异评估 + FAE/RI 审核',
state: `<span class="jc-rp-wait">共 <b data-slot="substitution_relation_rows"></b> 个候选 · <b data-slot="substitution_difference_item_rows"></b> 条比对</span>`,
body:
part('AI 结论说明', '结论是怎么算出来的') +
`<div class="jc-derive">
<p><b>AI 已完成的计算</b>:按 GOV 评估规则(锁定版本 <b class="mono" data-slot="rule_version" data-blank></b>),对原 PN <b class="mono" data-slot="original_internal_pn" data-blank></b>
与每个候选逐项比对,四态结论 —— MATCH <b data-slot="substitution_difference_item_difference_status_match"></b> /
MISMATCH <b data-slot="substitution_difference_item_difference_status_mismatch"></b> /
UNKNOWN <b data-slot="substitution_difference_item_difference_status_unknown"></b> /
CONDITIONAL <b data-slot="substitution_difference_item_difference_status_conditional"></b>,并给出风险等级、证据引用与置信度。
差异明细 = <b>P-11 差异明细子表</b>(一条参数一条记录);当前为 AI 草稿,<b>审核通过前不落账</b>。本单数据工单创建的主数据,其参数证据标注「人工录入」。</p>
</div>` +
part('验收判据', '你据此判断「通过 / 驳回」') +
criteria('判断这份差异评估结论是否可信,请逐项核对', [
'<b>关键参数差异均有明确状态</b>:逐项结论无漏项,每个参数都有四态之一的判定;',
'<b>UNKNOWN 项均有去向</b>:每个 UNKNOWN 都挂了验证任务或补来源动作,不允许悬空;',
'<b>证据链完整</b>:每项结论可追溯至 P-08 / P-10 / P-06 的具体记录,或本单数据工单的创建记录(人工录入);',
'<b>硬约束项未被自动判 MATCH</b>:封装 / 工艺等硬约束未经证据支持不得判 MATCH。',
]) +
part('各候选推荐等级', '只读展示 · 每个候选一条 P-11 主记录草稿') +
RELATION_TABLE +
part('AI 产出的差异明细', '只读展示 · P-11 差异明细子表草稿') +
ITEM_TABLE +
flow('<b>审批:</b>本工单只承载内容;「通过 / 驳回」请在对话区的【验收卡】中进行。审核意见与补充条件写入 P-06(验证要求);<b>通过后去向:</b>进入步骤 5 · 评审放行与落账确认。'),
foot: sign('SS02 · ACT-WORK-05 差异评估 · 验收工单 1/2 · 差异评估结论审核', '工单类型:验收'),
});
/* ==================== 局面 3 · 评审放行与落账确认(S5/T_release,只读)==================== */
const P11_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th style="min-width:150px">替代关系ID</th>
<th style="width:100px;min-width:100px">替代类型</th>
<th style="min-width:130px">原内部料号</th>
<th style="min-width:130px">候选内部料号</th>
<th style="width:100px;min-width:100px">推荐等级</th>
<th style="min-width:220px">推荐理由</th>
<th style="min-width:180px">差异摘要</th>
<th style="min-width:120px">替代分析报告</th>
<th style="width:100px;min-width:100px">分析状态</th>
<th style="width:120px;min-width:120px">客户批准状态</th>
<th style="width:70px;min-width:70px">风险</th>
<th style="min-width:180px">验证要求</th>
<th style="width:80px;min-width:80px">证据可信度</th>
<th style="width:100px;min-width:100px">规则版本</th>
</tr></thead>
<tbody data-rows="mine:substitution.relation"><tr>
<td class="jc-rn"></td>
<td data-h="替代关系ID">${ro('substitution_relation_id', 'mono')}</td>
<td data-h="替代类型">${tag('t-sub', 'substitution_type')}</td>
<td data-h="原内部料号">${ro('original_internal_pn', 'mono')}</td>
<td data-h="候选内部料号">${ro('candidate_internal_pn', 'mono')}</td>
<td data-h="推荐等级">${tag('t-rec', 'recommendation_level')}</td>
<td class="dimempty" data-h="推荐理由">${ro('recommendation_reason')}</td>
<td class="dimempty" data-h="差异摘要">${ro('difference_summary')}</td>
<td class="dimempty" data-h="替代分析报告">${ro('analysis_report_ref', 'mono')}</td>
<td data-h="分析状态">${tag('t-anal', 'analysis_status')}</td>
<td data-h="客户批准状态"><span class="jc-vcell">未提交 <i class="jc-sub">由审批流程维护,本活动不产生</i></span></td>
<td data-h="风险">${tag('t-risk', 'risk_level')}</td>
<td class="dimempty" data-h="验证要求">${ro('verification_requirement')}</td>
<td class="dimempty" data-h="证据可信度">${ro('confidence_score', 'mono')}</td>
<td data-h="规则版本">${ro('rule_version', 'mono')}</td>
</tr></tbody>
</table></div>`;
const P01_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th style="min-width:150px">需求编号</th>
<th style="width:80px;min-width:80px">风险等级</th>
<th style="min-width:220px">验证要求</th>
<th style="min-width:130px">输出链接</th>
<th style="min-width:180px">下一步动作</th>
<th style="width:100px;min-width:100px">当前阶段</th>
<th style="width:100px;min-width:100px">需求状态</th>
</tr></thead>
<tbody data-rows="mine:demand.request"><tr>
<td class="jc-rn"></td>
<td data-h="需求编号">${ro('request_id', 'mono')}</td>
<td data-h="风险等级">${tag('t-risk', 'risk_level')}</td>
<td class="dimempty" data-h="验证要求">${ro('verification_requirement')}</td>
<td class="dimempty" data-h="输出链接">${ro('output_ref', 'mono')}</td>
<td class="dimempty" data-h="下一步动作">${ro('next_action')}</td>
<td class="dimempty" data-h="当前阶段">${ro('current_stage', 'mono')}</td>
<td class="dimempty" data-h="需求状态">${ro('request_status', 'mono')}</td>
</tr></tbody>
</table></div>`;
export const LANDING_CONFIRM = doc({
tone: 'ledger',
badge: '验收',
tag: '验收工单',
kicker: 'SS02 · ACT-WORK-05 差异评估 · 验收工单 2/2',
title: '评审放行与落账确认',
step: '步骤 5/5 · 提交 ACT-GATE-03',
state: `<span class="jc-rp-wait">拟落账 <b data-slot="substitution_relation_rows"></b> 条 P-11</span>`,
body:
part('AI 结论说明', '本单确认什么') +
`<div class="jc-derive">
<p><b>本活动以 ACT-GATE-03 评审放行为终点。</b>本单确认拟落账内容:评估结论层写 <b>P-11 替代关系主记录</b>(每个候选一条)与差异明细子表,需求状态回写 <b>P-01</b>(风险等级 / 验证要求 / 输出链接 / 下一步动作 / 当前阶段)。
<b>客户批准不属于本活动</b> —— 客户批准状态由审批流程维护,本单只写分析层(分析状态 = FAE 确认)。</p>
</div>` +
part('验收判据', '你据此判断「通过 / 驳回」') +
criteria('放行与落账前,请逐项核对', [
'<b>推荐等级与差异明细一致</b>:存在 UNKNOWN / CONDITIONAL 的候选,推荐等级不得给「推荐」;',
'<b>分析状态与客户批准状态分离</b>:本单只写分析层(FAE 确认),客户批准状态保持「未提交」;',
'<b>GOV 规则版本已锁定</b>:规则版本随结论一并落账;',
'<b>回写 P-01 字段完整</b>:风险等级、验证要求、输出链接、下一步动作、当前阶段五项齐全。',
]) +
part('拟写 P-11 · 替代关系主记录', '只读展示') +
P11_TABLE +
part('拟回写 P-01 · 需求状态', '只读展示') +
P01_TABLE +
flow('<b>审批:</b>本工单只承载内容;「通过 / 驳回」请在对话区的【验收卡】中进行。<b>通过后去向:</b>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 与包内文件)。
git add activities/part-substitution-assessment
git commit -m "feat(w05): 五张报告模版 —— 两张数据工单件、评估输入清单、差异审核件、落账确认件(目标件平移)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 区加:
import { manifest as partSubstitutionAssessment } from '../activities/part-substitution-assessment/src/manifest.ts';
PACKAGES 数组末尾加:
{ dir: 'part-substitution-assessment', manifest: partSubstitutionAssessment },
- Step 2: 登记到安装通道并顺带注册纯工具
apps/api/src/app/seed-activities.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(); 之前插入:
/*
* **纯工具随活动一起注册**(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: 构建 + 装包(这一步同时物化六张主数据表、三张自持表,并播样例料号)
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: 库内验收(表、主键、性质、注释、样例行)
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_meta9 行: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: 全绿。
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 <noreply@anthropic.com>
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 w055/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()) { 改为
// 案例级按表的编辑叠在套件级之上(同一套件不同案例要建的料号不同 —— 参数集不能是同一份)
const edits = [...(stage.edits ?? []), ...((kase.stageEdits ?? {})[stage.address] ?? [])];
for (const [i, edit] of edits.entries()) {
在 if (stage.fillAllBlank) { … } 之后加:
/*
* 按条件整行扫空:只给某一档状态的行填(评估输入清单里只有 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 里
const rows = draftRows(body.drafts ?? [], stage.address);
const blank = rows.filter((r) => String(r[column] ?? '').trim() === '').length;
改为
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: {…}之后、};之前)
/*
* 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: 跑套件(先停稳定版,免得两台后端交替推同一单)
./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 才算),然后清运行数据、提交
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 <noreply@anthropic.com>
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: 建记录文档 + 走查前置
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 登录页就停下来请用户在浏览器里登录一次(凭证只有他有),登录后继续。每轮之间跑「轮间清场」:
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/<taskId> | 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)
- 测试台 → 活动页签 → 活动「ACT-WORK-05 · 差异评估」→ 样例「都在库·常路」→「起单」。记 taskId。
- 工作台:左栏点开这一单。等 AI 跑 S0(对话区应出一句「已绑定…原 PN 与候选都在库,不建主数据」之类的话;进度条 1/6 → 2/6)。分支 1a:确认工作区「已绑定」区四项灰底只读、没有编辑钮。
- 执行卡「评估输入与资料补齐确认」到达:看指导书(五条 steps)与依据;工作区件:料号核对表两行(原 PN 在库 / 候选 在库 徽标),评估输入清单里
application_scenario/bom_position两行状态「待补*」、三行「默认·可改」、齐备行。先按「提交」不填 → 应被闸拦,提示点名缺哪几行(记原文)。 - 件上「编辑」→ 两行补充内容各填一句(
12V→5V、工业温域、40V 输入上限/U3 电源位)→「保存」→ 红带消失、闸文案变;再「提交」→ 通过;对话区出现「人已交回」痕。 - AI 跑 S2/S3(三拍:比对 → 定级 → 写盘)。工作区换成差异审核件:关系表 1 行(
LM2596S-ADJ推荐等级「备选」),明细表 ≥8 行(输出电压 CONDITIONAL,其余 MATCH)。分支 6a。 - 审核决策卡到达 →「通过」。AI 跑 S5 → 工作区换成落账确认件(拟写 P-11 一行 analysis_status「FAE 确认」,拟回写 P-01 一行 current_stage
gate03)→ 验收卡到达:判据表 L1/L2 全绿、L4 三条带 AI 裁断与理由。 7.「确认通过」→ 中间态「字已签下,正在落地」→ 落地结果屏(进了哪几张表各几行、转出assessment-released)→ 终态 released。分支 5a。 - 工作成果:定稿两份(执行单快照 / 验收单快照)+ 决策卡快照,翻份翻页正常。
- 落库验证:
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)
轮间清场 → 起单样例「候选缺库·派数据工单」。
- S0 后对话区应说「XL2596S-5.0 不在库」;进度条停在 1/6「启动与存在性检查」。决策卡「候选 XL2596S-5.0 在芯片主数据里查无记录…」→ 点「创建主数据(派数据工单)」。分支 0b / 1d。
- 执行卡「数据工单 · 芯片主数据创建(P-08):XL2596S-5.0」到达;工作区 = P-08 件,一行骨架只有料号(分支 0c 人全填)。空手「提交」→ 闸拦并点名(记原文)。「编辑」→ 填品牌 / 规格 / 封装 / 生命周期(下拉 Active)/ 参数集(textarea 七行,使能阈值写「未披露」)/ Datasheet 引用与版本 →「保存」→ 红带收 →「+ 加一行」加一颗假料再「×」撤掉(增删行链)→「保存」→「提交」。
- 执行卡「数据工单 · 原厂技术资料创建(P-10):XL2596S-5.0」到达;件上一行骨架(类型 Datasheet、状态待核实);填标题 / 版本 / 链接 / 证据定位、状态改「有效」→ 保存 → 提交。进度条进 2/6。
- 执行卡「评估输入与资料补齐确认」:清单里多一行「XL2596S-5.0 使能阈值」待补(分支 1b);应用场景 / BOM 位填值,使能阈值那行补充内容写「原厂未披露,留 UNKNOWN 挂验证任务」(分支 1c)→ 保存 → 提交。
- S2/S3 后审核件:两条关系(XL 条件推荐 / ADJ 备选),明细里「使能阈值 UNKNOWN」那行 UNKNOWN 去向写着 VAL 号。分支 6b。
- 审核决策卡 → 在输入框写意见「使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件」提交(= custom 驳回,分支 4a)。看:对话区观察「意见按字面点名对上了…」,AI 改那一行(unknown_disposition / validation.record 的 test_condition),换问法再发一张卡;件上那一行确实变了。再驳回一次,意见「证据引用请指到 datasheet 的页码」(分支 4b),看 evidence_ref 变化;第三张卡「通过」。
- 拟落账 → 验收卡 → 确认通过 → released。落库验证:
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:
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)
轮间清场 → 起单样例「三候选·含硬约束不推荐」。
- 两张决策卡按数组顺序来:XL2596S-5.0 → 点「放弃这颗候选」(不写原因,看
abandon_reason是否落了「负责人本轮放弃(未注明原因)」)(分支 1d 放弃);CN-DCDC-32V →「创建主数据」。 - P-08 数据工单只有 CN 一行 → 填(输入电压范围
4.5~32)→ 提交;P-10 同。 - 执行单 → 补两行 → 提交。审核件:两条关系(ADJ 备选、CN 不推荐),CN 的「输入电压范围」行 MISMATCH 高风险、影响说明写着 32V < 40V(分支 3a);XL 不出现在关系表里、核对表里标「已放弃」。
- 审核通过 → 放行 → 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 才清)→ XLabandoned。
- Step 6: R4 · 原 PN 缺库 · 硬阻塞 · 放行驳回重做(样例 2)
轮间清场 → 起单样例「原PN缺库·硬阻塞」。
- S0 后不出决策卡,直接来 P-08 数据工单「LM2596S-3.3」(分支 1d 原 PN);填(输出电压 3.3)→ 提交;P-10 → 提交。
- 执行单 → 审核通过 → 落账确认件 → 验收卡:点「重做」,意见写「P-01 的验证要求请把测试条件也写进去」(分支 5b)。看:对话区「验收被打回重做:…」,AI 改
demand.request.verification_requirement(件上那一格变了),再次提交 → 新验收卡 →「确认通过」→ released。 - 若 AI 原样重提(基座闸「打回之后台账一个字没改」拒回)→ 这是剧本 T_release 的欠账,改剧本、升版、复跑。
- 落库:
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)
轮间清场 → 起单样例「新客户·先建档」。
- S0 发数据录入卡「客户 CU-9001 不在客户档案里…」:名称预填「深圳恒星电源有限公司」,行业 / 区域空 → 填「工业电源 / 广东」→「确认」(分支 0a)。对话区痕「已写入客户档案」;AI 接着写 P-01(customer_code 引用墙这时应过)。
- 其余同 R1 → released。落库:
select customer_code||' '||name||' '||coalesce(industry,'') from ledger.customer__profile where customer_code='CU-9001'→ 一行。 - 轮间清场(把 CU-9001 删掉)。
- Step 8: 收口记录
每轮结束把「结果 / 问题 / 修改 / 复测」填进对应节;分支覆盖表逐行填「结果」(通过 / 未覆盖 + 原因);「3. 结论与欠账」写:五轮终态与被拒数汇总、逼出的修改列表(文件 @ 版本)、未覆盖分支(2a / 2b / 6c)与建议夹具、留给 Task 9 的 debt。走查中改过的活动包随本任务提交:
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 <noreply@anthropic.com>
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同一套:一览表 + 每张表「重点字段(按重要程度)」+「数据结构」;★ = 重点,【人填】= 人在件上 / 录入卡填)
# 数据域字段说明 —— 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
「现状快照」末尾加一条(照既有条目的写法:事实 + 账):
- **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追加一节
## 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: 全量门禁(退出码)
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: 提交、收尾
git add docs/数据域字段说明-W05.md docs/debt.md CLAUDE.md
git commit -m "docs(w05): 数据域字段说明、现状快照、欠账登记
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 与提交列表报给用户。
自检清单(写完计划后过一遍)
- 规格覆盖:启动 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 五案例 / 浏览器五轮 + 分支覆盖表 / 测试记录文档 —— 各有任务落点。 - 未覆盖且已明示:2a / 2b / 6c(夹具缺)、6d(范围外)、数据卡卡皮、件上上传、双负责人。
- 名字一致性:活动 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)。 - 每个代码步骤给了完整代码;每个验证步骤给了命令与期望输出;没有「TBD / 后补 / 类似 Task N」。