M2: 基座能力 —— memory / toolbox / channel / message

- memory   任务记忆四性质(只追加·信封盖戳·治理先行·归侧)、活动记忆四层与条目三件套、
           任务日志两级投影(结构化零 LLM / 叙述性小结不入事实库)、度量八指标只读投影
- toolbox  注册两审(静态审 + 试运行审)、效应等级决定运行时待遇、契约演进只增不破坏、
           白名单与纯工具私有作用域、ToolInvoker(两缝之二,live/record/replay)
- channel  通道实例配置、入站归一(解析 → 系统写 → 数据事件)、出站发送与关联记录登记
- message  通知 / 跨任务征答(就地作答 · 驳回重答只增不改)/ 死信 / 无主兜底

app:message 接上 event 的 MessageFallback;派发器按目标 ID 取 advisory lock 串行消费 +
消费幂等表;/api/ingest 模拟邮件通道(mock 只在外界)

已验证:无主 RFQ 邮件入站 → 归属解析 no-match → 事件落库 → 兜底落 OP 岗位的消息要素
门禁五道全绿;150 个单元测试 + 1 个 PG 集成测试

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:16:52 +08:00
co-authored by Claude Opus 5
parent 14d4fa046a
commit f0444f1417
73 changed files with 7488 additions and 31 deletions
+48
View File
@@ -0,0 +1,48 @@
/**
* module: channel —— 对外类型(编码规范 §10.1)。
*/
import type { InboundMail, OwnershipResolution, TaskId } from '@contracts';
import type { ChannelInstanceConfig, ExtractRule, LedgerMapping, NormalizedWrite } from './internal/domain/payload-mapping.ts';
export type { ChannelInstanceConfig, ExtractRule, InboundMail, LedgerMapping, NormalizedWrite, OwnershipResolution };
export declare class ChannelNotFoundError extends Error {
readonly channelId: string;
constructor(channelId: string);
}
export declare class ChannelDisabledError extends Error {
readonly channelId: string;
constructor(channelId: string);
}
export type InboundOutcome = {
readonly eventId: string;
readonly resolution: OwnershipResolution;
readonly writes: readonly NormalizedWrite[];
readonly listenersHit: number;
readonly triggersHit: number;
readonly fellBackToMessage: boolean;
};
export type OutboundMail = {
readonly taskId: TaskId;
readonly to: readonly string[];
readonly cc?: readonly string[];
readonly subject: string;
readonly body: string;
readonly threadId?: string;
readonly attachments?: ReadonlyArray<{
name: string;
ref: string;
}>;
};
export type ChannelService = {
list(): Promise<ChannelInstanceConfig[]>;
upsert(config: ChannelInstanceConfig): Promise<ChannelInstanceConfig>;
/**
* 入站:归属解析 → 归一为**系统写** → 数据事件。
* 「环外回音经归属解析归一为数据写入后,以数据事件抛出 —— 活动认数据,不认通道。」
*/
ingestMail(channelId: string, mail: InboundMail): Promise<InboundOutcome>;
/** 出站:触达世界的动作;发送即登记出站关联记录(归属解析①③级的对表来源) */
sendMail(channelId: string, mail: OutboundMail): Promise<{
messageId: string;
threadId: string;
}>;
};
+11
View File
@@ -0,0 +1,11 @@
/**
* module: channel —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
*/
export type { ChannelInstanceConfig, ChannelService, ExtractRule, InboundMail, InboundOutcome, LedgerMapping, NormalizedWrite, OutboundMail, } from './contract.ts';
export { ChannelDisabledError, ChannelNotFoundError } from './contract.ts';
export type { AttachmentArchive, ChannelRepository, Clock, EventGateway, IdGenerator, LedgerSystemWriter, MailTransport, } from './port.ts';
export type { ChannelServiceDeps } from './internal/usecase/channel-service.ts';
export { createChannelService } from './internal/usecase/channel-service.ts';
export { extractFields, normalize } from './internal/domain/payload-mapping.ts';
export { createChannelRepository, createFileAttachmentArchive, createSimulatedMailTransport, } from './internal/adapter/channel-store.ts';
export { channelTable, outboundMailTable } from './internal/adapter/channel-schema.ts';
+94
View File
@@ -0,0 +1,94 @@
/**
* module: channel —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
*/
import type { EnvelopeDraft, OwnershipResolution, PositionId, TaskId } from '@contracts';
import type { ChannelInstanceConfig } from './internal/domain/payload-mapping.ts';
export type Clock = {
now(): Date;
};
export type IdGenerator = {
next(): string;
};
export type ChannelRepository = {
list(): Promise<ChannelInstanceConfig[]>;
get(channelId: string): Promise<ChannelInstanceConfig | undefined>;
upsert(config: ChannelInstanceConfig): Promise<ChannelInstanceConfig>;
};
/** 事件入口 —— app 注入 `event`(L1)的出口 */
export type EventGateway = {
ingestInbound(input: {
facts: {
threadId?: string;
from: string;
to: readonly string[];
subject: string;
body: string;
receivedAt: Date;
};
rules: {
patterns: ReadonlyArray<{
key: string;
regex: string;
source: 'subject' | 'body' | 'both';
}>;
addressWindowMs: number;
fallbackPosition?: PositionId | undefined;
};
draft: EnvelopeDraft;
}): Promise<{
eventId: string;
resolution: OwnershipResolution;
plan: {
taskTargets: readonly string[];
triggerTargets: readonly string[];
messageFallback: boolean;
};
}>;
recordOutbound(ref: {
taskId: TaskId;
threadId?: string;
address: string;
sentAt: Date;
}): Promise<void>;
};
/**
* 台账系统写 —— app 注入 `ledger`(L1)的 `systemWrite`。
* 入站归一是「登记世界已发生的事实」,走**系统写**:不过放行门,写正式版并即时抛数据事件(OS 09)。
*/
export type LedgerSystemWriter = {
systemWrite(input: {
address: {
domain: string;
object: string;
};
key: Record<string, string | number>;
values: Record<string, unknown>;
cause: {
kind: 'inbound';
ref: string;
};
taskId?: TaskId;
}): Promise<void>;
};
/** 邮件收发 —— 外界。MVP 允许模拟通道 / 录制回放替身(README MVP 完成判据) */
export type MailTransport = {
send(input: {
readonly from: string;
readonly to: readonly string[];
readonly cc: readonly string[];
readonly subject: string;
readonly body: string;
readonly threadId?: string;
readonly attachments: ReadonlyArray<{
name: string;
ref: string;
}>;
}): Promise<{
messageId: string;
threadId: string;
}>;
};
/** 原件存档 —— 附件对象的存放;返回存档地址(信封的原件指针) */
export type AttachmentArchive = {
put(name: string, content: string): Promise<string>;
};
+51
View File
@@ -0,0 +1,51 @@
/**
* module: memory —— 对外类型(编码规范 §10.1)。
*/
import type { AttributionSide, TaskId } from '@contracts';
import type { ActivityMemoryEntry, CandidateIncrement, Evidence, MemoryLayer } from './internal/domain/activity-memory.ts';
import type { EntryKind, GovernanceMark, MemoryEntry, VersionStamp } from './internal/domain/task-memory.ts';
import type { LogLine, Metrics, MetricsInput } from './internal/domain/task-log.ts';
export type { ActivityMemoryEntry, AttributionSide, CandidateIncrement, EntryKind, Evidence, GovernanceMark, LogLine, MemoryEntry, MemoryLayer, Metrics, MetricsInput, VersionStamp, };
export { NoEvidenceError } from './internal/domain/activity-memory.ts';
/** 追加一条任务记忆 —— 归侧由 kind 自动推导,调用方不得指定(零埋点) */
export type AppendEntry = {
readonly taskId: TaskId;
readonly kind: EntryKind;
readonly title: string;
readonly detail?: Record<string, unknown>;
readonly stamp: VersionStamp;
readonly governance?: Partial<GovernanceMark>;
readonly linksTo?: string;
readonly eventRef?: string;
};
export type DistillInput = {
readonly taskId: TaskId;
readonly activityId: string;
readonly trigger: 'terminal' | 'milestone';
readonly milestone?: string;
readonly prompt: string;
};
export type MemoryService = {
append(entry: AppendEntry): Promise<MemoryEntry>;
history(taskId: TaskId): Promise<MemoryEntry[]>;
/** 活动侧唯一可配置项:窗口大小 —— 每次 LLM 调用加载多少轮 */
window(taskId: TaskId, size: number): Promise<MemoryEntry[]>;
seal(taskId: TaskId): Promise<void>;
taskLog(taskId: TaskId, options?: {
canSeeMasked?: boolean;
}): Promise<LogLine[]>;
/** 叙述性小结(LLM,可选)—— 叙述是视图不是事实,不入事实库 */
narrate(taskId: TaskId): Promise<string | undefined>;
loadActivityMemory(activityId: string, options?: {
layers?: readonly MemoryLayer[];
limit?: number;
}): Promise<ActivityMemoryEntry[]>;
/** **不设直写旁路**:唯一写入口是蒸馏钩子 */
distill(input: DistillInput): Promise<ActivityMemoryEntry[]>;
/** 人可修剪 —— 治理台 · 记忆管理 */
prune(entryId: string): Promise<void>;
setIncrementState(entryId: string, state: CandidateIncrement['state']): Promise<void>;
metrics(input: Omit<MetricsInput, 'entries'> & {
taskIds: readonly TaskId[];
}): Promise<Metrics>;
};
+13
View File
@@ -0,0 +1,13 @@
/**
* module: memory —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
*/
export type { ActivityMemoryEntry, AppendEntry, AttributionSide, CandidateIncrement, DistillInput, EntryKind, Evidence, GovernanceMark, LogLine, MemoryEntry, MemoryLayer, MemoryService, Metrics, MetricsInput, VersionStamp, } from './contract.ts';
export { NoEvidenceError } from './contract.ts';
export type { ActivityMemoryStore, Clock, IdGenerator, SystemModel, TaskMemoryStore } from './port.ts';
export type { MemoryServiceDeps } from './internal/usecase/memory-service.ts';
export { createMemoryService } from './internal/usecase/memory-service.ts';
export { assertThreePart, isBinding, MEMORY_LAYER_LABEL, MEMORY_LAYER_TARGET, MEMORY_LAYERS, selectForLoading, } from './internal/domain/activity-memory.ts';
export { assertAppendOnly, deriveSide, distillable, ENTRY_KINDS, redactForReader, windowOf, } from './internal/domain/task-memory.ts';
export { computeMetrics, splitBySide, structuredLog } from './internal/domain/task-log.ts';
export { createActivityMemoryStore, createTaskMemoryStore } from './internal/adapter/memory-store.ts';
export { activityMemoryTable, taskMemorySealTable, taskMemoryTable } from './internal/adapter/memory-schema.ts';
+42
View File
@@ -0,0 +1,42 @@
/**
* module: memory —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
*/
import type { TaskId } from '@contracts';
import type { ActivityMemoryEntry, MemoryLayer } from './internal/domain/activity-memory.ts';
import type { MemoryEntry } from './internal/domain/task-memory.ts';
export type Clock = {
now(): Date;
};
export type IdGenerator = {
next(): string;
};
export type TaskMemoryStore = {
nextSeq(taskId: TaskId): Promise<number>;
append(entry: MemoryEntry): Promise<void>;
list(taskId: TaskId): Promise<MemoryEntry[]>;
tail(taskId: TaskId, size: number): Promise<MemoryEntry[]>;
seal(taskId: TaskId, at: Date): Promise<void>;
isSealed(taskId: TaskId): Promise<boolean>;
};
export type ActivityMemoryStore = {
list(activityId: string, layers: readonly MemoryLayer[] | undefined, limit: number): Promise<ActivityMemoryEntry[]>;
insertMany(entries: readonly ActivityMemoryEntry[]): Promise<void>;
prune(entryId: string, at: Date): Promise<void>;
setIncrementState(entryId: string, state: string): Promise<void>;
};
/**
* 模型调用 —— app 注入 `llm`(L1)的 ModelGateway。
* 蒸馏钩子与叙述性小结用**系统用途**绑定实例:计量入系统账、不占活动预算(OS 02)。
*/
export type SystemModel = {
generate(input: {
readonly model: string;
readonly prompt: string;
readonly outputSchema?: Record<string, unknown>;
}): Promise<{
text: string;
object?: unknown;
}>;
/** 系统用途模型名;未配置即 undefined —— 此时叙述与蒸馏静默跳过,不报错 */
systemModelName(): Promise<string | undefined>;
};
+41
View File
@@ -0,0 +1,41 @@
/**
* module: message —— 对外类型(编码规范 §10.1)。
*/
import type { Envelope, StaffId } from '@contracts';
import type { MessageKind, SystemMessage } from './internal/domain/message-kind.ts';
export type { Envelope, MessageKind, SystemMessage };
export declare class MessageNotAnswerableError extends Error {
readonly messageId: string;
constructor(messageId: string);
}
export type SendNotice = {
readonly staffId: StaffId;
readonly title: string;
readonly body: string;
readonly eventRef?: string;
};
export type SendElicitation = {
readonly staffId: StaffId;
readonly title: string;
readonly body: string;
readonly cardId: string;
readonly sourceTaskId: string;
readonly payload: Record<string, unknown>;
};
export type MessageService = {
inbox(staffId: StaffId, options?: {
unreadOnly?: boolean;
limit?: number;
}): Promise<SystemMessage[]>;
markRead(messageId: string): Promise<void>;
notify(input: SendNotice): Promise<SystemMessage>;
/** 跨人卡以系统消息送达对方,就地作答 —— 任务要素永远只装「我负责的任务」(OS 2.1) */
sendElicitation(input: SendElicitation): Promise<SystemMessage>;
answer(messageId: string, staffId: StaffId, answer: Record<string, unknown>): Promise<SystemMessage>;
/** 驳回重答 = 新发一条系统消息(新卡实例),原卡闭合 —— 只增不改 */
rejectAndReask(messageId: string, reason: string): Promise<SystemMessage>;
/** 死信:重试超限 → 系统通知送达相关人的消息要素与治理台 */
deadLetter(envelope: Envelope, error: string): Promise<void>;
/** 无主兜底:相关人 = 通道实例配置的默认认领岗位 */
unowned(envelope: Envelope, fallbackPosition: string | undefined): Promise<void>;
};
+11
View File
@@ -0,0 +1,11 @@
/**
* module: message —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
*/
export type { MessageKind, MessageService, SendElicitation, SendNotice, SystemMessage } from './contract.ts';
export { MessageNotAnswerableError } from './contract.ts';
export type { Clock, IdGenerator, MessageRepository, PositionResolver } from './port.ts';
export type { MessageServiceDeps } from './internal/usecase/message-service.ts';
export { createMessageService } from './internal/usecase/message-service.ts';
export { canAnswer, MESSAGE_KIND_LABEL, MESSAGE_KINDS, pendingAnswerCount, unreadCount, } from './internal/domain/message-kind.ts';
export { createMessageRepository } from './internal/adapter/message-store.ts';
export { messageTable } from './internal/adapter/message-schema.ts';
+29
View File
@@ -0,0 +1,29 @@
/**
* module: message —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
*/
import type { StaffId } from '@contracts';
import type { SystemMessage } from './internal/domain/message-kind.ts';
export type Clock = {
now(): Date;
};
export type IdGenerator = {
next(): string;
};
export type MessageRepository = {
list(staffId: StaffId, unreadOnly: boolean, limit: number): Promise<SystemMessage[]>;
get(messageId: string): Promise<SystemMessage | undefined>;
insert(message: SystemMessage): Promise<SystemMessage>;
markRead(messageId: string, at: Date): Promise<void>;
markAnswered(messageId: string, at: Date, answer: Record<string, unknown>): Promise<void>;
supersede(messageId: string, byMessageId: string, reason: string): Promise<void>;
};
/** 岗位解析 —— app 注入 `org`(L1):无主兜底要把消息落到默认认领岗位上的人 */
export type PositionResolver = {
staffOfPosition(positionId: string): Promise<Array<{
id: StaffId;
}>>;
/** 治理台收件人(任务管理岗)—— 死信同时送治理台 */
governanceStaff(): Promise<Array<{
id: StaffId;
}>>;
};
+109
View File
@@ -0,0 +1,109 @@
/**
* module: toolbox —— 对外类型(编码规范 §10.1)。
*
* 两条缝之二(§7.3):`ToolInvoker` 支持 record | replay | live ——
* **任何工具执行不经 ToolInvoker,回放审就对它失明**(§12.5#10)。
*/
import type { ActionIntent, TaskId } from '@contracts';
import type { AuditFinding } from './internal/domain/registration-audit.ts';
import type { CallDisposition, EffectLevel, EvolutionViolation, Preauthorization, ToolContract } from './internal/domain/tool-contract.ts';
export type { ActionIntent, AuditFinding, CallDisposition, EffectLevel, EvolutionViolation, Preauthorization, ToolContract };
export declare const INVOKER_MODES: readonly ["live", "record", "replay"];
export type InvokerMode = (typeof INVOKER_MODES)[number];
export declare class ToolNotFoundError extends Error {
readonly tool: string;
constructor(tool: string);
}
export declare class ToolNotAllowedError extends Error {
readonly tool: string;
readonly activityId: string;
constructor(tool: string, activityId: string);
}
export declare class ToolDisabledError extends Error {
readonly tool: string;
constructor(tool: string);
}
export declare class ToolReplayMissError extends Error {
readonly key: string;
constructor(key: string);
}
export declare class RegistrationRejectedError extends Error {
readonly findings: readonly AuditFinding[];
constructor(findings: readonly AuditFinding[]);
}
export type ToolCall = {
readonly tool: string;
readonly operation?: string;
readonly input: Record<string, unknown>;
readonly activityId: string;
readonly taskId?: TaskId;
readonly stepNo?: number;
};
export type ToolResult = {
readonly ok: boolean;
readonly output?: Record<string, unknown>;
readonly error?: {
readonly code: string;
readonly retryable: boolean;
readonly message: string;
};
readonly latencyMs: number;
readonly mode: InvokerMode;
};
/** 调用留痕:入参快照、出参、时延与错误自动留痕入任务记忆(OS 7.3) */
export type InvocationTrace = {
readonly tool: string;
readonly input: Record<string, unknown>;
readonly result: ToolResult;
readonly at: string;
};
/**
* 工具调用器 —— 活动基座六件里的「工具(调用契约)」。
* **读类**过程中自由调用;**写类**登记为动作意向,放行后落地(除契约显式预授权者)。
*/
export type ToolInvoker = {
readonly mode: InvokerMode;
/** 判定运行时待遇 —— 调用方据此决定是即调还是登记意向 */
disposition(call: ToolCall, preauthorized: readonly Preauthorization[]): Promise<CallDisposition>;
invoke(call: ToolCall): Promise<ToolResult>;
/** 落地预检第四项:工具契约含 validate 能力时调之(ADR-005) */
validate(call: ToolCall): Promise<ToolResult | undefined>;
withMode(mode: InvokerMode): ToolInvoker;
};
export type RegisterToolInput = {
readonly contract: ToolContract;
/** 契约测试集:样例入参 → 期望出参形状;试运行审的依据 */
readonly samples: ReadonlyArray<{
input: Record<string, unknown>;
expectShape: Record<string, unknown>;
}>;
/** 连接配置的值 —— 只存治理台,加密落库(ADR-009) */
readonly connectionValues?: Readonly<Record<string, string>>;
};
export type RegistrationVerdict = {
readonly accepted: boolean;
readonly staticFindings: readonly AuditFinding[];
readonly evolution: readonly EvolutionViolation[];
readonly trialRun: {
readonly ran: number;
readonly passed: number;
readonly failures: readonly string[];
};
};
export type ToolboxService = {
/** 注册两审:静态审读清单,试运行审验代码 */
register(input: RegisterToolInput): Promise<RegistrationVerdict>;
list(): Promise<ToolContract[]>;
get(name: string): Promise<ToolContract | undefined>;
/** 有活动声明引用的工具不得删除;可停用 —— 停用后调用返回结构化错误 */
setEnabled(name: string, enabled: boolean): Promise<void>;
bindConnection(name: string, values: Readonly<Record<string, string>>): Promise<void>;
/** 白名单:活动只经调用契约用工具,授权在白名单与契约,不在活动代码 */
setAllowlist(activityId: string, tools: readonly string[]): Promise<void>;
allowlistOf(activityId: string): Promise<string[]>;
/** 安装静态审:活动声明的工具契约齐备?装不齐不上架 */
assertContractsAvailable(names: readonly string[]): Promise<{
ok: boolean;
missing: string[];
}>;
};
+15
View File
@@ -0,0 +1,15 @@
/**
* module: toolbox —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
*/
export type { ActionIntent, AuditFinding, CallDisposition, EffectLevel, EvolutionViolation, InvocationTrace, InvokerMode, Preauthorization, RegisterToolInput, RegistrationVerdict, ToolCall, ToolContract, ToolInvoker, ToolResult, ToolboxService, } from './contract.ts';
export { INVOKER_MODES, RegistrationRejectedError, ToolDisabledError, ToolNotAllowedError, ToolNotFoundError, ToolReplayMissError, } from './contract.ts';
export type { Clock, IdGenerator, SchemaValidator, SecretBox, ToolRegistry, ToolReplayStore, ToolRuntime, } from './port.ts';
export type { ToolInvokerDeps } from './internal/usecase/tool-invoker.ts';
export { createToolInvoker, toolReplayKey } from './internal/usecase/tool-invoker.ts';
export type { ToolboxServiceDeps } from './internal/usecase/toolbox-service.ts';
export { createToolboxService } from './internal/usecase/toolbox-service.ts';
export { checkEvolution, dispositionOf, EFFECT_LEVEL_LABEL, EFFECT_LEVELS } from './internal/domain/tool-contract.ts';
export { auditPassed, staticAudit } from './internal/domain/registration-audit.ts';
export type { ToolHandler } from './internal/adapter/toolbox-store.ts';
export { createSchemaValidator, createToolRegistry, createToolReplayStore, createToolRuntime, isAllowed, } from './internal/adapter/toolbox-store.ts';
export { replayToolTable, toolAllowlistTable, toolTable } from './internal/adapter/toolbox-schema.ts';
+68
View File
@@ -0,0 +1,68 @@
/**
* module: toolbox —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
*/
import type { ToolContract } from './internal/domain/tool-contract.ts';
import type { ToolCall, ToolResult } from './contract.ts';
export type Clock = {
now(): Date;
};
export type IdGenerator = {
next(): string;
};
/** 凭证密封 —— 与 llm 同一实现,由 app 注入(ADR-009) */
export type SecretBox = {
seal(plaintext: string): string;
open(cipher: string): string;
};
export type ToolRegistry = {
list(): Promise<ToolContract[]>;
get(name: string): Promise<ToolContract | undefined>;
isEnabled(name: string): Promise<boolean>;
upsert(contract: ToolContract, connectionCipher: Record<string, string> | null): Promise<void>;
setEnabled(name: string, enabled: boolean): Promise<void>;
connectionCipher(name: string): Promise<Record<string, string> | undefined>;
setAllowlist(activityId: string, tools: readonly string[]): Promise<void>;
allowlistOf(activityId: string): Promise<string[]>;
/** 有活动声明引用即不得删除 */
referencedBy(name: string): Promise<string[]>;
};
/** 工具录制回放存储 —— 与 llm 的 ReplayStore 同构,另存一表(§7.3) */
export type ToolReplayStore = {
put(record: {
key: string;
call: ToolCall;
result: ToolResult;
recordedAt: Date;
}): Promise<void>;
get(key: string): Promise<{
call: ToolCall;
result: ToolResult;
} | undefined>;
};
/**
* 工具执行 —— 实现对系统:server 侧执行代码,运行时受限,
* **只可访问注册申报的连接目标,无其他环境能力**(OS 7.3)。
*/
export type ToolRuntime = {
execute(input: {
readonly contract: ToolContract;
readonly connection: Readonly<Record<string, string>>;
readonly call: ToolCall;
readonly idempotencyKey?: string;
}): Promise<{
ok: boolean;
output?: Record<string, unknown>;
error?: {
code: string;
retryable: boolean;
message: string;
};
}>;
};
/** JSON Schema 校验 —— ajv 在 adapter,usecase 只见本端口(ADR-003) */
export type SchemaValidator = {
validate(schema: Record<string, unknown>, value: unknown): {
ok: boolean;
errors: string[];
};
};