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/docs/superpowers/specs/2026-08-29-act-work-05-rewrite-design.md
T
xcsandClaude Opus 5 45cec82984 fix(w05): 终审两条 Critical + 地基缺陷的另一半 + 文档与欠账收口
**C1 新建芯片类型会改写既有字典行(共享主数据被静默污染)**
链条逐行复核成立:件上种子行漏挂 data-col → barred 与 isSeq 双保护同时失效
(isSeq 那条是因为 Task 7b 把 type_id 从 integer 改成了 text)→ 落进「全表一致」,
而 H35 的自投影恰好保证这张表只有一行 → 人新建的类型被认成既有 type_id=1 →
确认时随范围放行把既有字典行改名并落进正式表,屏上只说「改了 1 行」。
三条各自成立的裁定叠出来的静默污染。
修法:种子行改成 disabled 的真实 <input data-col=type_id>(desk.ts 零改动),
格子里摆「由系统生成」——不能一声不吭地拿掉,那样人会以为这张档少了一格。
测试不止断言结果:另加两条用**真实 resolveRowKey** 重放病灶/修法两种 cells 形状,
直接证明因果机制。平台根治(全表一致应排除 auto 列)记 debt。

**C2 两个启动参数都选填 → 留空则整单永久无解**
gate_reason 说「在下面把两格补上」,而块① 没有加一行、也不在 gateLedgers ——
一句指向空气的指路。改为必填;第一档改口为唯一真无解的那条路径(两格填了同一个号)。

**I1 地基缺陷只修了一半:recipe.let 同样按对象键序求值**
与 fields 是一模一样的假设,同样住 jsonb 列、同样被 PG 重排;今天不炸只因 W05 那五条
互不引用,而 ADR-014 是给后续所有活动用的。新增 orderLets/letReferences
(把 {in:X} 视为依赖 —— 语义与 fields 不同,故单独实现),auto-runner 与静态审同步接入。

**I2 审核卡按「重做」照样收步推进** —— 坐实为真实平台缺陷(「卡答过了就收步」快道不看裁决),
并证明活动侧加选项救不了、chosenGuidance 那条也救不了(那是喂模型的提示词,
而这条快道存在的目的就是跳过模型)。按裁定只留复现测试,平台未改。

**I4** 同一堵墙撞第二次即发兜底执行工单交人 + 立监听(头一次软化留痕),
不再空转到 exhausted 而人无处可改。

debt +4(C1 根治 / modules→app / thead-after-tbody / data-by 落回外层),
并补写此前被两处引用却从不存在的「refersTo 传递闭包」条目。
文档:ADR-014:201「按声明顺序求值」改对(照它写配方的人会重犯本轮的地基缺陷)、
陈旧注释四处、spec 四处分叉逐条判断处置、端到端测试记录标为历史、
CLAUDE.md 纠正「新建类型端到端通了」那句(它没通过真人路)并更新版本与文档表。

包版本 2.1.9(已装,SQL 直查库内 version/required/gate_reason/视图 HTML 四项核实)。
pnpm gates 退出码 0(2476 通过 / 34 跳过);E04 92/92、I01 55/55 无回归。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-30 16:09:18 +08:00

38 KiB

ACT-WORK-05 差异评估 · 整体重写设计

状态:已评审通过(2026-08-29,用户逐节点头) 落点:activities/part-substitution-assessment@2.0.0 + 平台侧「确定性步骤」机制 关联:ADR-014(确定性步骤,本设计是它的第一个落地者与两处改口)、ADR-004(不执行包代码)、 docs/活动开发规范.md、docs/HTML报告生成规范.md、docs/ss02-act-05-new/(业务素材)


1. 为什么推倒重来

现版 @1.7.3 是九步、两枚工具、全程 agent 驱动:每一拍问模型「下一步干什么」, 它自己 write 台账、自己 finish、自己发卡。docs/ACT-WORK-05-端到端测试记录.md 的 A–AE 发现表里,八条事故落在「本来就该是确定的」那几步上 —— 编号抄别单、判在库判错、 数据工单派给在库的料、没写骨架就发卡、库里有值当缺口、抄别的料的规格书。

用户 2026-08-29 定的新立场:

「不是把所有东西都丢给 ai 来处理,不要给 ai 暴露任何的工具,全部由系统确定性的来控制, 一定是系统先确定性的从数据库中取出数据后交给 ai 分析。」

agent 只在真需要判断的地方出现,与「人只在责任点与放行点出现」是同一条原则。 这个活动里,真需要判断的只有一件事:这一条参数,候选跟原厂比算不算事。别的全是确定的。

新版:四步、零工具、零 guide、AI 出场恰好一次。


2. 已定的八条(评审逐条通过)

