M1: 底座 —— @contracts 首版 + event / ledger / org / llm

@contracts 首版(§8.1 九个文件):
- id 品牌 ID / terminal 终态五名与归侧四标签 / envelope 信封六字段与四类事件
- card 工单两模式 + 对话卡族(形状对齐 2026-07-26 原型定稿)
- contract7 契约七件 / ledger 三级寻址与写三分 / process-package 流程包 DSL 与触发器三类
- manifest 活动包清单六区(含 CMMN·DMN·步序声明的资产形状)/ org / api 路由契约

模块(L1):
- event   信封存储、归属解析三级短路、投递五诺、监听注册表、三级消费裁定
- ledger  schema 自生长(最大化合并/最宽类型/只增不破坏)、任务视图叠加、写三分、冲突检查
- org     岗位授权(客体三类×动作三种)、停用前置检查、会话、种子播种
- llm     四元绑定、ModelGateway 三模式(live/record/replay)—— 两缝之一

app 装配:Hono 服务 + 鉴权中间件 + 治理台/工作台路由;drizzle 迁移;AES-256-GCM 凭证密封

ADR-007 工具链与门禁 · ADR-008 投递队列 · ADR-009 凭证加密
(ADR 编号自 007 续:编码规范 §14 已用到 006,README 写「从 005 续」是笔误)

