Archived
推荐理由 / 摘要 / 风险 / 验证要求都是规则算出来的:让人直接改它们, 等于让他给自己没做的判断盖章。要改就改上面那一条四态,系统重算。 同批修一条与本次设计矛盾的既有断言:「差异审核与落账确认只读」原把 difference-review 也算只读,而 spec §4.4/§6.2 已把 S3 改成可就地编辑 ——改窄成只管 landing-confirm,differerence-review 的只读面改由新增的 「件②」测试组精确覆盖。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
673 lines
42 KiB
TypeScript
673 lines
42 KiB
TypeScript
/**
|
||
* ACT-WORK-05 差异评估的案例模型 —— **双表示同版本**:CMMN XML(机器面)与案例说明书(叙事面)。
|
||
*
|
||
* **2026-08-29 整体重写(Task 3,spec `docs/superpowers/specs/2026-08-29-act-work-05-rewrite-design.md` §4)**:
|
||
* 九步砍成四步 —— `S1_inputs`(校验入参与补数据)→ `S2_assess`(逐项差异分析)→
|
||
* `S3_review`(结论审核,人工激活)→ `T_release`(落账放行,人工激活)。三道入场哨兵全读基座数,
|
||
* `caseFileModel` 里**一面记账旗都没有**——旗是活动自己写的数,「机检只读基座数」这条纪律在本仓
|
||
* 被违反过六次,每次都是同一种事故(见 `docs/ACT-WORK-05-端到端测试记录.md` A–K 发现表)。
|
||
*
|
||
* **全程零 `guide`**:四步的行为改由 `PlanStep.auto` 声明式配方接管(ADR-014)—— 这一份只搭骨架
|
||
* (每步 `auto` 先各放一笔 `say` 占位),配方的实体在 Task 4–6 逐步填进去;`view` 这一步不写
|
||
* (它是可选的,Task 7–9 各自补上自己那一张)。
|
||
*
|
||
* 触达世界的两步(`S3_review` 审核、`T_release` 放行)标了人工激活且**不给 `doneWhen`**——
|
||
* 完成与否不该由数据形状推出来。`ASSESSMENT_STEPS` / `ASSESSMENT_PHASES` 不用 `as const`
|
||
* (与 E04 / I01 同款):它会把 `writes`/`reads` 这类数组字面量收窄成只读元组,
|
||
* 装配进 `manifest.ts` 的 `businessModel.steps`(类型是可变的 `PlanStep[]`)时会因为
|
||
* `ReadonlyArray` 缺 `push` 等方法而过不了类型检查。
|
||
*/
|
||
import { RULE_VERSION } 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 就是任务视图里的键名 —— **全是基座读数**(本单草稿写下的行数 / 缺必填数 /
|
||
按值计数 / 不同值个数)。这一版里**一面记账旗都没有**:旗是活动自己写的数,
|
||
而「机检只读基座数」这条纪律在本仓被违反过六次,每次都是同一种事故。
|
||
-->
|
||
<caseFileModel>
|
||
<caseFileItem id="sub_eval_part_exists_status_found" name="两颗料里已在库的个数(基座现数)"/>
|
||
<caseFileItem id="sub_eval_part_missing" name="本单两颗料缺必填的行数(基座现数)"/>
|
||
<caseFileItem id="sub_eval_part_distinct_chip_type_id" name="两颗料用到的不同类型个数(基座现数不同值)"/>
|
||
<caseFileItem id="parts_profile_missing" name="本单碰过的芯片主数据缺必填的行数(基座现数)"/>
|
||
<caseFileItem id="parts_parameter_missing" name="本单碰过的参数行缺必填的行数(基座现数)"/>
|
||
<caseFileItem id="substitution_difference_item_rows" name="差异明细行数(基座现数)"/>
|
||
<caseFileItem id="substitution_difference_item_missing" name="差异明细缺必填的行数(基座现数)"/>
|
||
<caseFileItem id="substitution_relation_rows" name="替代关系行数(基座现数)"/>
|
||
<caseFileItem id="substitution_relation_missing" name="替代关系缺必填的行数(基座现数)"/>
|
||
<caseFileItem id="validation_record_rows" name="验证记录行数(基座现数)"/>
|
||
<caseFileItem id="validation_record_missing" name="验证记录缺必填的行数(基座现数)"/>
|
||
</caseFileModel>
|
||
<casePlanModel id="plan" name="差异评估" autoComplete="true">
|
||
<!-- ① 入参齐备:两颗都在库、都归了类型、且是同一个类型、主数据与参数都不缺 —— 分析那一步的入场前提 -->
|
||
<sentry id="s_inputs_ready">
|
||
<onPart id="op_i" caseFileItemRef="sub_eval_part_exists_status_found"><standardEvent>update</standardEvent></onPart>
|
||
<ifPart id="if_i"><condition>sub_eval_part_exists_status_found = 2 and sub_eval_part_missing = 0 and sub_eval_part_distinct_chip_type_id = 1 and parts_profile_missing = 0 and parts_parameter_missing = 0</condition></ifPart>
|
||
</sentry>
|
||
<!-- ② 结论已成形:逐条明细、一条关系、一条验证记录都写下了且不缺 —— 审核那一步的入场前提 -->
|
||
<sentry id="s_assessed">
|
||
<onPart id="op_a" caseFileItemRef="substitution_difference_item_rows"><standardEvent>update</standardEvent></onPart>
|
||
<ifPart id="if_a"><condition>substitution_difference_item_rows > 0 and substitution_difference_item_missing = 0 and substitution_relation_rows = 1 and validation_record_rows = 1</condition></ifPart>
|
||
</sentry>
|
||
<!-- ③ 放行的入场前提与 ② 同源:审核那一步收没收由人答卡决定,不在这儿判 -->
|
||
<sentry id="s_reviewable">
|
||
<onPart id="op_r" caseFileItemRef="substitution_relation_rows"><standardEvent>update</standardEvent></onPart>
|
||
<ifPart id="if_r"><condition>substitution_relation_rows = 1 and substitution_relation_missing = 0 and validation_record_missing = 0</condition></ifPart>
|
||
</sentry>
|
||
|
||
<!-- 1 校验入参与补数据:核对两颗料在不在库、归没归同一类型、参数齐不齐;缺什么派数据工单让人补(全 auto,doneWhen 收步) -->
|
||
<humanTask id="S1_inputs" name="校验入参与补数据"/>
|
||
|
||
<!-- 2 逐项差异分析:核心参数逐条比对,四态、推荐等级、风险由系统按规则算(全 auto,doneWhen 收步) -->
|
||
<humanTask id="S2_assess" name="逐项差异分析">
|
||
<entryCriterion id="e_assess" sentryRef="s_inputs_ready"/>
|
||
</humanTask>
|
||
|
||
<!-- 3 结论审核**触达的是人**:标人工激活,且不给 doneWhen —— 完成与否不该由数据形状推出来 -->
|
||
<humanTask id="S3_review" name="差异评估结论审核" manualActivation="true">
|
||
<entryCriterion id="e_review" sentryRef="s_assessed"/>
|
||
</humanTask>
|
||
|
||
<!-- 4 落账放行**触达的是人**:标人工激活,且不给 doneWhen —— 签字这件事只能由人做 -->
|
||
<humanTask id="T_release" name="落账放行" manualActivation="true">
|
||
<entryCriterion id="e_release" sentryRef="s_reviewable"/>
|
||
</humanTask>
|
||
</casePlanModel>
|
||
</case>
|
||
</definitions>`;
|
||
|
||
/**
|
||
* 案例说明书 —— **只写整单一以贯之的事**,不讲逐步做法(逐步做法归配方,而配方不是给模型看的,
|
||
* 是引擎按声明确定性执行的数据)。四步全部 `guide: ''`,这份说明书理论上不会再被任何一拍摊开给
|
||
* 模型看 —— 留着它是因为 `narrative` 契约仍要求非空,且它仍是给人读的活动说明。
|
||
*
|
||
* **示例一律占位形**(`<料号>` / `<参数名>`),不写具体料号与参数名 —— 本仓 2026-08-27 复现过三次:
|
||
* 关思考的模型会把示例里的具体值当成这一单的值照抄。
|
||
*/
|
||
export const NARRATIVE = `§0 本活动只回答一件事:**候选内部料号在技术上能不能替代原内部料号**——逐条核心参数给出可复核的
|
||
差异结论,人审核通过后落账。不做数量 / 价格 / 交期 / 采购判断;客户批准状态由独立审批流维护,
|
||
本活动没有那一列的写权。
|
||
|
||
入参两格,都选填:原内部料号、候选内部料号(如 <料号>)——库里有就用,没有就自己写一个新的,
|
||
启动后由第一步核对补齐,不必人先建好档。
|
||
|
||
**四步各自的产出**:
|
||
① 校验入参与补数据(S1_inputs)—— 核对两颗料在不在库、是否归了同一个芯片类型、核心参数
|
||
(如 <参数名>)是否齐备;缺什么就派数据工单让人补。产出:两颗料的存在性与类型核对、
|
||
补齐的芯片主数据与标准化参数。
|
||
② 逐项差异分析(S2_assess)—— 把两颗料的核心参数逐条比对,判定 MATCH / MISMATCH / UNKNOWN /
|
||
CONDITIONAL 四态,依规则算出推荐等级与风险。产出:替代关系一行 + 差异明细逐参数一行 +
|
||
验证记录一行(全推荐也要写「本轮无待验项」)。规则版本 ${RULE_VERSION} 随结论一并锁定。
|
||
③ 结论审核(S3_review,人工激活)—— 交人核对:参数比对对不对、硬约束有没有被含糊带过、
|
||
UNKNOWN 有没有去向、判断依据站不站得住;可就地改四态与影响说明,改到没问题为止。
|
||
④ 落账放行(T_release,人工激活)—— 把这一签下去会落哪几张表、几行摆清楚,人签字后正式落账,
|
||
发射 assessment-released。
|
||
|
||
**三条不许**:
|
||
· 不伪造参数值——查不到就显式写「未披露」,不许估、不许拿别的料号或别的参数的值顶替;
|
||
· 硬约束 MISMATCH 不许被措辞改判成 MATCH,要改只能补证据后重比;
|
||
· 不写客户批准状态——那是独立审批流的事,本活动只答技术可替代性。`;
|
||
|
||
/*
|
||
* **两步各自的 `auto.let` 都要指向这份绑定,不能只有一步有**(2026-08-29 评审 Critical 修正)。
|
||
*
|
||
* `createAutoRunner.run()` **每次调用都从它收到的那份 `recipe.let` 现算一遍** `lets`
|
||
* (`auto-runner.ts:93-97`)—— 步骤之间不传递 `let`,turn-runner 的两个调用点
|
||
* (`:1725-1738` 的 quiet 分支、`:2020-2022` 的常规分支)都是把 `step.auto` 原样递进去。
|
||
* `RELATION_PROJECTION`/`VALIDATION_PROJECTION` 引用的 `{in:'relationId'}`/`{in:'evalRef'}`/
|
||
* `{in:'originalPn'}`/`{in:'candidatePn'}`/`{in:'typeId'}` 若只在 S2 的 `auto.let` 里定义过、
|
||
* S3 没有自己的 `let`,这几个名字在 S3 那一拍就是**未绑定**——`{in:X}` 找不到就退回 `''`
|
||
* (`auto-expr.ts:44-47`)。后果两条,已用执行级测试实证(`apps/api/src/modules/loop-engine/
|
||
* __tests__/w05-s3-review-recompute.test.ts`):
|
||
* ① 关系表 `substitution_relation_id` 算成空串,整笔写被「主键算不出来先不写」悄悄丢掉
|
||
* ——**人在审核件上改了四态,那一行永远不会更新**,这正是 S3 存在的全部理由;
|
||
* ② 验证记录的主键 `cat:['VAL-', {in:'evalRef'}]` 反而拼出非空的 `'VAL-'`,被当成一个
|
||
* 新主键写出**第二行垃圾**,破了「P-06 一单固定一条」的不变量。
|
||
*
|
||
* spec(`docs/superpowers/specs/2026-08-29-act-work-05-rewrite-design.md:293-294`)说的是
|
||
* 「S3 的配方里**复制一份** ③④ 两笔(纯投影、幂等)……一步一份配方,不做跨步回跳」——**复制**,
|
||
* 不是共享一个只算一次的结果。两步各自持有一份**完整可求值**的 `let` 才是这句话的本意;
|
||
* 绑定的**文本**仍只有一处真源(这个常量),避免真的抄两份而分叉。
|
||
*/
|
||
/** **为什么不用 `{in:'chip_type_id'}`**:这个名字在三张表上都有,顶层同名以后写的赢 —— 拿它取值等于赌哪张表最后写。改成对准一张表 `lookup`,不歧义。 */
|
||
const SHARED_LET = {
|
||
evalRef: { lookup: 'mine:sub_eval.case', by: {}, take: 'eval_ref', else: '' },
|
||
typeId: { lookup: 'mine:sub_eval.case', by: {}, take: 'chip_type_id', else: '' },
|
||
originalPn: { lookup: 'mine:sub_eval.part', by: { role: 'original' }, take: 'internal_pn', else: '' },
|
||
candidatePn: { lookup: 'mine:sub_eval.part', by: { role: 'candidate' }, take: 'internal_pn', else: '' },
|
||
relationId: { cat: ['SUB-', { lookup: 'mine:sub_eval.case', by: {}, take: 'eval_ref', else: '' }] },
|
||
} as const;
|
||
|
||
/*
|
||
* **关系与验证记录的投影,S2 与 S3 共用同一份**。
|
||
* S2 算出第一版;人在审核件上改了某一条四态之后,S3 用**同一份**重算 ——
|
||
* 各写一份早晚分叉,而分叉的那一刻屏上会同时摆着两个互相矛盾的等级。
|
||
*
|
||
* 关系一行本身:**等级、风险、摘要、理由全由规则与模板拼,不让 AI 编** —— 于是永远不会出现
|
||
* 「有硬约束不符却写着推荐」;人在审核件上改了某一条四态,这一笔重跑,等级跟着变。
|
||
*/
|
||
const RELATION_PROJECTION = {
|
||
kind: 'project' as const,
|
||
into: 'substitution.relation',
|
||
from: { once: true as const },
|
||
fields: {
|
||
substitution_relation_id: { in: 'relationId' },
|
||
eval_ref: { in: 'evalRef' },
|
||
original_internal_pn: { in: 'originalPn' },
|
||
candidate_internal_pn: { in: 'candidatePn' },
|
||
chip_type_id: { in: 'typeId' },
|
||
recommendation_level: {
|
||
when: 'substitution_difference_item_judgement_class_hard_mismatch > 0', then: 'not_recommended',
|
||
else: {
|
||
when: 'substitution_difference_item_judgement_class_unknown > 0', then: 'conditional',
|
||
else: {
|
||
when: 'substitution_difference_item_judgement_class_conditional > 0', then: 'conditional',
|
||
else: { when: 'substitution_difference_item_judgement_class_soft_mismatch > 0', then: 'conditional', else: 'recommended' },
|
||
},
|
||
},
|
||
},
|
||
risk_level: {
|
||
when: 'recommendation_level = "not_recommended"', then: 'high',
|
||
else: { when: 'recommendation_level = "recommended"', then: 'low', else: 'medium' },
|
||
},
|
||
difference_summary: {
|
||
cat: [
|
||
'共 ', { in: 'substitution_difference_item_rows' }, ' 项核心参数:',
|
||
{ in: 'substitution_difference_item_judgement_class_match' }, ' 项一致 / ',
|
||
{ in: 'substitution_difference_item_judgement_class_hard_mismatch' }, ' 项硬约束不符 / ',
|
||
{ in: 'substitution_difference_item_judgement_class_soft_mismatch' }, ' 项软约束不符 / ',
|
||
{ in: 'substitution_difference_item_judgement_class_unknown' }, ' 项待确认 / ',
|
||
{ in: 'substitution_difference_item_judgement_class_conditional' }, ' 项有条件。',
|
||
],
|
||
},
|
||
recommendation_reason: {
|
||
when: 'recommendation_level = "not_recommended"',
|
||
then: '有硬约束不满足 —— 逐条见差异明细里判为 MISMATCH 且约束类型为硬的那几行;硬性边界不满足,不因措辞改判。',
|
||
else: {
|
||
when: 'recommendation_level = "recommended"',
|
||
then: '每一项核心参数都判为一致,没有待确认项,可直接替代。',
|
||
else: '没有硬约束不满足,但仍有待确认或需附条件的项 —— 逐条见差异明细;闭环之后方可采用。',
|
||
},
|
||
},
|
||
verification_requirement: {
|
||
when: 'recommendation_level = "conditional"',
|
||
/* `{joinRows}` 未实现 —— 不逐个列参数名,指向明细表(那里逐条摆着,且人能就地改) */
|
||
then: '实测差异明细里判为 UNKNOWN 与 CONDITIONAL 的每一项,并把实测值回填 P-08 标准化参数集;回填后重跑本评估。',
|
||
else: '',
|
||
},
|
||
validation_id: { cat: ['VAL-', { in: 'evalRef' }] },
|
||
analysis_status: 'fae_confirmed',
|
||
rule_version: RULE_VERSION,
|
||
},
|
||
} as const;
|
||
|
||
/* P-06 **一单固定一条**(用户定)。本活动只写「还得去验什么」,状态一律 `not_started`:它不是实测结果,**不许虚构实测**。 */
|
||
const VALIDATION_PROJECTION = {
|
||
kind: 'project' as const,
|
||
into: 'validation.record',
|
||
from: { once: true as const },
|
||
fields: {
|
||
validation_id: { cat: ['VAL-', { in: 'evalRef' }] },
|
||
eval_ref: { in: 'evalRef' },
|
||
validation_internal_pn: { in: 'candidatePn' },
|
||
validation_type: 'parametric',
|
||
validation_status: 'not_started',
|
||
test_condition: {
|
||
when: 'substitution_difference_item_judgement_class_unknown + substitution_difference_item_judgement_class_conditional > 0',
|
||
then: '差异明细里判为 UNKNOWN 与 CONDITIONAL 的每一项,按原厂规格书条件实测。',
|
||
else: '无',
|
||
},
|
||
next_validation_action: {
|
||
when: 'substitution_difference_item_judgement_class_unknown + substitution_difference_item_judgement_class_conditional > 0',
|
||
then: '实测上述各项并把实测值回填 P-08 标准化参数集,回填后重跑本评估。',
|
||
else: '本轮无待验项。',
|
||
},
|
||
},
|
||
} as const;
|
||
|
||
/**
|
||
* 步骤剧本 —— 一个计划项一条。四步**全程零 `guide`、零工具**:行为改由 `auto` 配方接管
|
||
* (ADR-014),这一份只放骨架 —— 每步 `auto.do` 先各摆一笔 `say` 占位,配方的实体
|
||
* (`project` / `analyze` / `card` / `submit`)在 Task 4–6 逐步填进去。
|
||
*
|
||
* 两处不给 `doneWhen`:`S3_review`(人拍板且可循环)、`T_release`(签下去)——
|
||
* 判据不许摆在还欠一张卡的那一步眼前(会诱使它去凑判据,2026-08-28 用户实测逼出的立墙)。
|
||
*/
|
||
export const ASSESSMENT_STEPS = [
|
||
{
|
||
item: 'S1_inputs' as const,
|
||
intent: '核对两颗料在不在库、归没归同一个类型、参数值齐不齐;缺什么就派一张数据工单让人补',
|
||
guide: '',
|
||
/* Task 7:发给人的数据工单摆的就是这一张 —— 三块竖排(存在性与主数据 / 芯片类型字段 / 参数值),
|
||
与 auto.do 第 ⑤ 笔 `card` 的 `gateLedgers` 同源(parts.type_param / parts.profile / parts.parameter ——
|
||
`parts.chip_type` 不列入,理由见那笔 `card` 定义处的注释)。 */
|
||
view: 'input-supplement',
|
||
writes: ['sub_eval.part', 'sub_eval.case', 'parts.profile', 'parts.parameter'],
|
||
reads: ['originalInternalPn', 'candidateInternalPn', 'case:openedAt', 'parts.profile', 'parts.parameter', 'parts.chip_type', 'parts.type_param'],
|
||
tools: [],
|
||
doneWhen:
|
||
'sub_eval_part_exists_status_found = 2 and sub_eval_part_missing = 0 and ' +
|
||
'sub_eval_part_distinct_chip_type_id = 1 and parts_profile_missing = 0 and parts_parameter_missing = 0',
|
||
auto: {
|
||
/** 整单只有这一个案号,从本单开案时刻取材 —— **不许从正式表里别单的号里挑**(踩过) */
|
||
let: { evalRef: { cat: ['W05-', { fmt: { in: 'case:openedAt' }, as: 'digits' as const }] } },
|
||
do: [
|
||
/*
|
||
* ① 本单两颗料 —— **后面每一笔遍历的唯一来源**。
|
||
* 挤进案卷一行里就没法 `where` 过滤,而不能过滤就会给已经在库的那颗也铺一遍空参数行。
|
||
*/
|
||
{
|
||
kind: 'project' as const,
|
||
into: 'sub_eval.part',
|
||
from: {
|
||
rows: [
|
||
{ pn: { in: 'originalInternalPn' }, role: 'original' },
|
||
{ pn: { in: 'candidateInternalPn' }, role: 'candidate' },
|
||
],
|
||
},
|
||
fields: {
|
||
eval_ref: { in: 'evalRef' },
|
||
internal_pn: { col: 'pn' },
|
||
role: { col: 'role' },
|
||
/* 先取依据,再按依据判 —— 字段按声明顺序求值,算出来的进同一行的 S-FEEL 作用域 */
|
||
found_pn_ref: { lookup: 'parts.profile', by: { internal_pn: { col: 'pn' } }, take: 'internal_pn', else: '' },
|
||
exists_status: { when: 'found_pn_ref != ""', then: 'found', else: 'absent' },
|
||
chip_type_id: { lookup: 'parts.profile', by: { internal_pn: { col: 'pn' } }, take: 'chip_type_id', else: '' },
|
||
},
|
||
},
|
||
/*
|
||
* ② 主数据骨架:**两颗料都铺、值回填现值**。
|
||
* 在库那颗回填出它此刻的样子(人不必重填);要建那颗全空等人填。
|
||
* **「在库的老料还没归类型」于是也被拦住** —— 那一格回填出来是空的,而 `chip_type_id` 必填。
|
||
* 人不必在件上「加一行」:要建哪颗料是启动参数就定死的事,行由系统摆好,他只填格子。
|
||
*/
|
||
{
|
||
kind: 'project' as const,
|
||
into: 'parts.profile',
|
||
prune: true,
|
||
from: { each: 'mine:sub_eval.part' },
|
||
fields: {
|
||
internal_pn: { col: 'internal_pn' },
|
||
chip_type_id: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'chip_type_id', else: '' },
|
||
brand_name: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'brand_name', else: '' },
|
||
lifecycle_status: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'lifecycle_status', else: '' },
|
||
specification_description: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'specification_description', else: '' },
|
||
manufacturer_name: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'manufacturer_name', else: '' },
|
||
datasheet_ref: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'datasheet_ref', else: '' },
|
||
datasheet_version: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'datasheet_version', else: '' },
|
||
},
|
||
},
|
||
/*
|
||
* ③ 参数骨架 —— 三件事一笔办完:
|
||
* · 人还没挑类型 → `{outer:'chip_type_id'}` 是空串,`where` 一行不命中,**什么也不铺**;
|
||
* 他挑了、按了保存,行就出来 —— **「保存即铺格」不是特判,是这一笔的自然结果**;
|
||
* · 在库那颗料**也**铺,值回填现值 → 屏上是它此刻的样子,`_missing` 不会误报;
|
||
* · **「在库但当年参数没录全」于是自动被拦住** —— 那一格回填出来是空的,红带点名、闸按不下。
|
||
*
|
||
* **`param_unit` 是有意不回填现值、直接取字典**(2026-08-29 评审 Minor 记录):
|
||
* 同一类型下两颗料必须按**同一个单位**比,`parts.type_param.param_unit` 才是这次比对的基准 ——
|
||
* 若某颗料自己记的单位与字典不同,以字典覆盖是**故意的**,不是漏写了 `lookup`;
|
||
* S2 喂给 AI 判定时用的 `unit` 同样取自这张字典,两处必须同源,换成回填现值反而会让
|
||
* 「展示的单位」与「判定时用的单位」分叉。
|
||
*/
|
||
{
|
||
kind: 'project' as const,
|
||
into: 'parts.parameter',
|
||
prune: true,
|
||
from: {
|
||
each: 'mine:sub_eval.part',
|
||
then: { each: 'parts.type_param', where: { type_id: { outer: 'chip_type_id' } } },
|
||
},
|
||
fields: {
|
||
internal_pn: { outer: 'internal_pn' },
|
||
param_name: { col: 'param_name' },
|
||
param_unit: { col: 'param_unit' },
|
||
param_value: {
|
||
lookup: 'parts.parameter',
|
||
by: { internal_pn: { outer: 'internal_pn' }, param_name: { col: 'param_name' } },
|
||
take: 'param_value',
|
||
else: '',
|
||
},
|
||
param_note: {
|
||
lookup: 'parts.parameter',
|
||
by: { internal_pn: { outer: 'internal_pn' }, param_name: { col: 'param_name' } },
|
||
take: 'param_note',
|
||
else: '',
|
||
},
|
||
},
|
||
},
|
||
/*
|
||
* ④ 案卷一行:`gate_reason` 是**摆在件上给人看**的那句话(条件判断另用基座读数,见 ⑤)。
|
||
*
|
||
* **排在 ②③ 之后 —— 这是承重的次序,不是随手写的**(2026-08-29 评审 Important 修正):
|
||
* `gate_reason` 的 when 链最深一层要读 `parts_profile_missing + parts_parameter_missing`,
|
||
* 而这两个数是 ②③ 刚铺出来的骨架撑出来的基座读数。这一笔若排在 ②③ **之前**
|
||
* (原来的次序),它读到的是这一拍**刷新之前**的旧数:当某颗料的 `chip_type_id`
|
||
* 刚在这一拍被 ① 设上、②③ 即将把它的主数据与参数骨架铺出来时,那一刻 ③ 即将新建的
|
||
* 参数骨架行还不存在,`parts_parameter_missing` 读到 0(不存在的行不算「缺」),
|
||
* 片刻之后 ③ 一跑就变成 1。收步与发卡的把关本身没坏(⑤ 排在 ②③④ 之后,`card.when`
|
||
* 用的是刷新之后的真实读数,卡照样正确发出),但案卷里存的这句话、连同卡面
|
||
* `basis` 里「还差什么」那一格,会是一句空话 —— 人最需要一句解释的时候,读到的是空的。
|
||
* **每一笔之前刷读数**(auto-runner 的硬规矩)保证的是「下一笔看得见上一笔的结果」,
|
||
* 保证不了「这一笔算的话用了后面几笔才有的数」—— 读别人数据的那一笔,必须排在被读的
|
||
* 那几笔之后。
|
||
*/
|
||
{
|
||
kind: 'project' as const,
|
||
into: 'sub_eval.case',
|
||
from: { once: true as const },
|
||
fields: {
|
||
eval_ref: { in: 'evalRef' },
|
||
chip_type_id: { lookup: 'mine:sub_eval.part', by: { role: 'original' }, take: 'chip_type_id', else: '' },
|
||
gate_reason: {
|
||
when: 'sub_eval_part_rows < 2',
|
||
then: '原料号与候选料号还没填全 —— 在下面把两格补上。',
|
||
else: {
|
||
when: 'sub_eval_part_exists_status_absent > 0',
|
||
then: '有料号库里查不到 —— 在下面把它的主数据建起来。',
|
||
else: {
|
||
when: 'sub_eval_part_missing > 0',
|
||
then: '有料号还没归芯片类型 —— 在下面给它选一个;没有合适的就新建一个,并把它的核心参数字段一并定下来。',
|
||
else: {
|
||
when: 'sub_eval_part_distinct_chip_type_id > 1',
|
||
then: '两颗料不是同一个芯片类型,逐项比对无从谈起 —— 换一颗料,或者把归错的那颗类型改对。',
|
||
else: {
|
||
when: 'parts_profile_missing + parts_parameter_missing > 0',
|
||
then: '还有必填格空着 —— 红带点名的那几格填上就能提交。',
|
||
else: '',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
/*
|
||
* ⑤ 缺什么就派一张数据工单。
|
||
* **条件只用基座读数**:`gate_reason` 为空串时那一列压根不写、于是未绑定,
|
||
* 而 S-FEEL 里未绑定的名字 `!= ""` 求值为 **true** —— 拿它当条件,恰恰在一切正常时把卡发出去。
|
||
*/
|
||
{
|
||
kind: 'card' as const,
|
||
when:
|
||
'sub_eval_part_exists_status_found < 2 or sub_eval_part_missing > 0 or ' +
|
||
'sub_eval_part_distinct_chip_type_id > 1 or parts_profile_missing > 0 or parts_parameter_missing > 0',
|
||
as: 'execution' as const,
|
||
variant: 'data' as const,
|
||
title: '数据工单 · 差异评估的输入还差几格',
|
||
basis: [
|
||
{ k: '原料号', v: { in: 'originalInternalPn' } },
|
||
{ k: '候选料号', v: { in: 'candidateInternalPn' } },
|
||
{ k: '还差什么', v: { in: 'gate_reason' } },
|
||
],
|
||
guide: [
|
||
'两颗料必须归到**同一个芯片类型**下 —— 要比的核心参数字段由类型定,类型不同就没有可比的字段。',
|
||
'类型下拉里没有合适的,就新建一个,并把它的**核心参数字段**一并定下来:每个字段要有名字、一句说明、以及硬约束还是软约束。',
|
||
'说明写「这个参数为什么要紧」——**它是后面判定差异时唯一的解释来源**,写成参数名的复述等于没写。',
|
||
'硬约束 = 不满足就不能用(烧芯片、超温域、装不上);软约束 = 影响表现但可权衡(功耗、噪声)。',
|
||
'参数值填**原厂规格书上的值**,查不到就留空 —— 留空会被红带点名,但**编一个值比留空坏得多**。',
|
||
],
|
||
standard: '红带消失、提交钮变绿:两颗料都在库、归了同一个类型、每个核心参数字段都有值。',
|
||
/*
|
||
* **`parts.chip_type` 永久不列在这里** —— 不是等 Task 7b 补了「新建类型」那张编辑表
|
||
* 就该加回来(2026-08-29 评审逮到的 Critical:这张件此刻没有任何 `data-edit`
|
||
* 能写 `parts.chip_type`,列了它,`mine:parts.chip_type` 永远是空数组,
|
||
* `desk.ts` 的 `emptyGate` 就永远判「这一单还一行都没有 —— 这张卡就是来建它的」,
|
||
* 提交钮当场且永久按不下去,三张表填得再全也一样)。
|
||
* 更深一层:即便 Task 7b 真的补上了那张编辑表,也不该把它放回来 —— `emptyGate`
|
||
* 的语义是「这张卡**就是来建**这张表的,一行都没有 = 没干活」,而
|
||
* `parts.chip_type` 的主路是**从下拉挑一个已有类型**,那时 `mine:parts.chip_type`
|
||
* 天然就是零行(没新建、没问题);只有走「新建类型」那条岔路时才会写它。
|
||
* 一张**只是有时候才写**的表,套「零行 = 没干完」这条闸的前提本身就不成立 ——
|
||
* 该拦的是 `parts.type_param`(核心参数字段清单,新建类型时**一定**要写)。
|
||
*/
|
||
gateLedgers: ['parts.type_param', 'parts.profile', 'parts.parameter'],
|
||
},
|
||
{ kind: 'say' as const, text: '已核对两颗料的在库状态与参数齐备情况。' },
|
||
],
|
||
},
|
||
},
|
||
{
|
||
item: 'S2_assess' as const,
|
||
intent: '把两颗料的核心参数逐条交给模型判四态,系统按规则算推荐等级、风险与验证要求',
|
||
guide: '',
|
||
writes: ['substitution.difference_item', 'substitution.relation', 'validation.record'],
|
||
reads: ['parts.type_param', 'parts.parameter', 'sub_eval.part', 'sub_eval.case'],
|
||
tools: [],
|
||
doneWhen:
|
||
'substitution_difference_item_rows > 0 and substitution_difference_item_missing = 0 and ' +
|
||
'substitution_relation_rows = 1 and validation_record_rows = 1',
|
||
auto: {
|
||
/* 绑定的真身在 `SHARED_LET`(本文件靠前处)—— S3 复用同一份投影时也要靠它,原地写一份会漂 */
|
||
let: SHARED_LET,
|
||
do: [
|
||
/*
|
||
* ① **整个活动唯一一次模型调用**。系统把两颗料在该类型下的每一条核心参数
|
||
* 组装好交给它,它只答「这一条算什么、为什么」——**不给工具、不给记忆、不多轮**。
|
||
*/
|
||
{
|
||
kind: 'analyze' as const,
|
||
model: 'deepseek-v4-flash',
|
||
into: 'items',
|
||
instruction: [
|
||
'你在做芯片替代的**逐参数差异判定**。输入是一组核心参数,每一条给了:参数名、这个参数为什么要紧(说明)、',
|
||
'硬约束还是软约束、单位,以及原料与候选料各自的值和值的备注。',
|
||
'',
|
||
'对**每一条**给出四态之一:',
|
||
'· match —— 候选满足原料在这个参数上的要求(相等,或候选的范围盖住原料的范围);',
|
||
'· mismatch —— 候选达不到(范围更窄、方向相反、量级不足);',
|
||
'· unknown —— 任一侧的值缺失或语义不明,判不了;',
|
||
'· conditional —— 改外围、限定工况、或加一道验证之后可以成立。',
|
||
'',
|
||
'三条硬规矩:',
|
||
'1. **值是多少不要复述** —— 系统手里已经有值了。影响说明要答的是「换上去会不会比现在差、差在哪儿」。',
|
||
'2. **判不了就判 unknown,不要猜**;判了 unknown 就必须在 unknown_disposition 里写清楚:实测哪一项、或去哪儿补这个值。',
|
||
'3. **硬约束不因为措辞变成 match** —— 硬约束不满足就是 mismatch,证据不足就是 unknown。',
|
||
'',
|
||
'范围值的读法:「下限~上限」是一个区间(分隔符可能是 ~ 或 - 或 至);只给一边的是半开区间;',
|
||
'单位可能写在值里。判「盖不盖得住」按区间比,不要拿端点当定值。',
|
||
'',
|
||
'只输出 JSON,不要解释、不要 markdown 代码围栏。',
|
||
].join('\n'),
|
||
input: {
|
||
from: { each: 'parts.type_param', where: { type_id: { in: 'typeId' } } },
|
||
fields: {
|
||
param_name: { col: 'param_name' },
|
||
/* **AI 分析时读的就是它** —— 参数名本身说不清「它为什么要紧」 */
|
||
param_desc: { col: 'param_desc' },
|
||
constraint_type: { col: 'constraint_type' },
|
||
unit: { col: 'param_unit' },
|
||
original_value: { lookup: 'parts.parameter', by: { internal_pn: { in: 'originalPn' }, param_name: { col: 'param_name' } }, take: 'param_value', else: '' },
|
||
original_note: { lookup: 'parts.parameter', by: { internal_pn: { in: 'originalPn' }, param_name: { col: 'param_name' } }, take: 'param_note', else: '' },
|
||
candidate_value: { lookup: 'parts.parameter', by: { internal_pn: { in: 'candidatePn' }, param_name: { col: 'param_name' } }, take: 'param_value', else: '' },
|
||
candidate_note: { lookup: 'parts.parameter', by: { internal_pn: { in: 'candidatePn' }, param_name: { col: 'param_name' } }, take: 'param_note', else: '' },
|
||
},
|
||
},
|
||
output: {
|
||
type: 'object',
|
||
required: ['items'],
|
||
properties: {
|
||
items: {
|
||
type: 'array',
|
||
items: {
|
||
type: 'object',
|
||
required: ['param_name', 'difference_status', 'impact_statement', 'evidence_note'],
|
||
properties: {
|
||
param_name: { type: 'string' },
|
||
difference_status: { type: 'string', enum: ['match', 'mismatch', 'unknown', 'conditional'] },
|
||
impact_statement: { type: 'string' },
|
||
evidence_note: { type: 'string' },
|
||
unknown_disposition: { type: 'string' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
/*
|
||
* ② 逐条明细 —— **行由字典 `each` 出来**,AI 的出参只是拿 `param_name` 去 `lookup` 的一张查表。
|
||
* 它漏一条,那一行的四态就是空 → `required` 立刻变成 `_missing > 0`,提交闸与放行门当场拦住。
|
||
* 从前是 `from each(AI 出参)`:它少给两条就少两行,**而每一行看着都合规**。
|
||
*/
|
||
{
|
||
kind: 'project' as const,
|
||
into: 'substitution.difference_item',
|
||
prune: true,
|
||
from: { each: 'parts.type_param', where: { type_id: { in: 'typeId' } } },
|
||
fields: {
|
||
substitution_relation_id: { in: 'relationId' },
|
||
difference_item_id: { cat: [{ in: 'relationId' }, '-', { col: 'param_name' }] },
|
||
parameter_name: { col: 'param_name' },
|
||
parameter_desc: { col: 'param_desc' },
|
||
parameter_unit: { col: 'param_unit' },
|
||
/* 约束来自字典这一行本身;值一律 lookup 台账 —— 基座读得到的事实不让模型转述 */
|
||
constraint_type: { col: 'constraint_type' },
|
||
original_value: { lookup: 'parts.parameter', by: { internal_pn: { in: 'originalPn' }, param_name: { col: 'param_name' } }, take: 'param_value', else: '' },
|
||
candidate_value: { lookup: 'parts.parameter', by: { internal_pn: { in: 'candidatePn' }, param_name: { col: 'param_name' } }, take: 'param_value', else: '' },
|
||
difference_status: { lookup: 'result:items', by: { param_name: { col: 'param_name' } }, take: 'difference_status', else: '' },
|
||
impact_statement: { lookup: 'result:items', by: { param_name: { col: 'param_name' } }, take: 'impact_statement', else: '' },
|
||
evidence_note: { lookup: 'result:items', by: { param_name: { col: 'param_name' } }, take: 'evidence_note', else: '' },
|
||
unknown_disposition: { lookup: 'result:items', by: { param_name: { col: 'param_name' } }, take: 'unknown_disposition', else: '' },
|
||
/* 合成列:先认四态,最后才按约束类型把 mismatch 分成硬 / 软 */
|
||
judgement_class: {
|
||
when: 'difference_status = "match"', then: 'match',
|
||
else: {
|
||
when: 'difference_status = "unknown"', then: 'unknown',
|
||
else: {
|
||
when: 'difference_status = "conditional"', then: 'conditional',
|
||
else: { when: 'constraint_type = "hard"', then: 'hard_mismatch', else: 'soft_mismatch' },
|
||
},
|
||
},
|
||
},
|
||
risk_level: {
|
||
when: 'judgement_class = "hard_mismatch"', then: 'high',
|
||
else: { when: 'judgement_class = "match"', then: 'low', else: 'medium' },
|
||
},
|
||
rule_version: RULE_VERSION,
|
||
},
|
||
},
|
||
RELATION_PROJECTION,
|
||
VALIDATION_PROJECTION,
|
||
{ kind: 'say' as const, text: '逐条差异结论已备好,推荐等级由规则按四态算出。' },
|
||
/*
|
||
* **`] as const` 是必须的,不是随手加的**:`do` 数组里一旦混入对已命名常量的
|
||
* *引用*(`RELATION_PROJECTION`/`VALIDATION_PROJECTION`),TS 对这个数组字面量的
|
||
* 逐位置精确类型推断就整体失效 —— 连数组里其它仍是内联字面量的位置(如 `acts[1]`)
|
||
* 也会被拉平成「这个数组里出现过的所有形状的并集」,`fields['具体列名']` 当场报
|
||
* TS7053(实测:去掉这个 `as const`,`pnpm typecheck` 在 `S2 配方` describe 块
|
||
* 报 9 处这类错误,尽管 `vitest run` 不做类型检查、照样能跑绿 —— 这是本仓已有的
|
||
* `: {}` vs `: undefined` 那类「并集吞窄类型」陷阱的第二种触发形态)。
|
||
*/
|
||
] as const,
|
||
},
|
||
},
|
||
{
|
||
item: 'S3_review' as const,
|
||
intent: '把逐条结论摆给人过目,他就地改四态与影响说明,改到没问题为止',
|
||
guide: '',
|
||
/* Task 8:人核对结论用的验收工单 —— 对决头(原 PN → 候选 PN)+ 四态分布带 + 推荐等级大徽章
|
||
只读,逐条明细可就地改(四态 / 影响说明 / 判断依据 / UNKNOWN 去向),收口四格
|
||
(推荐理由 / 差异摘要 / 风险 / 验证要求)只读——它们是配方按规则算的,人改了上面的四态,
|
||
这一步的 auto.do 里那两笔投影(RELATION_PROJECTION / VALIDATION_PROJECTION)会重算它们。 */
|
||
view: 'difference-review',
|
||
writes: ['substitution.difference_item', 'substitution.relation', 'validation.record'],
|
||
reads: ['substitution.difference_item', 'substitution.relation'],
|
||
tools: [],
|
||
cardVariant: 'review' as const,
|
||
/* 人工验收清单:一条一句他拿着这份工单要**亲自核**的动作,不是系统判的判据 */
|
||
acceptanceChecklist: [
|
||
'逐条对过原料与候选料的参数值,没有把需求边界写成候选料的问题',
|
||
'硬约束那几条逐条看过,没有一条是「没证据也判了 MATCH」',
|
||
'判 UNKNOWN 的都写了去向:实测哪一项、回填到哪儿',
|
||
'影响说明站得住:说的是「换上去会不会比现在差」,不是复述参数值',
|
||
],
|
||
auto: {
|
||
/*
|
||
* **这一步自己的 `let`,不是 S2 那份算好的结果**(2026-08-29 评审 Critical 修正)——
|
||
* `RELATION_PROJECTION`/`VALIDATION_PROJECTION` 里的 `{in:'relationId'}` 等名字要靠它才
|
||
* 绑得上,少了这一行两笔投影的主键在这一步就是空的(详见 `SHARED_LET` 定义处的头注)。
|
||
*/
|
||
let: SHARED_LET,
|
||
do: [
|
||
/* 人改了四态,等级 / 风险 / 摘要 / 验证要求跟着重算 —— 摘要只读,改的是四态 */
|
||
RELATION_PROJECTION,
|
||
VALIDATION_PROJECTION,
|
||
{
|
||
kind: 'card' as const,
|
||
as: 'decide' as const,
|
||
variant: 'review' as const,
|
||
title: '差异评估结论审核',
|
||
ask: '逐条差异结论已备好。请在工作区的件上逐条核对;要改的就地改(四态、影响说明、判断依据都可改),改完回到这里确认。',
|
||
options: [{ value: 'approve', label: '确认通过', detail: '逐条核对无误,可以进入落账放行' }],
|
||
basis: [
|
||
{ k: '原料号', v: { lookup: 'mine:sub_eval.part', by: { role: 'original' }, take: 'internal_pn', else: '' } },
|
||
{ k: '候选料号', v: { lookup: 'mine:sub_eval.part', by: { role: 'candidate' }, take: 'internal_pn', else: '' } },
|
||
{ k: '本轮结论', v: { lookup: 'mine:substitution.relation', by: {}, take: 'difference_summary', else: '' } },
|
||
],
|
||
},
|
||
{ kind: 'say' as const, text: '差异结论已送审,请在件上逐条核对。' },
|
||
/* `] as const`:这个数组也引用了 RELATION_PROJECTION/VALIDATION_PROJECTION —— 理由同 S2 的 do 数组头上那条注释 */
|
||
] as const,
|
||
},
|
||
},
|
||
{
|
||
item: 'T_release' as const,
|
||
intent: '把这一签下去会落哪几张表、几行摆清楚,人签字后落账并发口',
|
||
guide: '',
|
||
/* 人工验收清单:一条一句他拿着这份工单要**亲自核**的动作,不是系统判的判据 */
|
||
acceptanceChecklist: [
|
||
'推荐等级与四态分布对得上:有硬约束不符的没给「推荐」',
|
||
'待验项清单认可:该实测的都列出来了,没有该验而漏掉的',
|
||
'落账内容核对过:进哪几张表、各几行,与屏上摆的一致',
|
||
'参数值来源可追:每一条都能说出它是库里既有的还是这一单补录的',
|
||
'签下去就发给下游,这一步之后本活动不再改动这些行',
|
||
],
|
||
/* 被打回重做时要改的就是这几张 —— 不声明就写不进去 */
|
||
writes: ['sub_eval.case'],
|
||
reads: ['substitution.relation', 'substitution.difference_item', 'validation.record', 'sub_eval.case'],
|
||
tools: [],
|
||
auto: {
|
||
do: [
|
||
{
|
||
kind: 'project' as const,
|
||
into: 'sub_eval.case',
|
||
from: { once: true as const },
|
||
fields: {
|
||
eval_ref: { lookup: 'mine:sub_eval.case', by: {}, take: 'eval_ref', else: '' },
|
||
summary: {
|
||
cat: [
|
||
{ lookup: 'mine:substitution.relation', by: {}, take: 'candidate_internal_pn', else: '' },
|
||
' 替代 ',
|
||
{ lookup: 'mine:substitution.relation', by: {}, take: 'original_internal_pn', else: '' },
|
||
':',
|
||
{ lookup: 'mine:substitution.relation', by: {}, take: 'difference_summary', else: '' },
|
||
],
|
||
},
|
||
},
|
||
},
|
||
{
|
||
kind: 'submit' as const,
|
||
rationale: {
|
||
cat: [
|
||
'逐项差异评估已经审核通过:',
|
||
{ in: 'substitution_difference_item_rows' }, ' 条明细、1 条替代关系、1 条验证记录待落账。',
|
||
],
|
||
},
|
||
},
|
||
],
|
||
},
|
||
},
|
||
];
|
||
|
||
/**
|
||
* 阶段 —— 进度条的刻度,只给人看。除放行外每个计划项恰属一个阶段;
|
||
* 放行那一格由基座统一加(与 E04 / I01 同款),人看到的是 **x/4**。
|
||
*/
|
||
export const ASSESSMENT_PHASES = [
|
||
{ name: '校验入参', items: ['S1_inputs'] },
|
||
{ name: '差异分析', items: ['S2_assess'] },
|
||
{ name: '结论审核', items: ['S3_review'] },
|
||
];
|