M3(2/5): human-tool 与 release —— 卡协议与放行门

human-tool(L3):
- 三规矩机检:调用必附上下文(裸调用拒发)/ 回传即留痕 / 完成只出自放行通过
- 工单两模式 + 对话卡族的形态校验与回传校验;驳回必附意见;补全卡「没法填」旁路;
  决策卡自定义收尾项必附理由;已答不得再答(只增不改)
- 到达面两路(负责人 → 任务对话流;他人 → 系统消息);驳回重答 = 新卡实例、原卡闭合
- 调人预算由基座在环外强制;落地失败卡 = 执行工单的 preset

release(L4):
- 落地预检五项(入参 schema / 工具可用与授权 / 连接可达 / 目标预校验 / 数据冲突)
- 预检全过才发验收工单,卡面即已验证的动作清单;不过即打回环内,不发卡
- 放行写合并 + 幂等落地(任务 × 意向);落地收敛才发射输出口;
  受阻走落地失败卡且 emittedPorts 恒空;已成动作留存不回滚

215 个测试通过,门禁五道全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
JCAI Main Agent
2026-07-27 01:28:49 +08:00
co-authored by Claude Opus 5
parent 146e9bbfad
commit 561a2474ae
31 changed files with 4194 additions and 0 deletions
@@ -0,0 +1,176 @@
/**
* 人类工具三规矩的机检 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 OS 05,AM 06)。
*
* 调用必附上下文(必须)—— 每张卡自带依据与快照;**无上下文的裸调用,基座必须拒发**
* 回传即留痕(必须)—— 谁、何时、答了什么、依据什么,随卡入任务记忆
* 完成只出自放行(必须)—— 完成只能由验收工单的「放行通过」产生
*/
import type {
AcceptanceWorkOrder,
ChatCard,
ChatCardReply,
ExecutionWorkOrder,
FormField,
WorkOrder,
WorkOrderReply,
} from '@contracts';
import { elicitationReplyIsEmpty, rejectionNeedsComment } from '@contracts';
export type CardViolation = { readonly code: string; readonly message: string };
/** 到达面两路(OS 2.1):负责人的卡进任务对话流;其他人的卡以系统消息送达 */
export const ARRIVAL_SURFACES = ['task-chat', 'system-message'] as const;
export type ArrivalSurface = (typeof ARRIVAL_SURFACES)[number];
export function arrivalOf(recipientId: string, ownerId: string): ArrivalSurface {
return recipientId === ownerId ? 'task-chat' : 'system-message';
}
/* ── 发卡前的机检:裸调用必须被拒发 ─────────────────────────────────── */
export function checkWorkOrder(order: WorkOrder): CardViolation[] {
const out: CardViolation[] = [];
if (order.basis.length === 0) {
out.push({ code: 'bare-call', message: '调用必附上下文:卡面无依据快照,基座拒发(OS 05)' });
}
if (order.title.trim() === '') out.push({ code: 'title-missing', message: '工单标题不得为空' });
if (order.mode === 'execution') out.push(...checkExecution(order));
else out.push(...checkAcceptance(order));
return out;
}
function checkExecution(order: ExecutionWorkOrder): CardViolation[] {
const out: CardViolation[] = [];
if (order.ask.trim() === '') out.push({ code: 'ask-missing', message: '执行工单必须给出工作要求' });
if (order.fields.length === 0) {
out.push({ code: 'fields-missing', message: '执行工单 = 指导书 + 空表单,表单不得为空' });
}
out.push(...checkFieldKeys(order.fields));
return out;
}
function checkAcceptance(order: AcceptanceWorkOrder): CardViolation[] {
const out: CardViolation[] = [];
if (order.criteria.length === 0) {
out.push({ code: 'criteria-missing', message: '验收工单必须逐条列出判据(成果清单的来源)' });
}
// 验收等级不得谎报:L4 生成者不得自评 —— 这里只机检「等级必须标注」
for (const c of order.criteria) {
if (c.label.trim() === '') out.push({ code: 'criterion-unlabeled', message: '判据必须有可读的表述' });
}
out.push(...checkFieldKeys(order.fields));
return out;
}
function checkFieldKeys(fields: readonly FormField[]): CardViolation[] {
const seen = new Set<string>();
const out: CardViolation[] = [];
for (const field of fields) {
if (field.key.trim() === '') out.push({ code: 'field-key-empty', message: '表单字段 key 不得为空' });
if (seen.has(field.key)) out.push({ code: 'field-key-duplicate', message: `表单字段 key 重复:${field.key}` });
seen.add(field.key);
if (field.kind === 'radio' || field.kind === 'select') {
if (field.options === undefined || field.options.length === 0) {
out.push({ code: 'options-missing', message: `${field.kind} 字段 ${field.key} 缺选项` });
}
}
}
return out;
}
export function checkChatCard(card: ChatCard): CardViolation[] {
const out: CardViolation[] = [];
switch (card.k) {
case 'decision':
if (card.basis.length === 0) out.push({ code: 'bare-call', message: '决策卡必附依据快照,基座拒发' });
if (card.options.length < 2) out.push({ code: 'options-missing', message: '决策卡至少两个分支' });
break;
case 'elicitation':
if (card.fields.length === 0) out.push({ code: 'fields-missing', message: '补全卡必须给出要填的字段' });
break;
case 'rating':
if (card.ask.trim() === '') out.push({ code: 'ask-missing', message: '评价卡必须给出评价对象' });
break;
}
return out;
}
/* ── 回传校验:与前端提交校验同 schema(§9.2) ─────────────────────── */
export function checkWorkOrderReply(order: WorkOrder, reply: WorkOrderReply): CardViolation[] {
const out: CardViolation[] = [];
if (order.id !== reply.workOrderId) {
out.push({ code: 'mismatch', message: '回传的工单 ID 与卡不符' });
return out;
}
if (order.done !== undefined) {
out.push({ code: 'already-answered', message: '该工单已回传,只增不改' });
return out;
}
if (order.mode !== reply.mode) {
out.push({ code: 'mode-mismatch', message: '回传模式与工单模式不符' });
return out;
}
if (reply.mode === 'acceptance') {
// 驳回必附意见 —— 三规矩之一
if (rejectionNeedsComment(reply)) {
out.push({ code: 'comment-required', message: '驳回必附意见(OS 05)' });
}
return out;
}
const execution = order as ExecutionWorkOrder;
for (const field of execution.fields) {
if (field.required !== true) continue;
const value = reply.values[field.key];
if (value === undefined || value.trim() === '') {
out.push({ code: 'required-missing', message: `必填字段未回填:${field.label || field.key}` });
}
}
return out;
}
export function checkChatCardReply(card: ChatCard, reply: ChatCardReply): CardViolation[] {
const out: CardViolation[] = [];
if (card.id !== reply.cardId) return [{ code: 'mismatch', message: '回传的卡 ID 与卡不符' }];
if (card.k !== reply.k) return [{ code: 'mode-mismatch', message: '回传类型与卡类型不符' }];
if (card.k === 'decision' && reply.k === 'decision') {
if (card.chosen !== undefined) return [{ code: 'already-answered', message: '该决策已选,只增不改' }];
const known = card.options.some((o) => o.value === reply.value);
// 自定义收尾项:allowCustom 打开时允许选项之外的处置(必附理由)
if (!known && !card.allowCustom) out.push({ code: 'unknown-option', message: `分支不在选项内:${reply.value}` });
if (!known && card.allowCustom && (reply.note ?? '').trim() === '') {
out.push({ code: 'note-required', message: '自定义收尾项必附理由' });
}
}
if (card.k === 'elicitation' && reply.k === 'elicitation') {
if (card.done !== undefined) return [{ code: 'already-answered', message: '该补全已答,只增不改' }];
// 「没法填」旁路:要么填值,要么给理由 —— 两条路必走其一
if (elicitationReplyIsEmpty(reply)) {
out.push({ code: 'answer-required', message: '要么回填字段,要么说明「没法填」的理由' });
}
}
if (card.k === 'rating' && reply.k === 'rating') {
if (card.done !== undefined) return [{ code: 'already-answered', message: '该评价已提交,只增不改' }];
}
return out;
}
/**
* 完成只出自放行通过 —— 唯一判据。
* 判据全绿只是**可提交**,认账永远是人的动作(AM 06)。
*/
export function producesCompletion(reply: WorkOrderReply): boolean {
return reply.mode === 'acceptance' && reply.verdict === 'pass';
}
/** 判据全绿?—— L1/L2 由引擎预检,L3–L5 按分级执行;这里只看核对结果 */
export function allCriteriaGreen(order: AcceptanceWorkOrder): boolean {
return order.criteria.every((c) => c.pass);
}