feat(core): implement evaluator module with governance decision making

This commit is contained in:
thePR0M3TH3AN committed 2026-07-26 19:17:38 -04:00
1 parent d4c8e47b55
commit 5a7c1a26de
2 files changed
+810

No files matched your search

+441
View File
@@ -0,0 +1,441 @@
// Evaluator for Nostr Governance
//
// This module brings together all the core components to evaluate governance
// decisions for targets based on state and policies.
import { getTargetKey } from "./identifiers.js";
import { areTargetsEqual, isDescendantOf } from "./targets.js";
import { hasCapability } from "./authority.js";
import {
createGovernanceDecision,
getPolicyEffect,
composeGovernanceDecisions
} from "./policy.js";
/**
* @typedef {import('./identifiers.js').GovernanceTarget} GovernanceTarget
* @typedef {import('./authority.js').AuthorityState} AuthorityState
* @typedef {import('./policy.js').PolicyContext} PolicyContext
* @typedef {import('./policy.js').GovernanceDecision} GovernanceDecision
* @typedef {import('./policy.js').PolicyDefinition} PolicyDefinition
*/
/**
* @typedef {Object} Report
* @property {string} reporter - Pubkey of the reporter
* @property {GovernanceTarget} target - Target being reported
* @property {string} category - Report category
* @property {number} timestamp - Unix timestamp of the report
*/
/**
* @typedef {Object} TrustedMute
* @property {GovernanceTarget} target - Target being muted
* @property {number} count - Number of trusted mutes
* @property {Record<string, number>} categories - Count of mutes per category
*/
/**
* @typedef {Object} Override
* @property {GovernanceTarget} target - Target being overridden
* @property {"allow"|"restrict"|"hide"|"deny"} visibility - Visibility override
* @property {string} [reason] - Reason for the override
*/
/**
* @typedef {Object} GovernanceState
* @property {AuthorityState} authority - Authority state
* @property {Record<string, Report[]>} reports - Reports by target key
* @property {Record<string, TrustedMute>} trustedMutes - Trusted mutes by target key
* @property {Record<string, Override>} overrides - Overrides by target key
* @property {Record<string, PolicyDefinition>} policies - Policy definitions by ID
*/
/**
* Default thresholds for governance decisions
*/
export const DEFAULT_THRESHOLDS = {
// Number of trusted reports needed to hide a target
trustedReportHideThreshold: 5,
// Number of trusted reports needed to restrict a target
trustedReportRestrictThreshold: 3,
// Number of trusted mutes needed to hide a target
trustedMuteHideThreshold: 10,
// Number of trusted mutes needed to restrict a target
trustedMuteRestrictThreshold: 5
};
/**
* Evaluate a single target against the governance state
* @param {GovernanceTarget} target
* @param {GovernanceState} state
* @param {PolicyContext} context
* @param {string} [viewerPubkey] - Pubkey of the viewer (for personal overrides)
* @returns {GovernanceDecision}
*/
export function evaluateTarget(target, state, context, viewerPubkey) {
const decisions = [];
// 1. Check for personal overrides first
if (viewerPubkey) {
const overrideDecision = checkPersonalOverrides(target, state, viewerPubkey);
if (overrideDecision) {
decisions.push(overrideDecision);
}
}
// 2. Check for explicit overrides
const explicitOverrideDecision = checkExplicitOverrides(target, state, context);
if (explicitOverrideDecision) {
decisions.push(explicitOverrideDecision);
}
// 3. Check reports
const reportDecision = checkReports(target, state, context);
if (reportDecision) {
decisions.push(reportDecision);
}
// 4. Check trusted mutes
const muteDecision = checkTrustedMutes(target, state, context);
if (muteDecision) {
decisions.push(muteDecision);
}
// 5. Check if target is a descendant of a denied ancestor
const ancestorDecision = checkAncestorDeny(target, state, context);
if (ancestorDecision) {
decisions.push(ancestorDecision);
}
// Compose all decisions into a final decision
if (decisions.length === 0) {
// Default allow decision
return createGovernanceDecision(
{ effect: /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("allow") },
{ effect: /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("allow") }
);
}
return composeGovernanceDecisions(decisions);
}
/**
* Check for personal overrides for a viewer
* @param {GovernanceTarget} target
* @param {GovernanceState} state
* @param {string} viewerPubkey
* @returns {GovernanceDecision|null}
*/
function checkPersonalOverrides(target, state, viewerPubkey) {
// This would check for personal allowlists, blocklists, etc.
// For now, we'll return null to indicate no personal override
return null;
}
/**
* Check for explicit overrides
* @param {GovernanceTarget} target
* @param {GovernanceState} state
* @param {PolicyContext} context
* @returns {GovernanceDecision|null}
*/
function checkExplicitOverrides(target, state, context) {
const targetKey = getTargetKey(target);
const override = state.overrides[targetKey];
if (!override) {
return null;
}
// Check if the override should be applied based on context
if (context.enforcement && context.enforcement.allowOverrides === false) {
return null;
}
const reasons = [];
if (override.reason) {
reasons.push(override.reason);
}
return createGovernanceDecision(
{
effect: /** @type {import('./policy.js').PolicyEffect["effect"]} */ (override.visibility),
reason: override.reason
},
{
effect: /** @type {import('./policy.js').PolicyEffect["effect"]} */ (override.visibility),
reason: override.reason
},
undefined,
reasons,
[override]
);
}
/**
* Check reports against a target
* @param {GovernanceTarget} target
* @param {GovernanceState} state
* @param {PolicyContext} context
* @returns {GovernanceDecision|null}
*/
function checkReports(target, state, context) {
const targetKey = getTargetKey(target);
const targetReports = state.reports[targetKey] || [];
if (targetReports.length === 0) {
return null;
}
// Count trusted reports by category
/** @type {Record<string, number>} */
const trustedReportCounts = {};
/** @type {any[]} */
const evidence = [];
for (const report of targetReports) {
// Check if the reporter is trusted
if (hasCapability(report.reporter, "submit-reports", state.authority)) {
if (!trustedReportCounts[report.category]) {
trustedReportCounts[report.category] = 0;
}
trustedReportCounts[report.category]++;
evidence.push(report);
}
}
// Determine effect based on trusted report counts
let visibilityEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("allow");
let interactionEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("allow");
/** @type {string[]} */
const reasons = [];
for (const [category, count] of Object.entries(trustedReportCounts)) {
if (count >= DEFAULT_THRESHOLDS.trustedReportHideThreshold) {
visibilityEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("hide");
interactionEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("deny");
reasons.push(`Hidden due to ${count} trusted reports in category ${category}`);
} else if (count >= DEFAULT_THRESHOLDS.trustedReportRestrictThreshold) {
visibilityEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("restrict");
reasons.push(`Restricted due to ${count} trusted reports in category ${category}`);
}
}
if (visibilityEffect === "allow" && interactionEffect === "allow") {
return null;
}
return createGovernanceDecision(
{ effect: visibilityEffect, reason: reasons.join("; ") },
{ effect: interactionEffect, reason: reasons.join("; ") },
undefined,
reasons,
evidence
);
}
/**
* Check trusted mutes against a target
* @param {GovernanceTarget} target
* @param {GovernanceState} state
* @param {PolicyContext} context
* @returns {GovernanceDecision|null}
*/
function checkTrustedMutes(target, state, context) {
const targetKey = getTargetKey(target);
const trustedMute = state.trustedMutes[targetKey];
if (!trustedMute) {
return null;
}
// Determine effect based on trusted mute count
let visibilityEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("allow");
let interactionEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("allow");
/** @type {string[]} */
const reasons = [];
const evidence = [trustedMute];
if (trustedMute.count >= DEFAULT_THRESHOLDS.trustedMuteHideThreshold) {
visibilityEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("hide");
interactionEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("deny");
reasons.push(`Hidden due to ${trustedMute.count} trusted mutes`);
} else if (trustedMute.count >= DEFAULT_THRESHOLDS.trustedMuteRestrictThreshold) {
visibilityEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("restrict");
reasons.push(`Restricted due to ${trustedMute.count} trusted mutes`);
}
if (visibilityEffect === "allow" && interactionEffect === "allow") {
return null;
}
return createGovernanceDecision(
{ effect: visibilityEffect, reason: reasons.join("; ") },
{ effect: interactionEffect, reason: reasons.join("; ") },
undefined,
reasons,
evidence
);
}
/**
* Check if target is a descendant of a denied ancestor
* @param {GovernanceTarget} target
* @param {GovernanceState} state
* @param {PolicyContext} context
* @returns {GovernanceDecision|null}
*/
function checkAncestorDeny(target, state, context) {
// This would check if any ancestor of the target is denied
// For now, we'll return null to indicate no ancestor deny
return null;
}
/**
* Evaluate multiple targets against the governance state
* @param {GovernanceTarget[]} targets
* @param {GovernanceState} state
* @param {PolicyContext} context
* @param {string} [viewerPubkey] - Pubkey of the viewer (for personal overrides)
* @returns {Record<string, GovernanceDecision>}
*/
export function evaluateMany(targets, state, context, viewerPubkey) {
/** @type {Record<string, GovernanceDecision>} */
const results = {};
for (const target of targets) {
const targetKey = getTargetKey(target);
results[targetKey] = evaluateTarget(target, state, context, viewerPubkey);
}
return results;
}
/**
* Create a governance state
* @param {AuthorityState} [authority]
* @param {Record<string, Report[]>} [reports]
* @param {Record<string, TrustedMute>} [trustedMutes]
* @param {Record<string, Override>} [overrides]
* @param {Record<string, PolicyDefinition>} [policies]
* @returns {GovernanceState}
*/
export function createGovernanceState(authority, reports, trustedMutes, overrides, policies) {
return {
authority: authority || { roles: {}, actors: {} },
reports: reports || {},
trustedMutes: trustedMutes || {},
overrides: overrides || {},
policies: policies || {}
};
}
/**
* Create a report
* @param {string} reporter
* @param {GovernanceTarget} target
* @param {string} category
* @param {number} timestamp
* @returns {Report}
*/
export function createReport(reporter, target, category, timestamp) {
// Validate reporter pubkey
if (typeof reporter !== "string" || !/^[0-9a-f]{64}$/i.test(reporter)) {
throw new Error("Reporter must be a 64-character hex pubkey");
}
// Validate target
if (!target || typeof target !== "object") {
throw new Error("Target must be an object");
}
// Validate category
if (typeof category !== "string" || !category.trim()) {
throw new Error("Category must be a non-empty string");
}
// Validate timestamp
if (typeof timestamp !== "number" || timestamp <= 0) {
throw new Error("Timestamp must be a positive number");
}
return {
reporter: reporter.toLowerCase(),
target,
category: category.trim(),
timestamp
};
}
/**
* Create a trusted mute
* @param {GovernanceTarget} target
* @param {number} count
* @param {Record<string, number>} categories
* @returns {TrustedMute}
*/
export function createTrustedMute(target, count, categories) {
// Validate target
if (!target || typeof target !== "object") {
throw new Error("Target must be an object");
}
// Validate count
if (typeof count !== "number" || count < 0) {
throw new Error("Count must be a non-negative number");
}
// Validate categories
if (!categories || typeof categories !== "object") {
throw new Error("Categories must be an object");
}
// Validate each category count
for (const [category, categoryCount] of Object.entries(categories)) {
if (typeof category !== "string" || !category.trim()) {
throw new Error("Category names must be non-empty strings");
}
if (typeof categoryCount !== "number" || categoryCount < 0) {
throw new Error("Category counts must be non-negative numbers");
}
}
return {
target,
count,
categories
};
}
/**
* Create an override
* @param {GovernanceTarget} target
* @param {"allow"|"restrict"|"hide"|"deny"} visibility
* @param {string} [reason]
* @returns {Override}
*/
export function createOverride(target, visibility, reason) {
// Validate target
if (!target || typeof target !== "object") {
throw new Error("Target must be an object");
}
// Validate visibility
if (!["allow", "restrict", "hide", "deny"].includes(visibility)) {
throw new Error("Invalid visibility effect");
}
/** @type {Override} */
const override = {
target,
visibility
};
if (reason) {
override.reason = reason;
}
return override;
}
+369
View File
@@ -0,0 +1,369 @@
import { describe, it, expect } from "vitest";
import {
evaluateTarget,
evaluateMany,
createGovernanceState,
createReport,
createTrustedMute,
createOverride,
DEFAULT_THRESHOLDS
} from "../src/evaluator.js";
import {
createUserTarget,
createEventTarget
} from "../src/identifiers.js";
import {
createAuthorityState,
createActor
} from "../src/authority.js";
import {
createPolicyContext
} from "../src/policy.js";
describe("evaluator", () => {
describe("evaluateTarget", () => {
it("should return default allow decision for target with no reports or mutes", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const state = createGovernanceState();
const context = createPolicyContext("feed", "default");
const decision = evaluateTarget(target, state, context);
expect(decision).toEqual({
visibility: { effect: "allow" },
interaction: { effect: "allow" },
reasons: [],
evidence: []
});
});
it("should hide target with sufficient trusted reports", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const reporter = "1234567890123456789012345678901234567890123456789012345678901234";
// Create authority state with reporter as moderator (can submit reports)
const authorityState = createAuthorityState({}, {
[reporter]: createActor(reporter, ["moderator"])
});
const state = createGovernanceState(
authorityState,
{
"user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": [
createReport(reporter, target, "spam", Date.now())
]
}
);
const context = createPolicyContext("feed", "default");
// Add enough reports to exceed the hide threshold
for (let i = 0; i < DEFAULT_THRESHOLDS.trustedReportHideThreshold; i++) {
state.reports["user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"].push(
createReport(reporter, target, "spam", Date.now() + i)
);
}
const decision = evaluateTarget(target, state, context);
expect(decision.visibility.effect).toBe("hide");
expect(decision.interaction.effect).toBe("deny");
});
it("should restrict target with moderate number of trusted reports", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const reporter = "1234567890123456789012345678901234567890123456789012345678901234";
// Create authority state with reporter as moderator (can submit reports)
const authorityState = createAuthorityState({}, {
[reporter]: createActor(reporter, ["moderator"])
});
const state = createGovernanceState(
authorityState,
{
"user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": [
createReport(reporter, target, "spam", Date.now())
]
}
);
const context = createPolicyContext("feed", "default");
// Add enough reports to exceed the restrict threshold but not the hide threshold
for (let i = 0; i < DEFAULT_THRESHOLDS.trustedReportRestrictThreshold; i++) {
state.reports["user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"].push(
createReport(reporter, target, "spam", Date.now() + i)
);
}
const decision = evaluateTarget(target, state, context);
expect(decision.visibility.effect).toBe("restrict");
expect(decision.interaction.effect).toBe("allow");
});
it("should hide target with sufficient trusted mutes", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const state = createGovernanceState(
undefined,
undefined,
{
"user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789":
createTrustedMute(target, DEFAULT_THRESHOLDS.trustedMuteHideThreshold, {
"mute": DEFAULT_THRESHOLDS.trustedMuteHideThreshold
})
}
);
const context = createPolicyContext("feed", "default");
const decision = evaluateTarget(target, state, context);
expect(decision.visibility.effect).toBe("hide");
expect(decision.interaction.effect).toBe("deny");
});
it("should restrict target with moderate number of trusted mutes", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const state = createGovernanceState(
undefined,
undefined,
{
"user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789":
createTrustedMute(target, DEFAULT_THRESHOLDS.trustedMuteRestrictThreshold, {
"mute": DEFAULT_THRESHOLDS.trustedMuteRestrictThreshold
})
}
);
const context = createPolicyContext("feed", "default");
const decision = evaluateTarget(target, state, context);
expect(decision.visibility.effect).toBe("restrict");
expect(decision.interaction.effect).toBe("allow");
});
it("should apply explicit overrides", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const state = createGovernanceState(
undefined,
undefined,
undefined,
{
"user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789":
createOverride(target, "hide", "Administrative override")
}
);
const context = createPolicyContext("feed", "default");
const decision = evaluateTarget(target, state, context);
expect(decision.visibility.effect).toBe("hide");
expect(decision.interaction.effect).toBe("hide");
expect(decision.reasons).toContain("Administrative override");
});
it("should ignore overrides when enforcement disallows them", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const state = createGovernanceState(
undefined,
undefined,
undefined,
{
"user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789":
createOverride(target, "hide", "Administrative override")
}
);
const context = createPolicyContext("feed", "default", {
allowOverrides: false
});
const decision = evaluateTarget(target, state, context);
expect(decision.visibility.effect).toBe("allow");
expect(decision.interaction.effect).toBe("allow");
});
});
describe("evaluateMany", () => {
it("should evaluate multiple targets", () => {
const target1 = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const target2 = createEventTarget("1234567890123456789012345678901234567890123456789012345678901234");
const state = createGovernanceState();
const context = createPolicyContext("feed", "default");
const results = evaluateMany([target1, target2], state, context);
expect(results).toHaveProperty("user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(results).toHaveProperty("event:1234567890123456789012345678901234567890123456789012345678901234");
expect(results["user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"]).toEqual({
visibility: { effect: "allow" },
interaction: { effect: "allow" },
reasons: [],
evidence: []
});
expect(results["event:1234567890123456789012345678901234567890123456789012345678901234"]).toEqual({
visibility: { effect: "allow" },
interaction: { effect: "allow" },
reasons: [],
evidence: []
});
});
});
describe("createGovernanceState", () => {
it("should create a valid governance state", () => {
const authority = createAuthorityState();
const reports = {};
const trustedMutes = {};
const overrides = {};
const policies = {};
const state = createGovernanceState(authority, reports, trustedMutes, overrides, policies);
expect(state).toEqual({
authority,
reports,
trustedMutes,
overrides,
policies
});
});
it("should create a governance state with default empty objects", () => {
const state = createGovernanceState();
expect(state).toEqual({
authority: { roles: {}, actors: {} },
reports: {},
trustedMutes: {},
overrides: {},
policies: {}
});
});
});
describe("createReport", () => {
it("should create a valid report", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const report = createReport(
"1234567890123456789012345678901234567890123456789012345678901234",
target,
"spam",
Date.now()
);
expect(report).toEqual({
reporter: "1234567890123456789012345678901234567890123456789012345678901234",
target,
category: "spam",
timestamp: expect.any(Number)
});
});
it("should throw error for invalid reporter", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createReport("invalid", target, "spam", Date.now())).toThrow();
});
it("should throw error for invalid target", () => {
expect(() => createReport(
"1234567890123456789012345678901234567890123456789012345678901234",
null,
"spam",
Date.now()
)).toThrow();
});
it("should throw error for invalid category", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createReport(
"1234567890123456789012345678901234567890123456789012345678901234",
target,
"",
Date.now()
)).toThrow();
});
it("should throw error for invalid timestamp", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createReport(
"1234567890123456789012345678901234567890123456789012345678901234",
target,
"spam",
-1
)).toThrow();
});
});
describe("createTrustedMute", () => {
it("should create a valid trusted mute", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const trustedMute = createTrustedMute(target, 5, { "mute": 5 });
expect(trustedMute).toEqual({
target,
count: 5,
categories: { "mute": 5 }
});
});
it("should throw error for invalid target", () => {
expect(() => createTrustedMute(null, 5, { "mute": 5 })).toThrow();
});
it("should throw error for invalid count", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createTrustedMute(target, -1, { "mute": 5 })).toThrow();
});
it("should throw error for invalid categories", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createTrustedMute(target, 5, null)).toThrow();
});
it("should throw error for invalid category names", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createTrustedMute(target, 5, { "": 5 })).toThrow();
});
it("should throw error for invalid category counts", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createTrustedMute(target, 5, { "mute": -1 })).toThrow();
});
});
describe("createOverride", () => {
it("should create a valid override", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const override = createOverride(target, "hide", "Administrative override");
expect(override).toEqual({
target,
visibility: "hide",
reason: "Administrative override"
});
});
it("should create an override without reason", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const override = createOverride(target, "hide");
expect(override).toEqual({
target,
visibility: "hide"
});
});
it("should throw error for invalid target", () => {
expect(() => createOverride(null, "hide")).toThrow();
});
it("should throw error for invalid visibility", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createOverride(target, /** @type {any} */ ("invalid"))).toThrow();
});
});
});