docs: ADR-014 确定性步骤 + W05 重写设计 spec + 实施计划(第一份:平台机制)

spec 定死八条:配方驱动零工具 · 六张台账 + P-06 按结论入账 · 四态 AI 定等级规则算 ·
保存即铺格 · 类型不一致硬拦 · 审核卡只有确认 · 只做真用得到的那几式。
计划十三个任务,机制先在纯函数与假端口上钉死,再去接真活动。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-29 02:56:39 +08:00
co-authored by Claude Opus 5
parent 2d1a2ff442
commit b9951483bc
8 changed files with 6181 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,572 @@
# 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` ·
`exists_status` 必填 tally `found`/`absent` · `chip_type_id` integer **必填** · **`distinct: true`**
(「两颗料是不是同一个类型」于是成了基座数得出来的数:不同取值恰好 1 种)
**`sub_eval.case` · 本单案卷**,同上 —— 一单**一行**
`eval_ref`(主键)· `chip_type_id`(本单锁定的类型)· `gate_reason`(入参未就绪的原因,件上摆的就是它)·
`summary`(结论一句话)
`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 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 格没填」
其余 → ''
③ 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` 不会误报;
· **「在库但参数没录全」于是自动被拦住** —— 那一格回填出来是空的,红带点名、闸按不下。
⑤ 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 的范围放行)。它们是主数据,该立刻可用,不等整单放行。
### 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 difference_item_rows = 本类型字段数
and substitution_relation_rows = 1
and validation_record_rows = 1
```
**这一步一张卡都不发,人不出现。** 三处刻意的克制:
- **AI 决定不了「有几行」。** 明细的行由类型字典 `each` 出来,AI 的出参只是拿 `param_name` 去
`lookup` 的一张查表;它漏了哪一条,那一行的四态就是空 —— `required` 立刻把它变成
`_missing > 0`,提交闸与放行门当场拦住。**从前是 `from each(AI 出参)`,AI 少给两条就少两行,
而每一行看着都合规。**
- AI 只答「这一条算什么、为什么」,**值是多少一个字都不重述** —— 基座读得到的事实不让模型转述。
- 推荐等级、风险、摘要、推荐理由**全由规则与模板拼**。永远不会出现「有硬约束不符却写着推荐」。
- 出参不合 schema → **就地重问一次**;第二次仍不成 → 留痕交人,不退回模型自由发挥。
### 4.4 S3 结论审核 —— 一张验收工单,人就地改
配方只有一笔:`card` 审核卡(`variant: 'review'`,件 `difference-review`,**单一选项「确认通过」**,
带人工验收清单四条)。
人在件上改四态 / 影响 / 依据 → 保存 → tick → 配方重跑 → S2 的第③笔把推荐等级、摘要、风险
**重算一遍**(同 PK 覆盖)→ 件刷新。改几次都行,**不重跑 AI**。
> 重算发生在 S2 那一步的配方里,而 S2 已收步 —— 所以 S3 的配方里**复制一份 ③④ 两笔**
> (纯投影、幂等),不去回退步骤。这是刻意的:一步一份配方,不做跨步回跳。
这一步**不给 `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` 那一笔归 AI 蓝。治理台的活动详情能把「这一步系统会做什么」直接摆出来 ——
配方是数据,渲染得出来。
---
## 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 · 参数值不缺 |
| `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` 会撤本单草稿层的行** —— 只在 S1 参数骨架这一笔用,别处不许用。静态审可以只放行这一处。
5. **保存一次 = 一拍**。人反复保存十几次会吃步数,预算给到 40。
6. **表达力有上限**。上限之外的不要硬塞 —— 退回模型,或者把那一段切成独立的 agent 步。
---
## 12. 不做什么
- **不给 AI 任何工具、不多轮、不给它写台账的口** —— 它只出一份结构化判断
- **不做类型的去重与合并**(记 debt)
- **不迁 E04 / I01** —— 机制先在一个活动上跑通
- **不碰 P-01 需求主记录 / P-10 原厂技术资料 / 客户档案**
- **不在治理台上编辑配方** —— 那就成了低代码平台;配方随包走,改配方 = 升版本重装
- **不做三层以上的嵌套** —— 两层(类型字段 × 料)是本活动的上限
- **不在活动内部再引一套 Mastra** —— 它在流程包编排与模型网关的原位不动