# 决定
1 驱动:落地 ADR-014 声明式配方 + 第四动词「一次结构化 AI 调用」。活动内部不再引第二套引擎(Mastra 仍在流程包编排与模型网关的原位)
2 台账:两张新字典表 + P-08 主记录 + 参数子表 + P-11 主/明细 + P-06 + 一张活动自持案卷。P-01 需求主记录 / P-10 原厂资料 / 客户档案本活动不碰
3 P-06 芯片验证记录:每条替代关系固定落一条,全推荐的写「本轮无待验项」
4 结论谁定:逐条四态由 AI 判;推荐等级、风险、摘要、推荐理由由规则与模板算
5 补数据的执行工单:保存即铺格 —— 人选定/新建类型后按一次保存,系统按该类型的字段清单把参数值的行铺出来,人只填值,不加行不删行
6 两颗料类型不一致:硬拦,与「PN 不在库」同一条路(派工单让人改),保住「两边字段清单一定是同一套」这个前提
7 结论审核卡:只有「确认通过」一个钮,改全在件上;人改完不重跑 AI,只重算等级与摘要
8 机制规模:只实现本活动真用得到的那几式;没实现的形状装包时静态审当场拒

3. 数据模型

3.1 两张新字典表(parts 域,与 P-08 同域)

parts.chip_type · 芯片类型

列 型 档 说明
type_id integer 主键 · auto 人不填,基座续号(现有最大 +1)
type_name text 必填 「DC-DC 降压稳压器」;件上下拉显示它
description text 建议 这个类型是干什么的

parts.type_param · 芯片类型核心参数字段

列 型 档 说明
type_id integer 主键 · refersTo parts.chip_type.type_id 属于哪个类型
param_name text 主键 「输入电压范围」
param_desc text 必填 这个参数是什么意思 —— AI 分析时读它
constraint_type text 必填 · tally hard/soft 硬约束 / 软约束
param_unit text 建议 参考单位

关联:一行 P-08 关联一个类型;多行 type_param 关联一个类型。

3.2 P-08 及其子表的改动

parts.profile(声明后为八列,物理表其余列不声明 = 本活动不碰):

internal_pn(主键)· chip_type_id integer 必填 refersTo parts.chip_type.type_id · brand_name 必填 · lifecycle_status 必填 tally · specification_description 建议 · manufacturer_name 建议 · datasheet_ref 建议 · datasheet_version 建议

createOnly 撤掉(现版标着它,只许建新料、不许改已有行)。它当初是为了拦模型凭空建料; 模型现在不在场,行由配方按本单两颗料铺出来,凭空建在结构上不可能。而撤掉它换来一件必需的事: 给在库的老料补上「芯片类型」这一格 —— 迁移前建的行没有类型,不许改就永远补不了,单子卡死。

不声明:package 封装 · process_technology 工艺 · eol_pcn_status · application_scenarios。 封装和工艺本来就是「核心参数」 —— 它们该以类型字段的身份出现在字典里,不在主记录上另开一列。

parts.parameter:internal_pn(主键,refersTo profile)· param_name(主键)· param_value 必填 · param_unit 建议 · param_note 建议(值说明,AI 也读它)

parts.parameter 的 createOnly 同样撤掉(理由同上:④那一笔要把在库那颗料的现值回填进草稿层, 只许建新就写不进去)。

两处退休:

  • constraint_type 退休 —— 搬去 type_param。同一个参数在两颗料上标法不一致这件事从结构上消失。
  • param_name 的自指引用墙退休(refersTo: 'parts.parameter.param_name')。它当初是为了硬逼两颗料 用同一套参数名,措辞拦了三次没拦住才砌的;现在字段清单由类型字典给、行由系统按字典铺, 名字对不上根本不可能发生。distinct 一并退休。

3.3 活动自持两张(顶掉现在的 pn_check / input_gap / verdict 三张)

sub_eval.part · 本单两颗料,kind: record,caseKey: eval_ref —— 一单恰两行

eval_ref(主键)· internal_pn(主键)· role 必填 tally original/candidate · found_pn_ref(refersTo parts.profile.internal_pn,requiredWhen exists_status = found)· exists_status 必填 tally found/absent · chip_type_id integer 必填 · distinct: true (「两颗料是不是同一个类型」于是成了基座数得出来的数:不同取值恰好 1 种)

sub_eval.case · 本单案卷,同上 —— 一单一行

eval_ref(主键)· chip_type_id(本单锁定的类型)· gate_reason(入参未就绪的原因,件上摆的就是它)· summary(结论一句话)

found_pn_ref 是「在不在库」的依据:判 found 就得拿得出一个基座查得到的料号, refersTo 让查无此值当场拒 —— 「在库」从此是一次 lookup 的结果,不是一句断言。 exists_status 与两处 chip_type_id 每一拍由 lookup 重算 —— 人把料建好之后自己变 found, 不设 created 档、不靠谁记账。

为什么非拆成两张不可:part 是配方唯一能拿来遍历「哪几颗料要建」的数据源 —— each: 'mine:sub_eval.part' where exists_status = absent。挤进一行案卷里就没法 where 过滤, 而不能过滤就会给已经在库的那颗料也铺一遍空参数行,_missing 当场报缺、单子死在那儿。

3.4 结论三张

substitution.relation(P-11 主记录,一单一行)

substitution_relation_id(主键)· eval_ref 必填 · original_internal_pn / candidate_internal_pn 必填 refersTo profile · chip_type_id 必填 · recommendation_level 必填 tally recommended/conditional/not_recommended · recommendation_reason 必填 · difference_summary 必填 · risk_level 必填 tally low/medium/high · verification_requirement(requiredWhen recommendation_level = conditional)· validation_id · analysis_status 必填(固定 fae_confirmed)· rule_version 必填