门禁五道全绿;96 个测试通过,含对真实 PG 的 ledger 集成测试

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 00:59:14 +08:00
co-authored by Claude Opus 5
parent 7cd664af3a
commit 14d4fa046a
120 changed files with 15236 additions and 78 deletions
@@ -0,0 +1,218 @@
/**
* event 用例 —— 编排 domain,依赖 port.ts(编码规范 §5.3)。
*/
import type { Delivery, Envelope, EnvelopeDraft, EventId, ListenerSpec, RegisteredListener, TaskId } from '@contracts';
import { deliveryIdSchema, eventIdSchema, listenerIdSchema, taskIdSchema, triggerIdSchema } from '@contracts';
import type { EventService, InboundResult, PublishResult } from '../../contract.ts';
import { ListenerDeadlineError } from '../../contract.ts';
import type {
Clock,
ConditionEvaluator,
EventStore,
IdGenerator,
MessageFallback,
TriggerMatcher,
} from '../../port.ts';
import { decideRetry, planConsumption } from '../domain/delivery.ts';
import { deadlineMillis, matchingListeners } from '../domain/listener-match.ts';
import { extractKeys, resolveOwnership } from '../domain/ownership.ts';
export type EventServiceDeps = {
readonly store: EventStore;
readonly clock: Clock;
readonly ids: IdGenerator;
readonly evaluate: ConditionEvaluator;
readonly triggers: TriggerMatcher;
readonly messages: MessageFallback;
/** 地址+时间窗的默认窗口(毫秒);通道实例配置可覆盖 */
readonly addressWindowMs?: number;
};
export function createEventService(deps: EventServiceDeps): EventService {
function seal(draft: EnvelopeDraft, id: EventId): Envelope {
return { ...draft, id, trace: { storeRef: `sys.event/${id}` } };
}
/**
* 三级消费的裁定与投递。消息兜底在此**唯一**发生 —— 调用方传入无主兜底岗位,
* 避免 publish / ingestInbound 两处各发一次(OS 6.3:消息只兜无主)。
*/
async function dispatch(envelope: Envelope, fallbackPosition?: string): Promise<PublishResult> {
const [armed, triggerHits] = await Promise.all([
deps.store.armedListeners(),
deps.triggers.match(envelope),
]);
const hits = matchingListeners(armed, envelope, deps.evaluate).map((l) => ({ taskId: String(l.taskId) }));
const ownedTaskId =
envelope.correlation.target?.kind === 'task' ? String(envelope.correlation.target.id) : undefined;
const plan = planConsumption({ listenerHits: hits, triggerHits, ownedTaskId });
// 扇出:每个命中目标一份独立投递,信封「关联 = 单目标」保持单目标
const deliveries: Delivery[] = [
...plan.taskTargets.map(
(taskId): Delivery => ({
id: deliveryIdSchema.parse(deps.ids.next()),
eventId: envelope.id,
target: { kind: 'task', id: taskIdSchema.parse(taskId) },
attempt: 1,
state: 'pending',
}),
),
...plan.triggerTargets.map(
(triggerId): Delivery => ({
id: deliveryIdSchema.parse(deps.ids.next()),
eventId: envelope.id,
target: { kind: 'trigger', id: triggerIdSchema.parse(triggerId) },
attempt: 1,
state: 'pending',
}),
),
];
// 先持久后投递 —— 落库与入队同一事务
await deps.store.persistAndEnqueue(envelope, deliveries);
// 消息兜底:仅当既无监听命中亦无触发命中、且事件无主
if (plan.messageFallback) {
await deps.messages.unowned(envelope, fallbackPosition);
}
return { eventId: envelope.id, plan };
}
return {
async publish(draft) {
const id = eventIdSchema.parse(draft.id ?? deps.ids.next());
return dispatch(seal(draft, id));
},
async ingestInbound({ facts, rules, draft }): Promise<InboundResult> {
const keys = extractKeys(facts, rules);
const windowMs = rules.addressWindowMs || deps.addressWindowMs || 7 * 86_400_000;
const [outbound, armed] = await Promise.all([
deps.store.recentOutbound(windowMs, facts.receivedAt),
deps.store.armedListeners(),
]);
const resolution = resolveOwnership({
facts,
rules: { ...rules, addressWindowMs: windowMs },
keys,
outbound,
listeners: armed.map((l) => ({ taskId: l.taskId, keyFilter: l.keyFilter })),
});
const id = eventIdSchema.parse(draft.id ?? deps.ids.next());
const envelope = seal(
{
...draft,
correlation: {
...draft.correlation,
...(resolution.outcome === 'owned'
? { target: { kind: 'task' as const, id: resolution.taskId } }
: {}),
keys: { ...keys, ...draft.correlation.keys },
},
},
id,
);
// 无主入站的相关人 = 通道实例配置的默认认领岗位(OS 6.2)
const result = await dispatch(
envelope,
resolution.outcome === 'unowned' ? resolution.fallbackPosition : undefined,
);
return { ...result, resolution };
},
async getEvent(id) {
return deps.store.get(id);
},
async listByTask(taskId, limit = 200) {
return deps.store.listByTask(taskId, limit);
},
async registerListener(taskId: TaskId, spec: ListenerSpec): Promise<RegisteredListener> {
let ms: number;
try {
ms = deadlineMillis(spec.deadline);
} catch (error) {
throw new ListenerDeadlineError(error instanceof Error ? error.message : String(error));
}
const now = deps.clock.now();
const listener: RegisteredListener = {
...spec,
id: listenerIdSchema.parse(deps.ids.next()),
taskId,
registeredAt: now.toISOString(),
expiresAt: new Date(now.getTime() + ms).toISOString(),
state: 'armed',
};
await deps.store.saveListener(listener);
return listener;
},
async cancelListener(id) {
await deps.store.setListenerState(id, 'cancelled');
},
async listListeners(taskId) {
return deps.store.listenersOf(taskId);
},
async sweepExpiredListeners() {
const now = deps.clock.now();
const expired = await deps.store.expiredListeners(now);
for (const listener of expired) {
await deps.store.setListenerState(listener.id, 'expired');
// 到点抛时间事件 —— 无死等(OS 6.3)
await this.publish({
type: 'time',
correlation: {
target: { kind: 'task', id: listener.taskId },
cause: { kind: 'timer', ref: listener.id },
keys: listener.keyFilter,
},
payload: {
data: {
listenerId: listener.id,
timerRef: listener.id,
escalation: listener.escalation[0] ?? 'nudge',
},
},
occurredAt: now.toISOString(),
source: { kind: 'engine', detail: 'listener-sweep' },
});
}
return expired;
},
async recordOutbound(ref) {
await deps.store.saveOutbound(ref);
},
async claimConsumption(eventId, consumer) {
return deps.store.claimConsumption(eventId, consumer);
},
async reportFailure(deliveryId, error) {
const delivery = await deps.store.getDelivery(deliveryId);
if (delivery === undefined) throw new Error(`投递不存在:${deliveryId}`);
const decision = decideRetry(delivery.attempt);
if (decision.kind === 'retry') {
const next: Delivery = { ...delivery, attempt: decision.attempt, state: 'pending', lastError: error };
await deps.store.updateDelivery(next);
await deps.store.scheduleRetry(deliveryId, decision.delayMs);
return next;
}
const dead: Delivery = { ...delivery, state: 'dead-letter', lastError: error };
await deps.store.updateDelivery(dead);
const envelope = await deps.store.get(delivery.eventId);
// 死信以系统通知送达相关人的消息要素与治理台(OS 6.3)
if (envelope !== undefined) await deps.messages.deadLetter(envelope, error);
return dead;
},
};
}