/** * 活动包清单 —— ACT-WORK-05 差异评估(案例活动)。 * * **2026-08-29 整体重写(@2.0.0)**:从「九步、两枚工具、全程 agent 驱动」改成「四步、零工具、 * 零 `guide`、AI 出场恰好一次」。设计权威: * `docs/superpowers/specs/2026-08-29-act-work-05-rewrite-design.md`(下称「spec」)。 * 用户原话立的场:「不是把所有东西都丢给 ai 来处理…一定是系统先确定性的从数据库中取出数据后交给 * ai 分析」——`docs/ACT-WORK-05-端到端测试记录.md` 的 A–AE 发现表里,八条事故都落在 * 「本来就该是确定的」那几步(编号抄别单、判在库判错、数据工单派给在库的料…), * 真正需要判断的只有一件事:**这一条参数,候选跟原厂比算不算事**。 * * **2026-08-30 更新**:这份注释原写于重写 Task 2(只动 `declarations.ledger` 与 * `contract` 两段那一刻),彼时 `case-model.ts` 还是旧九步——**Task 3 已把它整体重写成 * spec §4 的四步**(`S1_inputs` / `S2_assess` / `S3_review` / `T_release`)+ 四道哨兵 + * 零 `guide`(行为改由 `PlanStep.auto` 声明式配方接管),清单与案例模型此刻同版一致, * 不再有「一边新一边旧」的过渡态。`assets` 里当时已不自洽的那几处(spec §8 的退役清单, * 见「计划裁定 G3」)同批处理完毕。 * * ## 九张台账声明(spec §3) * 两张新字典(`parts.chip_type` / `parts.type_param`,Task 1 已物化 + 播种)承接「约束类型按类型 * 标一次」;P-08 主记录 `parts.profile` 撤 `createOnly`(在库老料要能补上 `chip_type_id` 这一格)、 * 封装与工艺退休(它们本来就是「核心参数」,该以类型字段的身份出现在字典里);P-08 子表 * `parts.parameter` 撤 `constraint_type` 与参数名自指引用(名字对不对由 S1 配方铺格子这件事本身 * 保证,不必再靠一堵测三次都没拦住的墙)。原来的三张单据(`pn_check` / `input_gap` / `verdict`) * 由两张顶掉:`sub_eval.part`(**一单恰两行** —— 配方 `each` 遍历「哪几颗料要建」的唯一数据源, * 拆成两行的理由见 spec §3.3「为什么非拆成两张不可」)与 `sub_eval.case`(一单一行的案卷, * 挂入参未就绪的原因与结论摘要)。P-11 明细加合成列 `judgement_class`(约束类型 × 四态的合成, * 因为「硬约束不符有几条」不是任何单列 `tally` 数得出来的数)。P-01 需求主记录 / P-10 原厂资料 / * 客户档案**本活动不碰**(它们的存在性核对与录入不再是这个活动的事)。 * * ## 契约(spec §4.1/§4.5/§7) * 启动只剩 `originalInternalPn` / `candidateInternalPn` 两格(单数,一次评估恰一对一候选), * 都选填 —— 缺了由第一步的配方派数据工单补。零工具:`contract.resources.tools` 与 * `declarations.tools` 都是 `[]`,这是这次重写的立场,不是疏漏(四态改 S2 唯一一次 `analyze` * 判定,推荐等级改配方按 spec §3.5 的规则算)。判据全部改读基座数(`域_对象_rows` / `_missing` / * `_distinct_列` / tally 计数),没有一条读活动自己写的汇总列。 */ import type { ActivityManifest } from '@contracts'; import { activityIdSchema } from '@contracts'; import { CMMN_XML, NARRATIVE, ASSESSMENT_PHASES, ASSESSMENT_STEPS } from './case-model.ts'; import { LEDGER_BOARD, LEDGER_BOARD_DATA } from './views/ledger-board.ts'; import { INPUT_SUPPLEMENT, DIFFERENCE_REVIEW, LANDING_CONFIRM } from './views/report.ts'; import { ACTIVITY_ID_TEXT, D } from './constants.ts'; export { D, RULE_VERSION } from './constants.ts'; const ACTIVITY_ID = activityIdSchema.parse(ACTIVITY_ID_TEXT); /** 字段声明的短写(与 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 · 差异评估', /* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */ packageVersion: '2.14.0', category: '需求分析与方案匹配', sdkVersion: '0.1.0-m5', checksum: 'dev-checksum-part-substitution-assessment', }, /* ── 契约七件 ────────────────────────────────────────────────── */ contract: { activityId: ACTIVITY_ID, version: '1.0.0', input: { /* * **两格,都必填**(2026-08-30 修 Critical 2,推翻 spec §4.1 原来的「都选填」)—— * 一次评估恰是「一对一候选」,这两颗料号是这件事的**身份**,不是可以事后补的细节: * 库里有就选,没有就自己写一个新的(合法的新料号字符串,不是留空)。 * 「启动后由第一步的配方核对存在性、缺了就派数据工单补齐」这句话说的是料号的**主数据** * (品牌/类型/参数),从来不是料号**本身**——旧措辞把两件事含糊成了一件事。 * * 都选填时留空的下场(评审逐行核过的链条):S1 配方 `project into sub_eval.part` 拿 * `originalInternalPn`/`candidateInternalPn` 铸两行的主键 `internal_pn`,两个空字符串 * 是**同一个主键**——第二笔 `draftWrite` 覆盖第一笔,`sub_eval.part` 收成**一行**不是 * 两行;`gate_reason` 的 `sub_eval_part_rows < 2` 当场为真,提示「在下面把两格补上」—— * 而块① 的行源是 `mine:sub_eval.part`(人不加行,没有「+加一行」按钮),那两格根本 * 不存在。`doneWhen` 永不成立,提交也按不下去,**唯一出路是终止重开**,且五条样例与 * e2e 五案例都没填过空值,两条验证线都照不到这条死路。 * * 改必填**不是关上「留空」这扇门就完事**——平台早有配套机制接住它(2026-07-30 立的 * 「受理归一」:缺必填不拒单,任务落成 `awaiting-human`,一张补全卡问负责人要那几个 * 字段,答完合并进 `data` 才转 `running`;`apps/api/src/modules/task/internal/usecase/ * task-service.ts` 的 `missingRequired`/`askCompletion`,`__tests__/intake.test.ts` * 已经把这条路测得很实)。必填不是墙,是把「两颗料号是什么」这件唯一要人先说清楚的 * 事挪到起单那一刻问清楚,不留给配方去猜一个猜不出来的空字符串。 * * 运营端启动表单渲染这两格为「选择 + 新增」下拉(`x-control: combo`),候选从 * `parts.profile.internal_pn` 现读(`x-options`);`x-order` 定表单次序(清单存在 * jsonb 里,书写顺序到不了运营端)。 * * **两格按芯片类型联动**(2026-08-31 用户定):`x-options.group` 让候选带上各自的 * `chip_type_id` 与它在 `parts.chip_type` 里的人读名,`x-link: 'chip-type'` 把两格结成一组 —— * 组内**先填的那格是锚**(摆全量),另一格只列同类型的料 + 库里未标类型的(殿后一组)+「新增」。 * 锚没有类型、或锚填的是库里没有的新料号 → 不收窄(两颗都还没建档时不该拦着人)。 * **这只是表单的引导,不是墙**:服务端照收两格不同类型的组合(`launch-params.ts` 头注), * 类型一致由 S1 的收步判据 `sub_eval_part_distinct_chip_type_id = 1` 管 —— 那才是数得出来的门。 * * **两格不能是同一颗料**(`x-distinct: 'part'`,2026-08-31 用户定:「不应该能选到同样的两个芯片」)—— * 这一条**是墙**,不是引导,理由与上面「留空」那段是同一条链:两格同一个料号 → 上面那笔 `project` * 铸的两行撞上同一个主键(`eval_ref + internal_pn`,`role` 不在主键里)→ 后一笔覆盖前一笔 → * `sub_eval.part` 收成一行 → `sub_eval_part_exists_status_found = 2` 永不成立,S1 收不了步; * 而件上那两行人加不出来(块① 行源 `mine:parts.profile`,没有「+加一行」),**唯一出路是终止重开**。 * 墙立在起单这一刻,是因为**路只在这里** —— 那时换一格就好了。运营端表单据此不把另一格已经选走的 * 料号列出来;自己写一个同样的、或脚本 / API 直接起单,由服务端 `validateLaunchParams` 拒。 * * requestDescription / candidateInternalPns(数组)/ * datasheets 三键已随重写撤下 —— 这一版没有「需求主记录」「N 候选并行」「预填 * datasheet」这几件事。 */ data: { type: 'object', required: ['originalInternalPn', 'candidateInternalPn'], properties: { originalInternalPn: { type: 'string', title: '原内部料号', description: '差异评估基准料号(必填);库里有就选,没有就自己写一个新的 —— 它的主数据(品牌/类型/参数)缺了才由第一步派数据工单补齐。', examples: ['LM2596S-5.0'], 'x-control': 'combo', 'x-options': { ledger: 'parts.profile', value: 'internal_pn', label: 'internal_pn', group: { column: 'chip_type_id', ledger: 'parts.chip_type', value: 'type_id', label: 'type_name' } }, 'x-order': 1, 'x-link': 'chip-type', 'x-distinct': 'part', }, candidateInternalPn: { type: 'string', title: '候选内部料号', description: '被评估的候选料号(必填);库里有就选,没有就自己写一个新的 —— 它的主数据(品牌/类型/参数)缺了才由第一步派数据工单补齐。', examples: ['XL2596S-5.0'], 'x-control': 'combo', 'x-options': { ledger: 'parts.profile', value: 'internal_pn', label: 'internal_pn', group: { column: 'chip_type_id', ledger: 'parts.chip_type', value: 'type_id', label: 'type_name' } }, 'x-order': 2, 'x-link': 'chip-type', 'x-distinct': 'part', }, }, }, messageAllowed: true, messageHint: 'FAE 可在此说明客户的应用边界或验证条件', }, output: { /* 单一必发口:签下去 P-11 落账、P-06 落账;载荷是已落地的事实,不是待办 */ mandatory: [ { name: 'assessment-released', description: '差异评估结论已落账(P-11 替代关系主记录 + 差异明细、P-06 验证记录)—— 载荷即已落地的事实', payload: { type: 'object', required: ['evalRef', 'relationId', 'originalPn', 'candidatePn', 'recommendationLevel', 'matchTotal', 'mismatchTotal', 'unknownTotal', 'conditionalTotal', 'validationId'], properties: { evalRef: { type: 'string', title: '评估案号' }, relationId: { type: 'string', title: '替代关系ID' }, originalPn: { type: 'string', title: '原内部料号' }, candidatePn: { type: 'string', title: '候选内部料号' }, recommendationLevel: { type: 'string', title: '推荐等级' }, matchTotal: { type: 'integer', title: 'MATCH 条数' }, mismatchTotal: { type: 'integer', title: 'MISMATCH 条数' }, unknownTotal: { type: 'integer', title: 'UNKNOWN 条数' }, conditionalTotal: { type: 'integer', title: 'CONDITIONAL 条数' }, validationId: { type: 'string', title: '关联验证ID' }, }, }, }, ], selective: [], }, /* 一份成果常不止一张表:差异评估结论由明细 + 关系两张共同承载,落账内容由关系 + 验证记录承载 */ requiredDeliverables: [ { key: 'difference-items', label: '差异评估结论', ledger: 'substitution.difference_item', ledgers: ['substitution.difference_item', 'substitution.relation'], editableAtRelease: [] }, { key: 'landing', label: '落账内容', ledger: 'substitution.relation', ledgers: ['substitution.relation', 'validation.record'], editableAtRelease: [] }, ], /* * 验收判据(spec §7)—— 五条机检 L1/L2 全读基座数,两条 L4 裁量,一条 L5 责任关(不上卡)。 * `input-ready` 的谓词与 S1(Task 3 待写)的 `doneWhen` 同构:两颗都在库、类型一致、参数不缺 —— * **不写「参数行数 = 字段数×2」**,那不是基座数得出来的数(`parts_type_param_rows` 数的是 * 本单写下的字段行,不是整本字典;给判据编一个数不出来的数,是这个仓踩过六次的同一个坑)。 */ acceptanceCriteria: [ { key: 'input-ready', label: '评估输入已就绪:原 PN 与候选都在库、归了同一个芯片类型、主数据与参数值都不缺必填', level: 'L1', predicate: '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', }, { key: 'every-param-judged', label: '每条核心参数都有明确状态:每条差异明细都是 MATCH / MISMATCH / UNKNOWN / CONDITIONAL 之一,没有一条缺必填', level: 'L2', predicate: 'substitution_difference_item_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', }, { key: 'level-by-rule', /* §3.5 的规则原样译成析取式:推荐等级只有一种取值能配上当前的 judgement_class 分布,规则说了算不是话术说了算 */ label: '推荐等级与判定分类一致:有硬约束不符的候选不能是「推荐」,等级由规则算,不由措辞改判', level: 'L2', predicate: '(substitution_relation_recommendation_level_not_recommended = 1 and substitution_difference_item_judgement_class_hard_mismatch > 0) or ' + '(substitution_relation_recommendation_level_conditional = 1 and substitution_difference_item_judgement_class_hard_mismatch = 0 and ' + '(substitution_difference_item_judgement_class_unknown > 0 or substitution_difference_item_judgement_class_conditional > 0 or substitution_difference_item_judgement_class_soft_mismatch > 0)) or ' + '(substitution_relation_recommendation_level_recommended = 1 and substitution_difference_item_judgement_class_hard_mismatch = 0 and ' + 'substitution_difference_item_judgement_class_unknown = 0 and substitution_difference_item_judgement_class_conditional = 0 and substitution_difference_item_judgement_class_soft_mismatch = 0)', }, { key: 'unknown-has-disposition', label: '每个 UNKNOWN 都有去向:去向、影响说明、判断依据没有一条缺', level: 'L2', predicate: 'substitution_difference_item_missing = 0', }, { key: 'validation-landed', label: '验证记录已落账:P-06 恰有一行且不缺必填(全推荐也要写「本轮无待验项」)', level: 'L2', predicate: 'validation_record_rows = 1 and validation_record_missing = 0', }, /* * **两条 L4 补了谓词**(2026-08-30,真跑 e2e 撞见的结构性发现 —— **不是**记进 * `docs/ACT-WORK-05-端到端测试记录.md`:那份文档整篇是 2026-08-27 及更早的旧 * 九步设计,四步重写[2026-08-29]之后没有再往里追加过任何一条,已在文档开头标为 * 历史;这条发现连同其余 2026-08-30 的都只活在这份源码注释与 `docs/debt.md` 里)。 * 「裁量项带谓词时谓词说了算」这条纪律 * (2026-08-02 立,`engines.ts` 的 `criterionCheck`:`settledByData`)本仓已有先例, * 从前 W05 这两条 L4 是裸的(无 `predicate`),而 T_release 全程 `auto` 驱动、 * `submit` 是五个声明式动词之一(`packages/contracts/src/auto-recipe.ts`), * **没有第六个 `judge` 动词** —— 裁量项没谓词就只能等 `judgement`(Agent 的 `judge` * 行动)来结算,而这个活动压根不会有 Agent 走到这一步。结果是 `submittable()` * 对这两条永远读到 `pass:false`,`T_release` 的 `submit` 每一拍都被 * `raiseAcceptance` 拒,而拒绝不算错误(不触发 `failedAt`)、下一拍原样重来 —— * 稳定地把步数预算耗到 `exhausted`,而不是报错崩溃,所以静态审计与包内自测都没照见它。 * * 两条谓词只覆盖**客观、基座算得出来的那一半**(有没有值、值哪来的), * **主观那一半(写得好不好、可信不可信)已经由人工验收清单接住**——S3 审核卡 * 第 1/4 条「逐条对过原料与候选料的参数值…」「影响说明站得住…」正是这两条 L4 * 标签里「站得住」「对得上」那两个字的着落点,人签字通过那一刻已经确认过。 * 谓词为真只说「客观前提具备」,不越权替人下「写得好」这个主观判断。 */ { key: 'evidence-credible', label: '证据链可信:每条差异的影响说明与判断依据站得住', level: 'L4', /* 客观前提:每一行都有值(必填不缺)——写得「站不站得住」是 S3 人工验收清单的事 */ predicate: 'substitution_difference_item_missing = 0', }, { key: 'values-traceable', label: '参数值与原厂资料对得上:候选与原 PN 的取值来源可追溯', level: 'L4', /* 客观前提:原始参数集不缺、差异明细里逐条的原值候选值也不缺 —— 两值均来自 parts.parameter 的 lookup,不是模型现编;「对不对得上」是 S3 人工验收清单的事 */ predicate: 'parts_parameter_missing = 0 and substitution_difference_item_missing = 0', }, /* L5 是责任关:那张卡本身就是这道关,卡面上不摆它(基座统一滤) */ { key: 'gate03-release', label: 'ACT-GATE-03 评审放行:签下去 P-11 落账、P-06 落账,结论供下游消费', 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: '本活动只答技术可替代性:不做数量、价格、交期、采购判断' }, ], authorization: { discretion: [], preauthorizedActions: [], /* 调人三次(补数据 / 审核 / 放行);零工具、AI 只出场一次 —— 预算随之从 16/90 步收紧到 8/40 步 */ humanCallBudget: 8, }, resources: { tools: [], skills: [], models: ['deepseek-v4-flash'], roles: ['FAE/RI'], budget: { steps: 40, tokens: 300_000, activeMinutes: 60, humanCalls: 8 }, }, }, /* ── 声明 ────────────────────────────────────────────────────── */ declarations: { models: ['deepseek-v4-flash'], /* 零工具 —— 这是这次重写的立场,不是疏漏(见头注) */ tools: [], ledger: [ /* ── 两张类型字典(parts 域,与 P-08 同域;Task 1 已物化 + 播种)── */ { domain: 'parts', object: 'chip_type', kind: 'master', label: '芯片类型字典', fields: [ /* * **`type_id` 声明成 `text` 而不是 `integer`**(Task 7b,2026-08-29,件① 补「新建类型」 * 逼出来的两处平台级发现,记在这里免得下一个人重踩): * ①`resolveRowKey`(`apps/api/src/app/routes/desk.ts`)的「③ 续号」只认 `isSeq` * (declared type 是 integer/number),不看 `auto` —— 这个类型碰巧是**唯一**一个 * `auto:true` 又是 `integer` 的主键,于是它抢在 `writeEditedRows` 的 `autoKeys()` * (真正的续号器,数official+草稿叠加、不会撞号)前面,自己按「本单草稿有几行」 * 铸了个从 1 起的号——种子已经占了 `type_id=1/2`,人第一次新建类型就会撞车, * 把种子的「DC-DC 降压稳压器」悄悄改名。②即便①不发生,`autoKeys()` 的续号是 * 两位补零字符串(`'03'`),而**引用它的那几列**(`parts.profile.chip_type_id` 等) * 若声明成 `integer`,写入时会被 `coerceToColumn` 按 `Number()` 解析掉前导零—— * `'03'` 存成 `parts.chip_type.type_id`、`3` 存成 `parts.profile.chip_type_id`, * `refersTo`/`data-by` 那几处按 `String(...)` 比对的查找会全部失配(`'03' !== '3'`), * 写入当场被 `refersTo` 拒、或下拉/查表显示不出人刚建的类型。 * **两处病根都是「integer 类型的自增列」这个此前从未出现过的组合触发的**——本仓 * 唯一先例 `contact_code` 一直是 `text`。**`resolveRowKey`/`coerceToColumn` 这两处 * 平台代码本身仍带着这两个坑没修**(下一个建同样组合主键的人会重新踩上,已记 * `docs/debt.md`)——这里只是在活动自己的声明层绕开,不是修复。 * * 改成 `text` 为什么是安全的(此列与下面 5 处同引用列一起改,保持同一条身份链路 * 全程一种类型):**`ledger.__draft`(任务级草稿层)才是 JSONB** * (`ledger-store.ts` 的 `DRAFT_TABLE`/`DRAFT_DDL`:`key`/`patch`/`baseline` 三列都是 * `jsonb`)——但 `master`/`record` 这类**正式表是真实的物理列**(同文件 `PG_TYPE` * 映射,`integer → bigint`,`create table`/`add column`/`alter column ... type ...` * 都拼的是真 PG 类型)。改声明安全,靠的是 `schema-merge.ts` 的**单调加宽 lattice** * (`SCALAR_CHAIN.integer = ['integer', 'number', 'text']`,`widen()` * 判定 `integer → text` 是**合法加宽**、不是类型冲突)配合 `widen-column` 那支生成的 * 显式转换(`alter table … alter column … type text using col::text`——整数转文本 * 沿 PG 的 `bigint::text` 转型,没有失败路径)。这次重装(`activities:install`)会 * 把 `ledger.parts__chip_type.type_id` 等六列从 `bigint` 真的 `ALTER … TYPE text` * 过去一遍。 * * S-FEEL 那几处只做等值/去重比较,不做数值运算,不受影响(已逐处核对:distinct * 计数 / lookup·take / refersTo 皆按字符串比,`>` 只用在 `distinct_chip_type_id` * 这个计数列上,不用在值本身)。查证过程详见 Task 7b 报告 * (`.superpowers/sdd/2026-08-29-act-work-05-rewrite/task-7b-report.md`)。 */ col({ name: 'type_id', type: 'text', label: '类型编号', primaryKey: true, auto: true }), col({ name: 'type_name', type: 'text', label: '类型名称', required: true }), col({ name: 'description', type: 'text', label: '类型说明', expect: true }), ], }, { domain: 'parts', object: 'type_param', kind: 'master', label: '芯片类型核心参数字段', fields: [ /* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text——见上面那条大注释 */ col({ name: 'type_id', type: 'text', label: '所属类型', primaryKey: true, refersTo: 'parts.chip_type.type_id' }), col({ name: 'param_name', type: 'text', label: '参数名', primaryKey: true }), /* AI 分析时读它 —— 这一格说清「这个参数是什么意思」,是 S2 唯一一次 analyze 出场的入参之一 */ col({ name: 'param_desc', type: 'text', label: '参数说明', required: true }), /* 约束类型按类型标一次,长在这里(不在每颗料的 `parts.parameter` 上)—— 见该表的头注 */ col({ name: 'constraint_type', type: 'text', label: '约束类型', required: true, tally: ['hard', 'soft'] }), col({ name: 'param_unit', type: 'text', label: '参考单位', expect: true }), ], }, /* * P-08 芯片主数据 —— 声明后为八列,物理表其余列本活动不碰。`createOnly` 撤掉 * (spec §3.2):它当初是为了拦模型凭空建料,而配方现在按本单两颗料铺行, * 凭空建在结构上已不可能;撤掉它换来一件必需的事 —— 给在库的老料补上 * `chip_type_id` 这一格(迁移前建的行没有类型,不许改就永远补不了)。 * package / process_technology / eol_pcn_status / application_scenarios 不声明: * 封装和工艺本来就是「核心参数」,该以类型字段的身份出现在字典里,不在主记录上另开一列。 */ { domain: 'parts', object: 'profile', kind: 'master', label: '芯片主数据(P-08)', fields: [ col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }), /* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text —— 见该字段的大注释 */ col({ name: 'chip_type_id', type: 'text', label: '芯片类型', required: true, distinct: true, refersTo: 'parts.chip_type.type_id' }), col({ name: 'brand_name', type: 'text', label: '品牌名称', required: true }), col({ name: 'lifecycle_status', type: 'text', label: '生命周期状态', required: true, tally: ['active', 'nrnd', 'eol', 'discontinued', 'risk'] }), col({ name: 'specification_description', type: 'text', label: '规格描述', expect: true }), col({ name: 'manufacturer_name', type: 'text', label: '制造商名称', expect: true }), col({ name: 'datasheet_ref', type: 'text', label: 'Datasheet 引用', expect: true }), col({ name: 'datasheet_version', type: 'text', label: 'Datasheet 版本', expect: true }), ], }, /* * P-08 子表:标准化参数集,一行一参数。`createOnly` 撤掉(理由同上一张:在库那颗料的 * 现值要能回填进草稿层,只许建新就写不进去)。两处退休(spec §3.2): * `constraint_type` 搬去 `parts.type_param`(同一个参数在两颗料上标法不一致这件事从结构上 * 消失);`param_name` 的自指引用墙与 `distinct` 一并退休 —— 它当初是为了硬逼两颗料用同一套 * 参数名,而字段清单现在由类型字典给、行由配方按字典铺,名字对不上根本不可能发生。 */ { domain: 'parts', object: 'parameter', kind: 'master', label: '标准化参数集(P-08 子表)', fields: [ col({ name: 'internal_pn', type: 'text', label: '所属料号', primaryKey: true, refersTo: 'parts.profile.internal_pn' }), col({ name: 'param_name', type: 'text', label: '参数名', primaryKey: true }), col({ name: 'param_value', type: 'text', label: '值', required: true }), col({ name: 'param_unit', type: 'text', label: '单位', expect: true }), /* 值说明,AI 也读它(spec §3.2) */ col({ name: 'param_note', type: 'text', label: '说明', expect: true }), /* * **约束类型不在这里** —— 它长在 `parts.type_param`(按类型标一次)。 * 从前每颗料各标一次,同一个参数在两颗料上标法不一致就成了可能; * 而比对是逐参数对着比的,两边约束档不同,判出来的四态就没有意义。 */ ], }, /* * ── 活动自持两张(spec §3.3,顶掉退休的 pn_check / input_gap / verdict 三张)── * `sub_eval.part` 是配方唯一能拿来遍历「哪几颗料要建」的数据源(`each: 'mine:sub_eval.part' * where exists_status = absent`)—— 挤进一行案卷里就没法 `where` 过滤,而不能过滤就会给 * 已经在库的那颗料也铺一遍空参数行,这是非拆成两张不可的理由。 */ { domain: D, object: 'part', 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'] }), /* * **「在库」是一次 lookup 的结果,不是一句断言**(spec §3.3):判 found 就必须拿得出 * 一个基座查得到的料号,`refersTo` 让查无此值当场拒。`exists_status` 与这一列**每一拍 * 由配方重算**(不设 created 档、不靠谁记账)—— 人把料建好之后自己变 found。 */ col({ name: 'found_pn_ref', type: 'text', label: '在库核对依据', refersTo: 'parts.profile.internal_pn', requiredWhen: { column: 'exists_status', value: 'found' } }), col({ name: 'exists_status', type: 'text', label: '在库状态', required: true, tally: ['found', 'absent'] }), col({ name: 'chip_type_id', /* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text —— 见该字段的大注释 */ type: 'text', label: '芯片类型', required: true, /* * **`distinct`**:「两颗料是不是同一个类型」从此是基座数得出来的数 * (`sub_eval_part_distinct_chip_type_id`)—— 不同取值恰好 1 种才算一致。 * 这一条是 S1 收步与 S2 入场的硬门:两边字段清单不是同一套,逐项比对无从谈起。 */ distinct: true, }), ], }, { domain: D, object: 'case', kind: 'record', caseKey: 'eval_ref', label: '本单案卷', fields: [ col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }), /* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text —— 见该字段的大注释 */ col({ name: 'chip_type_id', type: 'text', label: '本单锁定的芯片类型' }), col({ name: 'gate_reason', type: 'text', label: '入参未就绪原因(件上摆的就是它)' }), /* * **红条的开关是真数据,不是那句话空不空**(2026-08-31 用户:「没有必填要警告了, * 红色区域就不要显示」):gate_reason 清成空串时按列归型**不落笔**(空值=不写这一列, * jsonb 合并保旧值)—— 那句话一旦写下就清不掉,红条永远亮着。开关列两个值都非空, * 两个方向都写得动;件上 data-class-of 投影 v-clear 收红条,判断留在样式表。 */ col({ name: 'gate_state', type: 'text', label: '入参闸门状态', tally: ['open', 'clear'] }), col({ name: 'summary', type: 'text', label: '结论摘要' }), ], }, /* * P-11 替代关系主记录 —— 一单一行。退休三列(spec §3.4):`substitution_type`(新流程里没有 * 它的定夺人)、`confidence_score`、`analysis_report_ref`;`recommendation_level` 的 * `alternate` 档一并退休(原 D02 的 configurable 来路已不存在,只剩三档)。 */ { 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: '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' }), /* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text —— 见该字段的大注释 */ col({ name: 'chip_type_id', type: 'text', label: '芯片类型', required: true }), col({ name: 'recommendation_level', type: 'text', label: '推荐等级', required: true, tally: ['recommended', 'conditional', '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'] }), col({ name: 'verification_requirement', type: 'text', label: '验证要求', requiredWhen: { column: 'recommendation_level', value: 'conditional' } }), col({ name: 'validation_id', type: 'text', label: '关联验证ID' }), /* 固定 fae_confirmed —— 分析状态与客户批准状态分离,本活动只写这一层 */ col({ name: 'analysis_status', type: 'text', label: '分析状态', required: true }), col({ name: 'rule_version', type: 'text', label: '差异规则版本', required: true }), ], }, /* * P-11 差异明细子表 —— 一参数一行。`evidence_ref` 改名 `evidence_note`「判断依据」: * 不写 P-10 了,AI 能给的就是依据不是文件引用,名字得说实话(spec §3.4)。 */ { 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 }), /* 字段说明快照,归档件不必 join 回字典 */ col({ name: 'parameter_desc', type: 'text', label: '参数说明', expect: true }), col({ name: 'constraint_type', type: 'text', label: '约束类型', required: true, tally: ['hard', 'soft'] }), 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 }), /* AI 判:逐参数比对,S2 唯一一次 analyze 出场的出参之一 */ col({ name: 'difference_status', type: 'text', label: '差异状态', required: true, tally: ['match', 'mismatch', 'unknown', 'conditional'] }), /* * **`judgement_class` 是合成列**:推荐等级要数的是「**硬约束**不符有几条」, * 而基座读数只会数单列 —— 把「约束类型 × 四态」先合成一列,规则才算得出来。 * 由配方按 §3.5 的 when 链算,不是 AI 出的。 */ col({ name: 'judgement_class', type: 'text', label: '判定分类', required: true, tally: ['match', 'soft_mismatch', 'hard_mismatch', 'unknown', 'conditional'] }), col({ name: 'impact_statement', type: 'text', label: '影响说明(MATCH 写「无」)', required: true }), col({ name: 'evidence_note', type: 'text', label: '判断依据', 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: 'rule_version', type: 'text', label: '差异规则版本', required: true }), ], }, /* P-06 验证记录:一单一行,关联的是本单案号(P-01 不在本活动里,顶掉 request_id) */ { domain: 'validation', object: 'record', kind: 'master', label: '芯片验证记录(P-06)', fields: [ col({ name: 'validation_id', type: 'text', label: '验证ID', primaryKey: true }), col({ name: 'eval_ref', 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', 'parametric'] }), /* 固定 not_started */ col({ name: 'validation_status', type: 'text', label: '验证状态', required: true }), col({ name: 'test_condition', type: 'text', label: '测试条件', expect: true }), col({ name: 'next_validation_action', type: 'text', label: '后续验证动作', required: true }), ], }, ], listeners: [], harness: { memoryWindow: 24, stallAfter: 'PT4H', nudge: [{ after: 'PT2H', action: 'nudge' }], retry: { times: 2, backoff: 'PT30S' }, }, }, /* ── 资产初始版 ──────────────────────────────────────────────── */ assets: { /* * DMN 判定表已退役:D01(逐参数四态)改由 S2 唯一一次 analyze 判定;D02(推荐等级)的规则 * 搬进配方的 when 链(spec §3.5)—— 规则只许有一处真源,再留一份 DMN 就是同一件事两处 * 各存一份。`steps` 已是四步、`guide` 全部清空(auto 配方接管行为,Task 3 完成)。 */ businessModel: { cmmnXml: CMMN_XML, narrative: NARRATIVE, dmn: { drd: [], tables: [] }, steps: ASSESSMENT_STEPS, phases: ASSESSMENT_PHASES }, /* 三条技能已随重写退役(全程无 agent,没有临场编排可言);skills.ts 已在 Task 10 删除 */ skills: [], /* * 五张旧局面全部作废(它们绑的九步与旧台账地址已经不存在)。**Task 7 已补回第一张**: * `input-supplement`(评估输入补全,S1 的数据工单,三块竖排 —— 存在性与主数据 / * 芯片类型字段 / 参数值)。**Task 8 已补回第二张**:`difference-review`(差异评估结论审核, * S3 的验收工单 —— 对决头 + 四态分布带 + 推荐等级大徽章只读,逐条明细可就地改四态 / * 影响说明 / 判断依据 / UNKNOWN 去向,收口四格只读)。**Task 9 已补回第三张**: * `landing-confirm`(落账确认,T_release 的终局验收工单 —— 全只读,答「这一签下去发生 * 什么」:进哪几张表各几行 / 结论卡 / 四态分布并把硬约束不符逐条点名 / 待验项 / 发哪一口)。 * 三张件此刻齐了。看板自己的 `data` 声明(`LEDGER_BOARD_DATA`)同批换成了新九张表 —— * `sub_eval.pn_check`/`sub_eval.verdict`/`demand.request`/`customer.profile`/ * `parts.document` 这五张已退役地址不再出现。 */ viewTemplates: [ { name: 'activity-ledger', html: LEDGER_BOARD, data: LEDGER_BOARD_DATA }, { name: 'input-supplement', html: INPUT_SUPPLEMENT }, { name: 'difference-review', html: DIFFERENCE_REVIEW }, { name: 'landing-confirm', html: LANDING_CONFIRM }, ], cardSchemas: {}, /* 蒸馏钩子撤空:它是给 agent 在终态回看轨迹用的,而这个活动全程无 agent(AI 只出场一次、答一份结构化判断,没有可供归纳的多轮轨迹) */ distillHooks: [], }, /* * ── 样例集:五条形状真不同(常备案例即验收案例)── * **2026-08-29 Task 11 换成新入参形状**(单数 `candidateInternalPn`,与 §4.1 的启动两参同步 —— * 旧九步遗留的 `customerCode`/`requestDescription`/`candidateInternalPns`(数组)三键随之撤下, * 起单表单本就只剩两格)。五条覆盖 S1 的五种入场局面:①候选缺库(新建类型那条路,S1 数据工单 * 三块都要动)②两颗都在库且同型(零卡直接进 S2)③两颗都在库但分属不同芯片类型(硬拦, * 光凭其它格子填满绕不过去)④原 PN 缺库(候选在库)⑤两颗都缺库。 * 库里播了 LM2596S-5.0 / LM2596S-ADJ(DC-DC 降压稳压器,type_id=1)与 AMS1117-3.3 * (LDO 线性稳压器,type_id=2)三颗(见 seed-mvp-data.ts);XL2596S-5.0 / CN-DCDC-32V / * LM2596S-3.3 刻意不在库。 */ samples: [ { name: '候选缺库·建档定型', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'XL2596S-5.0' }, expect: { ports: ['assessment-released'] } }, { name: '都在库·零卡直分析', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'LM2596S-ADJ' }, expect: { ports: ['assessment-released'] } }, { name: '类型不一致·硬拦', input: { originalInternalPn: 'LM2596S-5.0', candidateInternalPn: 'AMS1117-3.3' }, expect: { ports: ['assessment-released'] } }, { name: '原PN缺库', input: { originalInternalPn: 'LM2596S-3.3', candidateInternalPn: 'LM2596S-ADJ' }, expect: { ports: ['assessment-released'] } }, { name: '两颗都缺库', input: { originalInternalPn: 'CN-DCDC-32V', candidateInternalPn: 'XL2596S-5.0' }, expect: { ports: ['assessment-released'] } }, ], entry: { file: 'index.js', checksum: 'dev-entry-part-substitution-assessment' }, };