Archived
活动包 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
3776 lines
249 KiB
Markdown
3776 lines
249 KiB
Markdown
# 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 用):
|
||
```sql
|
||
select domain||'.'||object||' '||kind||' '||coalesce(case_key,'') from ledger.__object_meta
|
||
where domain in ('parts','demand','substitution','validation','sub_eval') order by 1;
|
||
select string_agg(column_name||':'||data_type, ', ' order by ordinal_position)
|
||
from information_schema.columns where table_schema='ledger' and table_name='parts__profile';
|
||
select conname||' '||pg_get_constraintdef(oid) from pg_constraint where conrelid='ledger.substitution__difference_item'::regclass;
|
||
select internal_pn, brand_name, lifecycle_status from ledger.parts__profile order by 1;
|
||
```
|
||
|
||
## 2. 文件结构
|
||
|
||
**新建**
|
||
- `activities/part-substitution-assessment/package.json` · `src/index.ts` · `src/manifest.ts`(清单 + DMN)· `src/case-model.ts`(CMMN / NARRATIVE / STEPS / PHASES)· `src/skills.ts` · `src/views/prd-style.ts`(逐字节抄 I01)· `src/views/report.ts`(五张模版)· `__tests__/activity.test.ts` · `__tests__/steps.test.ts`
|
||
- `apps/api/src/app/__tests__/first-party-substitution.test.ts`(两枚纯工具)
|
||
- `docs/ACT-WORK-05-端到端测试记录.md`(Task 8 逐轮追加)· `docs/数据域字段说明-W05.md`
|
||
|
||
**修改**
|
||
- `apps/api/src/app/seed-mvp-data.ts`(+4 域 6 表 + 样例料号)· `apps/api/src/app/__tests__/seed-mvp-data.test.ts`(四域 → 八域)
|
||
- `apps/api/src/app/tools/first-party.ts`(+2 handler)· `apps/api/src/app/seed-mvp.ts`(+2 工具契约)
|
||
- `tools/build-activities.ts`(PACKAGES)· `apps/api/src/app/seed-activities.ts`(REAL_PACKAGES)
|
||
- `tools/e2e-a2.mjs`(+`w05` 套件、`stageEdits`、`decide` 值)
|
||
- `CLAUDE.md`(现状快照 + 文档地图)· `docs/debt.md`
|
||
|
||
---
|
||
|
||
### Task 0: 开分支、确认环境
|
||
|
||
**Files:** 无(只动 git 与环境)
|
||
|
||
- [ ] **Step 1: 从 mvp-foundation 开功能分支**(工作树里已有的未提交改动跟着走,不碰它们)
|
||
|
||
```bash
|
||
cd /Users/xcs/project2/tmp/BPOS-MVP
|
||
git checkout -b feat/act-work-05
|
||
git status --short | wc -l # 记下这个数:本计划的提交只 add 自己点名的文件
|
||
```
|
||
|
||
- [ ] **Step 2: 确认库与两套服务在跑**
|
||
|
||
```bash
|
||
docker ps --format '{{.Names}} {{.Status}}' | grep jcai-postgres
|
||
lsof -nP -iTCP:8787 -iTCP:5173 -iTCP:8790 -iTCP:5180 -sTCP:LISTEN | awk '{print $1,$9}' | sort -u
|
||
```
|
||
Expected: `jcai-postgres Up`;8787(dev api)与 5173(dev web)在;8790/5180 是 `./stable.sh` 起的稳定版(与 dev 连同一个库,推进器靠跨进程租约互斥 —— 跑 e2e / 浏览器走查时**先停它**,避免两台交替推同一单干扰观察:`./stable.sh stop`;走查完 `./stable.sh start`)。
|
||
|
||
- [ ] **Step 3: 确认 e2e 凭证在环境里**(值不打印)
|
||
|
||
```bash
|
||
for k in KEYCLOAK_URL KEYCLOAK_REALM KEYCLOAK_E2E_CLIENT_ID KEYCLOAK_E2E_CLIENT_SECRET; do printf '%s=%s\n' "$k" "${(P)k:+set}"; done
|
||
```
|
||
Expected: 四个都 `set`。缺了就从开发会话 / `.stable/env` 抄进当前 shell(`.stable/env` 只有 `KEYCLOAK_URL/REALM/CLIENT_ID`,E2E 那两个要问用户要;拿不到就停在这一步,e2e 跑不了)。
|
||
|
||
---
|
||
|
||
### Task 1: 主数据基线 —— P-08 / P-10 / P-01 / P-11 / P-06 进平台声明,加样例料号
|
||
|
||
**Files:**
|
||
- Modify: `apps/api/src/app/seed-mvp-data.ts`(`MASTER_OBJECTS` 末尾追加 6 表;`MASTER_DOMAINS` 8 域;`MASTER_SAMPLE_ROWS` 末尾追加 4 行)
|
||
- Modify: `apps/api/src/app/__tests__/seed-mvp-data.test.ts:18-19`(四域 → 八域)
|
||
- Test: `apps/api/src/app/__tests__/seed-mvp-data.test.ts`
|
||
|
||
**Interfaces:**
|
||
- Produces: 物理表 `ledger.parts__profile` / `parts__document` / `demand__request` / `substitution__relation` / `substitution__difference_item` / `validation__record`(列名与类型见下);样例行 `parts.profile` × 2(`LM2596S-5.0`, `LM2596S-ADJ`)、`parts.document` × 2。Task 3 的清单只**追加**列,列名与类型必须与这里一字不差(同名异型会被 schema-merge 放宽或拦下)。
|
||
|
||
- [ ] **Step 1: 改测试 —— 八域**
|
||
|
||
在 `apps/api/src/app/__tests__/seed-mvp-data.test.ts` 把 `'只有客户 / 项目 / 供应商 / 市场四个域'` 那条改成:
|
||
|
||
```ts
|
||
it('八个域:客户 / 项目 / 供应商 / 市场 + 料件 / 需求 / 替代 / 验证(SS02 差异评估 2026-08-26 加后四域)', () => {
|
||
expect([...MASTER_DOMAINS]).toEqual(['customer', 'project', 'supplier', 'market', 'parts', 'demand', 'substitution', 'validation']);
|
||
expect([...new Set(MASTER_OBJECTS.map((o) => o.domain))].sort()).toEqual([...MASTER_DOMAINS].sort());
|
||
});
|
||
|
||
it('SS02 差异评估要读写的六张主数据表都在基线里,主键照《统一方案》', () => {
|
||
const pk = (domain: string, object: string) =>
|
||
MASTER_OBJECTS.find((o) => o.domain === domain && o.object === object)?.fields.filter((f) => f.primaryKey).map((f) => f.name);
|
||
expect(pk('parts', 'profile')).toEqual(['internal_pn']);
|
||
expect(pk('parts', 'document')).toEqual(['internal_pn', 'doc_seq']);
|
||
expect(pk('demand', 'request')).toEqual(['request_id']);
|
||
expect(pk('substitution', 'relation')).toEqual(['substitution_relation_id']);
|
||
expect(pk('substitution', 'difference_item')).toEqual(['substitution_relation_id', 'difference_item_id']);
|
||
expect(pk('validation', 'record')).toEqual(['validation_id']);
|
||
});
|
||
|
||
it('样例料号:原 PN 与同族可调版在库,候选 XL2596S-5.0 刻意不播(数据工单那条路要试得出来)', () => {
|
||
const parts = MASTER_SAMPLE_ROWS.filter((r) => r.address.domain === 'parts' && r.address.object === 'profile').map((r) => r.key['internal_pn']);
|
||
expect(parts).toEqual(['LM2596S-5.0', 'LM2596S-ADJ']);
|
||
expect(parts).not.toContain('XL2596S-5.0');
|
||
});
|
||
```
|
||
文件头 `import` 改成 `import { MASTER_DOMAINS, MASTER_OBJECTS, MASTER_SAMPLE_ROWS } from '../seed-mvp-data.ts';`,并把文件头注释里「只有这四个域,不多不少」改为「八个域,多一个就说明有人把单据塞进主数据了」。
|
||
|
||
- [ ] **Step 2: 跑测试确认它红**
|
||
|
||
Run: `pnpm vitest run apps/api/src/app/__tests__/seed-mvp-data.test.ts`
|
||
Expected: FAIL(`MASTER_DOMAINS` 仍是四域;`pk('parts','profile')` 为 undefined)。
|
||
|
||
- [ ] **Step 3: 在 `seed-mvp-data.ts` 的 `MASTER_OBJECTS` 数组末尾(`market.quote_index` 之后、`];` 之前)追加六张表**
|
||
|
||
```ts
|
||
/* ── 料件:P-08 芯片主数据 / P-10 原厂技术资料库(SS02 差异评估 2026-08-26 加)──
|
||
字段名与《SS02-SS03-SS04_Demo台账字段细化与统一方案》§4.2 一字不差;全链路只认 internal_pn。
|
||
参数集是 P-08 的一个字段(用户定:一张表,不建子表),一行一参数:
|
||
`参数名 | 值 | 单位 | hard/soft | 说明`。老的 market.part(主键 pn)不复用,记 debt 待并。 */
|
||
master('parts', 'profile', '芯片主数据(P-08)', [
|
||
f('internal_pn', 'text', '内部料号', true),
|
||
f('customer_pn', 'text', '客户料号'),
|
||
f('brand_name', 'text', '品牌名称'),
|
||
f('manufacturer_name', 'text', '制造商名称'),
|
||
f('specification_description', 'text', '规格描述'),
|
||
f('normalized_parameter_set', 'text', '标准化参数集(一行一参数:参数名 | 值 | 单位 | hard/soft | 说明)'),
|
||
f('package', 'text', '封装'),
|
||
f('process_technology', 'text', '工艺'),
|
||
f('lifecycle_status', 'text', '生命周期状态'),
|
||
f('eol_pcn_status', 'text', 'EOL/PCN 状态'),
|
||
f('datasheet_ref', 'text', 'Datasheet 引用'),
|
||
f('datasheet_version', 'text', 'Datasheet 版本'),
|
||
f('application_scenarios', 'text', '适配应用场景'),
|
||
f('pin_to_pin_relation_ref', 'text', 'Pin-to-Pin 关系引用'),
|
||
]),
|
||
master('parts', 'document', '原厂技术资料库(P-10)', [
|
||
f('internal_pn', 'text', '关联内部料号', true),
|
||
f('doc_seq', 'text', '资料序号(同一料号下 01/02…)', true),
|
||
f('document_type', 'text', '资料类型'),
|
||
f('brand_name', 'text', '品牌名称'),
|
||
f('document_title', 'text', '资料标题'),
|
||
f('document_version', 'text', '资料版本'),
|
||
f('document_date', 'date', '资料发布日期'),
|
||
f('document_ref', 'text', '资料文件链接'),
|
||
f('citation_locator', 'text', '证据定位(页码/章节/表格)'),
|
||
f('source_contact_ref', 'text', '原厂支持联系人'),
|
||
f('document_status', 'text', '资料状态'),
|
||
]),
|
||
|
||
/* ── 需求:P-01 客户具体需求与项目任务主台账 ── */
|
||
master('demand', 'request', '客户需求主记录(P-01)', [
|
||
f('request_id', 'text', '需求编号', true),
|
||
f('customer_code', 'text', '客户编码'),
|
||
f('project_code', 'text', '项目编码'),
|
||
f('requirement_version', 'text', '需求版本'),
|
||
f('request_type', 'text', '需求类型'),
|
||
f('request_description', 'text', '需求描述'),
|
||
f('input_snapshot_ref', 'text', '输入快照'),
|
||
f('bom_id', 'text', 'BOM ID'),
|
||
f('bom_position', 'text', 'BOM 位'),
|
||
f('original_internal_pn', 'text', '原内部料号'),
|
||
f('customer_pn', 'text', '客户料号'),
|
||
f('application_scenario', 'text', '应用场景'),
|
||
f('technical_constraints', 'text', '技术约束'),
|
||
f('risk_level', 'text', '风险等级'),
|
||
f('verification_requirement', 'text', '验证/复测/确认要求'),
|
||
f('request_status', 'text', '需求状态'),
|
||
f('current_activity', 'text', '当前 Activity'),
|
||
f('current_stage', 'text', '当前阶段'),
|
||
f('next_action', 'text', '下一步动作'),
|
||
f('output_ref', 'text', '标准输出链接'),
|
||
]),
|
||
|
||
/* ── 替代:P-11 替代选型与批准关系库 —— 主记录 + 差异明细子表(《统一方案》§六:重复明细用子表)── */
|
||
master('substitution', 'relation', '替代关系主记录(P-11)', [
|
||
f('substitution_relation_id', 'text', '替代关系ID', true),
|
||
f('request_id', 'text', '关联需求编号'),
|
||
f('project_code', 'text', '关联项目编码'),
|
||
f('bom_position', 'text', '关联 BOM 位'),
|
||
f('original_internal_pn', 'text', '原内部料号'),
|
||
f('candidate_internal_pn', 'text', '候选内部料号'),
|
||
f('substitution_type', 'text', '替代类型'),
|
||
f('recommendation_level', 'text', '推荐等级'),
|
||
f('recommendation_reason', 'text', '推荐理由'),
|
||
f('difference_summary', 'text', '差异摘要'),
|
||
f('risk_level', 'text', '替代风险等级'),
|
||
f('verification_requirement', 'text', '验证要求'),
|
||
f('validation_id', 'text', '关联验证ID'),
|
||
f('analysis_status', 'text', '分析状态'),
|
||
/* 客户批准三列由独立审批流维护 —— 基线建列,差异评估活动**不声明写权** */
|
||
f('customer_approval_status', 'text', '客户批准状态'),
|
||
f('customer_approval_at', 'timestamp', '客户批准时间'),
|
||
f('customer_approval_ref', 'text', '客户批准凭证'),
|
||
f('customer_feedback', 'text', '客户反馈'),
|
||
f('analysis_report_ref', 'text', '替代分析报告'),
|
||
f('confidence_score', 'number', '证据可信度'),
|
||
f('rule_version', 'text', '差异规则版本'),
|
||
]),
|
||
master('substitution', 'difference_item', '差异明细(P-11 子表)', [
|
||
f('substitution_relation_id', 'text', '替代关系ID', true),
|
||
f('difference_item_id', 'text', '差异明细ID', true),
|
||
f('parameter_name', 'text', '标准参数名'),
|
||
f('original_value', 'text', '原物料参数值'),
|
||
f('candidate_value', 'text', '候选物料参数值'),
|
||
f('parameter_unit', 'text', '参数单位'),
|
||
f('constraint_type', 'text', '约束类型'),
|
||
f('difference_status', 'text', '差异状态'),
|
||
f('impact_statement', 'text', '影响说明'),
|
||
f('risk_level', 'text', '风险等级'),
|
||
f('evidence_ref', 'text', '差异证据引用'),
|
||
f('confidence', 'number', 'AI 置信度'),
|
||
f('rule_version', 'text', '差异规则版本'),
|
||
]),
|
||
|
||
/* ── 验证:P-06 样品交付与芯片验证记录(事实类:只能实测产生,数据工单不许建)── */
|
||
master('validation', 'record', '芯片验证记录(P-06)', [
|
||
f('validation_id', 'text', '验证ID', true),
|
||
f('request_id', 'text', '关联需求编号'),
|
||
f('customer_code', 'text', '客户编码'),
|
||
f('validation_internal_pn', 'text', '验证内部料号'),
|
||
f('validation_type', 'text', '验证类型'),
|
||
f('test_condition', 'text', '测试条件'),
|
||
f('test_parameters', 'text', '测试参数'),
|
||
f('test_result', 'text', '测试结果'),
|
||
f('validation_status', 'text', '验证状态'),
|
||
f('issue_risk', 'text', '问题与风险'),
|
||
f('sample_version', 'text', '样品版本'),
|
||
f('sample_lot', 'text', '样品批次'),
|
||
f('evidence_ref', 'text', '验证证据引用'),
|
||
f('next_validation_action', 'text', '后续验证动作'),
|
||
]),
|
||
```
|
||
|
||
并把 `MASTER_DOMAINS` 改为:
|
||
|
||
```ts
|
||
/** 八域的呈现次序 —— 谁在买 → 为什么买 → 从哪买 → 买什么 → 料件 → 需求 → 替代 → 验证 */
|
||
export const MASTER_DOMAINS = ['customer', 'project', 'supplier', 'market', 'parts', 'demand', 'substitution', 'validation'] as const;
|
||
```
|
||
|
||
- [ ] **Step 4: 在 `MASTER_SAMPLE_ROWS` 数组末尾追加样例料号**(现有最后一个元素之后、`];` 之前)
|
||
|
||
```ts
|
||
/*
|
||
* ── SS02 差异评估的样例料号(2026-08-26)──
|
||
* 只播**原 PN**与**同族可调版**两颗:「都在库」那条路靠它们;候选 XL2596S-5.0 与国产 DCDC
|
||
* **刻意不播** —— 「候选缺库 → 派数据工单」「硬约束不推荐」两条路要试得出来,库里得真没有。
|
||
* 参数集一行一参数(参数名 | 值 | 单位 | hard/soft | 说明),与 P-08 声明的行语法同一份。
|
||
*/
|
||
{
|
||
address: { domain: 'parts', object: 'profile' },
|
||
key: { internal_pn: 'LM2596S-5.0' },
|
||
values: {
|
||
internal_pn: 'LM2596S-5.0',
|
||
brand_name: 'TI',
|
||
manufacturer_name: 'Texas Instruments',
|
||
specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz',
|
||
normalized_parameter_set: [
|
||
'输入电压范围 | 4.5~40 | V | hard | 客户输入上限 40V',
|
||
'输出电压 | 5.0 | V | hard | 固定版',
|
||
'输出电流 | 3 | A | hard | 满载能力',
|
||
'开关频率 | 150 | kHz | hard | 影响外围电感/电容',
|
||
'静态电流 | 5 | mA | soft | 待机功耗',
|
||
'工作温度 | -40~+85 | ℃ | hard | 工业温域',
|
||
'使能阈值 | 1.4 | V | hard | ON/OFF 引脚阈值',
|
||
].join('\n'),
|
||
package: 'TO-263-5',
|
||
process_technology: 'BCD',
|
||
lifecycle_status: 'nrnd',
|
||
eol_pcn_status: 'PCN-2026-07:转 NRND,2027-01 起不接新设计',
|
||
datasheet_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf',
|
||
datasheet_version: 'Rev H (2023-09)',
|
||
application_scenarios: '工业电源;消费电源',
|
||
},
|
||
},
|
||
{
|
||
address: { domain: 'parts', object: 'profile' },
|
||
key: { internal_pn: 'LM2596S-ADJ' },
|
||
values: {
|
||
internal_pn: 'LM2596S-ADJ',
|
||
brand_name: 'TI',
|
||
manufacturer_name: 'Texas Instruments',
|
||
specification_description: '降压型 DCDC 稳压器,3A,可调输出 1.23~37V,150kHz(LM2596 同族)',
|
||
normalized_parameter_set: [
|
||
'输入电压范围 | 4.5~40 | V | hard | 与固定版同',
|
||
'输出电压 | 1.23~37 | V | hard | 可调版,靠外围反馈网络定',
|
||
'输出电流 | 3 | A | hard | 满载能力',
|
||
'开关频率 | 150 | kHz | hard | 影响外围电感/电容',
|
||
'静态电流 | 5 | mA | soft | 待机功耗',
|
||
'工作温度 | -40~+85 | ℃ | hard | 工业温域',
|
||
'使能阈值 | 1.4 | V | hard | ON/OFF 引脚阈值',
|
||
].join('\n'),
|
||
package: 'TO-263-5',
|
||
process_technology: 'BCD',
|
||
lifecycle_status: 'active',
|
||
datasheet_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf',
|
||
datasheet_version: 'Rev H (2023-09)',
|
||
application_scenarios: '工业电源;消费电源',
|
||
},
|
||
},
|
||
{
|
||
address: { domain: 'parts', object: 'document' },
|
||
key: { internal_pn: 'LM2596S-5.0', doc_seq: '01' },
|
||
values: {
|
||
internal_pn: 'LM2596S-5.0',
|
||
doc_seq: '01',
|
||
document_type: 'datasheet',
|
||
brand_name: 'TI',
|
||
document_title: 'LM2596 SIMPLE SWITCHER Power Converter 150-kHz 3-A Step-Down Voltage Regulator',
|
||
document_version: 'Rev H (2023-09)',
|
||
document_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf',
|
||
citation_locator: '§6.5 Electrical Characteristics 表 / §6.3 Recommended Operating Conditions',
|
||
document_status: 'valid',
|
||
},
|
||
},
|
||
{
|
||
address: { domain: 'parts', object: 'document' },
|
||
key: { internal_pn: 'LM2596S-ADJ', doc_seq: '01' },
|
||
values: {
|
||
internal_pn: 'LM2596S-ADJ',
|
||
doc_seq: '01',
|
||
document_type: 'datasheet',
|
||
brand_name: 'TI',
|
||
document_title: 'LM2596 SIMPLE SWITCHER Power Converter 150-kHz 3-A Step-Down Voltage Regulator',
|
||
document_version: 'Rev H (2023-09)',
|
||
document_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf',
|
||
citation_locator: '§6.5 Electrical Characteristics 表(ADJ 列)',
|
||
document_status: 'valid',
|
||
},
|
||
},
|
||
```
|
||
|
||
同时把 `seed-activities.ts:143` 那行日志改成不依赖下标(新行插在数组末尾,`[1]` 仍是样例客户,不改也不炸;但下一个人再往前插就错)——改为:
|
||
```ts
|
||
log(` 样例主数据已就位:${MASTER_SAMPLE_ROWS.length} 行(客户 / 项目 / 联系人 / 料号)`);
|
||
```
|
||
|
||
- [ ] **Step 5: 跑测试确认绿**
|
||
|
||
Run: `pnpm vitest run apps/api/src/app/__tests__/seed-mvp-data.test.ts`
|
||
Expected: PASS(含新加 3 条)。
|
||
|
||
- [ ] **Step 6: 类型检查 + 提交**
|
||
|
||
```bash
|
||
pnpm -s typecheck
|
||
git add apps/api/src/app/seed-mvp-data.ts apps/api/src/app/__tests__/seed-mvp-data.test.ts apps/api/src/app/seed-activities.ts
|
||
git commit -m "feat(w05): 主数据基线加料件/需求/替代/验证四域六表(P-08/P-10/P-01/P-11/P-06)与样例料号
|
||
|
||
Co-Authored-By: Claude Fable 5 <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`:
|
||
|
||
```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` 之前加实现**
|
||
|
||
```ts
|
||
/* ── ACT-WORK-05 差异评估:逐参数比对 —— 纯工具(活动私有作用域)─────────────────
|
||
*
|
||
* 「能算的不问 LLM」:同输入必同结论。规则与活动包 manifest 里的 D01/D02 两张 DMN 同一份;
|
||
* 这里是求值器,那里是随包冻结的文档。`RULE_VERSION` 随结论落账(rule_version)。
|
||
*
|
||
* 比对语义(按硬/软约束):
|
||
* equal / covers(候选范围 ⊇ 原范围) → match,低风险;
|
||
* configurable(原是定值、候选是含它的可调范围)→ conditional(要改外围);
|
||
* narrower / differs / partial → 硬:mismatch 高风险;软:conditional 中风险;
|
||
* 任一侧未披露 → unknown 中风险;
|
||
* 硬约束 equal/covers 但没给 evidenceRef → conditional(硬约束未经证据支持不得判 MATCH);
|
||
* requirement(客户约束,如 ">= 40")候选不满足 → 一律 mismatch 高风险。
|
||
*/
|
||
export const SUBSTITUTION_RULE_VERSION = 'R-2026.07';
|
||
|
||
type Parsed = { kind: 'number'; value: number } | { kind: 'range'; lo: number; hi: number } | { kind: 'text'; value: string } | { kind: 'unknown' };
|
||
type CompareResult = 'equal' | 'covers' | 'configurable' | 'narrower' | 'partial' | 'differs' | 'unknown';
|
||
type DifferenceStatus = 'match' | 'mismatch' | 'unknown' | 'conditional';
|
||
type RiskLevel = 'low' | 'medium' | 'high';
|
||
type ParamPair = {
|
||
parameterName: string;
|
||
originalValue: string;
|
||
candidateValue: string;
|
||
unit?: string;
|
||
constraintType: 'hard' | 'soft';
|
||
requirement?: string;
|
||
evidenceRef?: string;
|
||
};
|
||
type CompareCounts = { match: number; mismatch: number; unknown: number; conditional: number; hardMismatch: number; softMismatch: number; configurable: number };
|
||
|
||
const UNKNOWN_WORDS = /^(未披露|未知|未给出|unknown|n\/?a|tbd|待定|-|—|–)$/i;
|
||
const NUM = '[-+]?\\d+(?:\\.\\d+)?';
|
||
const RANGE_RE = new RegExp(`^(${NUM})\\s*(?:~|~|-|–|—|至|to)\\s*(${NUM})`, 'i');
|
||
const NUM_RE = new RegExp(`^(${NUM})`);
|
||
|
||
export function parseParamValue(raw: unknown): Parsed {
|
||
const s = String(raw ?? '').trim().replace(/\s+/g, ' ');
|
||
if (s === '' || UNKNOWN_WORDS.test(s)) return { kind: 'unknown' };
|
||
const r = RANGE_RE.exec(s);
|
||
if (r !== null) {
|
||
const lo = Number(r[1]);
|
||
const hi = Number(r[2]);
|
||
if (Number.isFinite(lo) && Number.isFinite(hi)) return { kind: 'range', lo: Math.min(lo, hi), hi: Math.max(lo, hi) };
|
||
}
|
||
const n = NUM_RE.exec(s);
|
||
if (n !== null && Number.isFinite(Number(n[1]))) return { kind: 'number', value: Number(n[1]) };
|
||
return { kind: 'text', value: s.toLowerCase() };
|
||
}
|
||
|
||
function compareParsed(o: Parsed, c: Parsed): CompareResult {
|
||
if (o.kind === 'unknown' || c.kind === 'unknown') return 'unknown';
|
||
if (o.kind === 'range' && c.kind === 'range') {
|
||
if (c.lo === o.lo && c.hi === o.hi) return 'equal';
|
||
if (c.lo <= o.lo && c.hi >= o.hi) return 'covers';
|
||
if (c.lo >= o.lo && c.hi <= o.hi) return 'narrower';
|
||
return c.hi < o.lo || c.lo > o.hi ? 'differs' : 'partial';
|
||
}
|
||
if (o.kind === 'number' && c.kind === 'number') return Math.abs(o.value - c.value) <= 1e-9 ? 'equal' : 'differs';
|
||
if (o.kind === 'number' && c.kind === 'range') return c.lo <= o.value && o.value <= c.hi ? 'configurable' : 'differs';
|
||
if (o.kind === 'range' && c.kind === 'number') return o.lo <= c.value && c.value <= o.hi ? 'narrower' : 'differs';
|
||
if (o.kind === 'text' && c.kind === 'text') return o.value === c.value ? 'equal' : 'differs';
|
||
return 'differs';
|
||
}
|
||
|
||
/** 客户约束:">= 40" / "<= 85" / "-40~85"(候选必须盖住);解析不了当没给 */
|
||
function satisfiesRequirement(requirement: string | undefined, c: Parsed): boolean | undefined {
|
||
if (requirement === undefined || requirement.trim() === '' || c.kind === 'unknown' || c.kind === 'text') return undefined;
|
||
const s = requirement.trim();
|
||
const ge = new RegExp(`^(?:>=|≥)\\s*(${NUM})$`).exec(s);
|
||
const le = new RegExp(`^(?:<=|≤)\\s*(${NUM})$`).exec(s);
|
||
const hi = c.kind === 'range' ? c.hi : c.value;
|
||
const lo = c.kind === 'range' ? c.lo : c.value;
|
||
if (ge !== null) return hi >= Number(ge[1]);
|
||
if (le !== null) return lo <= Number(le[1]);
|
||
const req = parseParamValue(s);
|
||
if (req.kind === 'range') return lo <= req.lo && hi >= req.hi;
|
||
if (req.kind === 'number') return lo <= req.value && hi >= req.value;
|
||
return undefined;
|
||
}
|
||
|
||
/** 生命周期不是数值:候选比原料「更活」算 covers(替 NRND 料本就是为了这个),更差才是 differs */
|
||
const LIFECYCLE_ORDER: Record<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` 数组末尾登记:
|
||
|
||
```ts
|
||
['part-substitution-assessment.compare-parameters', compareParameters],
|
||
['part-substitution-assessment.recommend', recommend],
|
||
```
|
||
|
||
- [ ] **Step 4: 跑测试确认绿**
|
||
|
||
Run: `pnpm vitest run apps/api/src/app/__tests__/first-party-substitution.test.ts`
|
||
Expected: PASS 6 条。
|
||
|
||
- [ ] **Step 5: 工具契约放独立文件 `apps/api/src/app/tools-w05.ts`(seed-mvp 与 seed-activities 都要用,放 seed-mvp 里会撞循环 import)**
|
||
|
||
```ts
|
||
/**
|
||
* ACT-WORK-05 差异评估的两枚纯工具契约(活动私有作用域)。
|
||
* 两处消费:`seed-mvp.ts`(db:seed 全套播种)与 `seed-activities.ts`(activities:install 装活动时顺带注册 ——
|
||
* 「安装即用」包含它声明过的纯工具能调得动;从前 E04 的工具只在 db:seed 里注册,开发机上只跑
|
||
* activities:install 的人第一次跑活动就撞「工具不存在」)。
|
||
*/
|
||
const PURE = {
|
||
effect: 'read' as const,
|
||
errors: [] as Array<{ code: string; retryable: boolean; message: string }>,
|
||
idempotent: true,
|
||
timeoutMs: 5_000,
|
||
retries: 0,
|
||
connectionKeys: [] as string[],
|
||
pure: true,
|
||
ownerActivityId: 'part-substitution-assessment',
|
||
};
|
||
|
||
/** 逐参数比对 —— 出参键与台账列同名(difference_status / risk_level),模型照抄 */
|
||
export const COMPARE_PARAMETERS_TOOL = {
|
||
...PURE,
|
||
name: 'part-substitution-assessment.compare-parameters',
|
||
displayName: '替代料逐参数比对',
|
||
version: '1.0.0',
|
||
description:
|
||
'按硬/软约束逐参数比对原 PN 与候选 PN,输出四态 difference_status(match/mismatch/unknown/conditional)、' +
|
||
'compare_result、risk_level 与各候选的计数。一次可比一批候选(candidates 数组),每对参数可带客户约束 requirement(如 ">= 40")与证据引用 evidenceRef;' +
|
||
'硬约束没有证据引用不判 match。规则版本 R-2026.07。',
|
||
input: {
|
||
type: 'object',
|
||
properties: {
|
||
candidates: {
|
||
type: 'array',
|
||
description: '[{candidateInternalPn, pairs:[{parameterName, originalValue, candidateValue, unit?, constraintType:"hard"|"soft", requirement?, evidenceRef?}]}]',
|
||
},
|
||
},
|
||
},
|
||
output: {
|
||
type: 'object',
|
||
properties: {
|
||
ruleVersion: { type: 'string' },
|
||
results: {
|
||
type: 'array',
|
||
description: '[{candidateInternalPn, items:[{parameterName, unit, constraintType, compareResult, differenceStatus, riskLevel, note}], counts:{match,mismatch,unknown,conditional,hardMismatch,softMismatch,configurable}}]',
|
||
},
|
||
},
|
||
},
|
||
};
|
||
|
||
/** 推荐定级 —— 出参 recommendationLevel / riskLevel / reason 直接落 P-11 */
|
||
export const RECOMMEND_TOOL = {
|
||
...PURE,
|
||
name: 'part-substitution-assessment.recommend',
|
||
displayName: '替代推荐定级',
|
||
version: '1.0.0',
|
||
description:
|
||
'按每个候选的比对计数定推荐等级:硬约束 mismatch>0 → not_recommended;unknown>0 → conditional;' +
|
||
'只有可调配成(configurable)→ alternate;其余 conditional → conditional;全 match → recommended。',
|
||
input: {
|
||
type: 'object',
|
||
properties: {
|
||
candidates: { type: 'array', description: '[{candidateInternalPn, counts:{match,mismatch,unknown,conditional,hardMismatch,softMismatch,configurable}}]' },
|
||
},
|
||
},
|
||
output: {
|
||
type: 'object',
|
||
properties: {
|
||
ruleVersion: { type: 'string' },
|
||
results: { type: 'array', description: '[{candidateInternalPn, recommendationLevel, riskLevel, reason}]' },
|
||
},
|
||
},
|
||
};
|
||
|
||
export const W05_TOOLS = [COMPARE_PARAMETERS_TOOL, RECOMMEND_TOOL] as const;
|
||
|
||
/** 试运行审的样例(两件共用一份:并集覆盖各家入参) */
|
||
export const W05_TOOL_SAMPLE = {
|
||
input: {
|
||
candidates: [
|
||
{
|
||
candidateInternalPn: 'XL2596S-5.0',
|
||
pairs: [{ parameterName: '输出电压', originalValue: '5.0', candidateValue: '5.0', unit: 'V', constraintType: 'hard', evidenceRef: 'P-08' }],
|
||
counts: { match: 1, mismatch: 0, unknown: 0, conditional: 0, hardMismatch: 0, softMismatch: 0, configurable: 0 },
|
||
},
|
||
],
|
||
},
|
||
expectShape: { type: 'object' },
|
||
};
|
||
```
|
||
|
||
然后在 `seed-mvp.ts`:顶部加 `import { W05_TOOLS } from './tools-w05.ts';`,把 `for (const contract of [HISTORY_TOOL, MAIL_TOOL, CLASSIFY_TOOL, PLACEHOLDER_TOOL, COVERAGE_SCORE_TOOL, RANK_KEY_PERSONS_TOOL])` 改为 `for (const contract of [HISTORY_TOOL, MAIL_TOOL, CLASSIFY_TOOL, PLACEHOLDER_TOOL, COVERAGE_SCORE_TOOL, RANK_KEY_PERSONS_TOOL, ...W05_TOOLS])`(那条「读类 1 · 写类 1 · 纯工具 4」的日志改成「纯工具 6」)。
|
||
|
||
- [ ] **Step 6: 门禁子集 + 提交**
|
||
|
||
Run: `pnpm -s typecheck && pnpm -s lint && pnpm vitest run apps/api/src/app/__tests__`
|
||
Expected: 全绿。
|
||
|
||
```bash
|
||
git add apps/api/src/app/tools/first-party.ts apps/api/src/app/seed-mvp.ts apps/api/src/app/tools-w05.ts apps/api/src/app/__tests__/first-party-substitution.test.ts
|
||
git commit -m "feat(w05): 差异评估两枚纯工具 —— 逐参数四态比对与推荐定级(R-2026.07)
|
||
|
||
Co-Authored-By: Claude Fable 5 <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★ tally `active/nrnd/eol/discontinued/risk`;manufacturer_name / process_technology / eol_pcn_status / datasheet_ref / datasheet_version / application_scenarios expect)
|
||
- `parts.document`(internal_pn + doc_seq PK;document_type★ tally `datasheet/app_note/reference_design/white_paper/sample_doc/commitment_letter/other`;document_title★ document_version★ document_ref★ document_status★ tally `valid/expired/pending/void`;citation_locator / brand_name expect)
|
||
- `demand.request`(request_id PK;customer_code★ refersTo customer.profile.customer_code;requirement_version★ request_type★ request_description★ input_snapshot_ref★ original_internal_pn★ request_status★ tally `pending/responded/won/abandoned/closed`;application_scenario / bom_position expect;risk_level tally `low/medium/high/critical`、verification_requirement、output_ref、next_action、current_activity 均 `requiredWhen {current_stage:'gate03'}`;current_stage 自由文本)
|
||
- `substitution.relation`(substitution_relation_id PK;request_id★;original_internal_pn★ / candidate_internal_pn★ 均 refersTo parts.profile.internal_pn;substitution_type★ tally `pin_to_pin/functional/parametric/other`;recommendation_level★ tally `recommended/conditional/alternate/not_recommended`;recommendation_reason★ difference_summary★;risk_level★ tally `low/medium/high/critical`;verification_requirement requiredWhen {recommendation_level:'conditional'};validation_id;analysis_status★ tally `ai_draft/fae_confirmed/pending_customer/customer_approved/rejected/void`;bom_position / analysis_report_ref / confidence_score expect;rule_version★;eval_ref★)
|
||
- `substitution.difference_item`(substitution_relation_id PK distinct refersTo substitution.relation.substitution_relation_id;difference_item_id PK;parameter_name★ original_value★ candidate_value★;parameter_unit expect;constraint_type★ tally `hard/soft`;compare_result expect;difference_status★ tally `match/mismatch/unknown/conditional`;impact_statement★;unknown_disposition requiredWhen {difference_status:'unknown'};risk_level★ tally `low/medium/high`;evidence_ref★;confidence expect;rule_version★)
|
||
- `validation.record`(validation_id PK;request_id★;validation_internal_pn★ refersTo parts.profile.internal_pn;validation_type★ tally `pin_to_pin/functional/reliability/certification/retest`;validation_status★ tally `not_started/in_progress/passed/failed/conditional/pending_evidence`;next_validation_action★;test_condition / evidence_ref expect)
|
||
- `sub_eval.pn_check`(record,caseKey eval_ref;eval_ref + internal_pn PK;role★ tally `original/candidate`;exists_status★ tally `found/absent/created/abandoned`;abandon_reason requiredWhen {exists_status:'abandoned'};note)
|
||
- `sub_eval.input_gap`(record;eval_ref + item_key PK;input_item★;source_ledger expect;status★ tally `gap/default/ok`;detail expect;supplement requiredWhen {status:'gap'};origin)
|
||
- `sub_eval.verdict`(record;eval_ref PK;request_id、rule_version、original_internal_pn、candidate_total integer、input_confirmed_total、review_approved_total、landing_prepared_total、relation_total、recommended_total、conditional_total、alternate_total、not_recommended_total、summary)
|
||
- Produces 出口 `assessment-released`,载荷 `{ evalRef, requestId, relationTotal, recommendedTotal, conditionalTotal, alternateTotal, notRecommendedTotal }`(全部由 `sub_eval.verdict` 同名列承载)。
|
||
- Produces 视图名:`p08-master-create` / `p10-document-create` / `eval-input-list` / `difference-review` / `landing-confirm`(Task 5 必须导出同名常量 `P08_MASTER_CREATE` / `P10_DOCUMENT_CREATE` / `EVAL_INPUT_LIST` / `DIFFERENCE_REVIEW` / `LANDING_CONFIRM`)。
|
||
- Produces 计划项 id(Task 4 的 CMMN / STEPS 必须一字不差):`S0_bind` `S0d_master_data` `S1_input_confirm` `S2_normalize` `S3_assess` `S4_review` `S5_prepare_landing` `T_release`。
|
||
|
||
- [ ] **Step 1: 建包骨架**
|
||
|
||
```bash
|
||
mkdir -p activities/part-substitution-assessment/src/views activities/part-substitution-assessment/__tests__
|
||
cat > activities/part-substitution-assessment/package.json <<'EOF'
|
||
{ "name": "@activities/part-substitution-assessment", "version": "0.1.0", "private": true, "type": "module", "exports": { ".": "./src/index.ts" } }
|
||
EOF
|
||
cat > activities/part-substitution-assessment/src/index.ts <<'EOF'
|
||
/**
|
||
* ACT-WORK-05 · 差异评估 —— 活动包代码入口(ADR-004:默认导出 defineActivity 的返回值)。
|
||
*
|
||
* 这个活动没有 `run`。案例活动的推进由 Loop 驱动,行为全部写在清单里:
|
||
* 案例模型 + 说明书 + 步骤剧本 + 三条技能 + 台账声明 + 五张 HTML 报告模版。
|
||
* 这个文件之所以还存在,是因为清单的 `entry` 指着它 ——
|
||
* 它是**被静态审扫描的产物文本**(查 import 与全局引用,证明活动包零环境能力)。
|
||
*/
|
||
import { defineActivity } from '@sdk';
|
||
|
||
export default defineActivity({ name: 'part-substitution-assessment' });
|
||
EOF
|
||
# 共享常量单独一个文件:manifest.ts 与 case-model.ts 互相引用会撞 TDZ(E04 的 case-model 不 import manifest,这里两边都要用同一批常量)
|
||
cat > activities/part-substitution-assessment/src/constants.ts <<'EOF'
|
||
/** 清单与案例模型共用的常量 —— 单独成文件,免得 manifest ↔ case-model 循环 import 撞 TDZ。 */
|
||
export const ACTIVITY_ID_TEXT = 'part-substitution-assessment';
|
||
/** 活动自持单据的域名 */
|
||
export const D = 'sub_eval';
|
||
/** GOV 差异规则版本 —— 随包冻结,随每条结论落账(rule_version) */
|
||
export const RULE_VERSION = 'R-2026.07';
|
||
export const TOOL_COMPARE = 'part-substitution-assessment.compare-parameters';
|
||
export const TOOL_RECOMMEND = 'part-substitution-assessment.recommend';
|
||
EOF
|
||
# 视觉底逐字节抄 I01(两包之间没有代码通道,只能各持一份),只换头注
|
||
cp activities/brand-content-toolkit/src/views/prd-style.ts activities/part-substitution-assessment/src/views/prd-style.ts
|
||
python3 - <<'PY'
|
||
p='activities/part-substitution-assessment/src/views/prd-style.ts'
|
||
s=open(p,encoding='utf-8').read()
|
||
head_end=s.index('export const PRD_CSS')
|
||
new_head='''/**
|
||
* ACT-WORK-05 差异评估 · 视觉底 —— 逐字节抄自 I01(`brand-content-toolkit/src/views/prd-style.ts`)。
|
||
*
|
||
* 本活动的四份目标件(`docs/SS02-ACT-WORK-05-html/*.html`)与 E04 / I01 的目标件是同一套
|
||
* 设计语言产出的(卡壳 / 卡头渐变 / 判据框 / 表格排版全同);`activities/*` 只准 import
|
||
* `@sdk` / `@contracts`,包之间没有代码通道,只能各自持有一份。改这份不要改口径:
|
||
* 数据驱动的开关(三档占位 / 缺项红带 / 状态徽 / 编辑闸)在 `report.ts` 的 `BIND_CSS`。
|
||
*/
|
||
|
||
'''
|
||
open(p,'w',encoding='utf-8').write(new_head+s[head_end:])
|
||
PY
|
||
```
|
||
|
||
- [ ] **Step 2: 写技能 `src/skills.ts`**
|
||
|
||
```ts
|
||
/**
|
||
* ACT-WORK-05 差异评估的技能 —— 载方法,不载权力。
|
||
* 剧本写「这一步做什么、写哪几列」,技能写「为什么这么判、拿不准时按哪条走」。
|
||
*/
|
||
import { skillIdSchema } from '@contracts';
|
||
import type { Skill } from '@contracts';
|
||
|
||
export const ASSESSMENT_SKILLS: Skill[] = [
|
||
{
|
||
id: skillIdSchema.parse('substitution.normalize-parameters'),
|
||
label: '把多来源资料整成可比对的标准参数对照表',
|
||
applicability: '输入确认之后、逐项比对之前:原 PN 与每个候选的参数集要摆成同名同单位的一对一。',
|
||
method: [
|
||
'参数集在 `parts.profile.normalized_parameter_set` 里,一行一参数:`参数名 | 值 | 单位 | hard/soft | 说明`。',
|
||
'对照表按**原 PN 的参数名**为骨架:原 PN 有的参数,候选没有对应行 → 候选值写「未披露」(它会成为 UNKNOWN);',
|
||
'候选多出来的参数不比(比对只回答「能不能替」,多出来的能力不构成差异)。',
|
||
'单位以原 PN 为准:候选单位不同先换算(mV→V、kHz→MHz),换算不了就不改值、在说明里写明 —— 不许静默改数。',
|
||
'约束类型以原 PN 那一行的 hard/soft 为准;原 PN 没标的按 GOV 口径:封装 / 工艺 / 电气边界(电压 / 电流 / 温度 / 阈值)一律 hard,其余 soft。',
|
||
'**缺参数不得伪造**:查不到的写「未披露」,不写估计值、不写「约」。',
|
||
].join('\n'),
|
||
crystallized: [],
|
||
knowledgeRefs: ['标准化参数集行语法', '硬/软约束口径'],
|
||
version: '1.0.0',
|
||
},
|
||
{
|
||
id: skillIdSchema.parse('substitution.difference-judgement'),
|
||
label: '逐项四态由工具算,证据与影响由你写',
|
||
applicability: '对照表齐了,要出 MATCH / MISMATCH / UNKNOWN / CONDITIONAL 时。',
|
||
method: [
|
||
'四态**不许自己判**:调 `part-substitution-assessment.compare-parameters`,把每个候选的全部参数对一次性传进去(candidates 数组,一次一批)。',
|
||
'每对参数都带 `evidenceRef`(值从哪份记录来:「P-08」「P-10 Rev H §6.5」「数据工单 <单号> · 确认人 <人>」)——硬约束没有证据引用,工具不判 MATCH。',
|
||
'客户在应用场景里给了边界(如「输入上限 40V」)就写进那一对的 `requirement`(`>= 40`),候选不满足直接 MISMATCH。',
|
||
'工具出参照抄进 `substitution.difference_item`:difference_status / compare_result / risk_level 一字不改;',
|
||
'`impact_statement` 你写:MATCH 写「无」,其余说清对应用 / 验证 / 报价的影响;UNKNOWN 必须写 `unknown_disposition`(挂验证任务号或补来源动作),不许悬空。',
|
||
'推荐等级同样由工具定(`part-substitution-assessment.recommend`,传每个候选的 counts),`recommendation_reason` 照工具的 reason 扩写,不许把不推荐改成推荐。',
|
||
].join('\n'),
|
||
crystallized: [],
|
||
knowledgeRefs: ['四态口径', '推荐等级四档'],
|
||
version: '1.0.0',
|
||
},
|
||
{
|
||
id: skillIdSchema.parse('substitution.landing'),
|
||
label: '落账只写分析层,客户批准不碰',
|
||
applicability: '审核通过之后拟 P-11 / 回写 P-01 时。',
|
||
method: [
|
||
'`analysis_status` 从 `ai_draft` 推到 `fae_confirmed`;`customer_approval_status` 由独立审批流维护 —— 本活动**没有这一列的写权**,写了会被拒。',
|
||
'P-01 回写五项:risk_level(取各候选里最高的)、verification_requirement(把所有验证任务号列全)、output_ref(替代分析报告号)、next_action(「提交 ACT-GATE-03 评审放行」)、current_stage 写 `gate03`。',
|
||
'规则版本随结论落账:每条 P-11 主记录与差异明细都写 rule_version(工具出参的 ruleVersion),中途规则更新不影响进行中的评估。',
|
||
'被打回重做时改的是**台账本身**(理由 / 依据 / 去向),不是把同一份原样再递一遍。',
|
||
].join('\n'),
|
||
crystallized: [],
|
||
knowledgeRefs: ['analysis_status 与 customer_approval_status 分离'],
|
||
version: '1.0.0',
|
||
},
|
||
];
|
||
```
|
||
|
||
- [ ] **Step 3: 写清单 `src/manifest.ts`**
|
||
|
||
```ts
|
||
/**
|
||
* 活动包清单 —— ACT-WORK-05 差异评估(案例活动)。
|
||
*
|
||
* 业务出处:`docs/SS02-ACT-WORK-05-html/`(活动流程与工单说明 / 一键启动入参说明 /
|
||
* 《SS02-SS03-SS04_Demo台账字段细化与统一方案》/ 四份目标件 / 全流程分支图)。
|
||
* 骨架照 E04 / I01 已验证的样子:双级入参 → 活动自持单据 + 共享主数据 → decide 循环做中间审核 →
|
||
* 终局放行。三处与前两个活动刻意不同,写在这里免得下一个读者当成疏漏:
|
||
* ① **主数据是六张(P-08/P-10/P-01/P-11 主+子/P-06)**,物理表由平台基线
|
||
* (`apps/api/src/app/seed-mvp-data.ts`)物化,这里只**追加**本活动要用的列与标记
|
||
* (required / tally / refersTo / requiredWhen 是活动的授权面;声明合并是纯 add-column)。
|
||
* ② **数据工单**(PN 不在库时让人从零建整条主数据)= 零输入执行工单 + 工作区的件
|
||
* (`mine:parts.profile` / `mine:parts.document` 可加行),确认落草稿层、放行并入正式表
|
||
* (守铁律②;文档里「确认即入库」在本平台落成「无审核环节、放行并入」)。一张表一张单。
|
||
* ③ 逐项四态与推荐等级**由两枚纯工具算**(规则同时以 D01/D02 两张 DMN 随包冻结当文档),
|
||
* 模型只负责对照表、证据与影响说明 —— 硬约束 MISMATCH 不可能被措辞改判。
|
||
*/
|
||
import type { ActivityManifest, DmnModel } from '@contracts';
|
||
import { activityIdSchema } from '@contracts';
|
||
import { ASSESSMENT_SKILLS } from './skills.ts';
|
||
import { CMMN_XML, NARRATIVE, ASSESSMENT_PHASES, ASSESSMENT_STEPS } from './case-model.ts';
|
||
import { P08_MASTER_CREATE, P10_DOCUMENT_CREATE, EVAL_INPUT_LIST, DIFFERENCE_REVIEW, LANDING_CONFIRM } from './views/report.ts';
|
||
import { ACTIVITY_ID_TEXT, D, RULE_VERSION, TOOL_COMPARE, TOOL_RECOMMEND } from './constants.ts';
|
||
|
||
export { D, RULE_VERSION, TOOL_COMPARE, TOOL_RECOMMEND } from './constants.ts';
|
||
const ACTIVITY_ID = activityIdSchema.parse(ACTIVITY_ID_TEXT);
|
||
|
||
/**
|
||
* 两张判定表 —— **文档形态**(求值器在 `apps/api/src/app/tools/first-party.ts`,与这里同一份规则):
|
||
* D01 逐参数四态、D02 推荐等级。都是 FIRST:规则从上往下第一条命中即出结论,顺序就是优先级。
|
||
*/
|
||
export const D01_DMN: DmnModel['tables'][number] = {
|
||
id: 'd01-difference-status',
|
||
label: '差异四态 D01(R-2026.07)',
|
||
hitPolicy: 'FIRST',
|
||
inputs: [
|
||
{ name: 'compare_result', expression: 'compare_result' },
|
||
{ name: 'constraint_type', expression: 'constraint_type' },
|
||
{ name: 'evidence_present', expression: 'evidence_present' },
|
||
],
|
||
outputs: [{ name: 'difference_status' }, { name: 'risk_level' }],
|
||
rules: [
|
||
{ id: 'd01-u', when: ['"unknown"', '-', '-'], then: ['unknown', 'medium'], annotation: '一侧未披露 → UNKNOWN,挂验证任务' },
|
||
{ id: 'd01-m0', when: ['"equal","covers"', '"hard"', 'false'], then: ['conditional', 'medium'], annotation: '硬约束缺证据不判 MATCH' },
|
||
{ id: 'd01-m', when: ['"equal","covers"', '-', '-'], then: ['match', 'low'], annotation: '一致或候选范围盖住原范围' },
|
||
{ id: 'd01-c', when: ['"configurable","partial"', '-', '-'], then: ['conditional', 'medium'], annotation: '可调配成 / 部分重叠 → 条件' },
|
||
{ id: 'd01-x', when: ['"narrower","differs"', '"hard"', '-'], then: ['mismatch', 'high'], annotation: '硬约束不一致' },
|
||
{ id: 'd01-s', when: ['"narrower","differs"', '"soft"', '-'], then: ['conditional', 'medium'], annotation: '软约束不一致 → 评估影响' },
|
||
],
|
||
};
|
||
|
||
export const D02_DMN: DmnModel['tables'][number] = {
|
||
id: 'd02-recommendation',
|
||
label: '推荐等级 D02(R-2026.07)',
|
||
hitPolicy: 'FIRST',
|
||
inputs: [
|
||
{ name: 'hard_mismatch_total', expression: 'hard_mismatch_total' },
|
||
{ name: 'unknown_total', expression: 'unknown_total' },
|
||
{ name: 'configurable_total', expression: 'configurable_total' },
|
||
{ name: 'conditional_total', expression: 'conditional_total' },
|
||
],
|
||
outputs: [{ name: 'recommendation_level' }],
|
||
rules: [
|
||
{ id: 'd02-n', when: ['> 0', '-', '-', '-'], then: ['not_recommended'], annotation: '硬约束失败直接不推荐' },
|
||
{ id: 'd02-u', when: ['0', '> 0', '-', '-'], then: ['conditional'], annotation: '有 UNKNOWN → 等验证闭环' },
|
||
{ id: 'd02-a', when: ['0', '0', '> 0', '-'], then: ['alternate'], annotation: '只靠改外围配得成 → 备选' },
|
||
{ id: 'd02-c', when: ['0', '0', '0', '> 0'], then: ['conditional'], annotation: '其余条件项 → 条件推荐' },
|
||
{ id: 'd02-r', when: ['0', '0', '0', '0'], then: ['recommended'], annotation: '全 MATCH → 推荐' },
|
||
],
|
||
};
|
||
|
||
export const ASSESSMENT_DMN: DmnModel = {
|
||
drd: [
|
||
{ decision: 'd01-difference-status', requires: [], inputs: ['compare_result', 'constraint_type', 'evidence_present'] },
|
||
{ decision: 'd02-recommendation', requires: [], inputs: ['hard_mismatch_total', 'unknown_total', 'configurable_total', 'conditional_total'] },
|
||
],
|
||
tables: [D01_DMN, D02_DMN],
|
||
};
|
||
|
||
/** 字段声明的短写(与 E04 同一套写法:显式 primaryKey / masked,其余按需) */
|
||
type F = { name: string; type: 'text' | 'number' | 'integer' | 'boolean' | 'date' | 'timestamp' | 'json'; label: string; primaryKey?: boolean; required?: boolean; expect?: boolean; tally?: string[]; auto?: boolean; distinct?: boolean; refersTo?: string; requiredWhen?: { column: string; value: string } };
|
||
const col = (f: F) => ({ primaryKey: false, masked: false, ...f });
|
||
|
||
export const manifest: ActivityManifest = {
|
||
identity: {
|
||
activityId: ACTIVITY_ID,
|
||
name: 'ACT-WORK-05 · 差异评估',
|
||
packageVersion: '1.0.0',
|
||
category: '需求分析与方案匹配',
|
||
sdkVersion: '0.1.0-m5',
|
||
checksum: 'dev-checksum-part-substitution-assessment',
|
||
},
|
||
|
||
/* ── 契约七件 ────────────────────────────────────────────────── */
|
||
contract: {
|
||
activityId: ACTIVITY_ID,
|
||
version: '1.0.0',
|
||
input: {
|
||
/*
|
||
* 一键启动只收 4 项(文档《一键启动入参说明》极简原则):客户 / 客户需求 / 原 PN / 候选 PN 数组。
|
||
* 应用场景、BOM 位、替代类型、规则版本、负责人期限**不是入参** —— 启动后在执行工单 01 里补。
|
||
* datasheet 选填文件:传了 AI 在数据工单件上预填,没传人全填(文档 0c:同为合法路径)。
|
||
* `customerName` 只在客户不在库(分支 0a)时用:S0 发数据录入卡建档,名字先摆上去让人核。
|
||
*/
|
||
data: {
|
||
type: 'object',
|
||
required: ['customerCode', 'requestDescription', 'originalInternalPn', 'candidateInternalPns'],
|
||
properties: {
|
||
customerCode: { type: 'string', title: '客户编码(L-02 客户主台账)', examples: ['CU-0142'] },
|
||
customerName: { type: 'string', title: '客户名称(仅客户不在库时填,启动后先建档)', examples: ['深圳恒星电源有限公司'] },
|
||
requestDescription: {
|
||
type: 'string',
|
||
title: '客户需求(原始描述 —— 据此新建 P-01 需求主记录 · 版本 V1)',
|
||
examples: ['工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND,客户要求验证替代料'],
|
||
},
|
||
originalInternalPn: { type: 'string', title: '原内部料号(P-08)', examples: ['LM2596S-5.0'] },
|
||
candidateInternalPns: {
|
||
type: 'array',
|
||
title: '候选内部料号(数组,≥1;不在库的启动后经数据工单创建)',
|
||
items: { type: 'string' },
|
||
examples: [['XL2596S-5.0', 'LM2596S-ADJ']],
|
||
},
|
||
datasheets: {
|
||
type: 'array',
|
||
format: 'file',
|
||
title: '候选 datasheet(选填;传了 AI 在数据工单里预填,没传人全填)',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
name: { type: 'string', title: '文件名' },
|
||
contentType: { type: 'string', title: '类型' },
|
||
size: { type: 'integer', title: '字节数' },
|
||
ref: { type: 'string', title: '存档号' },
|
||
text: { type: 'string', title: '读出来的文本' },
|
||
note: { type: 'string', title: '读取说明' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
messageAllowed: true,
|
||
messageHint: 'FAE 可在此说明客户的应用边界、验证条件或本轮评估的特殊要求',
|
||
},
|
||
output: {
|
||
/* 单一必发口:本活动终点是 ACT-GATE-03 评审放行;下游 ACT-WORK-08 尚未实现,按「契约先立」声明 */
|
||
mandatory: [
|
||
{
|
||
name: 'assessment-released',
|
||
description: '差异评估结论已落账(P-11 替代关系主记录 + 差异明细、P-01 回写)—— 供 ACT-WORK-08 证据归集消费;当前无消费者,载荷 = 已落地事实',
|
||
payload: {
|
||
type: 'object',
|
||
required: ['evalRef', 'requestId', 'relationTotal'],
|
||
properties: {
|
||
evalRef: { type: 'string', title: '评估案号' },
|
||
requestId: { type: 'string', title: '需求编号(P-01)' },
|
||
relationTotal: { type: 'integer', title: '落账的替代关系条数' },
|
||
recommendedTotal: { type: 'integer', title: '推荐' },
|
||
conditionalTotal: { type: 'integer', title: '条件推荐' },
|
||
alternateTotal: { type: 'integer', title: '备选' },
|
||
notRecommendedTotal: { type: 'integer', title: '不推荐' },
|
||
},
|
||
},
|
||
},
|
||
],
|
||
selective: [],
|
||
},
|
||
requiredDeliverables: [
|
||
{ key: 'eval-input', label: '评估输入清单', ledger: `${D}.input_gap`, ledgers: [`${D}.input_gap`, `${D}.pn_check`, `${D}.verdict`], editableAtRelease: [] },
|
||
{ key: 'difference-items', label: '差异评估结论(P-11 差异明细)', ledger: 'substitution.difference_item', ledgers: ['substitution.difference_item', 'substitution.relation'], editableAtRelease: [] },
|
||
{ key: 'landing', label: '拟落账内容(P-11 主记录 + P-01 回写)', ledger: 'substitution.relation', ledgers: ['substitution.relation', 'demand.request'], editableAtRelease: [] },
|
||
],
|
||
/*
|
||
* 验收判据 —— L1/L2 只读基座读数;L4 三条是文档验收工单 01/02 里留给人的裁量;
|
||
* 「推荐等级与明细一致」「硬约束不判 MATCH」机器已由工具守住,这里仍留 L4 让签字的人过目。
|
||
*/
|
||
acceptanceCriteria: [
|
||
{
|
||
key: 'input-confirmed',
|
||
label: '评估输入已经 PW 补齐确认:执行工单 01 已交回,缺口行的补充内容都有,补不了的留了 UNKNOWN',
|
||
level: 'L1',
|
||
predicate: `input_confirmed_total > 0 and ${D}_input_gap_rows > 0 and ${D}_input_gap_missing = 0`,
|
||
},
|
||
{
|
||
key: 'review-approved',
|
||
label: '差异评估结论已经 FAE/RI 审核通过:验收工单 01 已过,不处在驳回待改状态',
|
||
level: 'L1',
|
||
predicate: 'review_approved_total > 0 and substitution_difference_item_rows > 0 and substitution_difference_item_missing = 0',
|
||
},
|
||
{
|
||
key: 'every-candidate-assessed',
|
||
label: '每个未放弃的候选都有一条替代关系记录(N 候选 = N 条 P-11,并行独立落账)',
|
||
level: 'L2',
|
||
predicate: `substitution_relation_rows > 0 and substitution_relation_rows = count(candidateInternalPns) - ${D}_pn_check_exists_status_abandoned`,
|
||
},
|
||
{
|
||
key: 'every-item-has-status',
|
||
label: '关键参数差异均有明确状态:每条明细都是 MATCH / MISMATCH / UNKNOWN / CONDITIONAL 之一',
|
||
level: 'L2',
|
||
predicate:
|
||
'substitution_difference_item_rows > 0 and substitution_difference_item_difference_status_match + substitution_difference_item_difference_status_mismatch + ' +
|
||
'substitution_difference_item_difference_status_unknown + substitution_difference_item_difference_status_conditional = substitution_difference_item_rows',
|
||
},
|
||
{
|
||
key: 'unknown-has-disposition',
|
||
label: 'UNKNOWN 项均有去向:每个 UNKNOWN 都挂了验证任务或补来源动作,影响说明与证据引用逐条齐全',
|
||
level: 'L2',
|
||
predicate: 'substitution_difference_item_missing = 0',
|
||
},
|
||
{
|
||
key: 'judged-by-rule',
|
||
label: '四态与推荐等级由规则算出:逐参数比对工具与推荐定级工具都真调过',
|
||
level: 'L2',
|
||
predicate: `tool_part_substitution_assessment_compare_parameters_calls > 0 and tool_part_substitution_assessment_recommend_calls > 0`,
|
||
},
|
||
{
|
||
key: 'analysis-status-confirmed',
|
||
label: '分析状态与客户批准状态分离:每条 P-11 只写分析层 analysis_status = FAE 确认,规则版本已锁定随结论落账',
|
||
level: 'L2',
|
||
predicate: 'substitution_relation_analysis_status_fae_confirmed = substitution_relation_rows and substitution_relation_missing = 0',
|
||
},
|
||
{
|
||
key: 'p01-writeback-complete',
|
||
label: '回写 P-01 字段完整:风险等级、验证要求、输出链接、下一步动作、当前阶段五项齐全',
|
||
level: 'L2',
|
||
predicate: 'demand_request_rows > 0 and demand_request_missing = 0',
|
||
},
|
||
{ key: 'evidence-credible', label: '证据链可信:每条结论都能追溯到 P-08 / P-10 / P-06 的具体记录,或本单数据工单的创建记录(人工录入)', level: 'L4' },
|
||
{ key: 'hard-not-auto-match', label: '硬约束项未被自动判 MATCH:封装 / 工艺 / 电气边界等硬约束的 MATCH 都有证据支持', level: 'L4' },
|
||
{ key: 'recommendation-consistent', label: '推荐等级与差异明细一致:存在 UNKNOWN / CONDITIONAL 的候选没有给「推荐」', level: 'L4' },
|
||
/* L5 是责任关:那张卡本身就是这道关,卡面上不摆它(基座统一滤) */
|
||
{ key: 'gate03-release', label: 'ACT-GATE-03 评审放行:签下去 P-11 落账、P-01 回写,结论供下游消费', level: 'L5' },
|
||
],
|
||
revisableAtRelease: [],
|
||
prohibitions: [
|
||
{ key: 'no-fabricated-parameter', statement: 'UNKNOWN 是一等公民:查不到的参数值必须显式写「未披露」并记去向,不得伪造、不得用估计值或空串冒充' },
|
||
{ key: 'no-override-hard-mismatch', statement: '硬约束 MISMATCH 不得被任何措辞改判 MATCH;要改只能补证据后重比' },
|
||
{ key: 'no-customer-approval-write', statement: '客户批准状态由独立审批流维护,本活动不写 customer_approval_status(没有这一列的写权)' },
|
||
{ key: 'no-commercial-judgement', statement: '本活动只答技术可替代性:不做数量、价格、交期、采购判断' },
|
||
{ key: 'no-rebind', statement: '启动绑定的 4 项(客户 / 需求 / 原 PN / 候选数组)工单内不可改;绑错只能退回触发方重新启动' },
|
||
{ key: 'no-external-write', statement: '放行前不得触达外部系统 —— 触达世界的动作集中在放行那一刻' },
|
||
],
|
||
authorization: {
|
||
discretion: [],
|
||
preauthorizedActions: [],
|
||
/*
|
||
* 基线:数据工单 ≤2 + 候选放弃/创建决策 ≤2 + 新客户录入卡 1 + 执行单 01 + 审核决策 1 + 放行 1 = 8;
|
||
* 审核循环预留两轮(+4)= 12。cap 必须严格大于最后一次拍板时的实际次数(E04 实测栽过),留到 16。
|
||
*/
|
||
humanCallBudget: 16,
|
||
},
|
||
resources: {
|
||
tools: [{ name: TOOL_COMPARE }, { name: TOOL_RECOMMEND }],
|
||
skills: ASSESSMENT_SKILLS.map((s) => s.id),
|
||
models: ['deepseek-v4-flash'],
|
||
roles: ['FAE/RI'],
|
||
budget: { steps: 90, tokens: 600_000, activeMinutes: 120, humanCalls: 16 },
|
||
},
|
||
},
|
||
|
||
/* ── 声明 ────────────────────────────────────────────────────── */
|
||
declarations: {
|
||
models: ['deepseek-v4-flash'],
|
||
tools: [{ name: TOOL_COMPARE }, { name: TOOL_RECOMMEND }],
|
||
ledger: [
|
||
/* L-02 客户主台账:只在分支 0a(客户不在库)由数据录入卡建一行 —— 录入卡写的列都得声明 */
|
||
{
|
||
domain: 'customer',
|
||
object: 'profile',
|
||
kind: 'master',
|
||
label: '客户档案',
|
||
fields: [
|
||
col({ name: 'customer_code', type: 'text', label: '客户编码', primaryKey: true }),
|
||
col({ name: 'name', type: 'text', label: '客户名称', required: true }),
|
||
col({ name: 'industry', type: 'text', label: '所属行业', expect: true }),
|
||
col({ name: 'region', type: 'text', label: '区域', expect: true }),
|
||
],
|
||
},
|
||
/* P-08 芯片主数据:数据工单建整条;参数集是它的一个字段(一张表,不建子表) */
|
||
{
|
||
domain: 'parts',
|
||
object: 'profile',
|
||
kind: 'master',
|
||
label: '芯片主数据(P-08)',
|
||
fields: [
|
||
col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }),
|
||
col({ name: 'brand_name', type: 'text', label: '品牌名称', required: true }),
|
||
col({ name: 'manufacturer_name', type: 'text', label: '制造商名称', expect: true }),
|
||
col({ name: 'specification_description', type: 'text', label: '规格描述', required: true }),
|
||
col({ name: 'normalized_parameter_set', type: 'text', label: '标准化参数集(一行一参数:参数名 | 值 | 单位 | hard/soft | 说明)', required: true }),
|
||
col({ name: 'package', type: 'text', label: '封装', required: true }),
|
||
col({ name: 'process_technology', type: 'text', label: '工艺', expect: true }),
|
||
col({ name: 'lifecycle_status', type: 'text', label: '生命周期状态', required: true, tally: ['active', 'nrnd', 'eol', 'discontinued', 'risk'] }),
|
||
col({ name: 'eol_pcn_status', type: 'text', label: 'EOL/PCN 状态', expect: true }),
|
||
col({ name: 'datasheet_ref', type: 'text', label: 'Datasheet 引用', expect: true }),
|
||
col({ name: 'datasheet_version', type: 'text', label: 'Datasheet 版本', expect: true }),
|
||
col({ name: 'application_scenarios', type: 'text', label: '适配应用场景', expect: true }),
|
||
],
|
||
},
|
||
/* P-10 原厂技术资料库:数据工单建;评估时只读做证据回链 */
|
||
{
|
||
domain: 'parts',
|
||
object: 'document',
|
||
kind: 'master',
|
||
label: '原厂技术资料库(P-10)',
|
||
fields: [
|
||
col({ name: 'internal_pn', type: 'text', label: '关联内部料号', primaryKey: true }),
|
||
col({ name: 'doc_seq', type: 'text', label: '资料序号(同一料号下 01/02…)', primaryKey: true }),
|
||
col({ name: 'document_type', type: 'text', label: '资料类型', required: true, tally: ['datasheet', 'app_note', 'reference_design', 'white_paper', 'sample_doc', 'commitment_letter', 'other'] }),
|
||
col({ name: 'brand_name', type: 'text', label: '品牌名称', expect: true }),
|
||
col({ name: 'document_title', type: 'text', label: '资料标题', required: true }),
|
||
col({ name: 'document_version', type: 'text', label: '资料版本', required: true }),
|
||
col({ name: 'document_ref', type: 'text', label: '资料文件链接', required: true }),
|
||
col({ name: 'citation_locator', type: 'text', label: '证据定位(页码/章节/表格)', expect: true }),
|
||
col({ name: 'document_status', type: 'text', label: '资料状态', required: true, tally: ['valid', 'expired', 'pending', 'void'] }),
|
||
],
|
||
},
|
||
/* P-01 需求主记录:S0 新建 V1,放行时回写五项(current_stage = gate03 那一刻五项必填) */
|
||
{
|
||
domain: 'demand',
|
||
object: 'request',
|
||
kind: 'master',
|
||
label: '客户需求主记录(P-01)',
|
||
fields: [
|
||
col({ name: 'request_id', type: 'text', label: '需求编号', primaryKey: true }),
|
||
col({ name: 'customer_code', type: 'text', label: '客户编码', required: true, refersTo: 'customer.profile.customer_code' }),
|
||
col({ name: 'requirement_version', type: 'text', label: '需求版本', required: true }),
|
||
col({ name: 'request_type', type: 'text', label: '需求类型', required: true }),
|
||
col({ name: 'request_description', type: 'text', label: '需求描述', required: true }),
|
||
col({ name: 'input_snapshot_ref', type: 'text', label: '输入快照', required: true }),
|
||
col({ name: 'original_internal_pn', type: 'text', label: '原内部料号', required: true }),
|
||
col({ name: 'bom_position', type: 'text', label: 'BOM 位', expect: true }),
|
||
col({ name: 'application_scenario', type: 'text', label: '应用场景', expect: true }),
|
||
col({ name: 'technical_constraints', type: 'text', label: '技术约束' }),
|
||
col({ name: 'risk_level', type: 'text', label: '风险等级', tally: ['low', 'medium', 'high', 'critical'], requiredWhen: { column: 'current_stage', value: 'gate03' } }),
|
||
col({ name: 'verification_requirement', type: 'text', label: '验证/复测/确认要求', requiredWhen: { column: 'current_stage', value: 'gate03' } }),
|
||
col({ name: 'request_status', type: 'text', label: '需求状态', required: true, tally: ['pending', 'responded', 'won', 'abandoned', 'closed'] }),
|
||
col({ name: 'current_activity', type: 'text', label: '当前 Activity', requiredWhen: { column: 'current_stage', value: 'gate03' } }),
|
||
col({ name: 'current_stage', type: 'text', label: '当前阶段' }),
|
||
col({ name: 'next_action', type: 'text', label: '下一步动作', requiredWhen: { column: 'current_stage', value: 'gate03' } }),
|
||
col({ name: 'output_ref', type: 'text', label: '标准输出链接', requiredWhen: { column: 'current_stage', value: 'gate03' } }),
|
||
],
|
||
},
|
||
/* P-11 替代关系主记录:一候选一条;两枚 PN 都是引用墙(查无此料当场拒 —— 存在性检查的硬门) */
|
||
{
|
||
domain: 'substitution',
|
||
object: 'relation',
|
||
kind: 'master',
|
||
label: '替代关系主记录(P-11)',
|
||
fields: [
|
||
col({ name: 'substitution_relation_id', type: 'text', label: '替代关系ID', primaryKey: true }),
|
||
col({ name: 'eval_ref', type: 'text', label: '评估案号', required: true }),
|
||
col({ name: 'request_id', type: 'text', label: '关联需求编号', required: true }),
|
||
col({ name: 'bom_position', type: 'text', label: '关联 BOM 位', expect: true }),
|
||
col({ name: 'original_internal_pn', type: 'text', label: '原内部料号', required: true, refersTo: 'parts.profile.internal_pn' }),
|
||
col({ name: 'candidate_internal_pn', type: 'text', label: '候选内部料号', required: true, refersTo: 'parts.profile.internal_pn' }),
|
||
col({ name: 'substitution_type', type: 'text', label: '替代类型', required: true, tally: ['pin_to_pin', 'functional', 'parametric', 'other'] }),
|
||
col({ name: 'recommendation_level', type: 'text', label: '推荐等级', required: true, tally: ['recommended', 'conditional', 'alternate', 'not_recommended'] }),
|
||
col({ name: 'recommendation_reason', type: 'text', label: '推荐理由', required: true }),
|
||
col({ name: 'difference_summary', type: 'text', label: '差异摘要', required: true }),
|
||
col({ name: 'risk_level', type: 'text', label: '替代风险等级', required: true, tally: ['low', 'medium', 'high', 'critical'] }),
|
||
col({ name: 'verification_requirement', type: 'text', label: '验证要求', requiredWhen: { column: 'recommendation_level', value: 'conditional' } }),
|
||
col({ name: 'validation_id', type: 'text', label: '关联验证ID' }),
|
||
col({ name: 'analysis_status', type: 'text', label: '分析状态', required: true, tally: ['ai_draft', 'fae_confirmed', 'pending_customer', 'customer_approved', 'rejected', 'void'] }),
|
||
col({ name: 'analysis_report_ref', type: 'text', label: '替代分析报告', expect: true }),
|
||
col({ name: 'confidence_score', type: 'number', label: '证据可信度', expect: true }),
|
||
col({ name: 'rule_version', type: 'text', label: '差异规则版本', required: true }),
|
||
],
|
||
},
|
||
/* P-11 差异明细子表:一条参数一条记录;relation id 上 distinct 让基座数「几条关系有明细」 */
|
||
{
|
||
domain: 'substitution',
|
||
object: 'difference_item',
|
||
kind: 'master',
|
||
label: '差异明细(P-11 子表)',
|
||
fields: [
|
||
col({ name: 'substitution_relation_id', type: 'text', label: '替代关系ID', primaryKey: true, distinct: true, refersTo: 'substitution.relation.substitution_relation_id' }),
|
||
col({ name: 'difference_item_id', type: 'text', label: '差异明细ID', primaryKey: true }),
|
||
col({ name: 'parameter_name', type: 'text', label: '标准参数名', required: true }),
|
||
col({ name: 'original_value', type: 'text', label: '原物料参数值', required: true }),
|
||
col({ name: 'candidate_value', type: 'text', label: '候选物料参数值', required: true }),
|
||
col({ name: 'parameter_unit', type: 'text', label: '参数单位', expect: true }),
|
||
col({ name: 'constraint_type', type: 'text', label: '约束类型', required: true, tally: ['hard', 'soft'] }),
|
||
col({ name: 'compare_result', type: 'text', label: '比对结果(工具出参)', expect: true }),
|
||
col({ name: 'difference_status', type: 'text', label: '差异状态', required: true, tally: ['match', 'mismatch', 'unknown', 'conditional'] }),
|
||
col({ name: 'impact_statement', type: 'text', label: '影响说明(MATCH 写「无」)', required: true }),
|
||
col({ name: 'unknown_disposition', type: 'text', label: 'UNKNOWN 去向(验证任务号 / 补来源动作)', requiredWhen: { column: 'difference_status', value: 'unknown' } }),
|
||
col({ name: 'risk_level', type: 'text', label: '风险等级', required: true, tally: ['low', 'medium', 'high'] }),
|
||
col({ name: 'evidence_ref', type: 'text', label: '差异证据引用', required: true }),
|
||
col({ name: 'confidence', type: 'number', label: 'AI 置信度', expect: true }),
|
||
col({ name: 'rule_version', type: 'text', label: '差异规则版本', required: true }),
|
||
],
|
||
},
|
||
/* P-06 验证记录:事实类 —— 本活动只写「验证要求」(状态 not_started),不经数据工单建 */
|
||
{
|
||
domain: 'validation',
|
||
object: 'record',
|
||
kind: 'master',
|
||
label: '芯片验证记录(P-06)',
|
||
fields: [
|
||
col({ name: 'validation_id', type: 'text', label: '验证ID', primaryKey: true }),
|
||
col({ name: 'request_id', type: 'text', label: '关联需求编号', required: true }),
|
||
col({ name: 'validation_internal_pn', type: 'text', label: '验证内部料号', required: true, refersTo: 'parts.profile.internal_pn' }),
|
||
col({ name: 'validation_type', type: 'text', label: '验证类型', required: true, tally: ['pin_to_pin', 'functional', 'reliability', 'certification', 'retest'] }),
|
||
col({ name: 'validation_status', type: 'text', label: '验证状态', required: true, tally: ['not_started', 'in_progress', 'passed', 'failed', 'conditional', 'pending_evidence'] }),
|
||
col({ name: 'test_condition', type: 'text', label: '测试条件', expect: true }),
|
||
col({ name: 'next_validation_action', type: 'text', label: '后续验证动作', required: true }),
|
||
col({ name: 'evidence_ref', type: 'text', label: '验证证据引用', expect: true }),
|
||
],
|
||
},
|
||
/* ── 活动自持单据(record,caseKey eval_ref)── */
|
||
{
|
||
domain: D,
|
||
object: 'pn_check',
|
||
kind: 'record',
|
||
caseKey: 'eval_ref',
|
||
label: '料号存在性核对',
|
||
fields: [
|
||
col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }),
|
||
col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }),
|
||
col({ name: 'role', type: 'text', label: '角色', required: true, tally: ['original', 'candidate'] }),
|
||
col({ name: 'exists_status', type: 'text', label: '在库状态', required: true, tally: ['found', 'absent', 'created', 'abandoned'] }),
|
||
col({ name: 'abandon_reason', type: 'text', label: '放弃原因', requiredWhen: { column: 'exists_status', value: 'abandoned' } }),
|
||
col({ name: 'note', type: 'text', label: '备注' }),
|
||
],
|
||
},
|
||
{
|
||
domain: D,
|
||
object: 'input_gap',
|
||
kind: 'record',
|
||
caseKey: 'eval_ref',
|
||
label: '评估输入清单',
|
||
fields: [
|
||
col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }),
|
||
col({ name: 'item_key', type: 'text', label: '输入项键', primaryKey: true }),
|
||
col({ name: 'input_item', type: 'text', label: '输入项', required: true }),
|
||
col({ name: 'source_ledger', type: 'text', label: '来源台账', expect: true }),
|
||
col({ name: 'status', type: 'text', label: '状态', required: true, tally: ['gap', 'default', 'ok'] }),
|
||
col({ name: 'detail', type: 'text', label: '绑定值 / 缺口说明', expect: true }),
|
||
col({ name: 'supplement', type: 'text', label: '补充内容', requiredWhen: { column: 'status', value: 'gap' } }),
|
||
col({ name: 'origin', type: 'text', label: '来源(ai/human-added)' }),
|
||
],
|
||
},
|
||
{
|
||
domain: D,
|
||
object: 'verdict',
|
||
kind: 'record',
|
||
caseKey: 'eval_ref',
|
||
label: '本次评估结论',
|
||
fields: [
|
||
col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }),
|
||
col({ name: 'request_id', type: 'text', label: '需求编号' }),
|
||
col({ name: 'rule_version', type: 'text', label: '规则版本' }),
|
||
col({ name: 'original_internal_pn', type: 'text', label: '原内部料号' }),
|
||
col({ name: 'candidate_total', type: 'integer', label: '候选数' }),
|
||
col({ name: 'input_confirmed_total', type: 'integer', label: '评估输入已确认' }),
|
||
col({ name: 'review_approved_total', type: 'integer', label: '差异结论已审核通过' }),
|
||
col({ name: 'landing_prepared_total', type: 'integer', label: '拟落账已备好' }),
|
||
col({ name: 'relation_total', type: 'integer', label: '替代关系条数' }),
|
||
col({ name: 'recommended_total', type: 'integer', label: '推荐' }),
|
||
col({ name: 'conditional_total', type: 'integer', label: '条件推荐' }),
|
||
col({ name: 'alternate_total', type: 'integer', label: '备选' }),
|
||
col({ name: 'not_recommended_total', type: 'integer', label: '不推荐' }),
|
||
col({ name: 'summary', type: 'text', label: '本次评估结论' }),
|
||
],
|
||
},
|
||
],
|
||
listeners: [],
|
||
harness: {
|
||
memoryWindow: 24,
|
||
stallAfter: 'PT4H',
|
||
nudge: [{ after: 'PT2H', action: 'nudge' }],
|
||
retry: { times: 2, backoff: 'PT30S' },
|
||
},
|
||
},
|
||
|
||
/* ── 资产初始版 ──────────────────────────────────────────────── */
|
||
assets: {
|
||
businessModel: { cmmnXml: CMMN_XML, narrative: NARRATIVE, dmn: ASSESSMENT_DMN, steps: ASSESSMENT_STEPS, phases: ASSESSMENT_PHASES },
|
||
skills: ASSESSMENT_SKILLS,
|
||
/*
|
||
* 五个局面各一张:
|
||
* · `p08-master-create` —— S0d 摆,数据工单(P-08),件上可加行、可编辑;
|
||
* · `p10-document-create` —— S0d 摆,数据工单(P-10),同上;
|
||
* · `eval-input-list` —— S1 摆,评估输入清单:绑定区只读 + 缺口表可编辑;
|
||
* · `difference-review` —— S2/S3/S4 摆,差异明细 + 各候选推荐等级,只读;
|
||
* · `landing-confirm` —— S5/T_release 摆,拟写 P-11 / 拟回写 P-01,只读;放行后即归档件。
|
||
*/
|
||
viewTemplates: [
|
||
{ name: 'p08-master-create', html: P08_MASTER_CREATE },
|
||
{ name: 'p10-document-create', html: P10_DOCUMENT_CREATE },
|
||
{ name: 'eval-input-list', html: EVAL_INPUT_LIST },
|
||
{ name: 'difference-review', html: DIFFERENCE_REVIEW },
|
||
{ name: 'landing-confirm', html: LANDING_CONFIRM },
|
||
],
|
||
cardSchemas: {},
|
||
distillHooks: [{ on: 'terminal', prompt: '归纳本次差异评估:哪些候选、各自推荐等级与关键依据、UNKNOWN 挂了什么验证任务、审核是否被驳回过(为什么)、数据工单建了哪些主数据。' }],
|
||
},
|
||
|
||
/*
|
||
* ── 样例集:五条形状真不同(常备案例即验收案例)──
|
||
* 第一条满载且走「候选缺库 → 数据工单」那条最长的路(测试台落地即预填的是它);样例名 ≤14 字。
|
||
* 库里只播了 LM2596S-5.0 / LM2596S-ADJ 两颗(见 seed-mvp-data.ts),XL2596S-5.0 / CN-DCDC-32V / LM2596S-3.3 刻意不在库。
|
||
*/
|
||
samples: [
|
||
{
|
||
name: '候选缺库·派数据工单',
|
||
input: {
|
||
customerCode: 'CU-0142',
|
||
requestDescription: '工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND,客户要求验证替代料;应用 12V→5V、工业温域、输入上限 40V',
|
||
originalInternalPn: 'LM2596S-5.0',
|
||
candidateInternalPns: ['XL2596S-5.0', 'LM2596S-ADJ'],
|
||
},
|
||
expect: { ports: ['assessment-released'] },
|
||
},
|
||
{
|
||
name: '都在库·常路',
|
||
input: {
|
||
customerCode: 'CU-0142',
|
||
requestDescription: 'LM2596S-5.0 转 NRND,客户先评估同族可调版 LM2596S-ADJ 能否替代(愿意改外围);应用 12V→5V、工业温域',
|
||
originalInternalPn: 'LM2596S-5.0',
|
||
candidateInternalPns: ['LM2596S-ADJ'],
|
||
},
|
||
expect: { ports: ['assessment-released'] },
|
||
},
|
||
{
|
||
name: '原PN缺库·硬阻塞',
|
||
input: {
|
||
customerCode: 'CU-0142',
|
||
requestDescription: '客户另一块板在用 LM2596S-3.3(3.3V 固定版),同批 PCN,要求评估 LM2596S-ADJ 替代;应用 12V→3.3V',
|
||
originalInternalPn: 'LM2596S-3.3',
|
||
candidateInternalPns: ['LM2596S-ADJ'],
|
||
},
|
||
expect: { ports: ['assessment-released'] },
|
||
},
|
||
{
|
||
name: '三候选·含硬约束不推荐',
|
||
input: {
|
||
customerCode: 'CU-0142',
|
||
requestDescription: '工业电源 12V→5V,输入上限 40V,工业温域;三个候选:XL2596S-5.0、LM2596S-ADJ、国产 CN-DCDC-32V(输入上限只有 32V)',
|
||
originalInternalPn: 'LM2596S-5.0',
|
||
candidateInternalPns: ['XL2596S-5.0', 'LM2596S-ADJ', 'CN-DCDC-32V'],
|
||
},
|
||
expect: { ports: ['assessment-released'] },
|
||
},
|
||
{
|
||
name: '新客户·先建档',
|
||
input: {
|
||
customerCode: 'CU-9001',
|
||
customerName: '深圳恒星电源有限公司',
|
||
requestDescription: '新客户口述:工业电源 12V→5V 在用 LM2596S-5.0,想评估 LM2596S-ADJ',
|
||
originalInternalPn: 'LM2596S-5.0',
|
||
candidateInternalPns: ['LM2596S-ADJ'],
|
||
},
|
||
expect: { ports: ['assessment-released'] },
|
||
},
|
||
],
|
||
|
||
entry: { file: 'index.js', checksum: 'dev-entry-part-substitution-assessment' },
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 4: 先放两份占位(让 Task 3 的测试能跑;Task 4/5 整份替换)**
|
||
|
||
`src/case-model.ts` 占位:
|
||
```ts
|
||
export const CMMN_XML = '';
|
||
export const NARRATIVE = '';
|
||
export const ASSESSMENT_STEPS: ReadonlyArray<{ item: string; intent: string; reads: string[]; writes: string[]; tools: string[]; view?: string; doneWhen?: string; guide: string }> = [];
|
||
export const ASSESSMENT_PHASES: ReadonlyArray<{ name: string; items: string[] }> = [];
|
||
```
|
||
`src/views/report.ts` 占位:
|
||
```ts
|
||
export const P08_MASTER_CREATE = '<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 收尾时全绿)
|
||
|
||
```ts
|
||
/**
|
||
* ACT-WORK-05 差异评估的包内自测 —— **只准 import @sdk / @contracts / @sfeel**。
|
||
* 案例活动没有 `run`,自测不跑回合;测的是清单本身与四处对齐
|
||
* (判据谓词 / 台账声明的授权面 / 决策卡循环骨架 / 报告模版的读写面),结构照 E04 / I01。
|
||
*/
|
||
import { describe, expect, it } from 'vitest';
|
||
import { activityManifestSchema } from '@contracts';
|
||
import { createMockHost } from '@sdk';
|
||
import { manifest, D, RULE_VERSION } from '../src/manifest.ts';
|
||
import activity from '../src/index.ts';
|
||
|
||
const ITEMS = ['S0_bind', 'S0d_master_data', 'S1_input_confirm', 'S2_normalize', 'S3_assess', 'S4_review', 'S5_prepare_landing', 'T_release'];
|
||
const NO_DONE_WHEN = new Set(['S0d_master_data', 'S1_input_confirm', 'S4_review', 'T_release']);
|
||
const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'difference-review', 'landing-confirm'];
|
||
const byAddress = (domain: string, object: string) => manifest.declarations.ledger.find((l) => l.domain === domain && l.object === object);
|
||
const viewHtml = (name: string) => manifest.assets.viewTemplates.find((t) => t.name === name)?.html ?? '';
|
||
|
||
describe('清单本身合规', () => {
|
||
it('过 @contracts 的 schema', () => {
|
||
expect(() => activityManifestSchema.parse(manifest)).not.toThrow();
|
||
});
|
||
|
||
it('没有静态监听(等外部回音一律落在执行工单上,listen 在本仓不可用)', () => {
|
||
expect(manifest.declarations.listeners).toEqual([]);
|
||
});
|
||
|
||
it('L1/L2 判据都给了谓词;L4 三条不给(裁量项);L5 恰一条', () => {
|
||
for (const c of manifest.contract.acceptanceCriteria) {
|
||
if (c.level === 'L1' || c.level === 'L2') expect(c.predicate, c.key).toBeDefined();
|
||
if (c.level === 'L4') expect(c.predicate, c.key).toBeUndefined();
|
||
}
|
||
expect(manifest.contract.acceptanceCriteria.filter((c) => c.level === 'L4')).toHaveLength(3);
|
||
expect(manifest.contract.acceptanceCriteria.filter((c) => c.level === 'L5')).toHaveLength(1);
|
||
});
|
||
|
||
it('机检谓词只读基座读数与记账旗:不出现活动自报的汇总列名', () => {
|
||
const forbidden = ['relation_total', 'candidate_total', 'recommended_total', 'not_recommended_total'];
|
||
for (const c of manifest.contract.acceptanceCriteria) {
|
||
for (const name of forbidden) expect(c.predicate ?? '', `${c.key} 读了自报数 ${name}`).not.toContain(name);
|
||
}
|
||
});
|
||
|
||
it('三件必交付物都有台账地址;人环预算严格大于「基线 8 + 两轮审核循环 4」', () => {
|
||
expect(manifest.contract.requiredDeliverables).toHaveLength(3);
|
||
for (const d of manifest.contract.requiredDeliverables) expect(d.ledger, d.key).toBeDefined();
|
||
expect(manifest.contract.authorization.humanCallBudget).toBeGreaterThan(12);
|
||
expect(manifest.contract.resources.budget.humanCalls).toBeGreaterThan(12);
|
||
});
|
||
|
||
it('起单只必填 4 项(客户 / 需求 / 原 PN / 候选数组);datasheet 是选填文件', () => {
|
||
expect(manifest.contract.input.data['required']).toEqual(['customerCode', 'requestDescription', 'originalInternalPn', 'candidateInternalPns']);
|
||
const properties = manifest.contract.input.data['properties'] as Record<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: 提交骨架**
|
||
|
||
```bash
|
||
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`;关系 id `REL-<数字串>-01/02…`(按候选数组顺序);明细 id `<关系id>-P01/P02…`;验证 id `VAL-<数字串>-01/02…`;报告号 `RPT-<数字串>`。
|
||
- 卡标题约定(e2e 按标题对号):数据工单 P-08 以「数据工单 · 芯片主数据创建」开头;P-10 以「数据工单 · 原厂技术资料创建」开头;执行单以「评估输入与资料补齐确认」开头。
|
||
|
||
- [ ] **Step 1: 写 `src/case-model.ts`**
|
||
|
||
```ts
|
||
/**
|
||
* ACT-WORK-05 差异评估的案例模型 —— **双表示同版本**:CMMN XML(机器面)与案例说明书(叙事面)。
|
||
*
|
||
* 业务出处:`docs/SS02-ACT-WORK-05-html/ACT-WORK-05-活动流程与工单说明.md`(步骤 0.5 + 5 步,
|
||
* 1 数据(条件)+ 1 执行 + 2 验收)与《SS02-SS03-SS04_Demo台账字段细化与统一方案》(字段口径)。
|
||
*
|
||
* 文档六步收成 8 个案例模型项:
|
||
* 启动期 → S0_bind(AI:铸案号、建 P-01 V1、锁规则版本、逐个 PN 查库、写存在性核对表)
|
||
* 0.5 等待数据 → S0d_master_data(人:数据工单 —— P-08 一张、P-10 一张;候选 ≥2 缺库先决策卡;
|
||
* 库里都有就 disable 这一项)
|
||
* 1 建立评估上下文 → S1_input_confirm(人:执行工单 01,评估输入清单,件上补缺口)
|
||
* 2 读取并标准化 → S2_normalize(AI:P-11 主记录骨架 + 差异明细对照行)
|
||
* 3 逐项差异评估 → S3_assess(AI + 两枚工具:四态 / 风险 / 证据 / 影响 / 推荐等级 / 验证任务)
|
||
* 4 FAE/RI 审核 → S4_review(人:验收工单 01 = 决策卡循环,驳回则 AI 改台账再发)
|
||
* 5 提交评审放行 → S5_prepare_landing(AI:analysis_status 推 fae_confirmed、回写 P-01)+ T_release(人:验收工单 02,终局放行)
|
||
*
|
||
* 四处不给 doneWhen:S0d(触达人)、S1(交人补缺口)、S4(拍板且可循环)、T_release(签下去)。
|
||
*/
|
||
import { D, RULE_VERSION, TOOL_COMPARE, TOOL_RECOMMEND } from './constants.ts';
|
||
|
||
export const CMMN_XML = `<?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`**
|
||
|
||
```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: 提交**
|
||
|
||
```bash
|
||
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`(数据驱动的开关全在那里:三档占位 / 缺项红带 / 编辑闸 / 增删行 / 状态徽基座);然后按下面写其余内容:
|
||
|
||
```ts
|
||
/**
|
||
* ACT-WORK-05 差异评估的 HTML 报告模版 —— **活动自己持有的资产**,随包版本冻结。
|
||
*
|
||
* 五个局面各一张:
|
||
* · `p08-master-create` —— S0d 摆,数据工单(P-08 芯片主数据),可加行、可编辑;
|
||
* · `p10-document-create` —— S0d 摆,数据工单(P-10 原厂技术资料),可加行、可编辑;
|
||
* · `eval-input-list` —— S0/S1 摆,评估输入清单:绑定区只读 + 缺口表可编辑;
|
||
* · `difference-review` —— S2/S3/S4 摆,各候选推荐等级 + 差异明细,**只读**(通过/驳回在对话区决策卡);
|
||
* · `landing-confirm` —— S5/T_release 摆,拟写 P-11 / 拟回写 P-01,**只读**,放行后即归档件。
|
||
*
|
||
* 版面语言逐字量自 `docs/SS02-ACT-WORK-05-html/` 四份目标件;取值三纪律:顶层只取汇总列与单值列,
|
||
* 逐条走 `mine:域.对象`,列名逐字照 `declarations.ledger` 抄。**模版不算不判**。
|
||
*/
|
||
import { PRD_CSS } from './prd-style.ts';
|
||
import { D } from '../constants.ts';
|
||
|
||
/* ==================== 绑定期补的样式(从 I01 report.ts 的 BIND_CSS 原样复制)==================== */
|
||
const BIND_CSS = `…(I01 那一段,原样)…`;
|
||
|
||
/* ==================== 本活动的枚举徽标与参数集格子 ==================== */
|
||
const EXTRA_CSS = `
|
||
/* 参数集一行一参数:只读那一份按行换行显示 */
|
||
.proto-root .jc-pre { white-space: pre-line; font-family: var(--mono); font-size: 12px; }
|
||
.proto-root textarea.jc-cell { min-height: 120px; font-family: var(--mono); font-size: 12px; }
|
||
/* 徽标按列作用域取人读名(同一个 v-conditional 在推荐等级与四态里是两个词) */
|
||
.proto-root .jc-tag.t-role.v-original::after { content: "原 PN"; }
|
||
.proto-root .jc-tag.t-role.v-candidate::after { content: "候选"; }
|
||
.proto-root .jc-tag.t-exist.v-found::after { content: "在库"; }
|
||
.proto-root .jc-tag.t-exist.v-absent::after { content: "不在库"; }
|
||
.proto-root .jc-tag.t-exist.v-created::after { content: "本单新建"; }
|
||
.proto-root .jc-tag.t-exist.v-abandoned::after { content: "已放弃"; }
|
||
.proto-root .jc-tag.t-exist.v-absent { color: var(--danger); background: var(--danger-soft); }
|
||
.proto-root .jc-tag.t-exist.v-created { color: var(--ok); background: var(--ok-soft); }
|
||
.proto-root .jc-tag.t-gap.v-gap::after { content: "待补*"; }
|
||
.proto-root .jc-tag.t-gap.v-default::after { content: "默认·可改"; }
|
||
.proto-root .jc-tag.t-gap.v-ok::after { content: "齐备"; }
|
||
.proto-root .jc-tag.t-gap.v-gap { color: var(--danger); background: var(--danger-soft); }
|
||
.proto-root .jc-tag.t-gap.v-ok { color: var(--ok); background: var(--ok-soft); }
|
||
.proto-root .jc-tag.t-life.v-active::after { content: "Active"; }
|
||
.proto-root .jc-tag.t-life.v-nrnd::after { content: "NRND"; }
|
||
.proto-root .jc-tag.t-life.v-eol::after { content: "EOL"; }
|
||
.proto-root .jc-tag.t-life.v-discontinued::after { content: "停产"; }
|
||
.proto-root .jc-tag.t-life.v-risk::after { content: "风险"; }
|
||
.proto-root .jc-tag.t-doc.v-datasheet::after { content: "Datasheet"; }
|
||
.proto-root .jc-tag.t-doc.v-app_note::after { content: "应用笔记"; }
|
||
.proto-root .jc-tag.t-doc.v-reference_design::after { content: "参考设计"; }
|
||
.proto-root .jc-tag.t-doc.v-white_paper::after { content: "白皮书"; }
|
||
.proto-root .jc-tag.t-doc.v-sample_doc::after { content: "样片资料"; }
|
||
.proto-root .jc-tag.t-doc.v-commitment_letter::after { content: "承诺函"; }
|
||
.proto-root .jc-tag.t-doc.v-other::after { content: "其他"; }
|
||
.proto-root .jc-tag.t-docst.v-valid::after { content: "有效"; }
|
||
.proto-root .jc-tag.t-docst.v-expired::after { content: "过期"; }
|
||
.proto-root .jc-tag.t-docst.v-pending::after { content: "待核实"; }
|
||
.proto-root .jc-tag.t-docst.v-void::after { content: "作废"; }
|
||
.proto-root .jc-tag.t-sub.v-pin_to_pin::after { content: "Pin-to-Pin"; }
|
||
.proto-root .jc-tag.t-sub.v-functional::after { content: "功能替代"; }
|
||
.proto-root .jc-tag.t-sub.v-parametric::after { content: "参数替代"; }
|
||
.proto-root .jc-tag.t-sub.v-other::after { content: "其他"; }
|
||
.proto-root .jc-tag.t-rec.v-recommended::after { content: "推荐"; }
|
||
.proto-root .jc-tag.t-rec.v-conditional::after { content: "条件推荐"; }
|
||
.proto-root .jc-tag.t-rec.v-alternate::after { content: "备选"; }
|
||
.proto-root .jc-tag.t-rec.v-not_recommended::after { content: "不推荐"; }
|
||
.proto-root .jc-tag.t-rec.v-recommended { color: var(--ok); background: var(--ok-soft); }
|
||
.proto-root .jc-tag.t-rec.v-conditional { color: var(--warn-accent); background: var(--warn-soft); }
|
||
.proto-root .jc-tag.t-rec.v-not_recommended { color: var(--danger); background: var(--danger-soft); }
|
||
.proto-root .jc-tag.t-anal.v-ai_draft::after { content: "AI 草稿"; }
|
||
.proto-root .jc-tag.t-anal.v-fae_confirmed::after { content: "FAE 确认"; }
|
||
.proto-root .jc-tag.t-anal.v-pending_customer::after { content: "待客户验证"; }
|
||
.proto-root .jc-tag.t-anal.v-customer_approved::after { content: "客户已批准"; }
|
||
.proto-root .jc-tag.t-anal.v-rejected::after { content: "不批准"; }
|
||
.proto-root .jc-tag.t-anal.v-void::after { content: "作废"; }
|
||
.proto-root .jc-tag.t-anal.v-fae_confirmed { color: var(--ok); background: var(--ok-soft); }
|
||
.proto-root .jc-tag.t-diff.v-match::after { content: "MATCH"; }
|
||
.proto-root .jc-tag.t-diff.v-mismatch::after { content: "MISMATCH"; }
|
||
.proto-root .jc-tag.t-diff.v-unknown::after { content: "UNKNOWN"; }
|
||
.proto-root .jc-tag.t-diff.v-conditional::after { content: "CONDITIONAL"; }
|
||
.proto-root .jc-tag.t-diff.v-match { color: var(--ok); background: var(--ok-soft); }
|
||
.proto-root .jc-tag.t-diff.v-mismatch { color: var(--danger); background: var(--danger-soft); }
|
||
.proto-root .jc-tag.t-diff.v-unknown { color: var(--ink-3); background: var(--soft); }
|
||
.proto-root .jc-tag.t-diff.v-conditional { color: var(--warn-accent); background: var(--warn-soft); }
|
||
.proto-root .jc-tag.t-con.v-hard::after { content: "硬约束"; }
|
||
.proto-root .jc-tag.t-con.v-soft::after { content: "软约束"; }
|
||
.proto-root .jc-tag.t-con.v-hard { color: var(--danger); background: var(--danger-soft); }
|
||
.proto-root .jc-tag.t-risk.v-low::after { content: "低"; }
|
||
.proto-root .jc-tag.t-risk.v-medium::after { content: "中"; }
|
||
.proto-root .jc-tag.t-risk.v-high::after { content: "高"; }
|
||
.proto-root .jc-tag.t-risk.v-critical::after { content: "关键"; }
|
||
.proto-root .jc-tag.t-risk.v-low { color: var(--ok); background: var(--ok-soft); }
|
||
.proto-root .jc-tag.t-risk.v-medium { color: var(--warn-accent); background: var(--warn-soft); }
|
||
.proto-root .jc-tag.t-risk.v-high, .proto-root .jc-tag.t-risk.v-critical { color: var(--danger); background: var(--danger-soft); }
|
||
/* 绑定区:四项锁定的灰底只读行 */
|
||
.proto-root .jc-bound { display: grid; grid-template-columns: 140px 1fr; gap: 6px 14px; padding: 10px 14px; margin: 8px 0 14px; background: var(--soft); border: 1px solid var(--line-soft); border-radius: 9px; font-size: 13px; }
|
||
.proto-root .jc-bound b { color: var(--ink-3); font-weight: 600; }
|
||
`;
|
||
|
||
/** 三层壳照目标件(收件人 / 关联任务两格由引擎注入;目标件的「最迟交回」与单号本仓没有真数据源,不摆) */
|
||
const doc = (input: { tone: 'human' | 'ledger'; badge: string; tag: string; kicker: string; title: string; step: string; state: string; body: string; foot: string }): string =>
|
||
`<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` 与包内文件)。
|
||
|
||
```bash
|
||
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 区加:
|
||
```ts
|
||
import { manifest as partSubstitutionAssessment } from '../activities/part-substitution-assessment/src/manifest.ts';
|
||
```
|
||
`PACKAGES` 数组末尾加:
|
||
```ts
|
||
{ dir: 'part-substitution-assessment', manifest: partSubstitutionAssessment },
|
||
```
|
||
|
||
- [ ] **Step 2: 登记到安装通道并顺带注册纯工具**
|
||
|
||
`apps/api/src/app/seed-activities.ts`:
|
||
```ts
|
||
const REAL_PACKAGES = ['crm-relationship-coverage', 'brand-content-toolkit', 'part-substitution-assessment'] as const;
|
||
```
|
||
顶部加 `import { W05_TOOLS, W05_TOOL_SAMPLE } from './tools-w05.ts';`,在 `installRealActivities` 里 `const root = activitiesRoot();` 之前插入:
|
||
```ts
|
||
/*
|
||
* **纯工具随活动一起注册**(2026-08-26):从前工具契约只在 db:seed 里注册,只跑 activities:install 的
|
||
* 开发机上,活动清单声明了工具、白名单也发了,`sys.tool` 里却没有那件工具 —— 第一次跑就撞「工具不存在」。
|
||
* 「安装即用」包含它声明过的纯工具能调得动。register 按 name+version 幂等,重复装是 no-op。
|
||
*/
|
||
for (const contract of W05_TOOLS) {
|
||
const verdict = await container.toolbox.register({ contract, samples: [W05_TOOL_SAMPLE] });
|
||
if (!verdict.accepted) log(` 工具 ${contract.name} 未过两审:${verdict.staticFindings.map((f) => f.message).join('; ')}`);
|
||
}
|
||
```
|
||
同文件那句 `主数据四域已物化:${MASTER_OBJECTS.length} 张表` 改成 `主数据 ${MASTER_DOMAINS.length} 域已物化:${MASTER_OBJECTS.length} 张表`(import 里把 `MASTER_DOMAINS` 一并从 `./seed-mvp-data.ts` 引进来)—— 日志说「四域」而库里八域,就是一处会撒谎的话。
|
||
|
||
- [ ] **Step 3: 构建 + 装包(这一步同时物化六张主数据表、三张自持表,并播样例料号)**
|
||
|
||
```bash
|
||
pnpm activities:install 2>&1 | tail -20
|
||
```
|
||
Expected 日志里有:`主数据 8 域已物化:17 张表`(原 11 张 + 6)、`工具 …` 无「未过两审」、`活动包已装:ACT-WORK-05 · 差异评估(part-substitution-assessment@1.0.0) · 已授权工具 2 件`、静态审**无 error**。若出现 `静态审提示:[error] …`:
|
||
- `predicate-unbound` 指名 `candidateInternalPns` → 把该谓词里的 `count(candidateInternalPns)` 换成读 `sub_eval.verdict.candidate_total`(模型写)并在 doneWhen/哨兵里保留 `_rows` 那一半;升版本重装;
|
||
- `reference-target-unbound` → 检查 `refersTo` 的三个目标列是否与清单里声明的列名一字不差;
|
||
- `ledger-kind-invalid` → 自持表的 `caseKey` 必须在主键里。
|
||
|
||
- [ ] **Step 4: 库内验收(表、主键、性质、注释、样例行)**
|
||
|
||
```bash
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select domain||'.'||object||' | '||kind||' | '||coalesce(case_key,'-') from ledger.__object_meta where domain in ('parts','demand','substitution','validation','sub_eval') order by 1"
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select conrelid::regclass||' '||pg_get_constraintdef(oid) from pg_constraint where contype='p' and conrelid::regclass::text ~ '^ledger\\.(parts|demand|substitution|validation|sub_eval)__' order by 1"
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select string_agg(column_name||':'||data_type, ', ' order by ordinal_position) from information_schema.columns where table_schema='ledger' and table_name='substitution__difference_item'"
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select obj_description('ledger.parts__profile'::regclass)||' / '||col_description('ledger.parts__profile'::regclass, 6)"
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select internal_pn||' | '||brand_name||' | '||lifecycle_status||' | '||left(normalized_parameter_set, 40) from ledger.parts__profile order by 1"
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select internal_pn||'/'||doc_seq||' '||document_type||' '||document_version from ledger.parts__document order by 1"
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select activity_id||' @'||version||' '||state from sys.activity_package where activity_id='part-substitution-assessment'"
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select name||' v'||version from sys.tool where name like 'part-substitution-assessment.%'"
|
||
```
|
||
Expected:
|
||
- `__object_meta` 9 行:`parts.profile master`、`parts.document master`、`demand.request master`、`substitution.relation master`、`substitution.difference_item master`、`validation.record master`、`sub_eval.pn_check record eval_ref`、`sub_eval.input_gap record eval_ref`、`sub_eval.verdict record eval_ref`;
|
||
- 主键:`parts__profile (internal_pn)`、`parts__document (internal_pn, doc_seq)`、`substitution__relation (substitution_relation_id)`、`substitution__difference_item (substitution_relation_id, difference_item_id)`、`sub_eval__pn_check (eval_ref, internal_pn)`、`sub_eval__input_gap (eval_ref, item_key)`、`sub_eval__verdict (eval_ref)`;
|
||
- `substitution__difference_item` 的列里含 `compare_result:text` 与 `unknown_disposition:text`(清单追加的两列真长出来了);
|
||
- 表注释 `芯片主数据(P-08)`,第 6 列注释是参数集的人读名;
|
||
- `parts__profile` 两行(`LM2596S-5.0 | TI | nrnd | 输入电压范围 | 4.5~40 …`、`LM2596S-ADJ | TI | active | …`);`parts__document` 两行;
|
||
- `sys.activity_package` 有 `part-substitution-assessment @1.0.0 installed`(state 值以库里 E04 那一行为准,同样即可);`sys.tool` 两件 `v1.0.0`。
|
||
把这几条输出原样贴进 `docs/ACT-WORK-05-端到端测试记录.md` 的「0. 库内验收」节(Task 8 Step 1 建文件时一起补)。
|
||
|
||
- [ ] **Step 5: 测试台冒烟(不起单,只看装没装对)**
|
||
|
||
用 claude-in-chrome 打开 `http://localhost:5173/design#/design/testbed/act`:活动下拉里有「ACT-WORK-05 · 差异评估」;选中后表单预填的是样例 0(客户 CU-0142、候选 `["XL2596S-5.0","LM2596S-ADJ"]`),样例钮五枚都显示名字(没有退成「样例 N」),「落真库」勾着。再打开 `http://localhost:5173/design#/design/ledgers` 数据域那一屏,按活动分组里能看到 ACT-WORK-05 名下 9 张表、`parts.profile` 正式 2 行。截图存 `docs/img/w05-testbed-smoke.png`(用 computer 工具 screenshot 后保存)。
|
||
|
||
- [ ] **Step 6: 门禁子集 + 提交**
|
||
|
||
Run: `pnpm -s typecheck && pnpm -s lint && pnpm -s depcruise && pnpm vitest run apps/api/src/app/__tests__ activities/part-substitution-assessment`
|
||
Expected: 全绿。
|
||
|
||
```bash
|
||
git add tools/build-activities.ts apps/api/src/app/seed-activities.ts activities/part-substitution-assessment/dist/manifest.json activities/part-substitution-assessment/dist/index.js
|
||
git commit -m "feat(w05): 差异评估活动登记进构建与安装通道;装包时顺带注册纯工具
|
||
|
||
Co-Authored-By: Claude Fable 5 <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 w05` 5/5 通过;每案例 `released`、被拒 0。
|
||
|
||
- [ ] **Step 1: 扩 `buildEdits` —— 案例级按表的编辑(`stageEdits`)与按条件整行扫空(`fillWhere`)**
|
||
|
||
把 `function buildEdits(stage, drafts) {` 改为 `function buildEdits(stage, drafts, kase = {}) {`,并把里面
|
||
`for (const [i, edit] of (stage.edits ?? []).entries()) {` 改为
|
||
```js
|
||
// 案例级按表的编辑叠在套件级之上(同一套件不同案例要建的料号不同 —— 参数集不能是同一份)
|
||
const edits = [...(stage.edits ?? []), ...((kase.stageEdits ?? {})[stage.address] ?? [])];
|
||
for (const [i, edit] of edits.entries()) {
|
||
```
|
||
在 `if (stage.fillAllBlank) { … }` 之后加:
|
||
```js
|
||
/*
|
||
* 按条件整行扫空:只给某一档状态的行填(评估输入清单里只有 status = gap 的行要补充内容,
|
||
* default / ok 的行本就不该填 —— fillAllBlank 会把它们也填上,那是替人说了他没说的话)。
|
||
*/
|
||
if (stage.fillWhere) {
|
||
const { column, equals, values } = stage.fillWhere;
|
||
for (const r of rows) {
|
||
if (String(r[column] ?? '') === equals && !taken.has(String(r[rowKey]))) {
|
||
lines.push({ [rowKey]: String(r[rowKey]), ...values });
|
||
}
|
||
}
|
||
}
|
||
```
|
||
`runCase` 里 `const lines = buildEdits(stage, body.drafts ?? []);` 改为 `const lines = buildEdits(stage, body.drafts ?? [], kase);`。
|
||
|
||
`expectGateBlocks` 里
|
||
```js
|
||
const rows = draftRows(body.drafts ?? [], stage.address);
|
||
const blank = rows.filter((r) => String(r[column] ?? '').trim() === '').length;
|
||
```
|
||
改为
|
||
```js
|
||
const rows = draftRows(body.drafts ?? [], stage.address);
|
||
// 有 fillWhere 的段只数该档的行:default / ok 行空着是本分,不是「闸没拦」
|
||
const scoped = stage.fillWhere ? rows.filter((r) => String(r[stage.fillWhere.column] ?? '') === stage.fillWhere.equals) : rows;
|
||
const blank = scoped.filter((r) => String(r[column] ?? '').trim() === '').length;
|
||
```
|
||
|
||
- [ ] **Step 2: 加套件(放在 `i01: {…}` 之后、`};` 之前)**
|
||
|
||
```js
|
||
/*
|
||
* ACT-WORK-05 差异评估 —— 1 数据(条件,P-08 / P-10 各一张)+ 1 执行 + 2 验收(审核走决策卡循环、放行终局)。
|
||
* 库里只播了 LM2596S-5.0 / LM2596S-ADJ:XL2596S-5.0、CN-DCDC-32V、LM2596S-3.3 刻意不在,
|
||
* 「候选缺库 / 原 PN 缺库 / 硬约束不推荐」三条路靠它们。e2e 关落真库,主数据不落正式表,案例可反复跑。
|
||
*/
|
||
w05: {
|
||
activityId: 'part-substitution-assessment',
|
||
address: 'sub_eval.input_gap',
|
||
blankCol: 'supplement',
|
||
stages: [
|
||
{
|
||
match: /数据工单 · 芯片主数据创建/,
|
||
address: 'parts.profile',
|
||
rowKey: 'internal_pn',
|
||
blankCol: 'brand_name',
|
||
expectGateBlock: true,
|
||
edits: [],
|
||
// 案例没点名的料号兜底(不该走到这里 —— 每个案例都在 stageEdits 里逐颗给了)
|
||
fillAllBlank: {
|
||
brand_name: '兜底品牌',
|
||
specification_description: '兜底规格(案例没给参数集)',
|
||
package: 'TO-263-5',
|
||
lifecycle_status: 'active',
|
||
normalized_parameter_set: '输入电压范围 | 4.5~40 | V | hard | 兜底',
|
||
},
|
||
},
|
||
{
|
||
match: /数据工单 · 原厂技术资料创建/,
|
||
address: 'parts.document',
|
||
rowKey: 'internal_pn',
|
||
blankCol: 'document_title',
|
||
expectGateBlock: true,
|
||
edits: [],
|
||
fillAllBlank: {
|
||
doc_seq: '01',
|
||
document_type: 'datasheet',
|
||
document_title: 'Datasheet(e2e 兜底)',
|
||
document_version: 'Rev A',
|
||
document_ref: 'https://example.com/datasheet.pdf',
|
||
citation_locator: 'p.1',
|
||
document_status: 'valid',
|
||
},
|
||
},
|
||
{
|
||
match: /评估输入与资料补齐确认/,
|
||
address: 'sub_eval.input_gap',
|
||
rowKey: 'item_key',
|
||
blankCol: 'supplement',
|
||
expectGateBlock: true,
|
||
edits: [
|
||
{ item_key: 'application_scenario', supplement: '12V→5V、工业温域(-40~85℃)、输入上限 40V' },
|
||
{ item_key: 'bom_position', supplement: 'U3 电源位' },
|
||
],
|
||
// 其余 gap 行(原厂未披露的参数)统一「留 UNKNOWN」—— 补不了也是合法答案
|
||
fillWhere: { column: 'status', equals: 'gap', values: { supplement: '原厂未披露,留 UNKNOWN 并挂验证任务' } },
|
||
},
|
||
],
|
||
cases: [
|
||
{
|
||
sample: 0,
|
||
name: '候选缺库 · XL2596S-5.0 派数据工单(P-08 + P-10)· 审核先点名驳回再通过',
|
||
// 决策卡序:候选 ≥2 缺库先问 → create;审核第一张点名意见驳回;第二张通过
|
||
decideSeq: ['create', 'custom:使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件', 'approve'],
|
||
expectDecision: true,
|
||
expectPort: 'assessment-released',
|
||
stageEdits: {
|
||
'parts.profile': [
|
||
{
|
||
internal_pn: 'XL2596S-5.0',
|
||
brand_name: 'XLSEMI',
|
||
manufacturer_name: 'XLSEMI',
|
||
specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz',
|
||
package: 'TO-263-5',
|
||
process_technology: 'BCD',
|
||
lifecycle_status: 'active',
|
||
datasheet_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf',
|
||
datasheet_version: 'Rev C',
|
||
application_scenarios: '工业电源;消费电源',
|
||
normalized_parameter_set: [
|
||
'输入电压范围 | 4.5~40 | V | hard | 客户上限 40V',
|
||
'输出电压 | 5.0 | V | hard | 固定版',
|
||
'输出电流 | 3 | A | hard | 满载能力',
|
||
'开关频率 | 150 | kHz | hard | 影响外围',
|
||
'静态电流 | 4.2 | mA | soft | 待机功耗',
|
||
'工作温度 | -40~+125 | ℃ | hard | 工业温域',
|
||
'使能阈值 | 未披露 | V | hard | 原厂未给',
|
||
].join('\n'),
|
||
},
|
||
],
|
||
'parts.document': [
|
||
{
|
||
internal_pn: 'XL2596S-5.0',
|
||
doc_seq: '01',
|
||
document_type: 'datasheet',
|
||
brand_name: 'XLSEMI',
|
||
document_title: 'XL2596S 150KHz 3A Buck DC/DC Converter',
|
||
document_version: 'Rev C',
|
||
document_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf',
|
||
citation_locator: 'p.3 Electrical Characteristics',
|
||
document_status: 'valid',
|
||
},
|
||
],
|
||
},
|
||
// 使能阈值未披露 → UNKNOWN → 条件推荐(工具算的,不是模型说的)
|
||
expectRevised: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'XL2596S-5.0', column: 'recommendation_level', contains: 'conditional' },
|
||
},
|
||
{
|
||
sample: 1,
|
||
name: '都在库 · 常路 · 可调版 → 备选 · 审核一次通过',
|
||
decide: 'approve',
|
||
expectPort: 'assessment-released',
|
||
expectRevised: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'LM2596S-ADJ', column: 'recommendation_level', contains: 'alternate' },
|
||
},
|
||
{
|
||
sample: 2,
|
||
name: '原 PN 缺库 · 硬阻塞建档(不问人)· 审核通过',
|
||
decide: 'approve',
|
||
expectPort: 'assessment-released',
|
||
stageEdits: {
|
||
'parts.profile': [
|
||
{
|
||
internal_pn: 'LM2596S-3.3',
|
||
brand_name: 'TI',
|
||
manufacturer_name: 'Texas Instruments',
|
||
specification_description: '降压型 DCDC 稳压器,3A,固定 3.3V,150kHz',
|
||
package: 'TO-263-5',
|
||
process_technology: 'BCD',
|
||
lifecycle_status: 'nrnd',
|
||
datasheet_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf',
|
||
datasheet_version: 'Rev H (2023-09)',
|
||
normalized_parameter_set: [
|
||
'输入电压范围 | 4.5~40 | V | hard | 客户上限 40V',
|
||
'输出电压 | 3.3 | V | hard | 固定版',
|
||
'输出电流 | 3 | A | hard | 满载能力',
|
||
'开关频率 | 150 | kHz | hard | 影响外围',
|
||
'静态电流 | 5 | mA | soft | 待机功耗',
|
||
'工作温度 | -40~+85 | ℃ | hard | 工业温域',
|
||
'使能阈值 | 1.4 | V | hard | ON/OFF 阈值',
|
||
].join('\n'),
|
||
},
|
||
],
|
||
'parts.document': [
|
||
{
|
||
internal_pn: 'LM2596S-3.3',
|
||
doc_seq: '01',
|
||
document_type: 'datasheet',
|
||
brand_name: 'TI',
|
||
document_title: 'LM2596 SIMPLE SWITCHER Power Converter',
|
||
document_version: 'Rev H (2023-09)',
|
||
document_ref: 'https://www.ti.com/lit/ds/symlink/lm2596.pdf',
|
||
citation_locator: '§6.5 Electrical Characteristics(3.3V 列)',
|
||
document_status: 'valid',
|
||
},
|
||
],
|
||
},
|
||
},
|
||
{
|
||
sample: 3,
|
||
name: '三候选 · 两颗缺库各问一次 · 国产 32V 硬约束不推荐',
|
||
decideSeq: ['create', 'create', 'approve'],
|
||
expectDecision: true,
|
||
expectPort: 'assessment-released',
|
||
stageEdits: {
|
||
'parts.profile': [
|
||
{
|
||
internal_pn: 'XL2596S-5.0',
|
||
brand_name: 'XLSEMI',
|
||
specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz',
|
||
package: 'TO-263-5',
|
||
lifecycle_status: 'active',
|
||
normalized_parameter_set: [
|
||
'输入电压范围 | 4.5~40 | V | hard | 客户上限 40V',
|
||
'输出电压 | 5.0 | V | hard | 固定版',
|
||
'输出电流 | 3 | A | hard | 满载能力',
|
||
'开关频率 | 150 | kHz | hard | 影响外围',
|
||
'静态电流 | 4.2 | mA | soft | 待机功耗',
|
||
'工作温度 | -40~+125 | ℃ | hard | 工业温域',
|
||
'使能阈值 | 未披露 | V | hard | 原厂未给',
|
||
].join('\n'),
|
||
},
|
||
{
|
||
internal_pn: 'CN-DCDC-32V',
|
||
brand_name: '国产某厂',
|
||
specification_description: '降压型 DCDC,3A,固定 5.0V,输入上限 32V',
|
||
package: 'TO-263-5',
|
||
lifecycle_status: 'active',
|
||
normalized_parameter_set: [
|
||
'输入电压范围 | 4.5~32 | V | hard | 上限 32V',
|
||
'输出电压 | 5.0 | V | hard | 固定版',
|
||
'输出电流 | 3 | A | hard | 满载能力',
|
||
'开关频率 | 150 | kHz | hard | 影响外围',
|
||
'静态电流 | 6 | mA | soft | 待机功耗',
|
||
'工作温度 | -40~+85 | ℃ | hard | 工业温域',
|
||
'使能阈值 | 1.3 | V | hard | ON/OFF 阈值',
|
||
].join('\n'),
|
||
},
|
||
],
|
||
'parts.document': [
|
||
{ internal_pn: 'XL2596S-5.0', doc_seq: '01', document_type: 'datasheet', document_title: 'XL2596S Datasheet', document_version: 'Rev C', document_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf', citation_locator: 'p.3', document_status: 'valid' },
|
||
{ internal_pn: 'CN-DCDC-32V', doc_seq: '01', document_type: 'datasheet', document_title: 'CN-DCDC-32V Datasheet', document_version: 'V1.0', document_ref: 'https://example.com/cn-dcdc-32v.pdf', citation_locator: 'p.2', document_status: 'valid' },
|
||
],
|
||
},
|
||
expectRevised: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'CN-DCDC-32V', column: 'recommendation_level', contains: 'not_recommended' },
|
||
},
|
||
{
|
||
sample: 4,
|
||
name: '新客户 · 录入卡先建档 · 审核通过',
|
||
entryValues: { name: '深圳恒星电源有限公司', industry: '工业电源', region: '广东' },
|
||
decide: 'approve',
|
||
expectPort: 'assessment-released',
|
||
},
|
||
],
|
||
},
|
||
```
|
||
|
||
`main()` 里 `['a1', 'a2', 'a3', 'a4', 'a6', 'a7', 'e04', 'i01']` 改为 `['a1', 'a2', 'a3', 'a4', 'a6', 'a7', 'e04', 'i01', 'w05']`。
|
||
|
||
- [ ] **Step 3: 跑套件(先停稳定版,免得两台后端交替推同一单)**
|
||
|
||
```bash
|
||
./stable.sh stop
|
||
pnpm --filter @app/api reset-runs
|
||
node tools/e2e-a2.mjs w05 2>&1 | tee /tmp/w05-e2e-round1.log | tail -60
|
||
```
|
||
Expected: `5/5 通过`;每案例一行 `→ released · 被拒 0 · N 拍 / Mk token · 决策卡 K 张 · 发口 [assessment-released]`,案例 0/3 的 `闸拦住空手交卷(400)—— 如设计` 至少各出现两次(P-08 / P-10 / 执行单),案例 4 有 `录入卡代答`。
|
||
**红了怎么办**(这是设计欠账不是模型的错,修清单 / 剧本 / 说明书,升 `packageVersion` 重装再跑;每一轮都记进 Task 8 的测试记录):
|
||
- `熔断交人` / 被拒 > 0 → 看 `轨迹末 12 条`:拒回指向哪一步 — `写了等于没写` 多半是剧本让它重写已有的值;`要先…` 类是墙,照设计律「墙要配路」改;`查无此值`(refersTo)= 存在性核对没做对,看 S0/S0d 剧本;
|
||
- `需要你判断下一步` 兜底卡 → 模型输出不成形两次,看 agent-runner 重问日志,多半是 S3 第三拍写盘太长(考虑把 ① 明细与 ③ 关系拆成两拍写);
|
||
- `认不出这张执行卡属于哪一段` → 卡标题没按说明书骨架,改说明书③④⑤的 title 或 `stages[].match`;
|
||
- `期望口 assessment-released 未在发射名单里` → 放行门某条 L2 没绿,看验收卡判据哪条红(测试台观测面 `cards` 里的判据表)。
|
||
|
||
- [ ] **Step 4: 再跑一遍确认稳定(两轮都 5/5 才算),然后清运行数据、提交**
|
||
|
||
```bash
|
||
node tools/e2e-a2.mjs w05 2>&1 | tee /tmp/w05-e2e-round2.log | tail -20
|
||
pnpm --filter @app/api reset-runs
|
||
git add tools/e2e-a2.mjs
|
||
git commit -m "test(w05): e2e 套件 —— 五案例(候选缺库 / 常路 / 原 PN 硬阻塞 / 三候选含不推荐 / 新客户建档)
|
||
|
||
Co-Authored-By: Claude Fable 5 <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: 建记录文档 + 走查前置**
|
||
|
||
```bash
|
||
cat > docs/ACT-WORK-05-端到端测试记录.md <<'EOF'
|
||
# ACT-WORK-05 差异评估 · 端到端测试记录
|
||
|
||
> 活动包 `part-substitution-assessment`;实施计划 `docs/superpowers/plans/2026-08-26-act-work-05-part-substitution-assessment.md`。
|
||
> 每一轮一节:**日期 · 案例 · 走的路径 · 结果(终态 / 被拒 / 拍数 / token / 人环次数)· 观察到的问题 · 修改(文件 @ 版本)· 复测结论**。
|
||
> 脚本 e2e 与浏览器走查都记;「界面不撒谎」:没跑到的分支写「未覆盖」并说原因,不写「通过」。
|
||
|
||
## 0. 库内验收(2026-08-26)
|
||
|
||
(Task 6 Step 4 的 SQL 输出原样贴这里)
|
||
|
||
## 1. 脚本 e2e(`node tools/e2e-a2.mjs w05`)
|
||
|
||
### 第 1 轮
|
||
(贴 tail -60;每案例一行结论;红了写原因与修改)
|
||
|
||
### 第 2 轮(稳定性复跑)
|
||
|
||
## 2. 浏览器全流程全分支走查
|
||
|
||
### 分支覆盖表
|
||
|
||
| 分支 | 触发条件 | 覆盖轮次 | 结果 | 备注 |
|
||
|---|---|---|---|---|
|
||
| 0a | 启动期:新客户无档案 | R5 | | 数据录入卡建档 |
|
||
| 0b / 1d | 候选无 P-08 主数据 → 派数据工单 | R2 / R3 | | P-08 + P-10 各一张 |
|
||
| 0c | 未传 datasheet → 人全填 | R2 / R3 / R4 | | R2b 另试「传了 datasheet → AI 预填」 |
|
||
| 1a | 绑定参数有误 → 退回重启 | R1 | | 工单内四项不可改(件上没有编辑面) |
|
||
| 1b | 输入存在缺口(有而不全) | R1 / R2 | | 应用场景 / BOM 位 / 未披露参数 |
|
||
| 1c | 缺口补不了 → 留 UNKNOWN | R2 | | 补充内容写「留 UNKNOWN」 |
|
||
| 1d(原 PN) | 原 PN 无数据 → 硬阻塞必须建 | R4 | | 不问人 |
|
||
| 1d(放弃) | 候选 ≥2 缺库 → 放弃并记原因 | R3 | | |
|
||
| 2a | 单位冲突 → 阻断回步骤 1 | — | 未覆盖 | 需要冲突单位的夹具料号;记 debt |
|
||
| 2b | 证据版本冲突 → 人工复核 | — | 未覆盖 | 同上 |
|
||
| 3a | 硬约束 MISMATCH → 不推荐 | R3 | | CN-DCDC-32V |
|
||
| 4a | UNKNOWN 无去向 → 驳回补证 | R2 | | 审核卡点名驳回 |
|
||
| 4b | 证据链不全 → 驳回 | R2 | | 同一张卡的第二种意见 |
|
||
| 5a | GATE-03 放行 | R1–R5 | | |
|
||
| 5b | GATE-03 驳回 → REWORK | R4 | | 验收卡「重做」 |
|
||
| 6a | 至少一个推荐 / 备选 | R1 | | 备选 |
|
||
| 6b | 全是条件推荐 | R2 | | XL 条件推荐 |
|
||
| 6c | 全不推荐 | — | 未覆盖 | 需要一颗全硬约束失败的原 PN 组合;记 debt |
|
||
| 6d | 客户不批准 | — | 范围外 | 独立审批流 |
|
||
|
||
### R1 · 都在库 · 常路(样例 1)
|
||
### R2 · 候选缺库 · 数据工单 · 审核驳回循环(样例 0)
|
||
### R2b · 传了 datasheet → AI 预填(样例 0 + 上传)
|
||
### R3 · 三候选 · 一颗放弃一颗新建 · 硬约束不推荐(样例 3)
|
||
### R4 · 原 PN 缺库 · 硬阻塞 · 放行驳回重做(样例 2)
|
||
### R5 · 新客户 · 录入卡建档(样例 4)
|
||
|
||
## 3. 结论与欠账
|
||
EOF
|
||
./stable.sh stop # 走查期间只留 dev(8787 / 5173),避免两台交替推同一单
|
||
pnpm --filter @app/api reset-runs
|
||
```
|
||
用 claude-in-chrome:`ToolSearch "select:mcp__claude-in-chrome__tabs_context_mcp,mcp__claude-in-chrome__navigate,mcp__claude-in-chrome__computer,mcp__claude-in-chrome__read_page,mcp__claude-in-chrome__find,mcp__claude-in-chrome__form_input,mcp__claude-in-chrome__get_page_text,mcp__claude-in-chrome__tabs_create_mcp,mcp__claude-in-chrome__file_upload,mcp__claude-in-chrome__read_console_messages"` → `tabs_context_mcp` → 新开一页 `http://localhost:5173/`。**出现 Keycloak 登录页就停下来请用户在浏览器里登录一次**(凭证只有他有),登录后继续。每轮之间跑「轮间清场」:
|
||
|
||
```bash
|
||
pnpm --filter @app/api reset-runs
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -c "
|
||
delete from ledger.substitution__difference_item where substitution_relation_id like 'REL-%';
|
||
delete from ledger.substitution__relation where substitution_relation_id like 'REL-%';
|
||
delete from ledger.validation__record where validation_id like 'VAL-%';
|
||
delete from ledger.demand__request where request_id like 'REQ-%';
|
||
delete from ledger.parts__document where internal_pn in ('XL2596S-5.0','CN-DCDC-32V','LM2596S-3.3');
|
||
delete from ledger.parts__profile where internal_pn in ('XL2596S-5.0','CN-DCDC-32V','LM2596S-3.3');
|
||
delete from ledger.customer__profile where customer_code = 'CU-9001';"
|
||
```
|
||
(浏览器走查「落真库」默认勾着,放行后主数据真落正式表 —— 不清掉,下一轮「候选缺库」那条路就试不出来。)
|
||
|
||
**每一轮记录的固定项**:起单时刻与 taskId(测试台起单后地址栏 / 观测面顶部);进度条刻度(应是 x/6,每个人环时条上有格);每张卡的文案是否人话、指导书是否显示;件上编辑 → 保存 → 闸 / 红带是否同源(空着必填按「提交」要被拦并点名行列);对话区 say 是否一轮一句;工作成果快照份数(执行单 / 验收单各一份 + 决策卡);终态与 `被拒 / 拍数 / token`(测试台「日志」页签或 `curl -s -H "authorization: Bearer $TOKEN" http://localhost:8787/api/gov/testbed/activity/<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)**
|
||
|
||
1. 测试台 → 活动页签 → 活动「ACT-WORK-05 · 差异评估」→ 样例「都在库·常路」→「起单」。记 taskId。
|
||
2. 工作台:左栏点开这一单。等 AI 跑 S0(对话区应出一句「已绑定…原 PN 与候选都在库,不建主数据」之类的话;进度条 1/6 → 2/6)。**分支 1a**:确认工作区「已绑定」区四项灰底只读、没有编辑钮。
|
||
3. 执行卡「评估输入与资料补齐确认」到达:看指导书(五条 steps)与依据;工作区件:料号核对表两行(原 PN 在库 / 候选 在库 徽标),评估输入清单里 `application_scenario` / `bom_position` 两行状态「待补*」、三行「默认·可改」、齐备行。**先按「提交」不填** → 应被闸拦,提示点名缺哪几行(记原文)。
|
||
4. 件上「编辑」→ 两行补充内容各填一句(`12V→5V、工业温域、40V 输入上限` / `U3 电源位`)→「保存」→ 红带消失、闸文案变;再「提交」→ 通过;对话区出现「人已交回」痕。
|
||
5. AI 跑 S2/S3(三拍:比对 → 定级 → 写盘)。工作区换成差异审核件:关系表 1 行(`LM2596S-ADJ` 推荐等级「备选」),明细表 ≥8 行(输出电压 CONDITIONAL,其余 MATCH)。**分支 6a**。
|
||
6. 审核决策卡到达 →「通过」。AI 跑 S5 → 工作区换成落账确认件(拟写 P-11 一行 analysis_status「FAE 确认」,拟回写 P-01 一行 current_stage `gate03`)→ 验收卡到达:判据表 L1/L2 全绿、L4 三条带 AI 裁断与理由。
|
||
7.「确认通过」→ 中间态「字已签下,正在落地」→ 落地结果屏(进了哪几张表各几行、转出 `assessment-released`)→ 终态 released。**分支 5a**。
|
||
8. 工作成果:定稿两份(执行单快照 / 验收单快照)+ 决策卡快照,翻份翻页正常。
|
||
9. 落库验证:
|
||
```bash
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select substitution_relation_id||' '||candidate_internal_pn||' '||recommendation_level||' '||analysis_status||' '||rule_version from ledger.substitution__relation"
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select count(*)||' 条明细;UNKNOWN '||sum(case when difference_status='unknown' then 1 else 0 end) from ledger.substitution__difference_item"
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select request_id||' '||request_status||' '||current_stage||' '||coalesce(risk_level,'')||' '||coalesce(output_ref,'') from ledger.demand__request"
|
||
```
|
||
Expected:一条关系 `alternate fae_confirmed R-2026.07`;P-01 一行 `responded gate03 … RPT-…`。
|
||
10. 记进 R1 节;有问题按固定流程修、升版、复跑。
|
||
|
||
- [ ] **Step 3: R2 · 候选缺库 · 数据工单 · 审核驳回循环(样例 0)**
|
||
|
||
轮间清场 → 起单样例「候选缺库·派数据工单」。
|
||
1. S0 后对话区应说「XL2596S-5.0 不在库」;进度条停在 1/6「启动与存在性检查」。决策卡「候选 XL2596S-5.0 在芯片主数据里查无记录…」→ 点「创建主数据(派数据工单)」。**分支 0b / 1d**。
|
||
2. 执行卡「数据工单 · 芯片主数据创建(P-08):XL2596S-5.0」到达;工作区 = P-08 件,一行骨架只有料号(**分支 0c 人全填**)。空手「提交」→ 闸拦并点名(记原文)。「编辑」→ 填品牌 / 规格 / 封装 / 生命周期(下拉 Active)/ 参数集(textarea 七行,使能阈值写「未披露」)/ Datasheet 引用与版本 →「保存」→ 红带收 →「+ 加一行」加一颗假料再「×」撤掉(增删行链)→「保存」→「提交」。
|
||
3. 执行卡「数据工单 · 原厂技术资料创建(P-10):XL2596S-5.0」到达;件上一行骨架(类型 Datasheet、状态待核实);填标题 / 版本 / 链接 / 证据定位、状态改「有效」→ 保存 → 提交。进度条进 2/6。
|
||
4. 执行卡「评估输入与资料补齐确认」:清单里多一行「XL2596S-5.0 使能阈值」待补(**分支 1b**);应用场景 / BOM 位填值,使能阈值那行补充内容写「原厂未披露,留 UNKNOWN 挂验证任务」(**分支 1c**)→ 保存 → 提交。
|
||
5. S2/S3 后审核件:两条关系(XL 条件推荐 / ADJ 备选),明细里「使能阈值 UNKNOWN」那行 UNKNOWN 去向写着 VAL 号。**分支 6b**。
|
||
6. 审核决策卡 → 在输入框写意见「使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件」提交(= custom 驳回,**分支 4a**)。看:对话区观察「意见按字面点名对上了…」,AI 改那一行(unknown_disposition / validation.record 的 test_condition),换问法再发一张卡;件上那一行确实变了。再驳回一次,意见「证据引用请指到 datasheet 的页码」(**分支 4b**),看 evidence_ref 变化;第三张卡「通过」。
|
||
7. 拟落账 → 验收卡 → 确认通过 → released。落库验证:
|
||
```bash
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select internal_pn||' '||brand_name||' '||lifecycle_status from ledger.parts__profile where internal_pn='XL2596S-5.0'"
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select validation_id||' '||validation_internal_pn||' '||validation_status||' '||left(next_validation_action,40) from ledger.validation__record"
|
||
docker exec -i jcai-postgres psql -U jcai -d jcai_wos -Atc "select candidate_internal_pn||' '||recommendation_level||' '||coalesce(verification_requirement,'') from ledger.substitution__relation order by 1"
|
||
```
|
||
Expected:XL 真落了 P-08;一条 VAL `not_started`;XL `conditional` 且验证要求含 VAL 号;ADJ `alternate`。
|
||
8. 记录:人环次数(决策 3 + 数据 2 + 执行 1 + 放行 1 = 7)、拍数、被拒。
|
||
|
||
- [ ] **Step 4: R2b · 传了 datasheet → AI 预填(样例 0 + 上传)**
|
||
|
||
轮间清场。先造一份文本 datasheet:
|
||
```bash
|
||
cat > "$TMPDIR/XL2596S-5.0-datasheet.txt" <<'EOF'
|
||
XL2596S-5.0 XLSEMI 150KHz 3A Buck DC/DC Converter Rev C
|
||
Package TO-263-5, BCD process, Active
|
||
Input voltage range 4.5V to 40V
|
||
Output voltage 5.0V fixed, output current 3A
|
||
Switching frequency 150kHz, quiescent current 4.2mA
|
||
Operating temperature -40 to +125 C
|
||
Enable threshold: not specified
|
||
EOF
|
||
```
|
||
测试台样例 0 → 「候选 datasheet」文件选择器上传这份 txt(claude-in-chrome `file_upload`)→ 起单。看 P-08 数据工单的件:品牌 / 规格 / 封装 / 生命周期 / 参数集**已预填**(参数集里使能阈值是「未披露」),卡上依据写「已按 datasheet 预填 N 项」。核对后直接提交(闸应判「无缺」放行)。其余同 R2,审核直接通过。记录预填了几格、有没有编造(**分支 0c 的另一半**)。
|
||
|
||
- [ ] **Step 5: R3 · 三候选 · 一颗放弃一颗新建 · 硬约束不推荐(样例 3)**
|
||
|
||
轮间清场 → 起单样例「三候选·含硬约束不推荐」。
|
||
1. 两张决策卡按数组顺序来:XL2596S-5.0 → 点「放弃这颗候选」(不写原因,看 `abandon_reason` 是否落了「负责人本轮放弃(未注明原因)」)(**分支 1d 放弃**);CN-DCDC-32V →「创建主数据」。
|
||
2. P-08 数据工单只有 CN 一行 → 填(输入电压范围 `4.5~32`)→ 提交;P-10 同。
|
||
3. 执行单 → 补两行 → 提交。审核件:两条关系(ADJ 备选、CN **不推荐**),CN 的「输入电压范围」行 MISMATCH 高风险、影响说明写着 32V < 40V(**分支 3a**);XL 不出现在关系表里、核对表里标「已放弃」。
|
||
4. 审核通过 → 放行 → released。落库:`select candidate_internal_pn||' '||recommendation_level from ledger.substitution__relation order by 1` → `CN-DCDC-32V not_recommended` + `LM2596S-ADJ alternate`;`select internal_pn||' '||exists_status from ledger.sub_eval__pn_check` 在放行前查(record 表放行后仍在,reset-runs 才清)→ XL `abandoned`。
|
||
|
||
- [ ] **Step 6: R4 · 原 PN 缺库 · 硬阻塞 · 放行驳回重做(样例 2)**
|
||
|
||
轮间清场 → 起单样例「原PN缺库·硬阻塞」。
|
||
1. S0 后**不出决策卡**,直接来 P-08 数据工单「LM2596S-3.3」(**分支 1d 原 PN**);填(输出电压 3.3)→ 提交;P-10 → 提交。
|
||
2. 执行单 → 审核通过 → 落账确认件 → 验收卡:**点「重做」**,意见写「P-01 的验证要求请把测试条件也写进去」(**分支 5b**)。看:对话区「验收被打回重做:…」,AI 改 `demand.request.verification_requirement`(件上那一格变了),再次提交 → 新验收卡 →「确认通过」→ released。
|
||
3. 若 AI 原样重提(基座闸「打回之后台账一个字没改」拒回)→ 这是剧本 T_release 的欠账,改剧本、升版、复跑。
|
||
4. 落库:`select original_internal_pn||' → '||candidate_internal_pn||' '||recommendation_level from ledger.substitution__relation`;`select internal_pn from ledger.parts__profile where internal_pn='LM2596S-3.3'`。
|
||
|
||
- [ ] **Step 7: R5 · 新客户 · 录入卡建档(样例 4)**
|
||
|
||
轮间清场 → 起单样例「新客户·先建档」。
|
||
1. S0 发数据录入卡「客户 CU-9001 不在客户档案里…」:名称预填「深圳恒星电源有限公司」,行业 / 区域空 → 填「工业电源 / 广东」→「确认」(**分支 0a**)。对话区痕「已写入客户档案」;AI 接着写 P-01(customer_code 引用墙这时应过)。
|
||
2. 其余同 R1 → released。落库:`select customer_code||' '||name||' '||coalesce(industry,'') from ledger.customer__profile where customer_code='CU-9001'` → 一行。
|
||
3. 轮间清场(把 CU-9001 删掉)。
|
||
|
||
- [ ] **Step 8: 收口记录**
|
||
|
||
每轮结束把「结果 / 问题 / 修改 / 复测」填进对应节;分支覆盖表逐行填「结果」(通过 / 未覆盖 + 原因);「3. 结论与欠账」写:五轮终态与被拒数汇总、逼出的修改列表(文件 @ 版本)、未覆盖分支(2a / 2b / 6c)与建议夹具、留给 Task 9 的 debt。走查中改过的活动包随本任务提交:
|
||
```bash
|
||
git add activities/part-substitution-assessment docs/ACT-WORK-05-端到端测试记录.md
|
||
git commit -m "test(w05): 浏览器全流程全分支走查五轮(R1–R5 + R2b)与测试记录;走查逼出的活动包修订
|
||
|
||
Co-Authored-By: Claude Fable 5 <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` 同一套:一览表 + 每张表「重点字段(按重要程度)」+「数据结构」;★ = 重点,【人填】= 人在件上 / 录入卡填)
|
||
|
||
```markdown
|
||
# 数据域字段说明 —— ACT-WORK-05 差异评估
|
||
|
||
> 对应设计中心 → 数据域「ACT-WORK-05 · 差异评估」分组。字段以活动清单的台账声明为准
|
||
> (`activities/part-substitution-assessment/src/manifest.ts` 的 `declarations.ledger`);
|
||
> 主数据六张的**物理结构**由平台基线 `apps/api/src/app/seed-mvp-data.ts` 物化(《SS02-SS03-SS04_Demo台账字段细化与统一方案》§4.2 全列),
|
||
> 活动只追加自己要用的列与标记。约束词表见 `docs/数据域字段说明-E04-I01.md`「怎么读这份文档」。
|
||
|
||
### 一览
|
||
|
||
| 全景 | 台账地址 | 人读名 | 性质 | 本活动用到的列 | 人填列 |
|
||
|---|---|---|---|---|---|
|
||
| L-02 | `customer.profile` | 客户档案 | 主数据 · 共享 | 4 | 3(录入卡,仅新客户) |
|
||
| P-08 | `parts.profile` | 芯片主数据 | 主数据 · 共享 | 12 | 12(数据工单件上) |
|
||
| P-10 | `parts.document` | 原厂技术资料库 | 主数据 · 共享 | 9 | 9(数据工单件上) |
|
||
| P-01 | `demand.request` | 客户需求主记录 | 主数据 · 共享 | 17 | 0(应用场景 / BOM 位经执行单由 AI 抄入) |
|
||
| P-11 | `substitution.relation` | 替代关系主记录 | 主数据 · 共享 | 17 | 0(只审不改) |
|
||
| P-11 子 | `substitution.difference_item` | 差异明细 | 主数据 · 共享 | 15 | 0 |
|
||
| P-06 | `validation.record` | 芯片验证记录 | 主数据 · 共享 | 8 | 0(只写验证要求) |
|
||
| — | `sub_eval.pn_check` | 料号存在性核对 | 本活动工作台账 | 6 | 0(决策卡定放弃) |
|
||
| — | `sub_eval.input_gap` | 评估输入清单 | 本活动工作台账 | 8 | 3(补充内容 / 加行) |
|
||
| — | `sub_eval.verdict` | 本次评估结论 | 本活动工作台账 | 14 | 0 |
|
||
|
||
## 一、`parts.profile` 芯片主数据(P-08)
|
||
|
||
一张表:料号一行,标准化参数集是它的一个字段(一行一参数 `参数名 | 值 | 单位 | hard/soft | 说明`)。
|
||
库里查无记录 → 数据工单,人在件上从零建整条(可加行);确认落草稿层,放行并入正式表。
|
||
|
||
**重点字段(按重要程度)**
|
||
1. `internal_pn` ★【人填】主键 —— 全链路只认它,不可自由文本
|
||
2. `normalized_parameter_set` ★【人填】必填 —— 差异评估逐项比对的唯一输入;「未披露」进 UNKNOWN
|
||
3. `lifecycle_status` ★【人填】必填 · 取值域 `active/nrnd/eol/discontinued/risk`
|
||
4. `package` ★【人填】必填 —— 硬约束
|
||
5. `specification_description` ★【人填】必填
|
||
6. `brand_name` ★【人填】必填
|
||
7. `datasheet_ref` / `datasheet_version` ★【人填】建议填(成对)—— 证据回链
|
||
|
||
**数据结构**
|
||
|
||
| 列名 | 人读名 | 类型 | 约束 | 写入者 | 说明 |
|
||
|---|---|---|---|---|---|
|
||
| `internal_pn` ★ | 内部料号 | text | 主键 | 人(件上)/ 种子 | |
|
||
| `brand_name` ★ | 品牌名称 | text | 必填 | 人(件上)/ AI 预填 | |
|
||
| `manufacturer_name` | 制造商名称 | text | 建议填 | 人(件上) | 与品牌不一致时分开记 |
|
||
| `specification_description` ★ | 规格描述 | text | 必填 | 人(件上)/ AI 预填 | |
|
||
| `normalized_parameter_set` ★ | 标准化参数集 | text | 必填 | 人(件上)/ AI 预填 | 一行一参数 |
|
||
| `package` ★ | 封装 | text | 必填 | 人(件上)/ AI 预填 | 硬约束 |
|
||
| `process_technology` | 工艺 | text | 建议填 | 人(件上) | 硬约束 |
|
||
| `lifecycle_status` ★ | 生命周期状态 | text | 必填 · 取值域 | 人(件上,下拉) | |
|
||
| `eol_pcn_status` | EOL/PCN 状态 | text | 建议填 | 人(件上) | |
|
||
| `datasheet_ref` ★ | Datasheet 引用 | text | 建议填 | 人(件上)/ AI 预填 | |
|
||
| `datasheet_version` ★ | Datasheet 版本 | text | 建议填 | 人(件上)/ AI 预填 | 有引用就该有版本 |
|
||
| `application_scenarios` | 适配应用场景 | text | 建议填 | 人(件上) | |
|
||
```
|
||
(其余九张表照这个格式写全:列名逐字照清单抄,「写入者」按剧本 —— S0 写 P-01 / pn_check / verdict;S0d 人建 P-08 / P-10;S1 人补 input_gap、AI 抄两列进 P-01;S2/S3 写 relation / difference_item / validation;S5 写 relation.analysis_status 与 P-01 五项。`customer_approval_status` 一栏写「审批流维护,本活动无写权」。)
|
||
|
||
- [ ] **Step 2: CLAUDE.md**
|
||
|
||
「现状快照」末尾加一条(照既有条目的写法:事实 + 账):
|
||
```markdown
|
||
- **ACT-WORK-05 差异评估补齐到验收线**(2026-08-26,`part-substitution-assessment@<最终版本>`):本仓第三个真活动包,
|
||
也是第一个把 SS02 主数据(P-08 / P-10 / P-01 / P-11 主+子 / P-06)建进库的活动 —— 六张表进平台基线
|
||
`MASTER_OBJECTS`(四域 → 八域),活动只追加列与标记;P-08 一张表(参数集是一个 text 字段,一行一参数)。
|
||
**数据工单** = 零输入执行工单 + 件上可加行(一张表一张单:P-08 / P-10),确认落草稿、放行并入(守铁律②)。
|
||
四态与推荐等级由两枚纯工具算(`compare-parameters` / `recommend`,规则 R-2026.07 同时以 DMN 随包冻结);
|
||
审核走决策卡循环,放行终局。**e2e 五案例 5/5 released、被拒 0**;浏览器五轮(R1–R5 + R2b)走查见
|
||
`docs/ACT-WORK-05-端到端测试记录.md`(18 条分支覆盖表:2a / 2b / 6c 未覆盖,记 debt)。
|
||
<走查逼出的修改各一句>
|
||
```
|
||
「文档权威地图」表里加两行:`docs/数据域字段说明-W05.md`(ACT-WORK-05 十张表字段口径)与 `docs/ACT-WORK-05-端到端测试记录.md`(逐轮测试与修改结论)。
|
||
|
||
- [ ] **Step 3: `docs/debt.md` 追加一节**
|
||
|
||
```markdown
|
||
## 2026-08-26 · ACT-WORK-05 差异评估落地时留下的欠账
|
||
|
||
- **数据工单还没有自己的卡皮**:对话区里它是一张标题带「数据工单 ·」的执行卡;文档要的「数据卡」
|
||
(`workOrder:'data'`)要走契约四步 + 前端卡皮,MVP 先不做。
|
||
- **datasheet 只能在起单时上传**:平台文件入口只有起单 `format:'file'`;文档说「在数据工单里上传」,
|
||
件上 / 卡上的上传控件是新基座能力。
|
||
- **PW 与 FAE 一人兼**:平台一任务恰一名负责人,执行单与验收单都发给 owner;派给第二个人是新能力。
|
||
- **参数集是 text 列的行语法**,不是《统一方案》字面上的 Structure / 子表:件上用 textarea 一行一参数;
|
||
要按行编辑就得物理拆 `parts.parameter`(用户定「一张表」,先不拆)。
|
||
- **`market.part`(老料号占位表,主键 `pn`)与 `parts.profile`(P-08,主键 `internal_pn`)重叠**:
|
||
前者空且无人读写,待并或退役。
|
||
- **分支 2a 单位冲突 / 2b 证据版本冲突 / 6c 全不推荐 未覆盖**:需要冲突单位与冲突版本的夹具料号、
|
||
以及一颗全硬约束失败的组合;补夹具后加 e2e 案例。
|
||
- **存在性检查靠模型读整表 + refersTo 墙**:没有「这颗料在不在库」的基座读数;料号表大了整表进上下文会撑爆
|
||
截断阈值,届时要补一枚 `parts.exists` 读工具或基座读数。
|
||
- **`customer_approval_status` 不在活动声明里**,所以件上那一格是静态文案「未提交」而不是取值 —— 审批流做起来后改成 data-slot。
|
||
```
|
||
|
||
- [ ] **Step 4: 全量门禁(退出码)**
|
||
|
||
```bash
|
||
pnpm gates; echo "gates exit=$?"
|
||
```
|
||
Expected: `gates exit=0`。红了逐个修(常见:lint 的 §12.5 禁止项、depcruise 活动包越界 import、api:check —— 本计划没改 `packages/contracts`,api-report 不该变;若变了说明改了契约,补 `pnpm api:check --update && git add api-report`)。
|
||
|
||
- [ ] **Step 5: 提交、收尾**
|
||
|
||
```bash
|
||
git add docs/数据域字段说明-W05.md docs/debt.md CLAUDE.md
|
||
git commit -m "docs(w05): 数据域字段说明、现状快照、欠账登记
|
||
|
||
Co-Authored-By: Claude Fable 5 <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` 与提交列表报给用户。
|
||
|
||
---
|
||
|
||
## 自检清单(写完计划后过一遍)
|
||
|
||
- [x] 规格覆盖:启动 4 项 / 双通道无感知(渠道落 `origin`)/ 存在性检查 / 数据工单 P-08 + P-10 一表一单 / 候选 ≥2 放弃决策 / 原 PN 硬阻塞 / 执行单 01 缺口补齐与 UNKNOWN 合法放行 / 步骤 2–3 纯 AI 不派单 / 四态 + 风险 + 证据 + 置信度 / 推荐四档 / 验收 01 四判据(决策卡循环、意见写 P-06)/ 验收 02 四判据(终局放行)/ P-11 主 + 子落账 / P-01 回写五项 / analysis_status 与 customer_approval_status 分离 / 规则版本锁定 / 多候选并行各一条 P-11 / 台账建进库(基线 + 清单)/ e2e 五案例 / 浏览器五轮 + 分支覆盖表 / 测试记录文档 —— 各有任务落点。
|
||
- [x] 未覆盖且已明示:2a / 2b / 6c(夹具缺)、6d(范围外)、数据卡卡皮、件上上传、双负责人。
|
||
- [x] 名字一致性:活动 id `part-substitution-assessment`;域 `sub_eval` / `parts` / `demand` / `substitution` / `validation`;视图名五个;计划项八个;工具两个;出口 `assessment-released`;读数名与 tally 值在 Task 3/4/5/7 之间逐字相同(`exists_status: found/absent/created/abandoned`,`status: gap/default/ok`,四态 `match/mismatch/unknown/conditional`,推荐 `recommended/conditional/alternate/not_recommended`)。
|
||
- [x] 每个代码步骤给了完整代码;每个验证步骤给了命令与期望输出;没有「TBD / 后补 / 类似 Task N」。
|