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/apps/api/src/modules/event/internal/domain/ownership.ts
T
JCAI Main AgentandClaude Opus 5 14d4fa046a 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
2026-07-27 00:59:14 +08:00

114 lines
4.3 KiB
TypeScript

/**
* 归属解析 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 OS 6.2)。
*
* 通道入站到统一事件流之间由归属解析完成路由。匹配次序确定、**逐级短路**:
* ① 线程精确匹配 —— 会话/线程 ID 精确匹配某在途任务的出站记录
* ② 关联键匹配监听 —— 提取的关联键命中某任务监听的过滤条件
* ③ 地址 + 时间窗 —— 往来地址在时间窗内匹配唯一在途任务;命中多个即无主
* 提取失败或歧义**一律降级为无主** —— 宁交人,不错投。
*/
import type { OwnershipResolution, PositionId, TaskId } from '@contracts';
/** 通道实例配置的归属键提取规则(声明式) */
export type OwnershipKeyRules = {
/** 从正文/主题提取关联键的正则(带一个捕获组),如单号 */
readonly patterns: ReadonlyArray<{ key: string; regex: string; source: 'subject' | 'body' | 'both' }>;
/** 地址 + 时间窗的窗口长度(毫秒) */
readonly addressWindowMs: number;
/** 无主入站的默认认领岗位 */
readonly fallbackPosition?: PositionId;
};
export type InboundFacts = {
readonly threadId?: string;
readonly from: string;
readonly to: readonly string[];
readonly subject: string;
readonly body: string;
readonly receivedAt: Date;
};
/** 出站关联记录:任务发过什么,线程 ID 与往来地址由此登记 */
export type OutboundRef = {
readonly taskId: TaskId;
readonly threadId?: string;
readonly address: string;
readonly sentAt: Date;
};
/** 在途任务的监听过滤面 */
export type ListenerKeyIndex = {
readonly taskId: TaskId;
readonly keyFilter: Readonly<Record<string, string>>;
};
/** 从入站事实里按规则提取关联键 */
export function extractKeys(facts: InboundFacts, rules: OwnershipKeyRules): Record<string, string> {
const keys: Record<string, string> = {};
if (facts.threadId !== undefined) keys['threadId'] = facts.threadId;
for (const rule of rules.patterns) {
const haystack =
rule.source === 'subject' ? facts.subject : rule.source === 'body' ? facts.body : `${facts.subject}\n${facts.body}`;
const match = new RegExp(rule.regex).exec(haystack);
const captured = match?.[1] ?? match?.[0];
if (captured !== undefined) keys[rule.key] = captured;
}
return keys;
}
export type ResolveInput = {
readonly facts: InboundFacts;
readonly rules: OwnershipKeyRules;
readonly keys: Readonly<Record<string, string>>;
readonly outbound: readonly OutboundRef[];
readonly listeners: readonly ListenerKeyIndex[];
};
export function resolveOwnership(input: ResolveInput): OwnershipResolution {
const { facts, rules, keys, outbound, listeners } = input;
// ① 线程精确匹配
if (facts.threadId !== undefined) {
const hits = outbound.filter((o) => o.threadId === facts.threadId);
const first = hits[0];
if (first !== undefined) return { outcome: 'owned', rule: 'thread', taskId: first.taskId };
}
// ② 关联键命中监听的过滤条件
const keyHits = listeners.filter((l) => {
const entries = Object.entries(l.keyFilter);
if (entries.length === 0) return false;
return entries.every(([k, v]) => keys[k] === v);
});
const uniqueKeyTasks = [...new Set(keyHits.map((h) => h.taskId))];
if (uniqueKeyTasks.length === 1) {
return { outcome: 'owned', rule: 'listener-key', taskId: uniqueKeyTasks[0]! };
}
if (uniqueKeyTasks.length > 1) return unowned('ambiguous', rules);
// ③ 地址 + 时间窗
const windowStart = facts.receivedAt.getTime() - rules.addressWindowMs;
const addresses = new Set([facts.from, ...facts.to].map((a) => a.toLowerCase()));
const windowHits = outbound.filter(
(o) => o.sentAt.getTime() >= windowStart && addresses.has(o.address.toLowerCase()),
);
const uniqueWindowTasks = [...new Set(windowHits.map((h) => h.taskId))];
if (uniqueWindowTasks.length === 1) {
return { outcome: 'owned', rule: 'address-window', taskId: uniqueWindowTasks[0]! };
}
if (uniqueWindowTasks.length > 1) return unowned('ambiguous', rules);
return unowned('no-match', rules);
}
function unowned(
reason: 'no-match' | 'ambiguous' | 'extract-failed',
rules: OwnershipKeyRules,
): OwnershipResolution {
return {
outcome: 'unowned',
reason,
...(rules.fallbackPosition === undefined ? {} : { fallbackPosition: rules.fallbackPosition }),
};
}