退休:substitution_type(新流程里没有它的定夺人)· confidence_score · analysis_report_ref · recommendation_level 的 alternate 档(原 D02 的 configurable 来路已不存在)。

substitution.difference_item(P-11 明细,一参数一行)

substitution_relation_id(主键 · distinct · refersTo relation)· difference_item_id(主键)· parameter_name 必填 · parameter_desc 建议(字段说明快照,归档件不必 join)· constraint_type 必填 tally · original_value / candidate_value 必填 · parameter_unit 建议 · difference_status 必填 tally match/mismatch/unknown/conditional(AI 判) · judgement_class 必填 tally match/soft_mismatch/hard_mismatch/unknown/conditional(系统合成) · impact_statement 必填(AI)· evidence_note 必填(AI)· unknown_disposition(requiredWhen difference_status = unknown,AI)· risk_level 必填 tally · rule_version 必填

judgement_class 是这套设计的关键一列:推荐等级要数的是「硬约束不符有几条」, 而基座读数只会数单列 —— 把「约束类型 × 四态」先合成一列,规则才算得出来。 evidence_ref 改名 evidence_note「判断依据」:不写 P-10 了,AI 能给的就是依据不是文件引用,名字得说实话。

validation.record(P-06,一单一行)

validation_id(主键)· eval_ref 必填(顶掉 request_id —— P-01 不在本活动里)· validation_internal_pn 必填 refersTo profile · validation_type 必填 tally(追加一档 parametric)· validation_status 必填(固定 not_started)· test_condition 建议 · next_validation_action 必填

3.5 规则(唯一真源在配方里,不再另留 DMN)

judgement_class:match → match;unknown → unknown;conditional → conditional; mismatch 且硬 → hard_mismatch;mismatch 且软 → soft_mismatch。

明细 risk_level:hard_mismatch → high;unknown/conditional/soft_mismatch → medium;match → low。

recommendation_level(读 judgement_class 的 tally 计数,自上而下第一条命中):

条件 等级
hard_mismatch > 0 not_recommended
unknown > 0 conditional
conditional > 0 conditional
soft_mismatch > 0 conditional
其余 recommended

关系 risk_level:not_recommended → high;conditional → medium;recommended → low。

模板串:

  • difference_summary = 「共 N 项核心参数:x 一致 / y 不一致(硬 z)/ u 待确认 / c 有条件」
  • recommendation_reason = 按等级三形:点名硬约束不符的参数 / 点名待确认与有条件的项 / 「N 项全部一致,无待确认项」
  • verification_requirement = 「实测并回填:<UNKNOWN 与 CONDITIONAL 的参数名,顿号连>」
  • P-06 test_condition = 同上参数名列表(没有就写「无」);next_validation_action = 「实测上述参数并回填 P-08 标准化参数集」/「本轮无待验项」

4. 四步案例模型

进度条四格:校验入参 → 差异分析 → 结论审核 → 落账放行。 全程 guide: ''、tools: []、declarations.tools: []。

4.1 启动参数(运营中心活动详情一键启动)

两格,都选填:

  • originalInternalPn「原内部料号」—— x-control: combo,x-options 从 parts.profile.internal_pn 现读;可选库里的,也可自己写一个新的
  • candidateInternalPn「候选内部料号」—— 同款

4.2 S1 校验入参与补数据 —— 纯系统 +(条件)一张数据工单

let  evalRef = cat('W05-', fmt(case:openedAt, digits))

① project sub_eval.part  from rows[{pn:入参原PN, role:'original'}, {pn:入参候选PN, role:'candidate'}]
     eval_ref      = {in:'evalRef'};internal_pn = {col:'pn'};role = {col:'role'}
     exists_status = when lookup('parts.profile' by internal_pn={col:'pn'} take internal_pn) != ''
                       then 'found' else 'absent'
     chip_type_id  = lookup('parts.profile' by internal_pn={col:'pn'} take chip_type_id, else '')

② project parts.profile  prune  from each('mine:sub_eval.part')     ← **两颗料都铺**
     internal_pn = {col:'internal_pn'}
     chip_type_id / brand_name / lifecycle_status / specification_description /
     manufacturer_name / datasheet_ref / datasheet_version
        = lookup('parts.profile' by internal_pn={col:'internal_pn'} take 同名列, else '')  ← 回填现值
     ↑ 与③同构。在库那颗回填出它此刻的样子;要建那颗全空等人填。
       **「在库的老料还没归类型」于是也被拦住** —— 那一格回填出来是空的,而 chip_type_id 必填。
       人不必在件上「加一行」:要建哪颗料是启动参数就定死的事,行由系统摆好,他只填格子。

