Archived
用户:「评审放行与落账确认这一步的验收卡不应该有重做的选项,因为这里都没有接入 ai」。 W05 的 T_release 整个由配方跑完(guide 是空串,不问模型),件又是只读的 —— 打回之后没有任何东西能接住那句意见:配方只会原样再提交一次,撞在「打回之后台账没动过 就不许再提交」那道闸上,连撞两拍落成一张「连续撞在同一条判据上」的兜底卡。 一颗按下去只会把单子推进死胡同的钮,不如不摆(墙要配路;没有路就别立那扇门)。 · 契约:AcceptanceWorkOrder.redoable(不写 = 打得回,老卡老行为); · 装配层按放行那一步的声明现算 —— releaseRedoable 纯函数(有 auto 且 guide 空 → 打不回; 半 auto 与模型驱动照旧打得回,那儿真有模型会读意见),与 decisionCardFace / executionCardVariant 同一种做法、同一个测试位; · 卡面收起理由框与那颗钮,换一句真路:「这一步只落账,退不回上一步 —— 有异议就别确认, 去终止这一单重开」; · **服务端才是门**:redoable:false 的卡收到非 pass 的回传当场拒(redo-unavailable), 直调 API 绕不过;拒回把真路指出来。 S3 结论审核那张审核卡不受影响 —— 它的重做真会带着意见重判(昨夜那一轮)。 测:card-rules 3 条 + releaseRedoable 4 条,两处变异验证;e2e w05 5/5 released、被拒 0。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
153 lines
5.4 KiB
TypeScript
153 lines
5.4 KiB
TypeScript
/**
|
|
* module: release —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
|
|
*/
|
|
import type { ActionIntent, StaffId, TaskId } from '@contracts';
|
|
import type { LandingOutcome } from './internal/domain/precheck.ts';
|
|
export type Clock = {
|
|
now(): Date;
|
|
};
|
|
export type IdGenerator = {
|
|
next(): string;
|
|
};
|
|
export type IntentStore = {
|
|
save(intent: ActionIntent): Promise<void>;
|
|
list(taskId: TaskId): Promise<ActionIntent[]>;
|
|
setState(id: string, state: ActionIntent['state'], error?: string): Promise<void>;
|
|
};
|
|
/** 台账 —— app 注入 `ledger`(L1);放行写与冲突检查都在它那 */
|
|
export type LedgerGateway = {
|
|
checkConflicts(taskId: TaskId): Promise<Array<{
|
|
field: string;
|
|
key: Record<string, string | number>;
|
|
}>>;
|
|
releaseWrite(taskId: TaskId): Promise<{
|
|
mergedRows: number;
|
|
emittedEvents: number;
|
|
/** 试跑单被挡在正式表外的主数据行数(非试跑恒为 0)—— 放行痕要如实说 */
|
|
simulatedRows: number;
|
|
/** 早已在数据工单确认时入库、这次不再写的行数 —— 放行痕上分开说,「合并 N 行」不多报 */
|
|
landedEarlierRows: number;
|
|
/** 合并进正式版的行 —— 输出口发射的载荷取自这里 */
|
|
merged: ReadonlyArray<{
|
|
domain: string;
|
|
object: string;
|
|
key: Record<string, unknown>;
|
|
values: Record<string, unknown>;
|
|
}>;
|
|
}>;
|
|
};
|
|
/** 工具 —— app 注入 `toolbox`(L2);预检的②③④项与落地执行都经它 */
|
|
export type ToolGateway = {
|
|
/** 工具可用(未停用)与授权有效 */
|
|
isInvocable(tool: string, activityId: string): Promise<{
|
|
ok: boolean;
|
|
reason?: string;
|
|
}>;
|
|
/** 入参 schema 校验 */
|
|
validateInput(tool: string, input: Record<string, unknown>): Promise<{
|
|
ok: boolean;
|
|
errors: string[];
|
|
}>;
|
|
/** 目标预校验:工具契约含 validate 能力时调之(ADR-005) */
|
|
validateTarget(input: {
|
|
tool: string;
|
|
activityId: string;
|
|
taskId: TaskId;
|
|
payload: Record<string, unknown>;
|
|
}): Promise<{
|
|
ok: boolean;
|
|
error?: string;
|
|
} | undefined>;
|
|
/** 落地执行 —— 附幂等键 */
|
|
execute(input: {
|
|
tool: string;
|
|
activityId: string;
|
|
taskId: TaskId;
|
|
payload: Record<string, unknown>;
|
|
idempotencyKey: string;
|
|
}): Promise<LandingOutcome>;
|
|
};
|
|
/** 人类工具 —— app 注入 `human-tool`(L3):发验收工单与落地失败卡 */
|
|
export type CardGateway = {
|
|
issueAcceptance(input: {
|
|
taskId: TaskId;
|
|
ownerId: StaffId;
|
|
humanCallCap: number;
|
|
title: string;
|
|
basis: ReadonlyArray<{
|
|
k: string;
|
|
v: string;
|
|
}>;
|
|
deliverable: string;
|
|
criteria: ReadonlyArray<{
|
|
label: string;
|
|
level: 'L1' | 'L2' | 'L3' | 'L4' | 'L5';
|
|
pass: boolean;
|
|
settled?: boolean | undefined;
|
|
rationale?: string | undefined;
|
|
}>;
|
|
actions: ReadonlyArray<{
|
|
label: string;
|
|
effect: 'read' | 'write';
|
|
detail: string;
|
|
}>;
|
|
/** 人工验收清单 —— 原样带上卡 */
|
|
checklist?: ReadonlyArray<string> | undefined;
|
|
/** 这张卡打回得了吗(`false` = 那颗钮不摆)—— 端口不声明它,值就在装配层被丢掉,这行不能省 */
|
|
redoable?: boolean | undefined;
|
|
/**
|
|
* 可修订字段 —— 验收报告「AI 已填好、可编辑」改的就是它们(带 `ai` 拟值与 `ledger` 落回地址)。
|
|
* **端口不声明它,值就在装配层被丢掉**:上游一路传下来,而 TypeScript 对条件展开
|
|
* 不做多余属性检查,于是编译不报、运行时静默丢 —— 卡面 `fields` 恒空(踩过)。
|
|
*/
|
|
fields?: ReadonlyArray<Record<string, unknown>>;
|
|
/**
|
|
* 验收页的正文块 —— 绑台账的可编辑成果表走这里。
|
|
* **端口不声明它,值就在装配层被丢掉**(与 `fields` 同一个坑,又踩了一次:
|
|
* 契约声明了、submit 传了、release 转了,到装配层那个显式对象里被抹平,
|
|
* 编译不报、运行时静默丢 —— 卡面 blocks 恒空)。
|
|
*/
|
|
blocks?: ReadonlyArray<Record<string, unknown>>;
|
|
}): Promise<{
|
|
id: string;
|
|
}>;
|
|
issueLandingFailure(input: {
|
|
taskId: TaskId;
|
|
ownerId: StaffId;
|
|
humanCallCap: number;
|
|
failures: ReadonlyArray<{
|
|
label: string;
|
|
reason: string;
|
|
}>;
|
|
}): Promise<{
|
|
id: string;
|
|
}>;
|
|
};
|
|
/** 任务上下文 —— app 注入(活动 ID 与契约的输出口声明) */
|
|
export type TaskContext = {
|
|
activityIdOf(taskId: TaskId): Promise<string>;
|
|
outputPortsOf(taskId: TaskId): Promise<{
|
|
mandatory: string[];
|
|
selective: Array<{
|
|
group: string;
|
|
ports: string[];
|
|
}>;
|
|
}>;
|
|
};
|
|
/** 输出口发射 —— app 注入 `event`(L1):落地收敛**之后**才发 */
|
|
export type PortEmitter = {
|
|
emit(input: {
|
|
taskId: TaskId;
|
|
port: string;
|
|
payload: Record<string, unknown>;
|
|
}): Promise<void>;
|
|
};
|
|
export type MemoryRecorder = {
|
|
append(input: {
|
|
taskId: TaskId;
|
|
kind: 'action-intent' | 'release-verdict' | 'ledger-write';
|
|
title: string;
|
|
detail?: Record<string, unknown>;
|
|
}): Promise<void>;
|
|
};
|