Archived
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:
co-authored by
Claude Opus 5
parent
146e9bbfad
commit
561a2474ae
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user