# 活动开发规范(任务类活动) > 2026-08-02 第三版。基准:`activities/quote-intake`(A1 询价受理,`@42`,**用户验收通过** —— > 三条形状不同的案例端到端 `released`、行动被拒 0)。目标读者:**开发活动的 AI(任务活动 > 开发 agent)或人** —— 读完这一份,你应当能独立把一个 SOP 节点做成能 `released` 的 > 案例活动,不必读 `apps/api` 的源码。 > > 本版按四条轴重组:**解耦机制**(§2)· **Harness 契约**(§3)· **Loop**(§4)· > **活动怎么写**(§5)。报告模版单独成册:`docs/HTML报告生成规范.md`(与本文互为姊妹篇, > 报告与表单的关系在那份里立成硬规矩)。 > > **本文与代码冲突时以代码为准**,发现即改本文。相关:`docs/编码规范.md` §8、 > 各模块 `CONTRACT.md`、`docs/debt.md`(欠账)。历史语义源已归档 `docs/archive/`。 --- ## 1. 世界观:一个活动是怎么跑起来的 **活动 = 任务的定义。** 编排引用一个活动,运行时长出一个任务(恰一名负责人)。 活动在系统外开发、经安装通道入库,版本恒新增、同版重装 no-op、在途任务按启动时版本跑完。 **活动没有代码逻辑。** 行为全部写在清单里:案例模型(CMMN)+ 逐步剧本(steps)+ 说明书(narrative)+ 判定表(DMN)+ 技能 + 台账声明 + 报告模版。 「系统不执行代码即知包为何物」。`src/index.ts` 只是被静态审扫描的产物文本, `defineActivity({ name })` 即可。 **推进由 Loop 逐拍驱动**(一拍 = 一次模型调用): ``` 语义引擎发布局面 ──> Agent 决断一串行动 ──> 引擎逐个执行 ──> 留痕 ──> 下一拍 · 当下可为(Enabled,标「须你启动」) (1-6 个) 被拒停在谁那儿 · 进行中(Active) tool 后截断 · 当前这一步的剧本(guide 摊开,其余步各留一行) 挂起型收拍 · 目标缺口 + 轨迹 + 工具清单 + 台账字典 + 基座读数 ``` **拍是按模型调用计费的** —— 一切设计围着「少一拍是一拍」:一拍连做、批量工具入参、 规则写死不留给模型判。A1 的账:六步的活,常路 **9-11 拍**收口(第一版 21 拍 —— 差距全在设计,不在模型)。 **人只在责任点与放行点出现**:确定交系统(判定表/基座读数),临场交 AI(编排、拆解、 落表),落地交人(审定、放行)。整套机制的反复主题只有一句: **该由人亲手落下的,不许引擎或 AI 悄悄替它发生;能由数据算出来的,不许活动自报。** ### 1.1 四条铁律(违者拒装或被引擎拒) | # | 铁律 | 机制承载 | |---|---|---| | ① | **只读世界**:活动只能读台账、调读类工具、问模型、发卡问人 | 产物文本静态审(`process.env`/`fetch`/`node:*`/`eval` 命中即拒装);depcruise 双向隔离 | | ② | **写只有草稿与意向**:写台账 = 草稿层(不抛事件);外部动作 = 登记动作意向(入参冻结 + 幂等键)。登记那一刻世界上什么都没变 | `write` 只落草稿;写类工具自动转意向 | | ③ | **终局唯放行**:任务完成的唯一出口是人验收通过 —— 那一刻才草稿合并、意向执行、输出口发射 | `submit` = 发验收卡挂起;放行落地见 §3.7 | | ④ | **活动不自转**:推进由引擎逐拍驱动;预算与熔断由 Harness 环外强制 | 迁移拓扑把 Completed/Terminated 设为死胡同 | --- ## 2. 解耦机制:活动与系统之间只有一条通道 ### 2.1 安装通道(唯一入口) ``` activities//src/ ──(pnpm activities:build)──> dist/{manifest.json,index.js} ──(pnpm activities:install)──> 库(经安装通道读文件,不经 import) ``` - 新包在 `tools/build-activities.ts` 的 PACKAGES 与 `apps/api/src/app/seed-activities.ts` 的 `REAL_PACKAGES` 两处登记。 - **别用 `db:seed` 装活动**:它重写整套治理台配置。 - **版本纪律**:改了任何内容必须升 `identity.packageVersion` —— 同版重装静默丢失(踩过)。 卸载只置状态不删行;**同名旧槽位要退役卸载**(库里两个同名活动会骗人)。 - 撤掉一张台账声明**不会 drop 物理表**(物化只增不破坏)—— 历史行还在库里,要清得单独迁移。 ### 2.2 import 白名单与静态审 活动包**只准 import `@sdk` / `@contracts` / `@sfeel`**;depcruise 双向隔离 (activities 不进 apps,apps 不 import activities)。产物文本黑名单: `process.env`、`require(`、`node:fs/net/child_process`、`fetch(`、`XMLHttpRequest`、 `eval(`、`new Function(`。视图模版含 `_<值>` 这些基座读数在审的白名单里)。 ### 2.4 边界纪律:别替下游产出 **随令牌流向下一个节点的,只有工作成果验收里的内容**(出口载荷 = 已落地的事实)。 下游活动拿到之后按自己的口径归一需要的数据。 实测账(2026-08-02,用户当场定):A1 从前逐条开「替代评估请求 / 询价请求」交给 A2/A3 —— 那是**替下游把它的输入先摆好**,而下游本来就要归一一遍:同一份事实两处各存一份, 改一处就分叉,而分叉之后没人看得出以哪份为准。两张表连同派单判据、报告明细表一并退休。 跨活动共享事实只有两条路:**输出口载荷**,或**共享域**(`shared`/`customer`/`market` 等 主数据域)。活动自持域用活动名(`quote_intake`)。 --- ## 3. Harness 契约:基座替你做什么 这一章列的是**你能依赖、也必须依赖**的基座行为 —— 活动侧不要重做它们, 更不要绕开它们。 ### 3.1 台账:三写一撤 + 归型 + 字段四标记 - **写三分**:`systemWrite`(世界已发生的事实,即时抛事件)/ `draftWrite`(任务草稿层, 读己之写,不抛事件)/ `releaseWrite`(放行合并,此刻抛事件)。三个方法不是一个带 flag 的方法。 - **`draftDrop`**:撤掉本单草稿层的一行 —— 不是第四种写(正式版碰不到、不抛事件)。 它存在的理由:门拦住一件办错的事却不给改正的手段,那扇门就只是个死胡同。 - **验收卡上的条目是人工验收清单,不是系统判据**(2026-08-27 用户定):派验收卡的那一步(终局 `submit` 的 T_release,或 `cardVariant:'review'` 的审核步)在剧本上声明 `acceptanceChecklist: string[]` —— 人拿着这份 工单要亲自核的步骤,一条一句可核对的动作,**每份验收工单固定**;引擎发卡时带上,卡上一条一枚「达标 / 不达标」 开关,全部达标才能「确认通过」(服务端同样把关)。系统判的 `acceptanceCriteria` 照旧把提交门,不再摆到卡上让人签。 写清单的口径:先看这份件人要看什么(汇总表 / 明细表 / 落库预览),每张表一条「核对什么、对到什么算对」; 别抄机检判据的话(那是引擎数的数),要写人眼睛能核的事。 - **数据工单确认即入库**(2026-08-27 用户定):`cardVariant: 'data'` 那一步派的卡,人一确认, 基座就把卡上 `gateLedgers`(或绑的 `blocks`)那几张表并进正式表 —— 与提交闸同一份名单,不等整单放行。 并过的行仍算「本单写下的行」(`mine:` / `_rows` / 任务视图照旧有它),整单放行时不再写。 活动侧要知道的只有两件:①剧本别再教模型「人填的东西已落草稿层、放行前还能改」—— 已入库的键 再 write 会被当场拒(拒回会说清是本单建好的行,别 drop);②**数据工单必须给 `gateLedgers`**, 不给就没有一张表会因确认而入库(闸也没牙)。轨迹里有一条「数据工单确认入库:表 N 行」的痕, 试跑单不落主数据时如实写「只留在活动缓存」。 - **归型前移**:`draftWrite` 按声明列型归(空值 = 不写这一列、数值/布尔字符串转真类型、 归不了当场拒)。**人手改的值与 AI 写的值过同一道门**。 - **字段五标记**(`fieldDeclarationSchema`): | 标记 | 语义 | 基座据此产出 | |---|---|---| | `required` | 这一列缺了就是活没干完(验收标准;不建 NOT NULL) | `域_对象_missing` / `_missing_keys`;执行卡提交闸;件上缺项红带 | | `expect` | 缺了不拦单,但那一问不能省(客户该给而没给的) | `域_对象_gaps` / `_gaps_keys`;L4「选填缺口」谓词读它 | | `tally: ['A','B','C']` | 分类列,给出取值域 | `域_对象_<列>_<值>` 按值现数(没出现的值也铺 0) | | `auto` | 内部流水号,**不问人、不让模型编** | 写入那一刻按「同组其余主键值-两位序号」生成(`CU-9001-01`);录入卡上那一格不出现,另摆一句「由系统按顺序生成」;**件上「加一行」同规**(2026-08-24):其余主键都填了、缺的恰好是 auto 列 → 基座续号落行,拒回也不点名人填不了的 auto 列 | | `distinct` | 这一列要数「有几个不同的值」(2026-08-24 加,E04 首用) | `域_对象_distinct_<列>`(空值不算一种;零行铺 0)。**覆盖闭合判据的落点**:「每家候选客户都进了计划」= `计划_distinct_客户码 = 候选_rows` —— 行数 ≠ 客户数,没有这个数,「盖没盖住每一家」就只能靠模型自觉(实测靠不住:人在确认卡上加的客户被从计划到收口无视了一整单) | 注意 `expect` 的边界:**我方写的注(如备注列)不算** —— 标了它 `_gaps` 恒大于 0, 那条判据永远绿不了,门就废了。 - **一行还可以是一份资料文件**(对象声明的 `file` 段,2026-08-23 加,I01 首用): `file: { titleColumn, bodyColumn, formatColumn }` —— 正文一律 **Markdown 原文** 落在 `bodyColumn`,格式列写 `md` / `html` / `docx` / `xlsx`。基座按它**现造**真字节 (docx / xlsx 是自己打的 OOXML 包,Word / Excel 打得开),件上给资料名挂 `data-file-of="主键列"` 人就点得开(详见《HTML报告生成规范》§4)。 两条纪律: · **不另存一份文件** —— 资料的正身是台账那一行,另存即同一份事实两处各存一份; · 这一段**同时是授权面**,没声明的表点不开(基座 403),与「写看台账声明」同款。 配套把 `bodyColumn` / `formatColumn` 都标 `required`:少一样,那一行就不是文件、 只是一行标题 —— 而 `_missing` 数得出来,收步判据与放行门自然拦得住。 ### 3.2 基座读数(机检的唯一合法数源) 任务视图顶层永远铺着这些数,**全部由基座现算,模型碰不着**: | 键 | 含义 | 备注 | |---|---|---| | `域_对象_rows` | 本单在这张表写了几行 | 声明过的表**铺 0**(没写过 ≠ 写了 0 行 —— S-FEEL 未绑定名字参与比较为假,踩过) | | `域_对象_keys` | 本单写的行的主键名单(JSON) | 封顶 12 行;拒回时摆出来,「多的是哪一行」一比便知 | | `域_对象_missing` / `_missing_keys` | 缺必填几行 / 缺在哪几行哪一列 | 名单用中文列名 | | `域_对象_gaps` / `_gaps_keys` | 缺选填几行 / 哪几行 | 同上 | | `域_对象_<列>_<值>` | 该分类值现有几行 | 取值域声明给,数得全 | **机检纪律(最重要的一条,已被违反三次)**: **L1/L2 判据、收步判据、放行门哨兵,一律只许读基座数得出来的数。** 活动自报的汇总列(`line_total` 这类)只能出现在等式的**一边**,或只做提示。 三次违反史,每次都是同一个死法 ——「那个数要模型自己重算,漏算/改小一次,门就废了」: ① `missing_key_total` 自报 → 人补了值门不开,十轮里三轮熔断; ② `missing_minor_total` 自报 → 门只接受「是」,模型把 1 改回 0,台账从此写着假数; ③ `untyped_total` 自报 → 模型跳过打标写个数就想过门(判据现读 `_type_tag_a + _b + _c = _rows`,两边都是基座出的数)。 ### 3.3 门与闸(从起单到放行,一路的拦截点) | 门 | 在哪 | 规则 | |---|---|---| | **起单宽进** | `process.start` | 不按 Schema 拒缺项。三活动 `input.required` 一律空(宽进严出),起单补全卡那条老路实际不再触发(彻底移除记 debt) | | **执行卡提交闸** | 卡面 `gate` + 服务端 | 必填缺着按不下提交(`{ready,why,blanks}`,与放行门 L1 **同一个数同一套算法**);**服务端在收下回传之前也把一遍**(前端灰按钮只是给人看的)。看哪几张表**三级取一**(`gateAddressesOf`,有测钉着):①卡上绑了 blocks 看绑的;②**卡自己声明的 `gateLedgers`**(零输入卡该给的就是它 —— 只有发卡的人知道这一张要人补哪几张表);③回落到必交付物声明的 `ledgers`/`ledger`。三级全空才不给闸 —— 没有依据的时候不拦人。**主数据类的数据工单必须给 `gateLedgers`**:它要补的表(`parts.*`)不在必交付物名单里,不给就回落到一堆无关的表、一格不缺,件上红带点着缺项而提交钮照样绿(2026-08-27 W05 e2e 实测) | | **提交门**(`submit` 预检) | engines | 机检(L1/L2)**必须为真**;裁量(L3/L4)**裁过即可**(裁成「不满足」照样能提交,裁断连理由随卡交给人 —— 一条只接受「是」的判据是一句教模型说谎的提示);**L4 带谓词时谓词说了算**(谓词真 = 已达标,模型那句相反的理由一并丢掉;谓词假才轮到裁量);必交付物按 ledger 地址数本单草稿行 | | **放行门入场哨兵** | CMMN sentry | **必须有 ifPart,且读基座数、认「办完了没有」** —— 只有 onPart 的哨兵等于「有人写过一笔就开门」,模型写个 `false` 就推开了(实测:跳过三步直达放行门) | | **熔断** | Harness | 连续 5 拍行动被拒 → 停问模型,发熔断卡把拒绝理由摆给人(比对削掉串位前缀、另开 60 条窗) | **拒回话术纪律**(拒回的话是模型下一拍的行动指南,指错方向比不给指引更糟): - 机检差数,先分清**数不对**(改汇总列)还是**行不对**(照主键名单 drop/补写); - 判据点名的表**这一单一行都没写**(`_rows` = 0)→ 直说「那一步还没办,回去办了再来」—— 这时数与行都是对的,指「改数/改行」是指向两扇打不开的门(实测五拍撞死); - 已收掉的项按状态分开说(「挂到当下这一步」+ 列出可为的项),别说「先 start」—— start 一个 Completed 项同样被拒,两堵墙对着撞。 ### 3.4 人类工具:四类卡(补全卡已退役) | 类 | 行动 | 形态与硬规矩 | |---|---|---| | **执行** | `human` + `workOrder:'execution'` | 卡 = 查看工单钮(四字粗体,与验收卡同形)+ ask + **作业指导书**(`steps`+`standard`,与判据同一套版面、不画对勾)+ 依据 + 提交闸。**零输入**(2026-08-02 用户定):卡上没有任何填写 —— 不摆 `fields`、不绑 `blocks`,填报全在工作区的件上(人按「保存」基座直写草稿层,**别再 write 一遍**);空表单是常态,基座照发(从前的「表单不得为空」拒发已删)。指导书写「哪几格必须有、哪几格值得追」,不写「怎么核表」。**`steps`/`standard` 是纯文本** —— 别写 `**` 这类记号,它们不会变粗体,只会原样摆在人眼前 | | **验收** | `submit` | 判据表(读时现算 —— 人在件上补一格判据跟着变;**L5 不上卡**:那张卡本身就是那道关,列进去等于摆一条恒红的项)+ 落地动作 + 可修订值 + 出口(算得出摆 `routedPort` 告知,算不出退 `portChoice` 让人选)。**办结之后换成落地结果**(landing):按交付物点名落了几行、转出哪一口、带过去哪些字段;一个口都没发直说「到此为止」 | | ~~补全~~ | ~~`ask`~~ | **已全局退役**(2026-08-02 用户定:「执行卡完全就是包含补全语义的人类工具」)—— 缺信息发**零输入执行工单**:指导书写清缺什么,人在工作区的件上补(保存即落草稿层)。旧输出里的 `ask` 由基座原地转成执行工单(归一不拒);引擎侧直发 `ask` 拒并指路 | | **决策** | `decide` | 选择题(≥2 个选项,少了拒发);人点哪个进轨迹 | | **录入** | `entry` | **人按下确认那一刻基座直接写库,AI 不经手** —— 建档这类主数据用它。可多段(`also`):建客户 = 公司档 + 联系人档,一张卡两段、确认一次落两行(值按 `段号.列名` 隔开)。卡面摆表的**人读名**(不摆 `customer.profile`);`auto` 列不进表单、另摆一句说明 | 通用规矩: - **一单同一张卡只发一次**。人交回来之后不论还缺什么都不许再发 —— 必填有提交闸兜着(卡回来时必填一定齐);**人留空交回来 = 对方给不了,那也是一个答案**: 如实记账、缺口照实留着、把这一步收掉往下走。 - 发卡即挂起,回传即续跑;在途验收先于一切。**值怎么落**:件上保存的、录入卡确认的, 基座直写;执行卡零输入(空答复 = 人照原样确认,痕上写明);决策卡选项进轨迹 —— 该记账的记账,**别当没答过再发一张**。 - 调人预算(`humanCallBudget`)环外扣账。 ### 3.5 视图渲染(概要,详见《HTML报告生成规范》) - 模版**活动持有**,一单一模版贯穿始终(硬规矩);剧本每步的 `view` 定推哪张。 - 渲染 = 纯取值绑定(`data-slot`/`data-rows`/`data-blank`/`data-class-of`/`data-col`); **模版不算不判**,基座算好的可以摆。 - 待处理那一屏**报告即表单**:可改格子渲孪生 ``,默认锁定、按「编辑」才开, 能增删行;保存按地址落草稿层(授权面 = 台账声明),**基座随即重渲**(repush)—— 类徽、分布条、去向这些算出来的东西跟着变。 - 报告与卡的分工、同源读数、保存链路 —— 全在姊妹篇 §2,那是这套里最容易做错的一章。 ### 3.6 进度条、日志、对话区 - **phases 是给人看的刻度**(引擎一眼不看):`{name, items[]}[]`, **除放行外每个计划项恰属一个阶段**(漏一项,人办那件事时进度条已经走过去了 —— 实测:S3 漏在名单外,人对着审定卡而条上写着「判定去向」)。收口那一格由基座统一加 (「放行收口」),总格数 = 声明数 + 1。 - **日志三色**:负责人绿 / 智能体蓝 / 系统灰;日志与卡面**不摆系统内英文名** (状态名、表名、列名、口名一律换人读名)。 - **对话区一轮一句**:`say` 只在交出控制权那一拍(ask/human/decide/entry/listen/submit) 才摆到人面前,别的拍进工作日志。到了要找人的那一拍,say 写「这一轮办完了什么、 为什么现在需要他」。顺利收尾对话区**不再出一条**(落地结果那一屏已说全); 没顺利收尾的出一条人话(「这一单受阻待人」)。 ### 3.7 放行落地(人按「通过」之后发生什么) 草稿合并进正式版(逐表计数留结构化痕)→ 动作意向执行(幂等键;失败发落地失败卡, 任务 `blocked`)→ 输出口发射:**载荷 = 已落地的事实**(`landedFacts`,放行后草稿层已清, 不从作用域猜)→ `released`。selective 出口按各口 `when` 谓词序求值,首个命中即出 —— **去向是算出来的,不是人选的**;人签的字确认的是算出来的那一口。 --- ## 4. Loop:一拍怎么跑(Agent 视角) ### 4.1 局面里有什么 当前这一步的剧本(guide 整段摊开,其余步各留一行)+ 说明书(整单的事)+ 活动记忆选摘 + 最近轨迹(含人答的值与工具出参)+ 当下可为(标「须你启动」/「裁量项」)+ **进行中**(有开着的活就不算断点)+ 目标缺口 + 待裁量前提 + 工具清单(带入参键名)+ 可写台账字典(地址/主键/列/列型)+ 本单草稿(`mine:*`)+ **基座读数** + 任务数据 (摊平字段在前、整表殿后截断 —— 但剧本 `reads` 声明的表**置前不截断**)。 ### 4.2 行动十四型(一拍一串,封顶 6) | 行动 | 形状 | 语义 | |---|---|---| | `start` | `{itemId}` | 显式启动一项(**可省**:2026-08-02 起动它就算启动);已 Active / 已收掉再按 = 无事发生 | | `plan` | `{itemId}` | 裁量项入场 | | `disable` | `{itemId, reason}` | 此单不走这条 | | `judge` | `{criterion, satisfied, rationale}` | 裁语义性前提 / L3-L4 判据;criterion 照判据名**整句原样抄** | | `write` | `{itemId, address, rows:[{key,patch}]}` | 写草稿。授权面是台账声明;`itemId` 只是记账,挂不上照写不拦;patch 只带有值的列;空转写(一行没变)即拒 | | `drop` | `{itemId, address, keys:[{主键}], reason}` | 撤本单草稿层的行;主键与写进去时一字不差 | | `tool` | `{itemId, tool, input}` | 读类即调;写类转意向;未命中转人挂起 | | `ask` | ~~已退役~~ | 基座原地转成零输入执行工单(字段名并进指导书);别再用 | | `entry` | `{itemId, ask, ledger, entryKey, fields, also?}` | 数据录入卡,挂起;确认那一下基座直写 | | `decide` | `{itemId, ask, options(≥2), basis}` | 决策卡,挂起 | | `human` | `{itemId, workOrder, card{title,ask,steps?,standard?,basis}}` | 执行/验收工单,挂起。**执行卡零输入**(2026-08-02 用户定):不摆 `fields` 不绑 `blocks`,填写全在工作区的件上(editGate + 孪生件),人按「保存」基座直写草稿层;必填由提交闸看住 —— **卡上加 `gateLedgers:["域.对象"]` 告诉闸这一张要拦哪几张表**,不给才回落到必交付物声明。**数据工单**(让人从零建一张主数据表)在那一步的剧本上声明 `cardVariant: 'data'` —— 引擎那一侧与执行工单只差一处:**人一确认,卡上 `gateLedgers` 那几张表就并进正式表**(2026-08-27,见 §3.1),对话区据此换成「数据工单」那副面孔;卡骨架上的 `variant:"data"` 只是给模型看的提示,模型抄丢了也不影响(实测它会丢) | | `listen` | `{itemId, eventType, keyFilter, deadline}` | 立监听,挂起;时限必配 | | `finish` | `{itemId, rationale}` | 宣告一项完成;非人工激活的项就地过 Active 再收 | | `submit` | `{rationale, fields?}` | 提交放行 → 预检 → 发验收卡挂起 | **串的纪律**(引擎强制):按序执行,谁被拒停在谁那儿(前缀已生效不回滚,理由喂回下一拍); `tool` 之后截断(出参没看见,后面是闭着眼计划的);挂起型收拍;超封顶丢弃留痕。 拍级附带 `say{text, tag, view?}`,不占行动位。 ### 4.3 人工激活:动它就算启动(start 墙已拆) 标了 `manualActivationRule` 的项,**模型显式动它(调工具/发卡/finish)就算启动**, 不必单发 `start`(2026-08-02 拆墙)。它的分量在引擎那一侧,原样保留: **引擎不因哨兵满足替它启动**(哨兵只推到 Available/Enabled), **人答卡之前不给 doneWhen 替人收**;答过卡之后 doneWhen 照常生效(见 §4.4)。 实测账(两轮,同一堵墙):先是「话与门同宽」—— 提示词写「先 start 再调工具」只说了 调工具,而门拦 tool/ask/human/decide/entry/finish 一整片,模型发 `human` 照字面漏了 start 白烧一拍;措辞写全之后**仍然熔断**(A1 一单连拒五拍,五次全是「要先 start」, 提示词与拒回都写着,它就是学不稳)。这才看清:那道墙保护不了任何东西 —— start 是**模型自己的**动作,同一串连发即过,拦下只是逼它多演一步礼仪, 而学不稳的礼仪就是死法。**状态机的礼仪不该长成墙**(与 finish 就地过 Active、 start 已收项无事发生同族)。剧本里也别再教 start —— 教它白烧 token。 ### 4.4 自动收步(doneWhen) 引擎每拍开头对任务视图求每一步的 `doneWhen`,**真了就自己收那一项,不问模型** (与模型同拍撞车时:引擎收过了,模型这拍的 finish 当已完成,不抛非法迁移); **拍尾再求一次**(人答卡那一拍里模型写完东西 doneWhen 才真,不等下一拍;这一拍刚发卡的项不算)。 **人填的表要声明 `humanWrites`**(2026-08-28 立):数据工单要人在件上补、而模型**永远没有来源**的那几张表 (P-10 的资料标题 / 版本 / 文件链接),写进剧本的 `humanWrites` —— 模型 write 当场拒、drop 仍可撤。 不声明的后果实测过:模型被 doneWhen 逼着凑「必填不缺」,把台账里**原 PN 那一行**的规格书逐字抄到候选料号头上, 行找对了料找错了,而这份假证据会喂进差异比对当「硬约束有证据支持」。 **只声明模型没有来源的表** —— 有附件时该预填的(P-08 的参数值从 datasheet 抽)是它的活,别拦。 配套:**还欠一张卡的那一步,提示词不摆 doneWhen** —— 判据同时是摆在模型眼前的目标,而它此刻只许发卡。 **一个闲着的模型 + 一个它够得着的判据 = 它一定会去凑那个判据。** **人工激活的项也可以给 doneWhen,引擎只在人答过卡之后才求它**(2026-08-27 晚立): 门槛是 `worldTouched`(卡真发过)且没有未答的卡 —— 卡没发过时 doneWhen 再真也不收, 那才是替人签字。答过之后,「办完了没有」若是基座数得出来的事实,就写成 doneWhen 交给引擎: W05 的两张数据工单(人填的值已入库、必填不缺、逐颗对账)就是这一类 —— 从前靠模型记得 finish, 剧本两处写死「先 finish 再发下一张」它照样忘,两步同时 Active、件停在上一步、人无处可填。 **指南让模型判的事,引擎有现成事实的,一律改成读事实** —— 这条也管收步,也管**工具入参**: 台账里有的值不让模型抄给工具(差异评估的比对工具 1.2.0 起只收料号 + 客户约束,参数由它自己从 parts.* 现读; 处理器经 `taskId` 读任务视图)。剧本示例里**不写具体参数值**,只留占位形 —— 模型会把示例当这单的数念出来(实测)。 仍不给 doneWhen 的:终局放行(T_release,由 submit 走)、审核循环、以及「办完了没有」 数不出来只能由人说了算的项。 --- ## 4.6 确定性步骤:这一步系统自己做(2026-08-29) 一步要么确定,要么不是。**不做「一半 auto」** —— 判断点先从那一步里切出去。 `PlanStep.auto` 是一份**声明式配方**,五个动词按序执行: | 动词 | 做什么 | |---|---| | `project` | 从某个来源的每一行投影出台账行,落草稿层。带 `prune` 时,这一笔投影出来的行**就是这张表在本单草稿里的全部** | | `analyze` | **一次结构化 AI 调用**:系统组装入参、声明出参 schema、系统把结果落表。不给工具、不给记忆、不多轮 | | `card` | 发一张卡(执行工单 / 数据工单 / **验收工单** / 决策卡,`variant` 定面孔)。**至多一张**,发过就不再发 | | `submit` | 提交放行(只许出现在声明了人工验收清单的那一步) | | `say` | 一句给人看的交代 | 地址只有四种:`域.对象`(任务视图 = 正式 + 本单草稿叠加,**默认该用的那一种**)· `mine:域.对象` · `in:入参数组` · `result:xxx`(本拍 analyze 的出参)。 **「至多一张」是把两条路写成两笔的前提**(2026-08-31,W05「数据齐了也叫人一次」):同一步里 摆两笔条件互斥的 `card`(缺东西 → 数据工单;都齐了 → 只读的验收工单),引擎的幂等保证一步 只发一张 —— 前一张发过之后,后一笔永远轮不到,人不会被叫第二次。 **这一步的活可能本来就干完了,而你仍想叫人过目一次 —— 那就把这一步标 `manualActivation`。** 引擎在拍首先扫一遍 `doneWhen`,为真就自己收步:输入本来就齐的单,那一步的 `doneWhen` 第一拍就为真,**配方一笔都还没跑**,步骤当场被收掉,卡永远发不出去。标了人工激活,那道扫描 多一个门槛(`worldTouched` —— 卡真发过),次序就变成「配方发卡 → 人答 → 下一拍 `doneWhen` 为真才收」。`doneWhen` 照旧保留,它仍是这一步的真门,只是不再抢在人前面。 (引擎测钉着这一对:`turn-runner-acceptance.test.ts`「auto 步:配方在拍首跑」那一组。) **guide 可以留空,但只有声明了 auto 才行**:`planStepSchema` 原来要求 `guide` 非空 (`.min(1)`)—— 那是防「作者忘了写做法」的老规矩,可它顺手把「这一步一次都不问模型」堵死了。 现在改成 `superRefine`:没声明 `auto` 的步照旧非空;声明了 `auto` 的步允许 `guide` 写空串 (`guide.trim() === ''`)——引擎正是按这个空串分「全 auto(一次都不问模型)」与 「半 auto(确定的先做完,剩下的判断仍问模型)」。**全 auto 的步就把 `guide` 写成空串**, 别为了过 schema 塞一句废话。 **半 auto 的步,人答完卡之后那一截判断仍然是模型的活**:配方发过一张卡、`worldTouched` 为真、这一拍没有未答的卡、配方这一拍也没再发新卡 —— 这几个条件都成立时,引擎本可以 「卡答过了就收步」抄近道直接收掉这一项。但**这条近道只走没有 `guide` 的步**(全 auto); 只要 `guide` 非空(半 auto),就算前面几个条件都成立,也仍然要往下问模型 —— 半 auto 存在的意义就是「确定的先做完,剩下的判断仍问模型」:人答完卡之后那份判断 (读人写的自由文本、落到具体的行上)恰恰是模型该干的活,收在它前面等于把半 auto 的 后半截整个删掉。写半 auto 的步时记住这条:`guide` 不空,这一步答完卡不会被引擎抢先收掉。 **十条纪律**(每一条都是踩出来的): 1. **行由确定的那一侧决定,不由 AI 决定。** 明细表要按类型字典 `each` 出来,AI 的出参只是拿主键 `lookup` 的一张查表 —— 它漏一条,那一格就是空,`required` 立刻变成 `_missing > 0`,闸与放行门当场拦住。 写成 `from each(AI 出参)` 的话,它少给两条就少两行,而每一行看着都合规。 2. **判据不许编一个数不出来的数。** 「本类型有几个字段」不是基座数得出来的数(`_rows` 数的是**本单写下的**行); 行数对账交给 `prune` 的结构保证,别写进 `doneWhen`。 3. **每一笔之前刷读数。** 后一笔常要数前一笔刚写下的行。 4. **出错交人,不重试、不退回模型。** 配方抛错是包的 bug;退回模型就是让它去凑。 5. **被 `prune` 投影的表,不该是人在件上能加行的表**(2026-08-29 立)。`prune` 说的是 「这一笔投影出来的行就是全部,多出来的撤掉」;而人在件上「加一行」加出来的行,配方的数据源里 当然没有 —— 下一拍对齐就把它**静默撤掉**了(2026-08-02 为人工撤行立过「撤要说出来、点名哪几行」 的规矩,这条路连说都没说)。基座给了两道护栏:有卡在等人(人正对着件填)那几拍**不 prune**; 同一张表既被 `prune` 又列在这一步 `humanWrites` 里,装包时报 warn。护栏不是许可 —— 要人加行的表就别 `prune` 它。 **`prune: 'live'`(2026-08-31)是这条纪律的反面特例**:行集**全由配方铺、人在件上加不了行** 的表(如 W05 的参数骨架 —— 行按「料 × 类型字段」派生,件上没有加行钮),quiet 拍也照撤 —— F3 防的风险在这种表上不存在,而「改了类型,旧类型的参数行还摆在屏上」是另一种撒谎 (实测:用户改选类型,行「只会跟着变多,不会跟着变少」)。有加行钮的表**永远不许用 `'live'`**。 两档都跳过已落地的行并出声(`draftDrop` 的 `skipLanded`)。 6. **`card` 之后的那几笔,要等人交回之后的那一拍才跑。** 卡真发出去那一拍到此为止; 人交回之后那一拍,`card` 这一笔被跳过、后面的照跑。所以「铺格 → 发卡 → 收口整理」读起来像一拍连做, 其实是两拍 —— 收口那一笔看得见人填的值,这正是它该在的位置。装包时会为此报一条 warn 提醒。 7. **`fields` 里的字段书写顺序不是依赖顺序,不能拿文本次序当保证**(2026-08-30,真跑 e2e 逼出的地基缺陷)。活动清单存进 `sys.activity_package.manifest` 这个 **jsonb 列**之后, PostgreSQL 按「先短后长、同长字典序」**重排**了每一个嵌套对象的键 —— 而 `project`/`analyze` 的 `fields`(以及 `analyze.input.fields`)原来是靠 `Object.entries` 逐字段求值,后一个 字段能通过 `ctx.computed` 看见前一个已经算出的值,「声明序 = 依赖序」是一条从没写下来、 一进数据库就被打破的隐藏假设,而且**不报错**,只是答案算错了(W05 实证:合成列 `judgement_class` 依赖 `difference_status`,被 jsonb 重排到它前面,读到 `undefined`, 三级 `when` 全部落空、摔进兜底档;`difference_status` 本身走 `lookup:'result:items'` 不依赖字段序,所以是对的 —— 「一个对一个全错」的怪现象由此而来。**隔离测试复现不出来**, 因为它直接读 `dist/manifest.json`,从不经数据库往返 —— 只有真跑 e2e 才会现形)。 **现在字段求值改按依赖关系做拓扑排序**(Kahn 算法,`packages/utils` 的 `topoOrder`), 不是「迭代到不动点」—— 后者是数值收敛判据,会把「结构上真有环、只是两个字段的 `else` 分支恰好在错误状态下互相稳定」这种配方误判成排好了,拓扑排序不会有这个假阴性。 真正的环在**装包静态审**那一关当场拒装,不兜底、不静默放行。**写配方时不要假设 「后一个字段能看见前一个」是因为你把它写在了后面**—— 它能看见,是因为拓扑排序算出来 该这么排;把 `when`/`lookup`/`cat` 引用了哪些字段名想清楚,排序自然是对的,想不清楚就 会在装包时被拒并点名牵涉的字段。 8. **跨笔的依赖仍然是你自己排的顺序**(拓扑排序只管同一笔 `fields` 对象内部的字段序, `do` 数组里 `project`/`analyze`/`card`/`say` 各笔之间的次序不受它管)—— **一笔读的是 另一笔写出来的数,就必须排在那一笔之后**。W05 的 S1 曾把摆给人看的 `gate_reason` (案卷那一笔)排在铺主数据/参数骨架那两笔之前:`gate_reason` 的 `when` 链要读 `parts_profile_missing`/`parts_parameter_missing`,而这两个基座读数是骨架那两笔刚 建出来的行撑出来的 —— 排在前面,它读到的是**这一拍刷新之前**的旧数(参数骨架那一刻 还不存在,`_missing` 读到 0),片刻之后骨架一建好又变了。收步与发卡的把关本身没坏 (`card.when` 用的是当下真实读数),坏的是**摆给人看那句话**—— 人最需要解释「还差 什么」的时候,案卷里存的、卡面上摆的,是一句空话。 9. **两笔之间可能存在方向相反的依赖,一趟写不出这个顺序**—— S2 曾把关系表 `substitution.relation` 整笔排在差异明细 `substitution.difference_item` 前面: 差异明细的主键列声明了 `refersTo`(要求指向的关系行已经真实存在),而关系表的 推荐等级/风险/摘要/理由这几个必填列又要读明细表的合成列分布**才算得出来**—— 关系表 排前面,`refersTo` 拦住明细表(「要指的那一行还不存在」);排后面,关系表自己那几个 必填计算列无从算起。**治法是把关系表拆成同主键的两笔投影,中间夹着差异明细**: 第一笔只把主键与不依赖统计的上下文列先摆出来(满足 `refersTo`,这一刻明细表还没有 行,所有分布读数都是基座铺的 0,`when` 链落到默认分支 —— 字段值不对但写得出、不报错); 第二笔排在明细表已经真实写入之后,用**同一份**投影重算一遍,把第一笔的占位值覆盖成 真值。这不是新发明的招式 —— 与 S3 每次人改了四态就要重算关系表,是同一套幂等重算 机制,写配方时复用同一个常量,不要抄两份(抄两份早晚分叉)。 10. **`gateLedgers` 同时管两件事,漏想一半就会出真人会踩到的坑**:①闸要求哪几张表非空 (`_rows > 0`)—— 但「零行 = 没干完」这句话只对**这张卡一定会写**的表成立,列进一张 **只是有时候才写**的表会在合法路径上误拦(W05 吃过这个亏:`parts.chip_type` 若不给 编辑手段就列进闸,挑一个已有类型是主路,这条路上它天然零行,闸永久判「没干完」, 人把别的表填得再全,提交也是 400);②数据工单确认那一刻把闸上点名的表**并进正式表** (`landsOnConfirm`,§3.1)—— 从闸的名单里删掉一张表只顾了①,却会让「引用它的表已经 落地、它自己没有落地」:别的表用 `refersTo` 指着它,落地那一刻起正式表里就有一行悬空 引用。W05 的解法是让配方**自己把选中的行原样自投影一遍**(不论新建还是选既有的都投, 幂等、不产生多余写入)—— 这样 `mine:` 恒非空,该表**同时**满足「闸不会误拦」与 「确认时有行可落地」两个要求,可以放心留在 `gateLedgers` 里。**卡上闸住的每一张表, 件上必须真有一种写它的手段**(人在件上直接编辑,或配方自己投影进去)—— 否则那颗 「确认」钮永远按不下去,而且错误提示只会说「这张卡就是来建它的」,指向一个界面上 压根摸不到的操作。 --- ## 5. 活动包怎么写 ### 5.1 包结构 ``` activities// ├── package.json # @activities/,private,type:module ├── src/ │ ├── index.ts # export default defineActivity({ name }) │ ├── manifest.ts # 清单六区 + 导出 DMN 常量 │ ├── case-model.ts # CMMN_XML + NARRATIVE + INTAKE_STEPS + INTAKE_PHASES │ ├── skills.ts # Skill[] │ └── views/{report.ts, prd-style.ts} # 报告模版(见姊妹篇) ├── __tests__/ # 自测;只准 import @sdk/@contracts/@sfeel └── dist/ # pnpm activities:build 产出 ``` ### 5.2 清单六区要点(全量 schema 见 `packages/contracts/src/manifest.ts`) **identity**:`activityId / name / packageVersion(改内容必升)/ category / sdkVersion(精确相等)/ checksum`。 **contract 七件**: | 件 | 硬规矩 | |---|---| | ① input | 起单宽进;`properties[k].title` 写中文(补全卡上给人看的);**`inboundComm`** 声明哪几个入参字段合起来是那封来件 —— 基座起单那一刻归档进沟通记录 | | ② output | mandatory 必发;selective 每组 ≥2 口、**每口写 `when` 谓词**(读台账列/基座读数)—— 放行时按序求值首个命中即出,算不出才退人选。载荷 = 已落地事实,别在出口另编载荷 | | ③ requiredDeliverables | `ledger` 必写(预检数行);**一份成果由几张表承载就写 `ledgers`**(A1 的受理记录 = record + type_table + verdict 三张表、**一份**成果 —— 从前各占一条,人在成果面上看到三样东西);`editableAtRelease` 列出验收页上人可就地改的列(**AI 判的结论列也要在其列**,如 `type_tag` —— 人改它就是在推翻一次判定,那是他的职权) | | ④ acceptanceCriteria | L1/L2 必带 predicate 且**只读基座读数**(§3.2);L4 可带谓词(谓词说了算,§3.3);**措辞写全**(「客户已确认:发件邮箱认出为老客户,或已为新客户建好档」——「(认出或已建档)」这种缩写人读不懂);**内部一致性不上卡**(打标依据对不对、两张表行数齐不齐 —— 该由收步判据看住,放行卡是给签字的人看的,一屏九条真要他过目的反而淹了);**判据得能指向一个可做的动作**(「去向合乎常理」这种让模型给自己没做的决定盖章的,删) | | ⑤ revisableAtRelease | 放行时人可当场改的整单级值;拟值基座从台账现读,不让模型重述 | | ⑥ prohibitions / authorization | 禁区短而硬;`humanCallBudget` 调人预算 | | ⑦ resources | budget 按实测留一倍余量(A1:steps 60 / tokens 400k / 60min / 调人 4;实测常路 9-11 拍 8-13 万 token) | **declarations**:台账(字段四标记,§3.1)、tools、models、listeners(时限必配)、harness。 **assets**:`businessModel{cmmnXml, narrative, dmn, steps, phases}` + skills + viewTemplates + cardSchemas + distillHooks。**steps 与 phases 都在这**(见 §5.4、§3.6)。 **samples ≥1 条**(常路 + 最长路径都给上;input 键与 contract.input 对齐 —— A1 是 `customerEmail / subject / body / attachments`)。 ### 5.3 案例模型(CMMN)建模纪律 —— 每条都是实测账 1. **图上有几步就是几项,不多建一件。** A1 第一版 6 步建成 12 项(自加阶段/里程碑/ 裁量项),模型全程跟 harness 较劲,一单二三十万 token 收不了口;扁平 7 项后零被拒。 2. **案卷项 id 就是任务视图键名**(入参与台账列摊平后的名字)。对不上 = 前提永不满足 = 第一个项都不 Enabled,而每一层单看都「正常」。 3. **`manualActivationRule` 只标触达世界与关键交付的项**;它的 doneWhen 只在人答过卡之后才求 —— 「办完了没有」基座数得出来的(数据工单确认入库)就给,数不出来的(放行 / 审核 / 交人补缺口)不给。 A1 标两项:S3_confirm(要打断真人)、T_release(签下去触达世界)。 4. **人的门要建成一项,不要挂成某一步收步条件里的半句。** 「缺东西才叫人」的写法, 一封齐整的询价进来那半句当场为真,引擎自己收步,人一眼没看过就被叫去验收 (用户原话:「我都没确认需求 它就让我验收了」)。做法:独立一项 + 人工激活 + 人答卡之前不收 + 下一步的入场前提取**只有人的回复才产生的计数列**(`confirmed_total`)或人填的值入库的基座数。 5. **哨兵必须有 ifPart,且读基座数、认「办完了没有」**(§3.3 放行门那行的教训)。 6. `repetitionRule` 标常来回的项;判断菱形 = 两条互斥前提的 sentry,不是一个计划项。 7. `autoComplete="true"`;里程碑前提达成即达成;`ifPart` 前缀 `semantic:` 交 judge。 8. **每个计划项(除放行)恰属一个 phase**;放行收口那格基座加,活动别自己起名。 ### 5.4 剧本(steps)—— 每步一条,引擎按它组织上下文 ```ts { item: 'S2_normalize', // CMMN 计划项 id,对不上安装拒 intent: '一句话:这一步交出什么', guide: `这一步的做法(只在轮到它时进上下文)`, writes: ['quote_intake.record'], // 可写哪几张表 reads: ['customer.contact'], // 要读哪几张表 —— 声明了置前不截断(不声明会被盲截, // 实测:客户联系人表落在刀口外,老客户被当成新客户) tools: ['crm.history-price-read'], doneWhen: 'line_total > 0 and quote_intake_record_rows = line_total', view: 'intake-overview', // 这一步人看哪张件 —— 写清单时定死,不归模型临场挑 } ``` **doneWhen 三坑**: - `X != null` —— S-FEEL 未绑定名字 `!= null` 为 **true**,那一步开局即收。判「写过没有」 用正向式(`X >= 0`)或计数列; - **布尔列不当判据** —— 合法值含 false,分不清「没写」与「写了 false」。 取同步显式写下的计数列(`route_decided_total`); - **收步判据与验收判据同源** —— 都读基座读数,比同一件事。松出来的那一截就是返工 (收了步却过不了验收,回头补写一张已收掉的表);同源还把发现点提前 (「多写一行」从第 10 拍提前到第 2 拍)。 ### 5.5 说明书与 guide 写作纪律 说明书(narrative)只讲**整单**的事(开案、成品、SLA、口径);**逐步做法归剧本 guide** (两处各写一份会分叉出模型里不存在的项名,有测试钉死)。guide 写作十条: 1. **规则写死,不留给模型判**(隐藏思考已关换十倍速度,边界必须写到不可能读歪: 「关键字段只有 PN/EAU/交期」)。 2. **写哪张表、哪些列,点名写死**;人在哪一步答的东西谁负责落表,写清。 3. **卡的骨架照抄给它**(整段 JSON 例子,含 `steps`/`standard`/`also`/绑表的 `blocks`)—— 散文描述不管用;系统提示词写死的形状盖过剧本,骨架里少一个键那个键就不存在。 4. **一拍连做指引** + 常路拍数预期;工具批量入参,一次传完。 5. **一单同一张卡只发一次**;人留空 = 给不了(§3.4)。别写「还缺就再发一张卡」—— 提交闸上线后那条路不存在,模型照它办每次都撞「刚问过,人也答过了」(实测连烧四拍)。 6. 绑台账的表**别自己抄行**;人提交后**别再 write 一遍**(值没变会被「写了等于没写」拒)。 7. 回填按主键对行 —— 主键换个写法就是新开一行(实测同一颗料成了两行); 发现主键抄错交给人,别用新主键再写一行。 8. **别替下游产出**(§2.4);去向不用你选也不用人选,你只负责把判据数写对。 9. 汇总列是你的活的,写明重算时机(人补完值你要把数改对);**机检读数你改不动, 别为过判据去改自报数**。 10. **卡面是纯文本**:`steps`/`standard`/say 里别写 `**` 星号记号。 ### 5.6 判定表(DMN)—— 能算的不问 LLM 单元格谓词 = S-FEEL(`'-'` 任意、`'"A","B"'` 析取、`'<= 90'`、`'[0..100]'`); **FIRST 的行序就是业务优先级**(C 类 EOL 必须排最前);任一输入缺 → incomplete; 命中留痕、同输入必同结论、零 token。推荐做成 `pure: true` 的私有功能工具 (`quote-intake.classify`),模型只取齐判据、调工具拿结论; **出参键名与台账列同名**(拿回直接 write)、连 `counts` 一起给(供照抄); 「没有」也当判据回(无历史 → `historyDays: 9999`),别回错误。 ### 5.7 工具 声明式(名字进 `declarations.tools`,系统没有的运行时转人 —— 执行卡代办, 人填的值就是出参);批量入参;读类即调、写类转意向、未授权报错。 系统内化零声明即得:write/drop/ask/entry/decide/human/listen。 ### 5.8 报告模版 **见《HTML报告生成规范.md》。** 那份是硬规矩:一单一模版贯穿始终、三个区各放什么、 报告与表单的关系、模版语言、编辑态、PRD 目标件平移法、陷阱表。 ### 5.9 技能与记忆 技能 `{id, label, applicability, method[], version}`:常驻只有目录,正文按需加载, 载方法不载权力。方法里写锚点、纪律、批量惯例,不写产品文案。**剧本改了口径, 技能里同一句也要跟着改**(A1 定去向那条技能与 S6 同步删过开单指引)。 任务记忆只增;终态封卷后 `distillHooks` 蒸馏进活动记忆。 --- ## 6. 自测与验收口径 ### 6.1 包内自测(照 `activities/quote-intake/__tests__/`) 1. **清单合规**:过 schema;监听时限;L1/L2 有谓词且**谓词名都在台账列或基座读数里**; 必交付物都有 ledger; 2. **案例模型**:双表示同在;触达世界的项标人工激活且无 doneWhen; **每个计划项恰属一个 phase(除放行)、一项不进两个阶段**; 剧本 item 与 CMMN 对得上;**doneWhen 开局不为真**(空局面求值必须 false); 3. **判定表逐行求值**; 4. **对齐线**:deliverable.ledger 在声明里;出口载荷标量有列承载; 剧本 view 都在 viewTemplates 里; 5. **模版纪律**(见姊妹篇 §9)。 ### 6.2 五道门禁与安装两审 `pnpm gates`(typecheck / lint / depcruise / api:check / test),以退出码验。 安装静态审 error 即拒(黑名单见 §2.2;`acceptance-level-misreported` = L1/L2 没谓词)。 ### 6.3 试跑口径与验收标准 - **试跑一律用 AI 账号**(密码 `jcai-dev-2026`),绝不派到用户工作台; 测完 `pnpm --filter @app/api reset-runs`。 - **三条常备案例**(形状真不同,不是同一封信改个号): ① 老客户 · BOM 附件齐整 · 全 A 类直通;② 老客户 · 正文点名一条本轮不报(测人手撤行); ③ 新客户 · 中文正文列料 · 无附件 · 缺必填(测建档 + 人补格子)。 - **验收标准:三案例全部 `released`、行动被拒 0。** 每一条「行动被拒」都是设计欠账 —— 修清单或剧本,不怪模型; 被拒理由指向死路(模型改不动的那种)是 harness 欠账,报上去。 --- ## 6.5 设计律(2026-08-02/03 一整轮实测沉淀 —— 每条都付过学费) 排障表治的是「这一处坏了」;这几条治的是「为什么总在坏」。改 Harness、写活动、 做报告之前先过一遍: 1. **墙要配路**:任何一道门拦住一件事时,必须存在一个模型(或人)做得出的动作 把事办对 —— 没有出路的门只是死胡同(drop 之于「多写一行」、撤行之于「替代未果」)。 2. **学不稳的礼仪就是死法**:纯礼仪的墙一律拆(start 墙、空表单拒发)。 模型十次里学会九次不算学会 —— 第十次熔断的是用户的单。幂等优于拒回: 按钮按两下不该报错(start / finish / 重发已答卡,同族三例)。 3. **拒回喂对抗,观察喂事实**:拒回的话是模型下一拍的行动指南,指错方向比不给更糟; 头几次犯浑先转成 observation 指路(卡已答过→接着办交回后的事),连犯才算撞墙进熔断。 4. **「机检只读基座数」管的是一整片**:验收判据、收步判据、放行哨兵、**入场哨兵**、 提交闸、红带 —— 任何一处读模型自报的数,那道门就等一个可能永远不来的记账 (第四次违反藏在 s_parsed 哨兵里:行落了门却不开,模型被「依赖断点」推去发卡绕路)。 5. **提示词分层**:全局提示词只说通用真理,活动级细节归说明书 —— 一句全局禁令 (「别再 write 一遍」)能把所有活动的既有约定顶翻,且症状出在别的活动身上, 查起来像闹鬼。改全局面 = 改所有活动,回归也要按「所有活动 × 全部常备案例」跑。 6. **措辞治不了的要结构治**:模型「不查工具直接编结论」这类顽疾,说明书写死到头 仍概率性犯 —— 根治靠机制(规则进判定表由引擎求值 / 工具出参直接带结论只让照抄), 措辞只能降频。一条纪律连续两轮被同一形状踩穿,就该停止改话、开始改结构。 7. **改共享面必须全量回归**:提示词、turn-runner、解析器、容器 —— 动一处, 三活动的全部常备案例都要重跑(A1 新客户路的熔断就是「只回归了老客户路」漏出来的)。 「稳定的活动出错」比新功能不 work 伤得多。 8. **用户重复提的要求,说明上一轮没打中病根**:往下再挖一层,别在同一层再修一遍 (「工作成果 1:1」挖了三层才到「办结那一刻存快照」这个真解)。 9. **测试要能咬住,不能只是好看**(2026-08-30,ACT-WORK-05 重写全程验证方法论沉淀): - **镜像测试证明的是机制,只有读真文件的测试才证明「那个文件此刻是对的」**—— 两码事, 要写两条测试。手写数据喂给一个函数、验证它的行为符合预期,证明的是这段逻辑本身没错; 但生产代码会漂移,fixture 常常把漂移的差别悄悄抹平(实测账:真身是 `f.label ?? f.name`, 手抄件是 `f.name`,fixture 从没暴露过这个差别)。少了一条「import 真身」或「读真文件 逐字比对」的测试,这个洞永远合不上。 - **变异测试本身也可能是空的**—— 撞见过单行行组下 `tr:last-of-type` 与 `tr:first-of-type` 天然等价(表只有一行时,「最后一个」与「第一个」是同一个元素); 往真代码里改一个明摆着错的选择器,6 条「变异」测试全绿零反应,证明测试从没真正执行到 那段逻辑。写变异测试要**先让它红过一次**,不红就是测试没咬住东西,不是代码没问题。 - **能 import 真东西的,一律 import,不要手抄**—— 手抄件从诞生那一刻就开始漂(同一轮 连吃三次这个亏:配方读数函数的手抄漂移、`let` 绑定的手抄漂移、DOM 选择器复刻的手抄 漂移)。跨模块分层边界(如 `activities/` 与 `apps/api/` 互不静态 import)是少数几个 不得不手抄的理由之一;分层边界**内部**(如 `apps/api` 各模块之间)没有理由手抄 —— 通常只是少加了一个 `export`。 ## 7. 排障速查表(症状 → 根因 → 处置;全部实测账) | 症状 | 根因 | 处置 | |---|---|---| | 起单后第一个项都不 Enabled | 案卷项 id 不是任务视图键名 | §5.3-2 | | 连续「迁移非法/项不存在」熔断 | 计划项建多了/嵌套了 | §5.3-1 扁平化 | | 一单 20+ 拍才收口 | 没写一拍连做/拍数预期;或项过多 | §5.5-4 | | 机检判据全绿而交付物是错的 | 等式两边都由活动自报 | 一边改读基座读数(§3.2) | | 一条 L1/L2 永远绿不了而数据合规 | 读了自报汇总列 | 标 `required`,读 `_missing`(§3.2 违反史①) | | 台账里某个数被改小、判据变绿 | 门只接受「是」,模型去改自报数 | 机检只读基座数;数不出的降 L3/L4(违反史②) | | 没打标就进了放行门 | 哨兵只有 onPart(写过一笔就开门) | ifPart 读基座数认「办完没有」(§3.3) | | 同一条理由连拒到熔断而数与行都对 | 拒回把「活没干完」说成「数/行不对」 | 已修:`_rows`=0 直说那一步没办 | | 行数对不上,模型只会改数字 | 拒回没摆主键名单 | 已修:`_keys` 名单;照名单 drop | | 「要先 start」而 start 也被拒 | 拒回没按状态分开说 | 已修;start 已收的项无事发生 | | 「要先 start」连拒五拍熔断(措辞已写全) | start 墙本身:学不稳的礼仪就是死法 | 已拆墙(2026-08-02):动它就算启动(§4.3) | | 零输入执行卡被「表单不得为空」拒发到熔断 | 基座旧机检把表单当交付通道 | 已删该拒发:空表单是常态(卡族表「执行」行) | | 整拍六个动作全对却退化成「需要你判断下一步」 | 模型多吐一个闭括号,单数花括号的配平切出半截 | 已修:括号栈 + 丢不配对闭括号(`parseJsonLoose`,有测) | | 「需要你判断下一步」卡面摆着一坨 JSON 原话 | 整拍输出没成形,旧兜底一次就发卡还把原话上卡 | 已修(2026-08-03):就地重问一次、原话归工作日志、卡面只说人话(agent-runner,有测) | | 模型逐字复读一句被明令禁止的话 | **反例把原话引进了提示词**(「别说『…』」)—— 关思考的模型不读否定框,只认框里现成的句子 | 反例只描述、不引原话;要引原话就引**正例**(想听哪句就示范哪句)。A1「照历史价估」逮过(2026-08-03) | | 执行卡上作业指导书整个是空的(basis 里是「当下可为(空)/目标缺口」这类家具) | **卡骨架写在那一步的剧本里**,而模型在「干活+收步+派卡」压成的一拍里发了它 —— 那一拍该步剧本没摊开,模型手上没有骨架,凭记忆拼出裸卡,基座兜底 basis 又摆上局面家具 | **凡要模型照抄的卡骨架,放说明书不放步骤剧本**(说明书每拍都在;A6 决策卡先立此法,A1 审定卡 2026-08-03 同坑)。另:`steps`/`standard` 已补进出参 schema —— 从前能到卡面纯靠厂商 JSON 模式宽松 | | 人答完后同一条拒回重复五遍熔断(「这一步已办完,不能再往它上面挂活」) | **重发判断走在站上场之后** —— 人答完那一步常已收掉,`readyToWork` 先抛,于是「重发已答过的卡 = 无事发生」这条活路在最需要时够不着 | 已修(2026-08-03):**先判重发再站上场**;重发一张已答过的卡本就不需要那一项在场(墙要配路,有测) | | 同一个岔路口连发四张决策卡,调人预算烧光落 `exhausted` | 决策卡没有幂等兜底(执行工单一直有) | 已修:`resendVerdict` 两族共用 —— 头两次无事发生、第三次起算被拒 | | 收上一步时连拒「写了等于没写」 | 值已在台账(件上保存/上一拍写过),模型照抄重写;或提前干了下一步的活(如在 S2 查行情) | 拒回已带指南「该办下一件事」;剧本明写这一拍只有哪几件事 | | 人答了卡,模型又发一张一样的 | 剧本残留「还缺就再发一张卡」死路 | §5.5-5:一单一卡;必填有闸兜着 | | 人在卡上填了值,台账还是空的 | doneWhen 写成「问过就算完」 | 等「答案落了表」的计数(`answered_total`) | | 该经过人的一步整个没发生 | 「经过人」写成了收步条件里的半句 | §5.3-4:独立一项+人工激活 | | 人改了表,分布条/去向不动 | 汇总数没人重算;或保存没落库 | 分类列标 `tally` 由基座数;保存走件上按地址那条路(姊妹篇 §6) | | 人在件上保存,一格都没落 | 保存借道当下那张卡的 blocks(熔断卡没有 blocks) | 已修:按 `data-edit` 地址收,授权=台账声明 | | L4 判错(人填了它说没填) | 整条只读裁断,模型凭记忆写理由 | L4 带谓词,谓词说了算(§3.3);`expect` 别标我方写的注 | | 判据卡面与提交门说的不一样 | 两处各算各的 | 已修:读时现算,同一段 `criterionCheck` | | 进度条在人办事时已走过那一格 | phases 漏了那一项 | §3.6:每项恰属一格,自测钉死 | | 收了步却过不了验收 | 收步判据比验收松 | §5.4:同源 | | 全 0 判据过不去连撞熔断 | 没写过 ≠ 写了 0 行 | 已修:声明过的表铺 0 | | 补全卡答了数据没进台账 | 系统不替 Agent 写 | §5.5:答案要自己 write(绑表除外) | | 同一颗料成果表里两行 | 主键换写法 = 新开一行 | §5.5-7 | | 数值列被写成 0 表示「没有」 | 模型拿 0 当空 | 写死:没有给 `""`(归型不写这一列) | | 制表符 BOM 拆错列 | 连续 `\t` 是中间格空 | 附件 `rows` 有值照它拆,别自己数制表符 | | 工作区画面不换 / 推错张 | view 名不在模版里 / 剧本没写 view | §5.4 view 定死 | | 归档件被冲成别的屏 | 收口后按固定名推退到第一张 | 已修:按剧本推,活步收完不推 | | 消息被「无主入站待认领」刷屏 | 台账 data 事件进了兜底 | 已修:只有 `source.kind==='channel'` 才兜 | | 下游网关取不到值 | 出口标量没有台账列承载 | §5.2-② | | 改了包重装没生效 | 同版重装 no-op | 升 packageVersion | | 收口前一两拍预算耗尽 | budget 给太紧 | §5.2-⑦ 留一倍余量 | | 代答脚本交执行卡被拒「必填字段未回填:结果」 | 卡没声明 `fields` 时基座兜底加必填「结果」回执字段 | 交卷 `values.result` 带一句 | | 验收 pass 后落地判「选择组未选出口」 | routedPort 只在前端回传里顶上(已修:服务端发事件前算有效口) | 升级基座;临时:回传自带 `selectedPorts` | | 排障时按协议等了、`advancing_until` 也查过是空,还是查不到那条旧任务的证据 | 诊断前先跑了 `reset-runs`,任务行与 `task_memory` 一并清空了 | 排障留痕要在 `reset-runs` 之前捞;要腾新案例的跑道,先把要查的证据记下来再清(2026-08-30) | | 修完平台代码,行为却和源码对不上,`touch` 源文件也没用 | dev 进程跑的是几小时前的旧代码,连自动重建的 `tsup --watch` 监视进程本身都可能已经退出 | 核 `ps -p -o lstart=` 与最近一次改动的时间对不对得上;不对就整套重启(kill + 重新 `pnpm dev:api`),别指望 `touch`(2026-08-30) | ## 8. 待实现与已知不一致 | # | 事项 | 现状 | 目标 | |---|---|---|---| | ④ | 受理分支误吞验收裁决 | `deliver` 的受理分支判 `awaiting-human && human` 不看 cardId | 按补全卡 cardId 匹配;开发侧规避:带缺口开跑的单尽早补齐 | | ⑤ | 治理台白名单授权口 | 只有种子常量表 | 治理台按活动配白名单 | | ⑦ | dist 真编译 | `index.js` 是源码文本拷贝(债 M5-3) | build-activities 真编译 | | ⑧ | 单拍工具调用缝 | 调用经 ToolInvoker 未走总线编译 | 开环转正或接总线 | | ⑨ | `preauthorizedActions` 死代码 | 无调用方 | manifest 里不要写 | | ⑩ | `SchemaValidator` 双死依赖 | 声明了不再调 | 清理 | | ⑪ | 评价卡(rate) | schema 在、链路未接 | 用户定:暂不做 | | ⑫ | `judge 还没裁`各花一拍 | 待裁量判据不在单拍局面里,撞拒回才知道 | 把「还没裁的判据」进局面(结构改动,未做) | 小不一致(不阻塞):`declarations.tools[].version` 无消费者;`contract.resources.tools` 双份声明;replay 跳过白名单;工具 `retries` 不重试;转人答案不经出参 Schema 校验(债 M6-1)。