/** * 多来源判定合并 —— 零依赖纯函数(§4 职责:最易测)。 * * 合并语义「取最严」:`block` > `flag` > `allow`。任一来源 block 即最终 block; * 无 block 但有 flag 即 flag;全 allow 才 allow。这是审核域唯一的优先级, * 显式可见、不藏隐式规则(§6 可读性)。 * * `guardLevel` 标注用于让调用方区分「rules + llm 双护栏」与「仅本地规则降级」 * 两种结果,避免假装做了 LLM 护栏(§5 review 关注点 4)。 */ /** * 三档判定的严格序(数值越大越严)。 * @type {Record} */ const SEVERITY = Object.freeze({ allow: 0, flag: 1, block: 2 }) const ORDER = Object.freeze(['allow', 'flag', 'block']) /** * 归一一个 decision 字符串;非法值保守落为 'block'(fail-closed,安全优先)。 * @param {unknown} d * @returns {'allow'|'flag'|'block'} */ function normDecision(d) { if (d === 'allow' || d === 'flag' || d === 'block') return d return 'block' } /** * 取两个 decision 中更严的一个。 * @param {'allow'|'flag'|'block'} a * @param {'allow'|'flag'|'block'} b * @returns {'allow'|'flag'|'block'} */ export function strictest(a, b) { const na = normDecision(a) const nb = normDecision(b) return SEVERITY[na] >= SEVERITY[nb] ? na : nb } /** * 合并多个来源的判定为最终决策(取最严)。 * * @param {Array<{ decision?: string, reasons?: string[], categories?: string[] }>} sources * 各来源的子判定(本地 rules、llm 护栏等)。空数组视为无问题 → 'allow'。 * @returns {{ decision:'allow'|'flag'|'block', reasons:string[], categories:string[] }} */ export function mergeDecisions(sources) { const list = Array.isArray(sources) ? sources : [] let decision = /** @type {'allow'|'flag'|'block'} */ ('allow') /** @type {string[]} */ const reasons = [] /** @type {string[]} */ const categories = [] for (const s of list) { if (!s) continue decision = strictest(decision, normDecision(s.decision)) if (Array.isArray(s.reasons)) { for (const r of s.reasons) if (r && !reasons.includes(r)) reasons.push(r) } if (Array.isArray(s.categories)) { for (const c of s.categories) if (c && !categories.includes(c)) categories.push(c) } } return { decision, reasons, categories } } /** * 标注护栏级别:注入了 llm 护栏 → 'rules+llm';否则降级 'rules-only'。 * @param {boolean} hasLlmGuard * @returns {'rules+llm'|'rules-only'} */ export function guardLevelOf(hasLlmGuard) { return hasLlmGuard ? 'rules+llm' : 'rules-only' } export { SEVERITY as DECISION_SEVERITY, ORDER as DECISION_ORDER }