decision.js
2.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* 多来源判定合并 —— 零依赖纯函数(§4 职责:最易测)。
*
* 合并语义「取最严」:`block` > `flag` > `allow`。任一来源 block 即最终 block;
* 无 block 但有 flag 即 flag;全 allow 才 allow。这是审核域唯一的优先级,
* 显式可见、不藏隐式规则(§6 可读性)。
*
* `guardLevel` 标注用于让调用方区分「rules + llm 双护栏」与「仅本地规则降级」
* 两种结果,避免假装做了 LLM 护栏(§5 review 关注点 4)。
*/
/**
* 三档判定的严格序(数值越大越严)。
* @type {Record<string, number>}
*/
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 }