③ project parts.parameter  prune
     from each('mine:sub_eval.part')                       ← **两颗料都铺,不只是缺库那颗**
       then each('parts.type_param' where type_id={outer:'chip_type_id'})
     internal_pn = {outer:'internal_pn'};param_name / param_unit ← {col:…}
     param_value = lookup('parts.parameter' by internal_pn={outer:'internal_pn'},
                          param_name={col:'param_name'} take param_value, else '')   ← **回填现值**
     param_note  = 同上 take param_note
     ↑ 三件事一笔办完:
       · 人还没挑类型 → {outer:'chip_type_id'} 是空串,where 一行不命中,什么也不铺;
         他挑了、按了保存,行就出来 —— **「保存即铺格」不是特判,是这一笔的自然结果**;
       · 在库那颗料**也**铺,值回填现值 → 屏上是它此刻的样子,`_missing` 不会误报;
       · **「在库但参数没录全」于是自动被拦住** —— 那一格回填出来是空的,红带点名、闸按不下。

④ project sub_eval.case  from once
     eval_ref     = {in:'evalRef'}
     chip_type_id = lookup('mine:sub_eval.part' by role='original' take chip_type_id, else '')
     gate_reason  = when 链(**全读基座数**):
                      两颗 PN 有一格为空                       → 「原 / 候选料号还没填」
                      sub_eval_part_exists_status_absent > 0    → 「<PN> 库里没有,请在下面建」
                      sub_eval_part_distinct_chip_type_id > 1   → 「两颗料类型不一致,换一颗或把类型改对」
                      parts_profile_missing + parts_parameter_missing > 0 → 「还差 n 格没填」
                      其余                                     → ''
     ↑ **这一笔必须排在②③之后,不许提前**(2026-08-29 走查实测):`gate_reason` 是**摆给人看的那句解释**,
       而它引的 `parts_profile_missing` / `parts_parameter_missing` 要等②③把骨架行铺出来才有值。
       排在前面时,引擎在②③写入**之前**给它读到 0 —— 卡照样正确发出(`card.when` 排在更后面,读到的是真缺口),
       但卡上那句「还差什么」是**空白的**,恰在人最想要一句解释的时候。
       **一笔读的是另一笔写出来的数,它就得排在那一笔后面。**

⑤ card 执行工单 · variant data · view 'input-supplement'      when gate_reason != ''
     gateLedgers = [parts.chip_type, parts.type_param, parts.profile, parts.parameter]

⑥ say

doneWhen  sub_eval_part_exists_status_found = 2
          and sub_eval_part_missing = 0                 ← 两行的 chip_type_id 都有值
          and sub_eval_part_distinct_chip_type_id = 1   ← 且是同一个类型
          and parts_profile_missing = 0
          and parts_parameter_missing = 0

doneWhen 里没有「行数 = 字段数×2」这条对账,是故意的:参数行由 prune + each 字典 铺出来,人不加行不删行,少一行在结构上不可能发生(真发生就是配方 bug,归静态审与包内自测)。 而「本类型有几个字段」根本不是基座数得出来的数 —— parts_type_param_rows 数的是本单写下的 那几行(人新建的字段),不是整本字典。给判据编一个数不出来的数,是这个仓踩过六次的同一个坑。

  • 两颗都在库、类型一致、参数齐 → 一张卡都不发,首拍 doneWhen 即真,直接进 S2。
  • 人在件上按「保存」→ 系统 tick → 配方原样重跑:类型这会儿知道了,④把参数行铺出来; ⑤因为卡发过(worldTouched)不再发;件就地刷新。 幂等靠同 PK 覆盖 + 发过不再发 + prune,不靠谁记得住。
  • 人点「提交」= 数据工单确认 → chip_type / type_param / profile / parameter 当场进正式表 (平台 2026-08-27 的范围放行)。它们是主数据,该立刻可用,不等整单放行。

2026-08-30 补记:实现是八笔,不是上面写的六笔 ——「提交」那一句已经点出原因: chip_type / type_param 要跟着数据工单一起进正式表(范围放行只并 gateLedgers 名单上的表,见 docs/debt.md「范围放行没有对 refersTo 做传递闭包」),而它们此刻 只在人新建类型时才有草稿行——挑一个已有类型时天然零行,闸拿「零行」误判成没干完。 Ruling H35(e2e 真跑撞见)在②③之间插了两笔自投影(原样把选中的类型字典行 / 字段 清单行抄回自己身上,值不变、幂等,唯一目的是让 mine: 不再天然为空),使这两张表 能安全回到 gateLedgers 上。次序变成:①两颗料 → ②类型字典自投 → ③字段清单自投 → ④主数据骨架 → ⑤参数骨架 → ⑥案卷 → ⑦卡 → ⑧交代(与上面①③④⑤⑥对应,原④⑤⑥ 顺移成 ⑥⑦⑧)。这两笔不带 prune(与③节 §11.4 那条改口同一个理由:字典/字段清单是共享 的,历史选型的陈旧行留着不算错)。详见 case-model.ts 该两笔头上的大注释。

4.3 S2 差异分析 —— 纯系统 + AI 唯一一次出场

① analyze  model: deepseek-v4-flash  into 'result:items'
     input  from each('parts.type_param' where type_id=本单类型):
              { 字段名, 字段说明, 约束(硬/软), 单位,
                原PN值      = lookup('parts.parameter' by internal_pn=原PN,   param_name=col),
                原PN值说明   = 同上 take param_note,
                候选值      = lookup('parts.parameter' by internal_pn=候选PN, param_name=col),
                候选值说明   = 同上 take param_note }
     output schema { items: [{ param_name, difference_status, impact_statement,
                               evidence_note, unknown_disposition? }] }

