This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/activities/part-substitution-assessment/src/manifest.ts
T
xcsandClaude Opus 5 91840067d4 chore(w05): 在途改动落下 —— 启动表单收到三格、humanWrites 那一版清单
上一轮的工作树遗留(packageVersion 1.6.5 → 1.7.3):客户编码与客户名称从启动表单撤下,
三格全选填;活动内部仍认客户,运营端起的单缺这两个键的账已记在注释里。
本轮机制改造不碰活动包,先把它落下,免得混进后面的 diff。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-29 02:56:39 +08:00

660 lines
42 KiB
TypeScript

/**
* 活动包清单 —— ACT-WORK-05 差异评估(案例活动)。
*
* 业务出处:`docs/SS02-ACT-WORK-05-html/`(活动流程与工单说明 / 一键启动入参说明 /
* 《SS02-SS03-SS04_Demo台账字段细化与统一方案》/ 四份目标件 / 全流程分支图)。
* 骨架照 E04 / I01 已验证的样子:双级入参 → 活动自持单据 + 共享主数据 → decide 循环做中间审核 →
* 终局放行。三处与前两个活动刻意不同,写在这里免得下一个读者当成疏漏:
* ① **主数据是七张(P-08 主 + P-08 参数子表 / P-10 / P-01 / P-11 主+子 / P-06)**,物理表由平台基线
* (`apps/api/src/app/seed-mvp-data.ts`)物化,这里只**追加**本活动要用的列与标记
* (required / tally / refersTo / requiredWhen 是活动的授权面;声明合并是纯 add-column)。
* **P-08 的标准化参数集是独立子表 `parts.parameter`**(2026-08-27 结构改:用户核对实跑
* 页面后指出必须与目标件结构一致——目标件《数据工单-01》本就是两张表,塞进一个文本列
* 违反《统一方案》§六「重复明细用子表承载,不在单一文本字段堆叠」)。
* ② **数据工单**(PN 不在库时让人从零建整条主数据)= 零输入执行工单 + 工作区的件
* (`mine:parts.profile` / `mine:parts.parameter` / `mine:parts.document` 可加行),
* 确认落草稿层、放行并入正式表(守铁律②;文档里「确认即入库」在本平台落成「无审核环节、
* 放行并入」)。一张台账表一张单(P-08 主记录 + 参数子表算同一张数据工单 —— 目标件是
* 同一份工单卡里的两张表)。
* ③ 逐项四态与推荐等级**由两枚纯工具算**(规则同时以 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 { LEDGER_BOARD, LEDGER_BOARD_DATA } from './views/ledger-board.ts';
import { ACTIVITY_ID_TEXT, D, 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.7.3',
category: '需求分析与方案匹配',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-part-substitution-assessment',
},
/* ── 契约七件 ────────────────────────────────────────────────── */
contract: {
activityId: ACTIVITY_ID,
version: '1.0.0',
input: {
/*
* 一键启动的表单只摆 3 格(2026-08-28 用户定):需求 / 原 PN / 候选 PN 数组 ——
* 客户编码、客户名称两格从**启动表单**撤下,上下文都写进「需求」那一格。
* 应用场景、BOM 位、替代类型、规则版本、负责人期限**不是入参** —— 启动后在执行工单 01 里补。
* datasheet 选填文件:传了 AI 在数据工单件上预填,没传人全填(文档 0c:同为合法路径)。
*
* ⚠️ **活动内部仍认客户**:说明书与 S0 剧本按 `customerCode` 查客户档案、P-01 的 `customer_code`
* 是必填且引用 `customer.profile` —— 测试台 / 样例仍带着这两个键,那条路照旧;
* 而**运营端启动的单没有这两个键**,S0 到客户那一步会缺料。要让运营端也跑通,得另定客户从哪儿来。
*/
data: {
type: 'object',
/* 2026-08-28 用户定:启动表单**一格必填都不设**(三格全选填) */
required: [],
/*
* 运营端「启动活动」表单照这几个字段渲染(2026-08-27,`docs/API文档-活动模块.md` §4):
* `x-control` 定控件(combo = 选择 + 新增,textarea = 多行),`x-options` 指候选从哪张正式台账现读,`x-order` 定表单次序
* (清单存在 jsonb 里,书写顺序到不了运营端 —— PG 会按键长重排),
* `description` 是控件下那句辅助说明。台账里没有的值**照样启动**,启动后由 S0 的存在性核对派数据工单。
* datasheets 是文件,本期不上启动表单(在数据工单里传)。
*/
properties: {
requestDescription: {
type: 'string',
title: '需求',
description: '这一单要评估什么、什么应用、什么约束,原话照写(客户、场景、温域、上限都写在这一格里);据此新建 P-01 需求主记录(版本 V1)。',
examples: ['工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND,要求验证替代料;应用 12V→5V、工业温域、输入上限 40V'],
'x-control': 'textarea',
'x-order': 1,
},
originalInternalPn: {
type: 'string',
title: '原内部料号',
description: '差异评估基准;库里没有该 PN 也可启动,启动后派数据工单补参数。',
examples: ['LM2596S-5.0'],
'x-control': 'combo',
'x-options': { ledger: 'parts.profile', value: 'internal_pn', label: 'internal_pn' },
'x-order': 2,
},
candidateInternalPns: {
type: 'array',
title: '候选内部料号',
description: '被评估物料;库里有就直接选,没有就自己填料号,启动后经数据工单创建。',
items: { type: 'string' },
examples: [['XL2596S-5.0', 'LM2596S-ADJ']],
/* 运营端启动表单只选一颗(2026-08-27 用户定):摆成与原料号同款的下拉;提交的单个料号由后端包成一元数组,活动契约不变 */
'x-control': 'combo',
'x-options': { ledger: 'parts.profile', value: 'internal_pn', label: 'internal_pn' },
'x-order': 3,
},
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 + ${D}_pn_check_exists_status_abandoned + 1 = ${D}_pn_check_rows`,
},
{
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: 2_000_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 芯片主数据:数据工单建整条;标准化参数集已改子表 parts.parameter(2026-08-27,
用户看了实跑页面指出「必须与目标件结构一致」——目标件本就是两张表,一张主记录一张
参数子表,塞进一个文本列违反《统一方案》§六「重复明细用子表承载」) */
{
domain: 'parts',
object: 'profile',
/* 数据工单从零建主数据:本单只建库里没有的,在库的料号写下来当场拒(2026-08-27) */
createOnly: true,
kind: 'master',
label: '芯片主数据(P-08)',
fields: [
/*
* **只许建本单核过的料号**(2026-08-27 实测):模型给一颗**不存在的** XL2596S-ADJ 铺了空骨架 ——
* 库里没有、入参里也没有,是把两个候选的名字拼出来的;件上于是多出一行要人填的记录。
* 引用核对表:料号必须是这一单(或别单)在 pn_check 里核过的,凭空拼出来的当场拒。
*/
col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true, refersTo: `${D}.pn_check.internal_pn` }),
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: '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-08 子表:标准化参数集,一行一参数,internal_pn 是引用墙(查无那颗主记录当场拒) */
{
domain: 'parts',
object: 'parameter',
/* 数据工单从零建主数据:本单只建库里没有的,在库的料号写下来当场拒(2026-08-27) */
createOnly: true,
kind: 'master',
label: '标准化参数集(P-08 子表)',
fields: [
col({ name: 'internal_pn', type: 'text', label: '所属料号', primaryKey: true, refersTo: 'parts.profile.internal_pn' }),
/*
* `distinct` 是为了让「两颗料是不是用同一套参数名」变成**基座算得出的数**
* (`parts_parameter_distinct_param_name`)—— 见 CMMN 的 s_pns_resolved:
* 参数行数 = 不同参数名数 × 建过的料数,只有每颗料的名字集合一模一样时才成立。
* 2026-08-27 实测:模型给第二颗料写英文 key(`input_voltage_range`),
* 而原 PN 在库里是中文「输入电压范围」—— 名字是主键的一半,对不上就没法逐项比。
* 措辞写过两遍都没拦住(设计律第六条:措辞治不了的结构治)。
*/
col({
name: 'param_name',
type: 'text',
label: '参数名',
primaryKey: true,
distinct: true,
/*
* **参数名只能用台账里已经在用的那几个**(自指引用)—— 比对是逐参数对着比的,
* 候选身上有一个原 PN 没有的维度,本来也无从比起;名字对不上更是直接判成 UNKNOWN。
* 2026-08-27 实测三次:模型给新料写英文 key(`input_voltage_range`),
* 而库里既有行是中文「输入电压范围」—— 剧本写过两遍、又加了一条乘法对账
* (行数 = 不同名字数 × 建过的料数)仍拦不住**只建一颗**的那一路(5 = 5×1 恒成立)。
* 自指是唯一在一颗料时也咬得住的:写下去那一刻基座去这一列里查,查无此值当场拒。
* 代价(已知约束):真要引入一个全新的参数维度,得先有一颗料用过它 ——
* MVP 的品类词汇表是固定的七项,够用;将来要放开,改成引用一张参数字典表。
*/
refersTo: 'parts.parameter.param_name',
}),
col({ name: 'param_value', type: 'text', label: '值', required: true }),
col({ name: 'param_unit', type: 'text', label: '单位', expect: true }),
col({ name: 'constraint_type', type: 'text', label: '约束类型', required: true, tally: ['hard', 'soft'] }),
col({ name: 'param_note', type: 'text', label: '说明', expect: true }),
],
},
/* P-10 原厂技术资料库:数据工单建;评估时只读做证据回链 */
{
domain: 'parts',
object: 'document',
/* 数据工单从零建主数据:本单只建库里没有的,在库的料号写下来当场拒(2026-08-27) */
createOnly: true,
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'] }),
/*
* 「在库」不是模型说了算的一句话 —— 判 found 就必须把**在主数据里真找到的那个料号**填在这里,
* 基座拿它去 `parts.profile` 查(refersTo),查无此值当场拒。requiredWhen 让这一格只对 found 行必填,
* 而 S0_bind 的 doneWhen 里有 `_missing = 0` —— 于是「判了 found 却填不出依据」这一步就收不掉。
* 2026-08-27 实测逼出来的:原 PN 根本不在库,模型把两颗都写成 found、交代里还说「都在库」,
* 硬阻塞那条路整个被跳过(剧本早写着「整表在你的作用域里逐个核对」—— 措辞治不了的结构治)。
*/
col({
name: 'found_pn_ref',
type: 'text',
label: '在库核对依据',
refersTo: 'parts.profile.internal_pn',
requiredWhen: { column: 'exists_status', value: 'found' },
}),
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 },
/* 运营中心 L2 的沙箱视图(2026-08-27,用户定替换掉全流程分支图):关键台账数据看板 —— 最近一次已放行
实例的落账快照,行由基座按 `data` 声明从正式表圈;无脚本,报高脚本由服务端出口注入 */
{ name: 'activity-ledger', html: LEDGER_BOARD, data: LEDGER_BOARD_DATA },
],
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' },
};