Archived
@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
114 lines
4.3 KiB
TypeScript
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 }),
|
|
};
|
|
}
|