② project substitution.difference_item  prune
     from each('parts.type_param' where type_id=本单类型)          ← **行由字典决定,不由 AI 决定**
     parameter_name / parameter_desc / constraint_type / parameter_unit ← {col:…}
     original_value / candidate_value  ← lookup('parts.parameter' …)
     difference_status / impact_statement / evidence_note / unknown_disposition
                                       ← lookup('result:items' by param_name={col:'param_name'}, else '')
     judgement_class = when 链(见 §3.5);risk_level 由它映射

③ project substitution.relation  from once
     recommendation_level / risk_level / difference_summary /
     recommendation_reason / verification_requirement  ← §3.5 规则与模板

④ project validation.record  from once

⑤ say

doneWhen  substitution_difference_item_rows > 0
          and substitution_difference_item_missing = 0
          and substitution_relation_rows = 1
          and validation_record_rows = 1

为什么不写 = 本类型字段数(这份 spec 早先的伪码里写过,是错的): 「本类型有几个字段」不是基座数得出来的数 —— parts_type_param_rows 数的是本单草稿写下的 字段行(人新建类型时加的那几个),不是整本字典。拿它当分母就是「编一个数不出来的数」, 本仓在这上面栽过六次。行数由 prune + each 字典 结构保证;判据只查「写了」与「不缺」。

这一步一张卡都不发,人不出现。 三处刻意的克制:

  • AI 决定不了「有几行」。 明细的行由类型字典 each 出来,AI 的出参只是拿 param_name 去 lookup 的一张查表;它漏了哪一条,那一行的四态就是空 —— required 立刻把它变成 _missing > 0,提交闸与放行门当场拦住。从前是 from each(AI 出参),AI 少给两条就少两行, 而每一行看着都合规。
  • AI 只答「这一条算什么、为什么」,值是多少一个字都不重述 —— 基座读得到的事实不让模型转述。
  • 推荐等级、风险、摘要、推荐理由全由规则与模板拼。永远不会出现「有硬约束不符却写着推荐」。
  • 出参不合 schema → 就地重问一次;第二次仍不成 → 留痕交人,不退回模型自由发挥。

4.4 S3 结论审核 —— 一张验收工单,人就地改

2026-08-30 改口:这一节写下时以为配方只有一笔(card),下一段自己就说漏了 —— 真正的配方是五笔:①差异明细重算笔(2026-08-30 task-12「结论纹丝不动」缺陷补的, 见下)→ ②③ 复制自 S2 的 RELATION_PROJECTION/VALIDATION_PROJECTION(同一个对象引用, 不是抄了两份)→ ④ card 审核卡(variant: 'review',件 difference-review,单一 选项「确认通过」,带人工验收清单四条)→ ⑤ say。

人在件上改四态 / 影响 / 依据 → 保存 → tick → 配方重跑 → ①重算笔先把人改的 difference_status 转译成 judgement_class/risk_level(这一步的存在理由:走查发现⑤ 逮到的真实缺陷——最初的设计只复制了②③,人改的四态没有任何一笔把它转成 judgement_class,②③读到的 tally 纹丝不动,人在件上改了却什么都没变;docs/debt.md 已记「S3 存在的全部意义就是让人纠正 AI;纠正流不下去,S3 就是摆设」)→ ②③把推荐等级、 摘要、风险重算一遍(同 PK 覆盖)→ 件刷新。改几次都行,不重跑 AI。

②③ 复制的是 S2 那一步配方同一个对象引用(纯投影、幂等),不去回退步骤 —— S2 已 收步,S3 不能跨步回跳,只能在自己的配方里再摆一份同样的投影逻辑。这是刻意的: 一步一份配方,判定分类/风险等级的 when 链只许有一处真源(共用引用而非各写一份)。

这一步不给 doneWhen(它触达世界),人点了「确认通过」即收步。

4.5 S4 落账放行 —— 纯系统派单

① project sub_eval.case.summary  from once   ← 模板串
② submit  view 'landing-confirm'  + 人工验收清单五条

人签 → 合并 substitution.relation / difference_item / validation.record / sub_eval.case 进正式表 → 发口 assessment-released。

出口载荷:evalRef · relationId · originalPn · candidatePn · recommendationLevel · matchTotal / mismatchTotal / unknownTotal / conditionalTotal · validationId。

4.6 预算

调人 3 次(补数据 / 审核 / 放行),cap 8;模型调用 1 次,tokens 30 万; 步数 40(保存一次 = 一拍,给人反复保存留余量);activeMinutes 60。 对照现版:90 步 / 200 万 token / 16 次调人。


5. 平台要补的能力

全是加法。E04 / I01 一个字不用改 —— 有 guide 的步照常问模型。

5.1 PlanStep.auto 配方 + 求值器

