// distribution-stats.mjs — Statistical divergence utilities for behavioral drift detection. // Zero external dependencies. <50 lines. // // Jensen-Shannon divergence measures how different two probability distributions are. // Used by post-session-guard.mjs to detect tool distribution shifts within a session. // // OWASP: ASI01 (Excessive Agency — behavioral pattern changes may indicate hijacking) /** * Kullback-Leibler divergence KL(P || Q). * @param {Map} P * @param {Map} Q * @returns {number} */ function klDivergence(P, Q) { let kl = 0; for (const [key, p] of P) { if (p === 0) continue; const q = Q.get(key) || 0; if (q === 0) return Infinity; kl += p * Math.log2(p / q); } return kl; } /** * Jensen-Shannon divergence. 0 = identical, 1 = fully disjoint (log2 basis). * Always finite, symmetric: JSD(P,Q) = JSD(Q,P). * @param {Map} P - Normalized probability distribution * @param {Map} Q - Normalized probability distribution * @returns {number} */ export function jensenShannonDivergence(P, Q) { const allKeys = new Set([...P.keys(), ...Q.keys()]); const M = new Map(); for (const key of allKeys) { M.set(key, 0.5 * (P.get(key) || 0) + 0.5 * (Q.get(key) || 0)); } return 0.5 * klDivergence(P, M) + 0.5 * klDivergence(Q, M); } /** * Build normalized probability distribution from category labels. * @param {string[]} labels * @returns {Map} Values sum to 1.0 (empty input → empty map) */ export function buildDistribution(labels) { if (labels.length === 0) return new Map(); const counts = new Map(); for (const label of labels) { counts.set(label, (counts.get(label) || 0) + 1); } const dist = new Map(); for (const [key, count] of counts) { dist.set(key, count / labels.length); } return dist; }