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/活动开发规范.md
T
ChaiHaoandClaude Opus 5 94f1c7a4ef 活动开发规范:立「HTML 报告模版活动持有,一单一模版贯穿始终」为硬规矩
用户强调补进规范(§9.3 整节重立,三条硬规矩):
  ① **关键表单逐张配 HTML 报告模版** —— 模版是活动自己持有的资产
    (assets.viewTemplates,随包版本冻结),不是系统临场生成;执行与验收都要配,
    人办工单看到的是已排好版的报告(AI 拟值已填、basedOn 的过程成果嵌在正文里),
    不是一堆裸字段;
  ② **从待处理工单到工作成果报告是同一张模版**:在办 = 报告 + 指导/判据;
    办结后指导与判据卸下,只剩报告原样进「工作成果」归档。不许两张模版 ——
    迟早长歪,人回头对不上账(语义出处:PRD ViewRegion 头注);
  ③ 一个局面一张模版,不在一张模版里堆条件分支。

同步三处:§4.2 assets 区硬规矩栏、§9.2 执行事项行、§11.2 自测清单
(关键表单逐张有模版、执行/验收都配、待处理与成果同模版)。

纯文档改动,五闸绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-30 03:22:25 +08:00

450 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 活动开发规范
> 2026-07-30 定稿。给 **AI 开发活动时查阅**的规范:开发一个活动要写什么、能用什么、
> 什么会被拒、跑起来是什么样。基于代码现状如实记述 —— 每条「已就位」的断言都带
> `file:line`;产品要求但现状还不满足的,标 **⚠️待实现** 并汇总在[第 12 章](#12-待实现清单)。
>
> 相关文档:`docs/编码规范.md` §8(活动包平台契约)、各模块 `CONTRACT.md`(模块级不变量)、
> `docs/产品设计.md`(三面一体)。本文与它们冲突时,**以代码现状为准**,并把冲突记进第 12 章。
---
## 1. 读者与用法
- **读者**:开发活动(`activities/<id>/`)的 AI 或人。你不需要读 apps/api 的源码 ——
活动的全部世界是 SDK 基座六件(第 3 章)与清单六区(第 4 章)。
- **用法**:开发前通读 2–4 章(边界与结构);写案例模型看第 8 章;写交互看第 9 章;
提交前对着第 11 章自测清单过一遍。
- **标注约定**:✅ = 现状已就位(带 file:line);⚠️ = 产品已定、代码未实现(汇总第 12 章)。
## 2. 总纲与铁律
**活动 = 任务的定义。** 编排引用一个活动,运行时就长出一个任务(恰一名负责人)。
活动在系统外开发、经安装通道入库(`registry.install`),版本恒新增、同版重装 no-op、
在途任务按启动时版本跑完(`apps/api/src/modules/activity-registry/CONTRACT.md`)。
**统一建模**(语义权威:`JCAI_PRD/workbench/src/app/(proto)/_data/model.ts` 头注,2026-07-25):
> 一个活动**完全由 AI Loop 自主完成**;Loop 按**阶段**推进(**阶段可重复**),阶段产出
> **过程成果**(Deliverable:草稿 → 待验收 → 已放行 / 已驳回)。一个任务可能有一或多个
> 过程成果,**每个成果都要调人类工具放行**。
活动不再分范式(2026-07-29 用户定):业务模型一律**案例模型**(CMMN + 说明书 + DMN),
业务分类是活动库唯一的一维(`packages/contracts/src/manifest.ts:20-23`)。
### 四条铁律
| # | 铁律 | 机制依据 |
|---|---|---|
| ① | **只读世界**:活动只能读台账、调读类工具、问模型、加工表单 | 基座六件即全部能力(§3);环境能力静态审拒装(§11) |
| ② | **写只有草稿与意向**:写台账 = 草稿层(不产生数据事件);外部动作(发邮件等)= 登记动作意向(入参冻结 + 幂等键),**登记那一刻世界上什么都没变** | `ledger.write` 只有草稿写(`base.ts:55-60`);写类工具自动转意向(`apps/api/src/app/tool-bus.ts:206-209`) |
| ③ | **终局唯放行**:任务完成的唯一出口是**人类验收通过** —— 那一刻才草稿合并、意向执行、输出口发射;验收不过 = 不完成 | `raiseAcceptance` 是唯一提交口(`turn-runner.ts:207-242`);`land()` 六步(§7.3) |
| ④ | **活动不自转**:`run(base)` 是**一个回合**的推进,由引擎驱动;不自设步数、不产生终局迁移;预算与时限由 Harness 在环外强制 | `define.ts:22-25`;迁移拓扑表把 Completed/Terminated 设为死胡同(`loop-engine/internal/domain/lifecycle.ts:29-38`) |
## 3. 能力边界:基座六件就是全部世界
`packages/activity-sdk/src/base.ts`(权威,100 行,逐行有注释)。
「基座即笼子:**内容归活动,笼子归系统**。活动代码无环境能力,一切能力经调用契约注入,
越权在结构上不可能。」
| 件 | 句柄 | API | 要点 |
|---|---|---|---|
| ① 契约 | `base.contract` | `activityId / version / data / message / outputPorts{mandatory,selective} / prohibitions / discretion` | **只读**,启动前冻结;`message` 是委托消息,不得携带扩大授权的效力 |
| ② 交互 | `base.interaction` | `pushView(blocks)` / `pushHtml(html)` / `say(text, tag?)` / `issueWorkOrder(order)` / `issueCard(card)` | 视图模版过净化门(含脚本即拒);工单两模式、对话卡三族(§9) |
| ③ 记忆 | `base.memory` | `window(size)` / `recall({layers?, limit?})` | 任务记忆窗口是活动侧唯一记忆接口;活动记忆渐进披露,语义引擎不读 |
| ④ 台账 | `base.ledger` | `read(address, where?)` / `write(address, key, patch)` | 读 = 任务视图(正式版 + 本任务草稿层,读己之写);写 = **只有草稿写**,不产生数据事件 |
| ⑤ 工具 | `base.tools` | `invoke(tool, input)` / `intend(tool, input, label)` / `listen({eventType, keyFilter, deadline})` | AI 对世界只有**触达**(动作)与**等待**(监听)两种姿势;**监听时限必配** |
| ⑥ 模型 | `base.llm` | `generate({model, prompt, system?, outputSchema?})` | `model` 是**自定义模型名**(逻辑名),不触厂商与密钥;给 outputSchema 即要求结构化出参并校验 |
**outcome 三态**(`define.ts:10-16`):`{kind:'submit', deliverable, rationale}`(提交放行)/
`{kind:'await', reason}`(等人或等事件)/ `{kind:'blocked', reason}`(推不动,如实报)。
**禁区**(安装静态审黑名单,`activity-registry/internal/domain/static-audit.ts:144-159`):
`process.env`、`require(`、`node:fs`、`node:net`、`node:child_process`、`globalThis.fetch`、
`fetch(`、`XMLHttpRequest`、`eval(`、`new Function(` —— 产物文本扫描命中即拒装
(`environment-capability` / `dynamic-eval`)。另有 depcruise 双向隔离(activities 不得
import apps,反向也不得)与 eslint 禁 `fetch`/`process` 全局。
**SDK 三道守卫**(本地 mock 宿主即可测出,`packages/activity-sdk/src/host.ts`):
1. 视图模版**无脚本**(`host.ts:64`):`<script` 即抛 —— 容器无脚本执行权;
2. **裸调用拒发**(`host.ts:71`):`issueWorkOrder` 的 `basis` 为空即抛 —— 调用必附上下文;
3. **监听时限必配**(`host.ts:116`):`deadline` 空串即抛 —— 无死等。
> 如实说明:SDK 面是**本地开发面**(线上 `.js` 产物只当数据存,不 import 不 eval,
> ADR-004)。线上真正的闸是工具总线 + `ToolInvoker` 白名单 + `card-rules` 机检 ——
> 与 SDK 守卫同一套规矩,两头都得过。
## 4. 包结构与清单六区
### 4.1 目录树(照 `activities/quote-intake`,六包结构一致)
```
activities/<activity-id>/
├── package.json # @activities/<id>,private,type:module,exports "." → ./src/index.ts
├── src/
│ ├── index.ts # 代码入口:export default defineActivity({...})
│ ├── manifest.ts # 清单六区 + 导出 DMN 常量(供测试直接求值)
│ ├── case-model.ts # 导出 CMMN_XML 与 NARRATIVE(双表示,同版本发布)
│ ├── skills.ts # 导出 Skill[](id 用 skillIdSchema.parse 品牌化)
│ └── views/report.ts # HTML 视图模版常量(viewShell/kvCard/tableCard/noteCard 拼装)
├── __tests__/activity.test.ts # 自测四段(§11.2)
└── dist/{manifest.json,index.js} # 构建产物(⚠️ 现为过期占位,债 M5-3)
```
**入口写法**(`activities/quote-intake/src/index.ts`):只 import `@sdk`;台账地址提成
顶部常量;`export default defineActivity({ name, async run(base) {...} })`;典型 run 体 =
`ledger.read` 取数 → `pushView` 推块 → 分支 `say` + `return {kind:'await'}` 或
`return {kind:'submit', deliverable, rationale}`;本地纯函数放文件末尾。
⚠️ `defineActivity` 目前仍要求 `paradigm` 参数 —— 陈旧残留,统一填 `'loop'`,待清理(第 12 章⑥)。
### 4.2 清单六区(`packages/contracts/src/manifest.ts:163-172`)
| 区 | 内容 | 硬规矩 |
|---|---|---|
| ① identity | `activityId / name / packageVersion / category / sdkVersion / checksum / signature?` | **无 paradigm**;`sdkVersion` 与 SDK 精确相等否则拒装;**改了包内容必须升 packageVersion**(同版重装 no-op,改了不升版 = 静默丢失) |
| ② contract | 七件契约(§4.3) | |
| ③ declarations | `models[] / tools[] / ledger[] / listeners[] / harness` | 运行时**只读这里**的工具与模型声明;监听时限必配否则 `dangling-listener` 拒装 |
| ④ assets | `businessModel{cmmnXml, narrative, dmn} / skills / viewTemplates / cardSchemas / distillHooks` | CMMN 与说明书**同版本发布不允许分叉**(空任一即拒装);**关键表单逐张配 HTML 报告模版**(执行与验收都要,待处理与成果同模版,§9.3) |
| ⑤ samples | `{name, input, message?, expect:{ports[], payloadShape?}}[]` | **至少 1 条**;首装无历史时以静态审 + 样例集替代回放审 |
| ⑥ entry | `{file, checksum}` | 指 .js 产物 |
`declarations.harness`(`manifest.ts:33-45`):`memoryWindow`(默认 20)/ `stallAfter`
(ISO-8601 时长,默认 PT30M —— 停滞熔断的判据)/ `nudge[]` / `retry`。
**台账声明**(`packages/contracts/src/ledger.ts:59-76`):`{domain, object, label?, fields[]}`,
字段 `{name, type, label?, primaryKey, masked}`;标识符**只准小写字母开头、字母数字下划线**。
域的归属纪律:活动自持域用活动名;**跨活动的事实只有两条路 —— 输出口载荷,或共享域
`shared`**。物化三律(`ledger/internal/domain/schema-merge.ts`):最大化合并(同名字段
masked/primaryKey 取或)、最宽类型(integer→number→text 等加宽链,json 与标量互斥即冲突)、
只增不破坏(只产出建表/加列/加宽,**新增主键即冲突** → `ledger-type-conflict` 拒装)。
### 4.3 契约七件(`packages/contracts/src/contract7.ts:129-140`)
| 件 | 写什么 | 硬规矩 |
|---|---|---|
| ① input | `data`(JSON Schema)+ `messageAllowed / messageHint` | `data` 即**启动 Schema**(§5);message 不得携带扩大授权的效力 |
| ② output | `mandatory[]` + `selective[]` | mandatory 每次放行必发(AND);selective 每组**至少 2 口、恰发一口**,选口即分支结论(§7.4) |
| ③ requiredDeliverables | `{key, label, schema?}[]` | 缺一不得提交放行;验收工单「成果清单」的来源 |
| ④ acceptanceCriteria | `{key, label, level: L1-L5, predicate?, modelRef?}[]` | **L1/L2 必带 predicate(S-FEEL)或 modelRef**,否则 `acceptance-level-misreported` 拒装 —— 验收等级不得谎报 |
| ⑤ prohibitions | `{key, statement}[]` | 短而硬;负面清单优于正面枚举 |
| ⑥ authorization | `discretion[] / preauthorizedActions[] / humanCallBudget` | humanCallBudget = 调人预算(环外强制);⚠️ preauthorizedActions 现为死代码,**暂不使用**(第 12 章⑨) |
| ⑦ resources | `tools[] / skills[] / models[] / roles[] / budget{steps, tokens, activeMinutes, humanCalls}` | 预算四项由基座环外强制,**不得由活动代码自查自守**;models 是自定义模型名 |
> 坑位:`contract.resources.tools` 与 `declarations.tools` 是**双份声明**,运行时只读
> declarations(`engines.ts:443-447`)——两处要写一致,安装审只查 declarations。
> **出口字段要有台账列着落**:出口载荷从放行落地的事实推导(§7.5),声明一个没有任何
> 台账列承载的标量出口字段,下游网关会因取不到值而死路(`route-dead-end`)。
## 5. 启动与补全(活动输入无强制类型)
### 5.1 编排层确认结论(✅ 全部成立)
用户语义「活动间流转数据没有强制类型、活动输入可接受任何类型」在编排层**已成立**:
- **起单宽进**:`process.start` 不按调用接口拒缺项(`process-service.ts:270-315`,
注释写死「门口不设缺项闸,收敛点在活动」;`start-guard.test.ts:109-115` 守着)。
- **数据按字段名流动**:`flatScope` 铺平作用域(起单入参垫底 → 各输出口字段依次盖上 →
刚发射的载荷最后盖,后发生的赢,零类型检查,`routing.ts:32-41`);节点入参 =
**整个铺平作用域**,下游按自己的契约取,无契约裁剪(`self-hosted-runtime.ts:256`)。
- **闭合校验不查字段**:只剩结构闭合(dangling-node / multi-default-edge)与活动装载
(unknown-activity)四条(`closure.ts:33-81`)。字段够不够是活动自己的事。
### 5.2 机械门:required 缺项 → 补全卡(✅)
任务启动时(`task-service.ts:88-134`):
1. `missingRequired(inputSchema, data)`:**只查启动 Schema 顶层 `required`**,
「缺」= `undefined | null | ''`(`0`/`false`/`[]` 都算有);标签取
`properties[key].title ?? label ?? key`(给人看「客户码」不是 `customerCode`)。
**不做类型校验** —— 这是机械门的全部(语义级判断归 Agent,§5.4)。
2. 缺项**照建单**,任务落 `awaiting-human`,引擎照常装载;
3. 发**补全卡**(elicitation 对话卡,`engines.ts:715-736`):字段 = 缺的 `{key, label}`;
**不占活动调人预算**(补全是受理的事,不是活动的打扰);挂 P2D 人事件监听。
**答卡合并**(`task-service.ts:262-299`)四条硬规矩:
- 只收**当前还缺的键**(白名单),契约外的键、已填过的键静默丢弃;
- 只收**非空字符串** —— 补全卡回传一律按字符串,数字/布尔会被静默丢弃(设计表单时注意);
- 补齐(或「没法填」)→ 置 `running` 并立即 tick;答一半 → 只再派一张**只含剩余项**的卡,引擎一步不走;
- **「没法填」**:回传 `reason` 非空即放行**带缺口开跑** —— 收敛责任从此归活动内部。
两条路必走其一(答值或给理由),空答被 `card-rules` 拒收。
### 5.3 前端呈现(✅)
补全卡渲染在对话区(`desk-task/internal/chat-bubbles.tsx:214-251`,题头「请你补全 · N 项」);
「没法填?告诉 AI 原因」是显式按钮;**有未答的补全/决策卡时输入框锁定 ——「先办卡,再说话」**。
### 5.4 Agent 案内随时可补(✅ 机制在,活动要自己用)
开跑后发现「字段在但内容不像话」(机械门查不出的语义级不满足),活动应:
- **小信息**:`interaction.issueCard({k:'elicitation', ask, fields, basis})` —— 对话区就地要几个小信息;
- **要人干活**:`interaction.issueWorkOrder({mode:'execution', ...})` —— 执行工单
(PRD 统一建模里「补全」并入执行事项的那条路,§9.2)。
> ⚠️ 边界隐患(第 12 章④):走过「没法填」的任务,若后续验收被驳回(runtime 回到
> awaiting-human),到达的 human 事件会被受理分支误吞(它不看 cardId)。在修复落地前,
> 活动拿到「带缺口开跑」的单,应尽早在环内把缺项补齐或显式置值,别让缺口一路挂到验收。
## 6. 工具调用:完全声明式
### 6.1 设计口径
设计活动时**只考虑需要什么工具**:工具名 + 输入 + 输出(+ 给人看的指导段⚠️)。
不关心系统里有没有实现 —— 声明的工具进 `declarations.tools`(只有名,版本字段现无消费者)。
已注册工具的契约形状(`toolbox/internal/domain/tool-contract.ts:13-39`):
`name / displayName(中文名)/ version / description(**面向 AI** 的适用说明,≥8 字)/
input / output(JSON Schema)/ effect: 'read'|'write' / errors[] / idempotent(写类必须 true)/
timeoutMs / retries / connectionKeys[](只声明键,值在治理台)/ capabilities.validate? /
pure? / ownerActivityId?`。
⚠️ 增加 `guide` 字段(**面向人**的作业指导段):工具未命中转人时,执行卡上的作业指导书
从这里来 —— 声明式、可审、不临场生成(第 12 章②)。
### 6.2 运行时五分叉
| 情形 | 目标行为 | 现状 |
|---|---|---|
| 命中 · 读类 | 直接执行 | ✅ `tool-invoker.ts` 执行(超时 race;入参不合 schema 回 `{ok:false, code:'input-invalid'}` 不抛) |
| 命中 · 写类 | **登记动作意向**,放行后由系统执行(附幂等键) | ✅ 登记(`app/tool-bus.ts:206-209`「现在世界上什么都没变」);⚠️ 但落地链路断着 —— 意向永停 drafted,放行时不执行(§7.3、第 12 章①) |
| **未注册**(系统里没有) | **生成人类执行卡 + 作业指导书**:卡面 = 工具 guide 段 + 本次输入数据 + 按**输出 Schema** 生成的回填表单;人填回的表单值**就是这次工具调用的出参** | ⚠️ 现状抛 `ToolNotFoundError`(`tool-invoker.ts:43`),单拍路径 catch 后记忆留痕「行动被拒」回合不崩 —— **转人待实现**(第 12 章②) |
| 已注册 · **未白名单授权** | **报错**(越权是治理配置问题,不静默转人) | ✅ `ToolNotAllowedError`(`tool-invoker.ts:45-51`,文案「越权在结构上不可能」) |
| 已停用 | 失败路径(活动按失败处理) | ✅ `ToolDisabledError` |
配套现状(如实记):
- 工具总线 = **声明 × 整张目录**,编译时未命中收进 `missing[]` 如实回报、写观察进任务
记忆、**不阻断**(`compileToolBus`,`tool-bus.ts:184-213`;`engines.ts:449-455`)——
这半边已经是「声明式」的形状,缺的是运行时把 miss 转人;
- ⚠️ 安装静态审现把 `tool-contract-missing` 判 **error 拒装**(`static-audit.ts:108-111`)——
与「声明式工具、系统没有就转人」正面冲突,需降级为 warn(第 12 章②);
- ⚠️ 白名单授权现状只有种子常量表(`seed-mvp.ts:109-117`),无治理台授权口(第 12 章⑤);
- 缺省单拍 Agent 路径**不经工具总线**(工具名是自由字符串,唯一闸门是 ToolInvoker);
`JCAI_AGENT_OPEN_LOOP=1` 的开环路径才吃总线的声明×目录编译(第 12 章⑧)。
### 6.3 系统自带的工具(不用声明/只需声明模型)
- **human.ask 恒在**(`tool-bus.ts:52-75`):调人执行卡,零声明即得,不进目录不进白名单 ——
Harness 给的能力;每发一卡计 `humanCalls: 1`。
- **LLM 五件**(`tool-bus.ts:84-159`):`llm.extract / classify / summarize / judge / rewrite`,
仅当 `declarations.models` 非空才上总线(没声明模型却问模型是越权,用量无从归因);
提示词归系统,活动不写也改不了;`llm.judge` 只作参考,**不代替人的放行裁决**。
## 7. 写动作与验收(所有写必须经人类验收)
### 7.1 写三分(✅,`ledger/contract.ts:85-114`)
| 写法 | 谁用 | 事件 |
|---|---|---|
| `systemWrite` | 系统(受理落件等) | 即时抛数据事件 |
| `draftWrite` | **活动过程写的唯一形态**(`base.ledger.write`) | **不抛事件**;写时记基线值 |
| `releaseWrite` | 放行那一刻(系统调) | 先查冲突(草稿基线 ≠ 当前正式版即 `DraftConflictError`)→ 事务合并 → 读回落地行 → **此刻才抛事件** → 清草稿 |
### 7.2 动作意向(✅ 登记,⚠️ 执行断链)
写类工具调用(`base.tools.intend`,或 invoke 写类工具被自动转意向)→ 登记
`sys.action_intent`:入参冻结 + 幂等键 + 状态机 `drafted → prechecked → landed/failed/abandoned`。
**登记那一刻世界上什么都没变** —— 这是铁律②的机制承载。
### 7.3 放行落地六步(`release-service.ts:144-244`)
① `releaseWrite` 草稿合并(原子、过冲突检查)→ ② 记痕「放行写合并 N 行」→
③ **只取 `prechecked` 的意向**逐个执行(附幂等键 `${taskId}:${intentId}`,已成动作不回滚)→
④ 裁定(有失败即 blocked)→ ⑤ 受阻:发落地失败卡、口一个不发、任务落 `blocked` 终态 →
⑥ 收敛:恰选一口校验(兜底)→ 发射输出口。
> ⚠️ **重大现状洞(第 12 章①)**:把意向从 `drafted` 推到 `prechecked` 的
> `submitForRelease`(预检 + 已验证动作清单上卡)**没有任何调用方** —— 活路径
> (`turn-runner.raiseAcceptance`)绕过了它。后果:**写类动作登记后永不执行**、
> 验收卡的 `actions` 恒空、`criteria` 恒为硬编码单条 L5、幂等键登记/落地两套不一致。
> 实现工具调用逻辑时这是第一优先级。
### 7.4 验收工单与恰选一口(✅)
验收工单 schema(`card.ts:328-350`):`deliverable / criteria(L1-L5 逐条,pass 标记)/
actions?(通过即执行的动作清单)/ fields(报告可修订)/ portChoice?(选择组)/ passLabel?`。
回传(`card.ts:473-491`):`verdict: pass | redo | revise` + `values` + `comment?` + `selectedPorts`。
机检三规矩(`card-rules.ts:101-150`):驳回**必附意见**;**恰选一口**在 pass 时强制
(未选 / 选组外即拒收,主闸在回传收单,落地 `validateSelection` 兜底);工单只增不改。
`producesCompletion` 仅 `acceptance + pass` —— **验收通过是任务完成的唯一来源**。
选中的那一口**就是这单的分支结论**:放行注入 `facts.flat.outcome = 选中口名`
(`engines.ts:642-644`),下游异或网关的条件读的就是它(`outcome = "won"` 等)。
> ⚠️ 前端链两端断(第 12 章③):验收工单的 `fields` 前端不渲染、`values` 恒传 `{}`
> (人工修订报告落不下来);驳回 UI 只有 redo,引擎侧把 redo/revise 都塌成 reject。
### 7.5 出口载荷从落地事实推导(✅,契约设计的硬约束)
`outputPortsOf(manifest, landed)`(`engines.ts:535-574`):数组字段 ← `landed.rows` 整体;
标量字段 ← `landed.flat[field]`(驼峰/下划线两认);`landed.flat` 还会全量兜底铺进每个口。
**推论**:出口声明的标量字段必须有台账列承载(活动放行前写进草稿的行),否则下游取不到 ——
这是活动契约与台账声明之间必须对齐的一条线(历史事故:两枚去向旗没有列,G1 全流死路)。
## 8. 案例模型(CMMN + 说明书 + DMN)
### 8.1 CMMN 子集封顶(`loop-engine/internal/domain/cmmn-parse.ts`)
计划项 **8 型**:`humanTask / decisionTask / processTask / caseTask / stage / milestone /
timerEventListener / userEventListener`;结构标签 23 个(sentry/onPart/ifPart/planningTable/
discretionaryItem/itemControl/…)。**表外构件直接拒**(`assertSubset` 抛错,不静默忽略)——
子集封顶要能被机器证明。
| 记号 | 语义 | 纪律 |
|---|---|---|
| `manualActivationRule` | Enabled ≠ Active,要按一下才动;**Agent 发 `start` 来按** | **触达世界与关键交付的项必须标**(AM 4.1 原理一) |
| `repetitionRule` | 项可重复(阶段可多轮) | 撒网/追问这类常来回的事要标 |
| `planningTable / discretionaryItem` | 裁量项:Agent 临场规划入场 | 「看情况做」的事放这里 |
| `entryCriterion / exitCriterion` | sentry = onPart(听任务数据/项迁移)+ ifPart(S-FEEL) | 条件读任务数据字段 |
| `ifPart` 前缀 `semantic:` | **语义性前提**:引擎不机算,交 Agent 裁量 | 「回价是否可信」这类判断这样写 |
| `autoComplete` | 齐了即收(必做的都完成、没有进行中的)| 案计划级与阶段级都可标;齐了不问 Agent 直接提验收 |
| `timerEventListener` | 时限(ISO-8601 时长) | 催办/过半提醒 |
| `userEventListener` | 人事件(客户改需求等) | 外部变化的入口 |
**AI 不得产生终局迁移**:迁移拓扑表把 `Completed`/`Terminated` 设为死胡同,
`Available → Completed` 不在表内 —— Agent 想跳步会吃 `IllegalTransitionError`,
被回合循环捕获成「行动被拒」留痕,不崩(`turn-runner.ts:363-372`)。
### 8.2 说明书(narrative)
与 CMMN **同版本发布、不允许分叉**(空任一即拒装)。写法照 `activities/quote-intake/src/case-model.ts`:
`§0 开案`(什么事、成品是什么)→ `S1..Sn`(各阶段怎么办、判断口径、宁可空着不许臆造这类
纪律)→ `§Z 结案`(什么算办完、交出去什么)。说明书是 Agent 每一拍叙事面的第一段 ——
**写给 AI 执行看的,不是给人读的产品文案**;判断口径要写死(「吃不准降 B,不许臆断 A」)。
### 8.3 DMN(能算的不问 LLM)
`{drd[], tables[]}`;表 = `hitPolicy(默认 FIRST)/ inputs(name+expression)/ outputs / rules`。
单元格谓词 = S-FEEL:`'-'` 或空串任意匹配、`'"A","B"'` 析取、`'<90'` 比较、`'[0..100]'` 区间。
求值(`dmn.ts:33-76`):任一输入缺 → `incomplete`(带 missing);零命中 → `no-match`;
命中留痕 `DecisionTrace{inputs, hitRules, outputs}`。决策表在 `decisionTask` 上以
`decisionRef` 引用;**同输入必同结论、零 token** —— 规则能写死的判断一律进 DMN。
## 9. 工作台视图逻辑(PRD 为语义权威)
语义权威:`JCAI_PRD/workbench/src/app/(proto)/_data/model.ts`(统一建模)与
`_components/{ViewRegion,TaskChat}.tsx`;WOS 平移件:`apps/web/src/features/desk-task/internal/
{ViewRegion,TaskChat,chat-bubbles,work-order-report}.tsx`。卡族 schema 前后端同源:
`packages/contracts/src/card.ts`。
### 9.1 三栏分工(结构性定调)
| 栏 | 承载 | 铁律 |
|---|---|---|
| **视图区** | **全部结构化交互**:HTML 交互界面(表单读写 + 图形展示)+ 工作事项 + 过程成果 | 待处理**翻页不排队**(队列是活动内部的事);五页签:待处理/沟通记录/工作成果/任务详情/工作日志 |
| **对话区** | **只承载非结构化消息** + 决策选择题;系统事件 = 灰头像 | **气泡二元制**:纯文本气泡或完整表单卡,不允许中间态;补全/决策卡吐出期间 composer 锁定 ——「先办卡,再说话」 |
| **任务日志** | 全部任务记忆的结构化呈现 | 归侧四类(机器产出/人产出/人裁决/世界等待)= 度量口径,零埋点 |
**三类同级唤醒事件**:①对话输入 ②外部事件(数据/时间/控制)③页面交互(含工单回传)。
### 9.2 工作事项(WorkItem):人类工作清单
**AI 决定、只增不减**,只有两类(决策不是工作事项):
| 类 | 字段(PRD `model.ts:106-133` ↔ WOS `card.ts:279-350`) | 用途 |
|---|---|---|
| **执行** | `ask`(工作要求)/ `steps[]`(作业指导书分步要点)/ `standard?`(完成标准)/ `basedOn?`(报告正文取自哪份过程成果 —— 人要在数据上作判断,就得把数据摆在表单旁)/ `blocks?` / `fields[]`(回填表单,字段可带 **ai 拟值**)/ `submitLabel?` | 做 AI 的工作要求 —— **原「补全」并入此类**;回填报告用活动持有的 HTML 模版(§9.3);⚠️ 工具未命中转人也走这张卡(第 12 章②) |
| **验收** | `deliverable / criteria(L1-L5)/ actions?(待落地动作意向,读/写)/ portChoice? / passLabel?` | 收 AI 的成果:放行 / 驳回(必附意见) |
对话区另有三张卡:**决策**(选择题,recommend + 后果说明)、**fill 补全卡**(就地要几个
小信息;「没法填的理由」是另一条必走之路)、**rate 评价卡 —— 暂不做**(schema 在,不接)。
### 9.3 HTML 报告模版:活动持有,一单一模版贯穿始终
**活动的每一张关键表单,都必须有对应的 HTML 报告模版** —— 模版是**活动自己持有的资产**
(`assets.viewTemplates`,随包版本冻结),不是系统临场生成的。三条硬规矩:
1. **执行与验收都要配**:执行工单的回填报告、验收工单的成果报告,各有其模版 ——
人办工单时看到的不是一堆裸字段,是一份**已排好版的报告**(AI 拟值已填、可编辑,
数据摆在表单旁边,`basedOn` 引用的过程成果就嵌在报告正文里);
2. **从待处理工单到工作成果报告是同一张模版**:工单在办时,卡面 = 报告模版 +
作业指导 / 验收判据;办结后**指导与判据卸下,只剩报告** —— 它原样进「工作成果」
页签成为归档件。不许为「在办」与「归档」各做一张模版:两张迟早长歪,
人回头核对时对不上账。语义出处:PRD ViewRegion 头注「一张工单 → 卡组一
(HTML 报告 · AI 已填好、可编辑)+ 卡组二(作业指导 / 验收判据);办结后只剩
报告卡 —— 指导与判据是办理时的参照,不进归档件」;
3. **一个局面一张模版**(受理中/已分类/待放行……):局面变了换模版,
不在一张模版里堆条件分支。
推送两条路:`pushView(blocks)`(结构化块,11 型:callout/kv/badges/table/bars/donut/
mail/cards/waiting/files,`card.ts:103-201`)与 `pushHtml(html)`(HTML 模版)。
模版纪律(`activity-sdk/src/view.ts`):容器是 `sandbox=""` iframe,**无脚本执行权、
样式必须自带**;数据由基座推送时填(`data-slot` 取值、`data-rows` 逐行展开);
SDK 给了 `viewShell / kvCard / tableCard / noteCard / stagesCard` 拼装件,照
`activities/quote-intake/src/views/report.ts` 用。
### 9.4 运行时的卡怎么到人(✅ 链路)
活动 `issueWorkOrder`/`issueCard` → `card-rules` 机检(裸调用拒发/字段齐备)→ 调人预算
环外扣账 → 落 `sys.work_order` / `sys.chat_card` → 到达面两路:收件人是负责人 →
**直接长在任务对话流**;他人 → 系统消息要素。人答卡 → 校验(§7.4)→ human 事件 →
任务 `deliver` → 引擎下一拍收裁决。**在途验收先于一切**:有未裁决的验收卡时,
回合原地等,不重发卡、不问模型(`turn-runner.ts:274-287`)。
## 10. 记忆与技能
- **任务记忆**:只增不减;`memory.window(n)` 取最近轨迹(n = `declarations.harness.memoryWindow`);
终态封卷后不能再追加;蒸馏钩子(`distillHooks`:on terminal / on milestone + prompt)
在封卷后跑,失败不掀翻终态。
- **活动记忆**:跨任务沉淀,四层;`memory.recall({layers?, limit?})` 选摘;**语义引擎不读**
(它只进叙事面)。
- **技能**(`manifest.ts:117-131`):`{id, label, applicability, method, crystallized[],
knowledgeRefs[], version}`。**渐进披露**:常驻上下文只有目录(`id label:applicability`),
正文 Agent 说要才加载 —— 一次塞全部正文会把局面淹掉。**载方法不载权力**:技能里的
结晶片段调工具,仍过活动自己的白名单,技能不带来新授权。
## 11. 开发流程与自测
### 11.1 本地开发(mock host)
`createMockHost({contract, tools?, models?, ledger?, memory?})` → `{base, recording, run}`。
`recording` 十通道:`views / htmls / says / workOrders / cards / toolCalls / intents /
listeners / llmCalls / writes` —— 断言活动这一回合做了什么。工具未 mock 回
`{ok:false, code:'not-mocked'}`(活动要能按失败路径走);模型未 mock 直接抛。
### 11.2 自测四段(照 `activities/quote-intake/__tests__/activity.test.ts`)
1. **清单合规**:过 `activityManifestSchema`;监听时限已配;L1/L2 判据可机检;禁区非空 + 调人预算 > 0;
2. **案例模型与模版**:CMMN 双表示齐备;触达世界的项全标 manualActivationRule;末尾有收口验收项;**关键表单逐张有报告模版**(执行/验收都配、无脚本、待处理与成果同模版);
3. **DMN**:逐规则求值断言(同输入必同结论、优先级、任意格);
4. **入口**:mock host 跑 run —— 各局面的 outcome、推了什么视图、说了什么话、守卫真的拦
(无脚本/裸调用/时限必配)。
### 11.3 安装两审与版本纪律
- **静态审 11 code**(`static-audit.ts`):signature-invalid / sdk-incompatible / schema-invalid /
tool-contract-missing(⚠️ 待降级)/ model-name-unbound / dangling-listener /
template-has-script / environment-capability / dynamic-eval / ledger-type-conflict /
acceptance-level-misreported。任一 error 即拒装。
- **回放审**:历史成功样本现在必须仍过(regressions 硬门);首装无历史以样例集替代。
- **版本纪律**:安装恒新增、同版重装 no-op(**改了内容必须升 packageVersion**,否则静默
丢失 —— 实测踩过);卸载只置状态不删行;资产(业务模型/DMN/技能)播下一版不覆盖,
覆盖只能是显式治理回滚。
- 白名单授权:装入 ≠ 能调 —— 授权是另一步(⚠️ 现只有种子常量表,第 12 章⑤)。
## 12. 待实现清单
按对四条产品语义的阻塞度排序。实现时逐条销账,销一条更新本表。
| # | 事项 | 现状(证据) | 目标 |
|---|---|---|---|
| ① | **写类意向落地断链**(阻塞语义①) | `submitForRelease`/`precheck` 无调用方(`release-service.ts:119-141` 全仓无引用);活路径 `raiseAcceptance` 绕过预检;`land()` 只执行 `prechecked`(`:156`)→ 意向永停 `drafted`,写类动作永不执行;验收卡 `actions` 恒空、`criteria` 恒为硬编码单条 L5(`engines.ts:301`);幂等键两套(`engines.ts:360` vs `precheck.ts:93-95`) | 提交放行走 `submitForRelease`(预检 → 意向置 prechecked → 已验证动作清单与契约 acceptanceCriteria 上卡);幂等键统一为 `${taskId}:${intentId}` |
| ② | **工具未命中转人执行卡**(阻塞语义③) | 未注册抛 `ToolNotFoundError`(`tool-invoker.ts:43`);静态审 `tool-contract-missing` = error 拒装(`static-audit.ts:108-111`);工具契约无面向人的指导段 | 未注册 → 生成执行工单(卡面 = 工具 `guide` 段 + 输入数据 + 按输出 Schema 生成的回填表单,回传值即工具出参,幂等键沿用);静态审降级为 warn;工具契约加 `guide` 字段。未授权仍报错不转人 |
| ③ | **验收报告修订链两端断** | 验收工单 `fields` 前端不渲染(`work-order-report.tsx:75`)、`values` 恒传 `{}`(`chat-bubbles.tsx:500,519`);驳回 UI 只有 redo,引擎把 redo/revise 塌成 reject(`turn-runner.ts:432`) | fields 渲染 + values 回传(AI 拟值可改);驳回两式贯通到引擎 |
| ④ | **受理分支误吞验收裁决** | `deliver` 的受理分支判 `awaiting-human && kind==='human'` 不看 cardId(`task-service.ts:262-264`);「没法填」带缺口开跑的单再遇验收驳回,human 事件被误判成补全 | 受理分支按补全卡 cardId 匹配 |
| ⑤ | **治理台白名单授权口** | 唯一入口是种子常量表(`seed-mvp.ts:109-117`),无 API 无 UI;安装不授权 | 治理台按活动配工具白名单(读走 read、改走 govern) |
| ⑥ | `defineActivity` 去 paradigm | schema 已删,SDK 仍要求(`define.ts:21`);六包里还有 `'flow'` 字样 | SDK 签名去掉;六包清理 |
| ⑦ | dist 构建产物真编译 | `dist/manifest.json` 是过期 flow 形状、`index.js` 是源码文本拷贝(债 M5-3) | build-activities 真编译 + manifest 同步 |
| ⑧ | 缺省单拍路径无工具总线 | 只有 `JCAI_AGENT_OPEN_LOOP=1` 走总线(`engines.ts:464-491`);缺省 tool 是自由字符串 | 工具调用逻辑实现时统一走总线(开环转正或单拍接总线) |
| ⑨ | `preauthorizedActions` 死代码 | `dispositionOf`/`ToolInvoker.disposition` 无调用方;两包 manifest 里声明了但不生效 | 暂不启用:manifest 里**不要写**;将来要么接线要么删字段 |
| ⑩ | `SchemaValidator` 双死依赖 | task 与 process 都声明了 `schemas` 端口但不再调(宽进后退役) | 清理端口与注入 |
| ⑪ | **评价卡(rate)暂不做** | schema 在(`card.ts:424-438`),链路未接 | 用户定:暂不做 —— 不设计、不接线 |
其余记录在案的小不一致(不阻塞,修到顺手时清):`declarations.tools[].version` 无消费者;
`contract.resources.tools` 双份声明;replay 模式跳过白名单检查(`tool-invoker.ts:58-61`
在 loadContract 之前);注册路由造不出纯工具;工具 `retries` 声明了不重试;
`activity-registry/contract.ts:54`「两维管理」注释陈旧;`chat-bubbles.tsx:6` /
`work-order-report.tsx:6`「当前未挂载」注释已过期(实际都已挂载)。