契约照 ADR-014 §4 的形状,只实现这几式:

  • 动词:project · card · analyze · submit · say
  • 表达式:常量 · {in:入参|基座读数|let} · {col:本行列} / {outer:外层行列} · {lookup:地址, by, take, else} · {when:S-FEEL, then, else} · {cat:[…]} · {fmt:…, as:'digits'} —— lookup 的地址除了 域.对象(任务视图)/ mine:域.对象,还能是拍内作用域的数组 result:xxx (analyze 的出参当查表用,S2 的明细就靠这一条)
  • 数据源:{once} · {rows:[…]} · {each:…, where?, then?{each:…}}(两层封顶)
  • 地址只有四种,配方里出现的每一处都得是其中之一:
    • 域.对象 —— 任务视图 = 正式表 + 本单草稿叠加。这是配方默认该用的那一种。 (它不是新东西:engines.ts 的 taskData.view 早就把 ledger.taskView 的结果铺在这个键上。 这一点是这套设计的命门 —— 人在件上新建的料与字段,按保存那一刻只在草稿层; 拿纯正式表去看永远看不见它们,参数格就永远铺不出来。)
    • mine:域.对象 —— 只有本单草稿写下的行。「哪几颗料是这一单要办的」只有它答得出
    • in:入参数组 —— 起单入参里的数组
    • result:xxx —— 本拍 analyze 的出参 没有「纯正式表」这一种,配方不需要它:要区分「本单写的」与「别人放行的」,用 mine: 就够了。
  • project 加 prune: true:这一笔投影出来的行就是这张表在本单草稿里的全部,多出来的撤掉。 只动本单草稿层,正式表一个字不碰。(换类型时上一个类型铺的参数行必须消失。)

求值器是纯函数(域层、零 IO);执行器在 usecase,拿 ledger / humanTool / llm 端口。

必须写死的一条:每一笔 do 执行之前刷新基座读数。S2 的第③笔要数第②笔刚写下的行 —— 不刷新就恒为 0,等级永远算成「推荐」。

5.2 analyze 动词 —— 声明式 AI 调用

入参由声明组装、出参给 JSON schema、结果落进拍内作用域 result:* 供后面的 project 消费。 走现有模型网关(四元绑定、凭证隔离、录制回放、用量计入预算),不给工具、不给记忆、不多轮: 一次 generate,结构化出参。出参不合 schema → 就地重问一次 → 仍不成 → 留痕交人。

5.3 视图绑定加一件:data-options

data-options="parts.chip_type" + data-option-value="type_id" + data-option-label="type_name" → 渲染时把 <option> 铺进那个 <select>,与 data-rows 同一层机制:只取值,不判断。 地址写法与 data-rows 一致(域.对象 = 任务视图,mine:域.对象 = 本单草稿)。

只读那一份不用新东西 —— 走已有的 data-by 查表 (<span data-rows="parts.chip_type" data-by="type_id=chip_type_id"> 摆 type_name)。 「枚举列上下两态得说同一个字」那条账因此不会重演:上面是 label,下面也是 label。

5.4 件上「保存」之后 tick 一次

保存路由落草稿后调 TaskService.wake(现在只有两条回传路由调它)。

护栏:有未答的卡在场时,配方只跑 project / say,不发新卡、不 submit、不 analyze。 —— 只算、只写。

5.5 审核卡放宽到单选项 + 卡答复即收步

  • 决策卡「少于两个选项拒发」对 variant: 'review' 放宽到 1。
  • auto 步发过的卡被答复、且这一步没有 doneWhen → 收步。 人已经签了,收步是照实记录,不是替人签字 ——「人工激活项不给 doneWhen」那条纪律 防的是引擎抢在人前面,这里人在前面。

5.6 submit 动词

ADR-014 §8 原写「不 submit、不发决策卡」。那是假设还有 agent 在场时的保守话; 这个活动全程无 agent,没有别人来干这两件事,不开口最后两步就没人做。

护栏:submit 只许出现在声明了 acceptanceChecklist 的那一步;提交门(判据全绿)照旧把关 —— 系统提交不比模型提交宽松。

5.7 静态审在装包时把住

与 refersTo 目标检查同款:

  • into / each / lookup 的台账地址必须在 declarations.ledger 里(与写的授权面同一份名单)
  • fields 的列名必须在该对象的列声明里
  • when 的 S-FEEL 必须能解析、引用的名字绑得上(boundNames,已有)
  • card / submit 的 view 必须在 viewTemplates 里
  • analyze 的出参 schema 必须是对象,into 指向的键必须被后面某一笔消费
  • 没实现的表达式形式当场拒 —— 说好「只做真用得到的那几式」,静态审就是这句话的执行人

5.8 引擎拍内次序

tick
 ├─ ① 拍首自动收步(doneWhen 为真的项;人工激活项需 worldTouched)
 ├─ ② 算 current
 ├─ ③ current.auto → 跑配方(每一笔之前刷新基座读数)
 │     · 有未答卡在场 → 只跑 project / say
 │     · 抛错 → 留痕 + 交人(不重试、不退回模型)
 ├─ ④ 发了卡 / submit → 收拍(零 token)
 ├─ ⑤ 没发卡 → 重求 doneWhen;真了收步,回 ②(封顶 8 轮)
 ├─ ⑥ 这一步有 guide → 照常问模型(老活动走这条)
 └─ ⑦ 无 guide、doneWhen 不成立、也没有卡在等 → 交人(配方没做到位,是包的 bug)

5.9 观测

