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
+31
View File
@@ -0,0 +1,31 @@
# module: release
职责: 放行与落地 —— 落地预检五项、动作意向登记、放行写合并、幂等落地、
落地失败卡、输出口发射
不负责: 放行卡的卡协议与回传校验(见 human-tool)、草稿层与冲突检测的实现(见 ledger)、
工具执行的实现(见 toolbox)、终态命名(见 task)
公开出口: index.ts
- 类型:`ReleaseService` `PrecheckReport` `PrecheckFinding` `LandingVerdict` `LandOutcome`
`SubmitForReleaseInput` `SubmitOutcome` `DeliverableCheck` `CriterionCheck`
- 错误:`PrecheckFailedError` `DeliverablesMissingError`
- 端口类型:`IntentStore` `LedgerGateway` `ToolGateway` `CardGateway` `TaskContext`
`PortEmitter` `MemoryRecorder`
- 用例工厂:`createReleaseService(deps): ReleaseService`
- 纯函数:`landingVerdict` `missingDeliverables` `submittable` `summarize` `idempotencyKey`
- adapter:`createIntentStore`;Drizzle 表:`actionIntentTable`
层级: L4(编码规范 §4)
依赖: @contracts;@shared/db;port 注入:ledger(L1)、toolbox(L2)、human-tool(L3)、
event(L1,输出口发射)、memory(L2)
被依赖: loop-engine, flow-engine, task, app
不变量(OS 7.3 / AM 12.1):
- **预检先于放行**:`submitForRelease` 预检不过即抛 `PrecheckFailedError`,**不发卡**
- **卡面即已验证的动作清单**:验收工单的 actions 只列预检通过的意向
- **必交付物缺一不得提交放行**
- **放行通过即刻落地**,每个动作附幂等键(任务 × 意向)—— 重放不重复触达世界
- **落地收敛,输出口才发射** —— 下游只见落地完成的任务;受阻时 `emittedPorts` 恒为空
- **已成动作留存不回滚**;残余失败走落地失败卡(执行工单的预置形态),不静默成功
- **落地受阻是运行态不是终态**:处置后仍收敛于终态名录
- 放行写经 `ledger` 出口的 `releaseWrite`,不自行碰台账(§12.5#9)
@@ -0,0 +1,81 @@
/** 落地预检与落地收敛 —— domain 测试零 mock(OS 7.3) */
import { describe, expect, it } from 'vitest';
import {
idempotencyKey,
landingVerdict,
missingDeliverables,
submittable,
summarize,
} from '../internal/domain/precheck.ts';
import type { CriterionCheck, LandingOutcome } from '../internal/domain/precheck.ts';
describe('summarize —— 预检全过才可发卡', () => {
it('无 finding 即通过', () => {
const report = summarize([], [{ id: 'i1', label: '发报价邮件', effect: 'write', detail: '' }]);
expect(report.passed).toBe(true);
expect(report.verified).toHaveLength(1);
});
it('任一 finding 即不通过', () => {
const report = summarize([{ item: 'data-conflict', message: '字段已被他单改动' }], []);
expect(report.passed).toBe(false);
});
});
describe('必交付物缺一不得提交放行', () => {
it('列出缺失项', () => {
const missing = missingDeliverables([
{ key: 'a', label: '统一受理记录', present: true },
{ key: 'b', label: '料号分类表', present: false },
]);
expect(missing.map((m) => m.key)).toEqual(['b']);
});
});
describe('submittable —— 判据全绿只是可提交', () => {
const base: CriterionCheck[] = [
{ key: 'a', label: '八字段齐', level: 'L1', pass: true },
{ key: 'b', label: '偏离度合规', level: 'L2', pass: true },
];
it('L1/L2 全绿即可提交', () => {
expect(submittable(base)).toBe(true);
});
it('L2 未过即不可提交', () => {
expect(submittable([...base.slice(0, 1), { key: 'b', label: 'x', level: 'L2', pass: false }])).toBe(false);
});
it('L5 是责任关,不参与可提交判定 —— 认账永远是人的动作', () => {
expect(submittable([...base, { key: 'c', label: '人放行', level: 'L5', pass: false }])).toBe(true);
});
});
describe('landingVerdict —— 落地收敛', () => {
const ok: LandingOutcome = { id: 'i1', ok: true, retryable: false };
const bad: LandingOutcome = { id: 'i2', ok: false, error: '连接超时', retryable: true };
it('全成即收敛', () => {
expect(landingVerdict([ok])).toEqual({ kind: 'converged' });
});
it('零动作也算收敛', () => {
expect(landingVerdict([])).toEqual({ kind: 'converged' });
});
it('有失败即落地受阻,并带出失败清单', () => {
const verdict = landingVerdict([ok, bad]);
expect(verdict.kind).toBe('blocked');
if (verdict.kind === 'blocked') {
expect(verdict.failures.map((f) => f.id)).toEqual(['i2']);
}
});
});
describe('idempotencyKey —— 重放不重复触达世界', () => {
it('任务 × 意向', () => {
expect(idempotencyKey('t1', 'i1')).toBe('t1:i1');
expect(idempotencyKey('t1', 'i1')).toBe(idempotencyKey('t1', 'i1'));
expect(idempotencyKey('t1', 'i1')).not.toBe(idempotencyKey('t2', 'i1'));
});
});
+89
View File
@@ -0,0 +1,89 @@
/**
* module: release —— 对外类型(编码规范 §10.1)。
*
* 放行门:草稿态 → 事实态的**唯一通道**。门归系统,启门归人(OS 10)。
*/
import type { ActionIntent, StaffId, TaskId } from '@contracts';
import type {
CriterionCheck,
DeliverableCheck,
LandingOutcome,
LandingVerdict,
PrecheckFinding,
PrecheckItem,
PrecheckReport,
} from './internal/domain/precheck.ts';
export type {
ActionIntent,
CriterionCheck,
DeliverableCheck,
LandingOutcome,
LandingVerdict,
PrecheckFinding,
PrecheckItem,
PrecheckReport,
};
export class PrecheckFailedError extends Error {
constructor(readonly report: PrecheckReport) {
super(`落地预检未过,不发放行卡:${report.findings.map((f) => f.message).join('; ')}`);
this.name = 'PrecheckFailedError';
}
}
export class DeliverablesMissingError extends Error {
constructor(readonly missing: readonly DeliverableCheck[]) {
super(`必交付物缺一不得提交放行:${missing.map((d) => d.label).join('、')}`);
this.name = 'DeliverablesMissingError';
}
}
export type SubmitForReleaseInput = {
readonly taskId: TaskId;
readonly ownerId: StaffId;
readonly humanCallCap: number;
readonly deliverableId: string;
readonly title: string;
readonly basis: ReadonlyArray<{ k: string; v: string }>;
readonly deliverables: readonly DeliverableCheck[];
readonly criteria: readonly CriterionCheck[];
/** 报告可修订:AI 拟值已填的表单字段 */
readonly fields?: ReadonlyArray<Record<string, unknown>>;
};
export type SubmitOutcome = {
readonly report: PrecheckReport;
/** 预检全过才发卡;这里是发出的验收工单 ID */
readonly workOrderId: string;
};
export type LandInput = {
readonly taskId: TaskId;
readonly ownerId: StaffId;
readonly humanCallCap: number;
readonly selectedPorts: Readonly<Record<string, string>>;
};
export type LandOutcome = {
readonly verdict: LandingVerdict;
/** 落地收敛后才发射的输出口 */
readonly emittedPorts: readonly string[];
readonly ledgerRowsMerged: number;
/** 落地受阻时发出的落地失败卡 */
readonly failureWorkOrderId?: string;
};
export type ReleaseService = {
/** 登记写类动作意向(入参冻结,存入草稿态) */
registerIntent(taskId: TaskId, intent: Omit<ActionIntent, 'taskId' | 'state'>): Promise<ActionIntent>;
listIntents(taskId: TaskId): Promise<ActionIntent[]>;
/** 预检 → 全过才发验收工单;不过即抛 `PrecheckFailedError`,打回环内修正 */
submitForRelease(input: SubmitForReleaseInput): Promise<SubmitOutcome>;
/** 只跑预检不发卡 —— 环内随时可看「还差什么」 */
precheck(taskId: TaskId): Promise<PrecheckReport>;
/** 放行通过即刻落地:草稿合并 + 动作执行(附幂等键)→ 收敛才发射输出口 */
land(input: LandInput): Promise<LandOutcome>;
};
+47
View File
@@ -0,0 +1,47 @@
/**
* module: release —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
*/
export type {
ActionIntent,
CriterionCheck,
DeliverableCheck,
LandInput,
LandOutcome,
LandingOutcome,
LandingVerdict,
PrecheckFinding,
PrecheckItem,
PrecheckReport,
ReleaseService,
SubmitForReleaseInput,
SubmitOutcome,
} from './contract.ts';
export { DeliverablesMissingError, PrecheckFailedError } from './contract.ts';
export type {
CardGateway,
Clock,
IdGenerator,
IntentStore,
LedgerGateway,
MemoryRecorder,
PortEmitter,
TaskContext,
ToolGateway,
} from './port.ts';
export type { ReleaseServiceDeps } from './internal/usecase/release-service.ts';
export { createReleaseService } from './internal/usecase/release-service.ts';
export {
idempotencyKey,
landingVerdict,
missingDeliverables,
PRECHECK_ITEM_LABEL,
PRECHECK_ITEMS,
submittable,
summarize,
} from './internal/domain/precheck.ts';
export { createIntentStore } from './internal/adapter/intent-store.ts';
export { actionIntentTable } from './internal/adapter/release-schema.ts';
@@ -0,0 +1,60 @@
/**
* release 存储 adapter —— Drizzle 只准出现在 adapter(编码规范 §5.3)。
*/
import { asc, eq } from 'drizzle-orm';
import type { ActionIntent, TaskId } from '@contracts';
import { actionIntentSchema } from '@contracts';
import type { Db } from '@shared/db.ts';
import type { IntentStore } from '../../port.ts';
import { actionIntentTable } from './release-schema.ts';
type Row = typeof actionIntentTable.$inferSelect;
function toIntent(row: Row): ActionIntent {
return actionIntentSchema.parse({
id: row.id,
taskId: row.taskId,
tool: row.tool,
input: row.input,
idempotencyKey: row.idempotencyKey,
state: row.state,
label: row.label,
...(row.detail === null ? {} : { detail: row.detail }),
...(row.error === null ? {} : { error: row.error }),
});
}
export function createIntentStore(db: Db): IntentStore {
return {
async save(intent) {
await db
.insert(actionIntentTable)
.values({
id: intent.id,
taskId: intent.taskId,
tool: intent.tool,
input: intent.input,
idempotencyKey: intent.idempotencyKey,
state: intent.state,
label: intent.label,
detail: intent.detail ?? null,
error: intent.error ?? null,
})
.onConflictDoNothing();
},
async list(taskId: TaskId) {
const rows = await db
.select()
.from(actionIntentTable)
.where(eq(actionIntentTable.taskId, taskId))
.orderBy(asc(actionIntentTable.createdAt));
return rows.map(toIntent);
},
async setState(id, state, error) {
await db
.update(actionIntentTable)
.set({ state, error: error ?? null })
.where(eq(actionIntentTable.id, id));
},
};
}
@@ -0,0 +1,24 @@
/**
* release 的 sys schema 表(编码规范 §7 / ADR-002)。
* 动作意向持久于系统自持存储 —— 写即持久,断点可续(OS 09)。
*/
import { index, jsonb, text, timestamp } from 'drizzle-orm/pg-core';
import { sys } from '@shared/db.ts';
export const actionIntentTable = sys.table(
'action_intent',
{
id: text('id').primaryKey(),
taskId: text('task_id').notNull(),
tool: text('tool').notNull(),
/** 入参冻结 —— 登记时即定,放行时按幂等键执行 */
input: jsonb('input').notNull(),
idempotencyKey: text('idempotency_key').notNull(),
state: text('state').notNull().default('drafted'),
label: text('label').notNull(),
detail: text('detail'),
error: text('error'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index('action_intent_task_idx').on(t.taskId, t.state)],
);
@@ -0,0 +1,95 @@
/**
* 落地预检 —— 纯判定,零 IO(编码规范 §5.3;语义出处 OS 7.3,AM 06)。
*
* 「**检查可落地,才允许放行**:发放行卡前,系统对全部动作意向执行预检 ——
* ①入参 schema 校验 ②工具可用(未停用)与授权有效 ③连接可达
* ④目标预校验(工具契约含 validate 能力时)⑤数据冲突检查。
* 预检全过才发放行卡,卡面呈现的即已验证的动作清单;预检不过的意向打回环内修正,**不发卡**。」
*/
import type { AcceptanceLevel } from '@contracts';
export const PRECHECK_ITEMS = [
'input-schema',
'tool-available',
'connection-reachable',
'target-validate',
'data-conflict',
] as const;
export type PrecheckItem = (typeof PRECHECK_ITEMS)[number];
export const PRECHECK_ITEM_LABEL: Readonly<Record<PrecheckItem, string>> = {
'input-schema': '入参 schema 校验',
'tool-available': '工具可用与授权有效',
'connection-reachable': '连接可达',
'target-validate': '目标预校验',
'data-conflict': '数据冲突检查',
};
export type PrecheckFinding = {
readonly item: PrecheckItem;
/** 关联的动作意向;数据冲突项无意向,留空 */
readonly intentId?: string;
readonly message: string;
};
export type PrecheckReport = {
/** 全过才可发卡 */
readonly passed: boolean;
readonly findings: readonly PrecheckFinding[];
/** 已验证的动作清单 —— 通过时即验收工单卡面呈现的那份 */
readonly verified: ReadonlyArray<{ id: string; label: string; effect: 'read' | 'write'; detail: string }>;
};
export function summarize(
findings: readonly PrecheckFinding[],
verified: PrecheckReport['verified'],
): PrecheckReport {
return { passed: findings.length === 0, findings, verified };
}
/* ── 必交付物与判据 ─────────────────────────────────────────────────── */
export type DeliverableCheck = { readonly key: string; readonly label: string; readonly present: boolean };
export type CriterionCheck = { readonly key: string; readonly label: string; readonly level: AcceptanceLevel; readonly pass: boolean };
/** 必交付物**缺一不得提交放行**(AM 01) */
export function missingDeliverables(checks: readonly DeliverableCheck[]): DeliverableCheck[] {
return checks.filter((c) => !c.present);
}
/**
* 判据全绿只是**可提交**,完成只出自放行通过(AM 10.2)。
* L1/L2 由引擎预检;L3–L5 按分级执行 —— L5 是责任关,不参与「可提交」判定。
*/
export function submittable(checks: readonly CriterionCheck[]): boolean {
return checks.filter((c) => c.level !== 'L5').every((c) => c.pass);
}
/* ── 落地收敛 ───────────────────────────────────────────────────────── */
export type LandingOutcome = {
readonly id: string;
readonly ok: boolean;
readonly error?: string;
readonly retryable: boolean;
};
export type LandingVerdict =
/** 落地收敛 —— 输出口可发射,终态方成「放行完成」 */
| { readonly kind: 'converged' }
/** 落地受阻 —— **运行态**不是终态;走落地失败卡,处置后仍收敛于终态名录 */
| { readonly kind: 'blocked'; readonly failures: readonly LandingOutcome[] };
/**
* 「放行通过即刻执行(附幂等键);落地收敛,输出口才发射 —— 下游只见落地完成的任务。
* 已成动作留存**不回滚**。」
*/
export function landingVerdict(outcomes: readonly LandingOutcome[]): LandingVerdict {
const failures = outcomes.filter((o) => !o.ok);
return failures.length === 0 ? { kind: 'converged' } : { kind: 'blocked', failures };
}
/** 幂等键:任务 × 意向 —— 重放不重复触达世界(OS 7.3) */
export function idempotencyKey(taskId: string, intentId: string): string {
return `${taskId}:${intentId}`;
}
@@ -0,0 +1,220 @@
/**
* release 用例 —— 放行门(编码规范 §5.3;语义出处 OS 7.3 / 09,AM 06)。
*
* 「预检放行,落地收敛」:检查可落地才允许放行 → 放行通过即刻落地(附幂等键)
* → 落地收敛输出口才发射 → 终态方成「放行完成」。
*/
import type { ActionIntent, TaskId } from '@contracts';
import { emissionPortNames } from '@contracts';
import type {
LandInput,
LandOutcome,
ReleaseService,
SubmitForReleaseInput,
SubmitOutcome,
} from '../../contract.ts';
import { DeliverablesMissingError, PrecheckFailedError } from '../../contract.ts';
import type {
CardGateway,
Clock,
IdGenerator,
IntentStore,
LedgerGateway,
MemoryRecorder,
PortEmitter,
TaskContext,
ToolGateway,
} from '../../port.ts';
import type { LandingOutcome, PrecheckFinding } from '../domain/precheck.ts';
import { idempotencyKey, landingVerdict, missingDeliverables, summarize } from '../domain/precheck.ts';
export type ReleaseServiceDeps = {
readonly intents: IntentStore;
readonly ledger: LedgerGateway;
readonly tools: ToolGateway;
readonly cards: CardGateway;
readonly tasks: TaskContext;
readonly ports: PortEmitter;
readonly memory: MemoryRecorder;
readonly clock: Clock;
readonly ids: IdGenerator;
};
export function createReleaseService(deps: ReleaseServiceDeps): ReleaseService {
async function runPrecheck(taskId: TaskId) {
const activityId = await deps.tasks.activityIdOf(taskId);
const intents = (await deps.intents.list(taskId)).filter((i) => i.state === 'drafted' || i.state === 'prechecked');
const findings: PrecheckFinding[] = [];
const verified: Array<{ id: string; label: string; effect: 'read' | 'write'; detail: string }> = [];
for (const intent of intents) {
// ① 入参 schema 校验
const schema = await deps.tools.validateInput(intent.tool, intent.input);
if (!schema.ok) {
findings.push({ item: 'input-schema', intentId: intent.id, message: `${intent.label}:${schema.errors.join('; ')}` });
continue;
}
// ② 工具可用与授权有效 / ③ 连接可达
const invocable = await deps.tools.isInvocable(intent.tool, activityId);
if (!invocable.ok) {
findings.push({
item: 'tool-available',
intentId: intent.id,
message: `${intent.label}:${invocable.reason ?? '工具不可用'}`,
});
continue;
}
// ④ 目标预校验 —— 工具契约含 validate 能力时调之
const target = await deps.tools.validateTarget({
tool: intent.tool,
activityId,
taskId,
payload: intent.input,
});
if (target !== undefined && !target.ok) {
findings.push({
item: 'target-validate',
intentId: intent.id,
message: `${intent.label}:${target.error ?? '目标预校验未过'}`,
});
continue;
}
verified.push({ id: intent.id, label: intent.label, effect: 'write', detail: intent.detail ?? '' });
}
// ⑤ 数据冲突检查:他单已改同一字段即预检不过,打回环内基于新正式版重拟
const conflicts = await deps.ledger.checkConflicts(taskId);
for (const conflict of conflicts) {
findings.push({
item: 'data-conflict',
message: `字段 ${conflict.field} 已被他单改动,请基于新正式版重拟`,
});
}
return summarize(findings, verified);
}
return {
async registerIntent(taskId, draft) {
const intent: ActionIntent = { ...draft, taskId, state: 'drafted' };
await deps.intents.save(intent);
await deps.memory.append({
taskId,
kind: 'action-intent',
title: `登记动作意向:${intent.label}`,
detail: { tool: intent.tool, idempotencyKey: intent.idempotencyKey },
});
return intent;
},
async listIntents(taskId) {
return deps.intents.list(taskId);
},
async precheck(taskId) {
return runPrecheck(taskId);
},
/** 预检全过才发放行卡 —— 不过即抛,**不发卡**(OS 7.3) */
async submitForRelease(input: SubmitForReleaseInput): Promise<SubmitOutcome> {
const missing = missingDeliverables(input.deliverables);
if (missing.length > 0) throw new DeliverablesMissingError(missing);
const report = await runPrecheck(input.taskId);
if (!report.passed) throw new PrecheckFailedError(report);
for (const item of report.verified) await deps.intents.setState(item.id, 'prechecked');
const card = await deps.cards.issueAcceptance({
taskId: input.taskId,
ownerId: input.ownerId,
humanCallCap: input.humanCallCap,
title: input.title,
basis: input.basis,
deliverable: input.deliverableId,
criteria: input.criteria.map((c) => ({ label: c.label, level: c.level, pass: c.pass })),
// 卡面呈现的即**已验证**的动作清单
actions: report.verified.map((v) => ({ label: v.label, effect: v.effect, detail: v.detail })),
});
return { report, workOrderId: card.id };
},
/** 放行通过即刻落地 —— 草稿合并 + 动作执行;收敛才发射输出口 */
async land(input: LandInput): Promise<LandOutcome> {
const activityId = await deps.tasks.activityIdOf(input.taskId);
// 放行写:草稿层合并入正式版(原子,过冲突检查),此刻才产生数据事件
const merged = await deps.ledger.releaseWrite(input.taskId);
await deps.memory.append({
taskId: input.taskId,
kind: 'ledger-write',
title: `放行写合并 ${merged.mergedRows} 行`,
});
// 外部动作意向执行,附幂等键;已成动作留存不回滚
const intents = (await deps.intents.list(input.taskId)).filter((i) => i.state === 'prechecked');
const outcomes: LandingOutcome[] = [];
for (const intent of intents) {
const outcome = await deps.tools.execute({
tool: intent.tool,
activityId,
taskId: input.taskId,
payload: intent.input,
idempotencyKey: idempotencyKey(input.taskId, intent.id),
});
outcomes.push(outcome);
await deps.intents.setState(intent.id, outcome.ok ? 'landed' : 'failed', outcome.error);
}
const verdict = landingVerdict(outcomes);
if (verdict.kind === 'blocked') {
// 落地受阻是**运行态**不是终态:发落地失败卡,处置后仍收敛于终态名录
const card = await deps.cards.issueLandingFailure({
taskId: input.taskId,
ownerId: input.ownerId,
humanCallCap: input.humanCallCap,
failures: verdict.failures.map((f) => ({
label: intents.find((i) => i.id === f.id)?.label ?? f.id,
reason: f.error ?? '未知错误',
})),
});
await deps.memory.append({
taskId: input.taskId,
kind: 'release-verdict',
title: `落地受阻:${verdict.failures.length} 个动作失败`,
});
return {
verdict,
emittedPorts: [],
ledgerRowsMerged: merged.mergedRows,
failureWorkOrderId: card.id,
};
}
// 落地收敛,输出口才发射 —— 下游只见落地完成的任务
const ports = await deps.tasks.outputPortsOf(input.taskId);
const emitted = emissionPortNames(
{
mandatory: ports.mandatory.map((name) => ({ name, payload: {} })),
selective: ports.selective.map((g) => ({
group: g.group,
ports: g.ports.map((name) => ({ name, payload: {} })),
})),
},
input.selectedPorts,
);
for (const port of emitted) {
await deps.ports.emit({ taskId: input.taskId, port, payload: {} });
}
await deps.memory.append({
taskId: input.taskId,
kind: 'release-verdict',
title: `落地收敛,发射输出口:${emitted.join('、') || '(无)'}`,
});
return { verdict, emittedPorts: emitted, ledgerRowsMerged: merged.mergedRows };
},
};
}
@@ -0,0 +1,7 @@
{
"name": "@modules/release",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": { ".": "./index.ts" }
}
+83
View File
@@ -0,0 +1,83 @@
/**
* 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 }>;
};
/** 工具 —— 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 }>;
actions: ReadonlyArray<{ label: string; effect: 'read' | 'write'; detail: string }>;
}): 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>;
};