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/packages/contracts/src/contract7.ts
T
ChaiHaoandClaude Opus 5 259192f736 人手改的那一手真正落得下去;界面不撒谎那几处补齐
手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的:

件上就地改
· 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名,
  于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 ——
  而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。
· **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。
  删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)——
  「没回传就是删了」会把一次不完整的回传变成一次删表。
· **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。
· 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。

判据与门
· 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来,
  人填好的目标价它照样判「客户未提供」的毛病由此消掉。
· 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。
· 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开,
  于是一单没打标就进了放行门。三个数全由基座现数。
· 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。
  **指向两扇打不开的门,比不指还糟。**

界面不撒谎
· 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。
· 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、
  往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。
· 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。
· 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。
· 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。

A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。
五道门禁全绿(971 passed)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
2026-08-02 13:41:44 +08:00

265 lines
12 KiB
TypeScript

/**
* 契约七件(编码规范 §8.1;语义出处 OS 4.1,AM 01)。
*
* 契约是活动对调用方暴露的全部真相,也是对 Agent 机制的全部授权;
* 两种范式同一契约格式 —— 这是「统一套执行契约」的锚点。
* 契约必须在活动启动前冻结;运行中不得变更 —— 变更即新版本。
*
* ADR-003:**声明层 = JSON Schema 2020-12 子集**(活动声明是运行时数据,可 diff 可版本),
* 代码层 = zod。本文件用 zod 描述「声明的形状」,声明里的 schema 字段本身是 JSON Schema。
*/
import { z } from 'zod';
import { acceptanceLevelSchema } from './terminal.ts';
import { activityIdSchema, skillIdSchema } from './id.ts';
/** JSON Schema 2020-12 子集的运行时形态 —— 由 ajv 校验,不在编译期展开 */
export const jsonSchemaSchema = z.record(z.string(), z.unknown());
export type JsonSchema = z.infer<typeof jsonSchemaSchema>;
/* ── ① 输入口:单一入口,data + message ──────────────────────────────── */
export const contractInputSchema = z.object({
/** data:结构化载荷,按 schema 校验,缺项不得启动 */
data: jsonSchemaSchema,
/** message:委托消息(自然语言),可为空;**不得携带扩大授权的效力** */
messageAllowed: z.boolean().default(true),
messageHint: z.string().optional(),
/**
* **这一单是被一封信叫起来的** —— 声明入参里哪几个字段合起来是那封来件。
*
* 声明了,基座在起单那一刻就把它归进任务的**沟通记录**(工作台那一屏),
* 原件与附件一并存下:谈过什么、什么时候谈的,回头查得到。
*
* 为什么是**声明**而不是活动跑起来之后再做一遍:这件事没有临场判断的成分 ——
* 「入参的 subject 就是来件主题」是活动作者写清单时就知道的事实。
* 交给模型每单认一遍,是拿一整拍的 token 去换一个常量。
* 值填的是**入参里的字段名**,不是字面内容。
*/
inboundComm: z
.object({
channel: z.string().default('mail'),
/** 对方是谁 —— 填入参里那个字段的名字(如 `customerEmail`) */
partyField: z.string().min(1),
subjectField: z.string().optional(),
bodyField: z.string().optional(),
attachmentsField: z.string().optional(),
})
.optional(),
});
export type ContractInput = z.infer<typeof contractInputSchema>;
/* ── ② 输出口:必发(AND)与选择组(XOR) ──────────────────────────── */
export const outputPortSchema = z.object({
name: z.string().min(1),
/** 交付物 schema —— 调用方仅凭口名与 schema 即可衔接下游 */
payload: jsonSchemaSchema,
description: z.string().optional(),
/**
* **这一口什么时候走** —— 对任务视图求值的 S-FEEL 谓词(选择组内**按序求值,首个命中即出**)。
*
* 组内每一口都写了 `when`,去向就是**算出来的**:放行时基座自己判,
* 卡上只如实告诉人走哪一口、凭哪一条,不摆成选择题。
* 「这一单往哪走」多半是台账里那几个数的函数(有没有 C 类、有没有 B 类)——
* 让人凭印象点一下,等于把一个可回放的判定换成一次不可回放的手感;
* 让模型选,又多一处它可以说了算的地方。
*
* 有一口没写,整组就算不出来,退回「人选一口」那条老路(不是所有分支都算得出来:
* 议价的 成交/有异议/拒绝 就得人裁)。
*/
when: z.string().optional(),
});
export type OutputPort = z.infer<typeof outputPortSchema>;
export const outputPortsSchema = z.object({
/** 必发口:每次「放行完成」必发;可多口,同时发射 */
mandatory: z.array(outputPortSchema).default([]),
/** 选择组:组内恰发一口;选择由环内决断,在放行(验收工单)上一并确认 */
selective: z
.array(
z.object({
group: z.string().min(1),
ports: z.array(outputPortSchema).min(2),
}),
)
.default([]),
});
export type OutputPorts = z.infer<typeof outputPortsSchema>;
/* ── ③ 必交付物 ─────────────────────────────────────────────────────── */
export const requiredDeliverableSchema = z.object({
key: z.string().min(1),
label: z.string(),
/** 缺一不得提交放行 —— 验收工单「成果清单」的来源 */
schema: jsonSchemaSchema.optional(),
/**
* 「有没有产出」的**可机检依据**:台账地址 `域.对象`,该地址在任务视图里有行即算有。
*
* 不给依据的话,「缺一不得提交放行」就只能靠活动自报 —— 自报等于没有门。
* 声明了地址,预检就能自己数行数:活动说交付了、台账里一行没有,当场拒收。
*/
ledger: z.string().optional(),
/**
* **这份成果由哪几张表承载**(2026-08-02 用户当场指出:「料号分类表和受理结论
* 不都是包含在询价受理记录的结构里的吗,为啥单独陈列?」)。
*
* 一份成果常常不止一张表:A1 交出的是**一份询价受理记录**,而它由受理记录、
* 料号分类表(判据来源)、受理结论(结论栏)三张表共同承载。
* 从前三张表各占一条 `requiredDeliverables`,人在成果面上就看到三样东西 ——
* 而在业务上他只拿到一份。
*
* 声明了它,落地那一屏按**交付物**点名,三张表的行数并成一行;
* 不声明就退回 `ledger` 那一张 —— 老清单不必改一个字。
*/
ledgers: z.array(z.string()).optional(),
/**
* **验收页上让人就地改的那几列** —— 台账列名,顺序即列序。
*
* 验收是这一单最重的一道门:人得能在**成果本身**上改,而不只是在旁边几个整单级字段上改。
* 给了这几列,基座在放行卡上铺一张绑着 `ledger` 的可编辑表(整份行都在),
* 人改完由基座写回草稿层、随放行合并 —— AI 不经手。
* 不给就没有这张表:哪些列该让人动,是写清单时就该定的事,不是临场发挥。
*/
editableAtRelease: z.array(z.string()).optional(),
});
export type RequiredDeliverable = z.infer<typeof requiredDeliverableSchema>;
/* ── ④ 验收判据:逐条标注真实等级 L1–L5 ───────────────────────────── */
export const acceptanceCriterionSchema = z.object({
key: z.string().min(1),
label: z.string(),
level: acceptanceLevelSchema,
/** L1/L2 的机检表达式(S-FEEL);L3–L5 由环内实证 / 独立评审 / 人放行承担 */
predicate: z.string().optional(),
/** loop 可引用业务模型的完成判据或 DMN 决策 */
modelRef: z.string().optional(),
});
export type AcceptanceCriterion = z.infer<typeof acceptanceCriterionSchema>;
/* ── ⑤ 禁区 ─────────────────────────────────────────────────────────── */
export const prohibitionSchema = z.object({
key: z.string().min(1),
/** 应短而硬;负面清单优于正面枚举 */
statement: z.string().min(1),
});
export type Prohibition = z.infer<typeof prohibitionSchema>;
/* ── ⑥ 授权边界 ─────────────────────────────────────────────────────── */
export const authorizationBoundarySchema = z.object({
/** AI 可自决的范围:金额 / 折扣 / 措辞等,以 S-FEEL 谓词或叙述给出 */
discretion: z.array(z.object({ key: z.string(), statement: z.string() })).default([]),
/** 预授权动作清单 —— 必须逐项列举,方可在过程中即时执行写类动作(OS 7.3) */
preauthorizedActions: z
.array(z.object({ tool: z.string(), operation: z.string(), limit: z.string().optional() }))
.default([]),
/** 调人预算(打扰上限) */
humanCallBudget: z.number().int().min(0),
});
export type AuthorizationBoundary = z.infer<typeof authorizationBoundarySchema>;
/* ── ⑦ 资源声明 ─────────────────────────────────────────────────────── */
/** 预算四类上限 —— 由基座在环外强制,不得由活动代码自查自守(AM 10.1) */
export const budgetCapsSchema = z.object({
steps: z.number().int().min(1),
tokens: z.number().int().min(1),
/** 活跃时长(分钟);挂起、监听等待、待卡回传不计入 */
activeMinutes: z.number().int().min(1),
humanCalls: z.number().int().min(0),
});
export type BudgetCaps = z.infer<typeof budgetCapsSchema>;
export const resourceDeclarationSchema = z.object({
/** 工具调用契约引用 + 版本约束 */
tools: z
.array(z.object({ name: z.string().min(1), version: z.string().optional() }))
.default([]),
skills: z.array(skillIdSchema).default([]),
/** 活动使用的 LLM,以自定义模型名(逻辑名)声明;活动不触厂商与密钥 */
models: z.array(z.string().min(1)).default([]),
/** 岗位:调人时卡投递到哪个工位 */
roles: z.array(z.string().min(1)).default([]),
budget: budgetCapsSchema,
});
export type ResourceDeclaration = z.infer<typeof resourceDeclarationSchema>;
/* ── 七件合体 ───────────────────────────────────────────────────────── */
export const sevenPartContractSchema = z.object({
activityId: activityIdSchema,
version: z.string().min(1),
input: contractInputSchema,
output: outputPortsSchema,
requiredDeliverables: z.array(requiredDeliverableSchema).default([]),
acceptanceCriteria: z.array(acceptanceCriterionSchema).default([]),
/**
* **放行时人可当场改的值** —— 验收报告「AI 已填好、可编辑」改的就是这几个。
*
* 它是**设计期的事实**,和验收判据一样:哪几个值该让负责人有机会覆盖、
* 改完落回哪个台账地址,由做这个活动的人一次定死,过安装审。
* 让 Agent 每单临场重述一遍是不可靠的 —— 实测它会漏(说了「可直接编辑」而没给字段),
* 而漏掉的后果是验收报告只能看不能改,人工修订形同虚设。
*
* `ai` 不在这儿:拟值是**运行期**的事实 —— 放行前由基座从台账现读活动写下的那个值,
* 不让模型重述一遍(重述就会和台账里的值不一致,而以哪个为准说不清)。
*/
revisableAtRelease: z
.array(
z.object({
key: z.string().min(1),
label: z.string(),
/** 改完落回哪儿:`域.对象`;只改本单在该对象的那一行 */
ledger: z.string().min(1),
}),
)
.default([]),
prohibitions: z.array(prohibitionSchema).default([]),
authorization: authorizationBoundarySchema,
resources: resourceDeclarationSchema,
});
export type SevenPartContract = z.infer<typeof sevenPartContractSchema>;
/**
* 完成时的发射集合 = 全部必发口 + 每个选择组各一口(AM 1.2)。
* 纯函数,前后端同源:前端预演、后端裁决。
*/
export function emissionPortNames(
ports: OutputPorts,
selected: Readonly<Record<string, string>>,
): string[] {
const out = ports.mandatory.map((p) => p.name);
for (const group of ports.selective) {
const choice = selected[group.group];
if (choice !== undefined) out.push(choice);
}
return out;
}
/** 选择组必须恰选一口 —— 未选或选了组外的口即非法 */
export function validateSelection(
ports: OutputPorts,
selected: Readonly<Record<string, string>>,
): string[] {
const errors: string[] = [];
for (const group of ports.selective) {
const choice = selected[group.group];
if (choice === undefined) {
errors.push(`选择组「${group.group}」未选出口`);
continue;
}
if (!group.ports.some((p) => p.name === choice)) {
errors.push(`选择组「${group.group}」的出口「${choice}」不在组内`);
}
}
return errors;
}
/** 验收等级不得谎报:L4 不得标注为 L1–L3;生成者不得自评(AM 12.1) */
export function machineCheckable(criterion: AcceptanceCriterion): boolean {
return criterion.level === 'L1' || criterion.level === 'L2';
}