配方每一笔进 sys.task_memory,detail.auto = '<步名>#<第几笔>',工作日志归系统灰; analyze 那一笔也归系统灰(2026-08-29 改口)—— 那一行说的是「系统把 N 条参数交给模型、 拿回 M 条结论」,发起与落表都是系统做的,模型只提供了其中的内容;内容的归属在它落下的 台账行上看得见,不必让日志行冒充。归属规则保持单一(detail.auto → 系统),不必为一个动词开例外。 (实测踩过的病根:kind: 'result' 不在工作日志的骨架集合里,带 internal 的又会被 machinery() 再滤一道 —— 配方留痕能不能被看见,靠的是「kind 选对 + 不标 internal」, 没有机制保证。)治理台的活动详情能把「这一步系统会做什么」直接摆出来 —— 配方是数据,渲染得出来。


6. 三张件 + 一张看板

6.1 input-supplement · 评估输入补全(数据工单)

三块竖排,一张贯穿(在办可编辑 ↔ 办结 finalHtml 静态归档)。

  1. 要评估什么 —— 原 PN / 候选 PN 两行(行由系统铺,人不加行)。在库的挂品牌与类型徽; 不在库的那一行摊开成可填的主数据格(类型下拉 / 品牌 / 生命周期 / 规格描述…)。 顶上一条红带说清 gate_reason。
  2. 芯片类型 —— 一个下拉(data-options 从 parts.chip_type 现读)+ 下面 「这个类型的核心参数字段」表。选已有类型 → 表只读回显;+新建类型 → 类型表加一行(type_id 空着由基座续号),字段表加行(字段名 / 说明 / 硬软 / 单位)。
  3. 参数值 —— 左右两栏(原 PN / 候选 PN),一字段一行,两栏都可填:在库那颗回填现值 (它的参数当年没录全的话,那一格就是空的,照样点名要填),要建那颗全是空格。 行由系统铺,人不加行不删行。

底下编辑闸 + 缺项红带,与提交闸、S1 的 doneWhen 同一份数。

6.2 difference-review · 差异评估结论审核(验收工单)

  • 顶:对决头(原 PN → 候选 PN、类型徽)+ 四态分布带 + 推荐等级大徽章 (规则算的,人改四态它当场跟着变)。
  • 中:逐条差异明细表,一行一参数 —— 字段名 · 说明 · 硬/软徽 · 原值 · 候选值 · 单位 · 四态(下拉,可改) · 影响说明(可改) · 判断依据(可改) · UNKNOWN 去向(判 UNKNOWN 才必填)。
  • 下:推荐理由 / 差异摘要 / 风险 / 验证要求 —— 只读。 它们是系统算出来的,要改就去改上面那一条四态 —— 让人直接改摘要, 等于让他给自己没做的判断盖章。

6.3 landing-confirm · 落账确认(终局验收工单)

全只读,答「这一签下去发生什么」:进哪几张表各几行(摆人读名)· 结论卡(等级 + 一句话)· 四态分布,硬约束不符的逐条点名 · 待验项清单(P-06 的测试条件与后续动作)· 发哪一口。

6.4 运营中心看板 activity-ledger

重绑到新表(最近一次已放行实例:案卷 → 两颗料 → 类型与字段 → 参数对照 → 明细 → 关系 → P-06)。


7. 判据与人工验收清单

机检(全部基座数)

key level 谓词大意
input-ready L1 两颗都在库 · 类型一致 · 主数据与参数值都不缺必填(不写「参数行数 = 字段数×2」——2026-08-30 与实现一起改口:那不是基座数得出来的数,§4.2 doneWhen 尾注已经讲过同一条道理,这里当时漏改)
every-param-judged L2 substitution_difference_item_missing = 0 且四态四档计数之和 = _rows(AI 漏一条 → 那行四态为空 → 两边都不成立)
level-by-rule L2 推荐等级与 judgement_class 计数一致(硬不符>0 必须是「不推荐」…)
unknown-has-disposition L2 substitution_difference_item_missing = 0
validation-landed L2 P-06 一行且 _missing = 0
evidence-credible L4 影响说明与判断依据站得住(裁量)
values-traceable L4 参数值与原厂资料对得上(裁量)
gate03-release L5 责任关,不上卡

人工验收清单

  • S3 审核卡四条:逐条对过原值与候选值 / 硬约束项逐条看过 / UNKNOWN 都写了去向 / 影响说明能站住
  • S4 放行卡五条:等级与四态一致 / 待验项清单认可 / 落账内容核对过 / 参数值来源可追 / 签了就发给下游

8. 包与退役

原地重写 part-substitution-assessment@2.0.0。跟着退役的:

  • 两枚工具 compare-parameters / recommend(全仓只有 W05 用)—— 注册表停用,留墓志铭;
  • 判定表 D01 退役(四态改 AI 判);D02 的规则搬进配方的 when 链后 DMN 也退役 —— 规则只许有一处真源,再留一份 DMN 就是同一件事两处各存一份;
  • 说明书 NARRATIVE 瘦成「这个活动是干什么的」(全程无 guide,它不再进提示词; analyze 的提示词是那个动词自己的字段);
  • 现有五张视图模版全部作废,重做三张 + 看板重绑;
  • sub_eval.pn_check / input_gap / verdict 三张单据退役,由 sub_eval.case 一张顶掉。

样例集五条(名 ≤14 字,与 e2e 案例一一对应): 「都在库·零卡直分析」「候选缺库·类型已有」「候选缺库·新建类型」「类型不一致·硬拦」「两颗都缺库」


9. 测试与验收线

  • 包内自测:配方每一笔的口径钉死(字段齐全 / when 链 / 模板串 / 静态审能过 / 说明书不出现具体料号与参数名)。
  • 平台侧:求值器纯函数测(七式表达式 × 三种数据源 × 两层嵌套 × prune)· analyze 出参不合 schema 就地重问 · 卡答复即收步 · 保存后 tick · data-options 渲染 · 未答卡在场时不发第二张卡 · submit 只许在有验收清单的步 · 静态审逐条拒。
  • e2e w05 五案例,验收线:5/5 released · 配方零出错 · 提交闸每单真拦 · 模型调用恰 1 次。 (「行动被拒 0」这条对无 agent 的活动自动成立,改数模型调用次数。)
  • 浏览器走查:「候选缺库·新建类型」那条最长的路走完(新建类型 → 加字段 → 保存铺格 → 填值 → 提交 → 结论审核就地改 → 确认 → 落账放行 → released)。
  • 五道门禁以退出码验:pnpm gates。契约变更走四步(depcruise 反查 → CONTRACT.md → pnpm api:check --update && git add api-report)。

10. 迁移次序

  1. 契约与求值器:PlanStep.auto、五动词、七式表达式、prune —— 纯函数先行,单测钉死。
  2. 引擎接线:拍内次序 §5.8、留痕、观测;拿一个最小假活动(两步:project + card)验证机制。
  3. analyze 动词:走模型网关,schema 重问,用量计预算。
  4. 平台三件小的:data-options 渲染、保存后 tick、审核卡单选项 + 卡答复即收步、submit 动词。
  5. 静态审:扩到配方,把没实现的形状拒死。
  6. 数据模型:两张新表 + chip_type_id 由声明合并物化;种子给库里两颗料建 「DC-DC 降压稳压器」+ 核心参数字段,并把 parts.parameter 现有行的 constraint_type 迁进字段表;再加一颗 LDO 料(如 AMS1117-3.3)+「LDO 线性稳压器」类型, 供「类型不一致·硬拦」那条 e2e 用。
  7. 活动包 2.0.0:四步配方 + 三张件 + 判据 + 样例。
  8. e2e 五案例 + 浏览器走查;旧 W05 跑出来的运行数据 reset-runs 清掉。

11. 风险与代价

  1. 代码错是必然的错,模型错是概率的错。 配方写错 = 每一单都错。 对策:静态审 + 包内自测 + 五案例 e2e。但必然的错比概率的错好查 —— 这正是把它从模型手里拿走的理由。
  2. AI 那一步是唯一的自由面。 提示词与 schema 是它的全部约束;出参不合 schema 就地重问一次, 仍不成交人。值一律 lookup 台账,所以它就算胡说也污染不了数据,只会污染判断 —— 而判断正是人在 S3 要核的东西。
  3. 类型字典会被建脏(同一类型建两遍、字段名不一致)。件上下拉优先选已有、新建是次选; MVP 不做去重合并,记 debt。
  4. prune 会撤本单草稿层的行(2026-08-30 改口:这一条与实现一起对照过 —— 实际用在三处 project,不是一处:S1 的 parts.profile(④ 主数据骨架)/ parts.parameter(⑤ 参数骨架)、 S2 的 substitution.difference_item(②,§4.3)。§4.2/§4.3 的伪码从写下那一刻起就是这三处 都带 prune,这里当时漏改成了「只一处」。真正的约束不是「只许一处」,是只许用在 「这一笔的数据源就是这张表在本单该有的全部」这一类骨架/明细投影上——H35 后来新增的 parts.chip_type/parts.type_param 两笔自投影(4.2 节之外,原样抄字典行那两笔) 就刻意不带 prune:它们写的是共享字典/字段清单,不是这一单独占的骨架,历史选型的 陈旧行留着不算错,带 prune 反而会在人改选类型后把已经落地的陈旧行撤成死锁——静态审 要放行的是「哪一类数据源」,不是「哪一个固定笔数」)。
  5. 保存一次 = 一拍。人反复保存十几次会吃步数,预算给到 40。
  6. 表达力有上限。上限之外的不要硬塞 —— 退回模型,或者把那一段切成独立的 agent 步。

12. 不做什么

  • 不给 AI 任何工具、不多轮、不给它写台账的口 —— 它只出一份结构化判断
  • 不做类型的去重与合并(记 debt)
  • 不迁 E04 / I01 —— 机制先在一个活动上跑通
  • 不碰 P-01 需求主记录 / P-10 原厂技术资料 / 客户档案
  • 不在治理台上编辑配方 —— 那就成了低代码平台;配方随包走,改配方 = 升版本重装
  • 不做三层以上的嵌套 —— 两层(类型字段 × 料)是本活动的上限
  • 不在活动内部再引一套 Mastra —— 它在流程包编排与模型网关的原位不动