@consensus-tools/universal 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/consensus-llm.test.d.ts +2 -0
  2. package/dist/consensus-llm.test.d.ts.map +1 -0
  3. package/dist/consensus-llm.test.js +244 -0
  4. package/dist/consensus-llm.test.js.map +1 -0
  5. package/dist/defaults.d.ts +10 -0
  6. package/dist/defaults.d.ts.map +1 -1
  7. package/dist/defaults.js +63 -2
  8. package/dist/defaults.js.map +1 -1
  9. package/dist/index.d.ts +13 -11
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +130 -49
  12. package/dist/index.js.map +1 -1
  13. package/dist/persona-reviewer-factory.d.ts +22 -0
  14. package/dist/persona-reviewer-factory.d.ts.map +1 -0
  15. package/dist/persona-reviewer-factory.js +318 -0
  16. package/dist/persona-reviewer-factory.js.map +1 -0
  17. package/dist/reputation-manager.d.ts +38 -0
  18. package/dist/reputation-manager.d.ts.map +1 -0
  19. package/dist/reputation-manager.js +154 -0
  20. package/dist/reputation-manager.js.map +1 -0
  21. package/dist/reputation-manager.test.d.ts +2 -0
  22. package/dist/reputation-manager.test.d.ts.map +1 -0
  23. package/dist/reputation-manager.test.js +111 -0
  24. package/dist/reputation-manager.test.js.map +1 -0
  25. package/dist/risk-tiers.d.ts +10 -0
  26. package/dist/risk-tiers.d.ts.map +1 -0
  27. package/dist/risk-tiers.js +46 -0
  28. package/dist/risk-tiers.js.map +1 -0
  29. package/dist/risk-tiers.test.d.ts +2 -0
  30. package/dist/risk-tiers.test.d.ts.map +1 -0
  31. package/dist/risk-tiers.test.js +40 -0
  32. package/dist/risk-tiers.test.js.map +1 -0
  33. package/dist/types.d.ts +59 -6
  34. package/dist/types.d.ts.map +1 -1
  35. package/package.json +9 -9
  36. package/src/consensus-llm.test.ts +23 -4
  37. package/src/defaults.ts +10 -4
  38. package/src/index.ts +22 -18
  39. package/src/persona-reviewer-factory.ts +90 -70
  40. package/src/reputation-manager.ts +46 -31
  41. package/src/risk-tiers.test.ts +8 -0
  42. package/src/risk-tiers.ts +7 -5
@@ -0,0 +1,318 @@
1
+ import crypto from "node:crypto";
2
+ import { resolveConsensus } from "@consensus-tools/core";
3
+ import { createGuardTemplate, GUARD_CONFIGS } from "@consensus-tools/guards";
4
+ import { classifyTool } from "./risk-tiers.js";
5
+ // ── Persona Reviewer Factory ─────────────────────────────────────────
6
+ // Creates LLM-backed persona reviewers that use resolveConsensus()
7
+ // for multi-model deliberation with reputation-weighted voting.
8
+ //
9
+ // Architecture:
10
+ // 1. Regex pre-screen (sub-ms, deterministic)
11
+ // 2. Risk tier check (low = fast-path regex only)
12
+ // 3. Parallel LLM calls per persona (with timeout + fallback)
13
+ // 4. Parse votes from LLM responses
14
+ // 5. Synthesize ConsensusInput: ONE "allow" submission, all personas
15
+ // vote on it (YES = +1, NO = -1). resolveConsensus aggregates.
16
+ // 6. Determine action from consensus result
17
+ // 7. Return LlmDecisionResult
18
+ // ── Safe JSON Serialization ──────────────────────────────────────────
19
+ function safeStringify(obj, indent) {
20
+ const seen = new WeakSet();
21
+ return JSON.stringify(obj, (_key, value) => {
22
+ if (typeof value === "object" && value !== null) {
23
+ if (seen.has(value))
24
+ return "[Circular]";
25
+ seen.add(value);
26
+ }
27
+ return value;
28
+ }, indent);
29
+ }
30
+ // Match VOTE: YES/NO/REWRITE on its own line (anchored to reduce injection risk)
31
+ const VOTE_LINE_PATTERN = /^(?:VOTE:\s*)?(YES|NO|REWRITE)\s*$/im;
32
+ // Fallback: match anywhere but only as a last resort
33
+ const VOTE_FALLBACK_PATTERN = /\b(YES|NO|REWRITE)\b/i;
34
+ const CONFIDENCE_PATTERN = /confidence[:\s]*([0-9]*\.?[0-9]+)/i;
35
+ function parseVoteFromLlm(response) {
36
+ // Prefer line-anchored match (harder to inject)
37
+ const lineMatch = response.match(VOTE_LINE_PATTERN);
38
+ const voteMatch = lineMatch ?? response.match(VOTE_FALLBACK_PATTERN);
39
+ if (!voteMatch)
40
+ return null;
41
+ const vote = voteMatch[1].toUpperCase();
42
+ const confMatch = response.match(CONFIDENCE_PATTERN);
43
+ const confidence = confMatch?.[1] ? Math.min(1, Math.max(0, parseFloat(confMatch[1]))) : 0.5;
44
+ const rationale = response
45
+ .replace(/^.*\b(YES|NO|REWRITE)\b.*$/im, "")
46
+ .replace(/^.*confidence.*$/im, "")
47
+ .trim()
48
+ .slice(0, 500) || "No rationale provided";
49
+ return { vote, confidence, rationale };
50
+ }
51
+ // ── Prompt Construction ──────────────────────────────────────────────
52
+ function buildPersonaPrompt(persona, toolName, args, regexSignals) {
53
+ const evalPersona = persona;
54
+ const systemPrompt = evalPersona.systemPrompt ??
55
+ `You are a ${persona.role} reviewer named ${persona.name}.` +
56
+ (persona.bias ? ` Your bias: ${persona.bias}.` : "") +
57
+ (persona.non_negotiables?.length
58
+ ? ` Non-negotiables: ${persona.non_negotiables.join(", ")}.`
59
+ : "") +
60
+ (persona.failure_modes?.length
61
+ ? ` Watch for: ${persona.failure_modes.join(", ")}.`
62
+ : "") +
63
+ ` Respond ONLY with a VOTE line, CONFIDENCE line, and RATIONALE line. Do not echo the tool arguments.`;
64
+ const regexContext = regexSignals.length > 0
65
+ ? `\n\nRegex pre-screen detected these signals:\n${regexSignals.map((s) => `- ${s}`).join("\n")}`
66
+ : "\n\nRegex pre-screen: no pattern matches detected.";
67
+ // Sanitize args: truncate to prevent prompt stuffing, use safe serialization
68
+ const argsStr = safeStringify(args, 2).slice(0, 2000);
69
+ const userPrompt = `Evaluate this tool call for risks:\n\n` +
70
+ `Tool: ${toolName}\n` +
71
+ `Arguments:\n${argsStr}\n` +
72
+ regexContext +
73
+ `\n\nRespond with exactly these three lines:\n` +
74
+ `VOTE: YES (safe to proceed), NO (block this action), or REWRITE (needs modification)\n` +
75
+ `CONFIDENCE: 0.0 to 1.0\n` +
76
+ `RATIONALE: Brief explanation of your decision`;
77
+ return [
78
+ { role: "system", content: systemPrompt },
79
+ { role: "user", content: userPrompt },
80
+ ];
81
+ }
82
+ // ── Regex Pre-Screen ─────────────────────────────────────────────────
83
+ // Fallback guard domains when configured guards have no matching configs
84
+ const FALLBACK_GUARDS = ["security", "compliance", "user-impact"];
85
+ function runRegexPreScreen(toolName, args, guards) {
86
+ const signals = [];
87
+ // Use provided guards, falling back to DEFAULT_PERSONA_TRIO
88
+ const effectiveGuards = guards.filter((g) => GUARD_CONFIGS[g]).length > 0
89
+ ? guards
90
+ : FALLBACK_GUARDS;
91
+ for (const domain of effectiveGuards) {
92
+ const config = GUARD_CONFIGS[domain];
93
+ if (!config)
94
+ continue;
95
+ try {
96
+ const template = createGuardTemplate(domain, config);
97
+ const votes = template.evaluate({
98
+ boardId: "facade",
99
+ action: { type: toolName, payload: args },
100
+ });
101
+ for (const vote of votes) {
102
+ if (vote.vote === "NO" || (vote.risk && vote.risk > 0.5)) {
103
+ signals.push(`[${domain}] ${vote.reason} (risk: ${vote.risk ?? "unknown"})`);
104
+ }
105
+ }
106
+ }
107
+ catch {
108
+ // Regex pre-screen failure is non-fatal
109
+ }
110
+ }
111
+ return signals;
112
+ }
113
+ // ── LLM Call with Timeout ────────────────────────────────────────────
114
+ async function callLlmWithTimeout(model, messages, timeoutMs) {
115
+ let timer;
116
+ try {
117
+ const result = await Promise.race([
118
+ model(messages),
119
+ new Promise((_, reject) => {
120
+ timer = setTimeout(() => reject(new Error("LLM call timed out")), timeoutMs);
121
+ }),
122
+ ]);
123
+ return result;
124
+ }
125
+ finally {
126
+ if (timer)
127
+ clearTimeout(timer);
128
+ }
129
+ }
130
+ // ── Regex Fallback Vote ──────────────────────────────────────────────
131
+ function regexFallbackVote(_persona, toolName, args, guards) {
132
+ const signals = runRegexPreScreen(toolName, args, guards);
133
+ if (signals.length > 0) {
134
+ return {
135
+ vote: "NO",
136
+ confidence: 0.6,
137
+ rationale: `Regex fallback: ${signals.join("; ")}`,
138
+ };
139
+ }
140
+ // When LLM is unavailable AND regex finds nothing, default to block for safety.
141
+ // This prevents fail-open when all LLMs are down.
142
+ return {
143
+ vote: "NO",
144
+ confidence: 0.3,
145
+ rationale: "Regex fallback: no pattern matches but LLM unavailable (fail-closed)",
146
+ };
147
+ }
148
+ /**
149
+ * Run LLM persona deliberation on a tool call.
150
+ *
151
+ * Returns an LlmDecisionResult with per-persona votes, consensus trace,
152
+ * and final action (allow/block/escalate).
153
+ */
154
+ export async function deliberate(config, toolName, args) {
155
+ const decisionId = `dec_${crypto.randomUUID().slice(0, 12)}`;
156
+ const personas = config.reputationManager.getPersonas();
157
+ const guards = config.guards ?? FALLBACK_GUARDS;
158
+ // 1. Regex pre-screen
159
+ const regexSignals = runRegexPreScreen(toolName, args, guards);
160
+ // 2. Risk tier check
161
+ const tier = classifyTool(toolName, config.riskTiers);
162
+ if (tier === "low") {
163
+ const hasRisk = regexSignals.length > 0;
164
+ return {
165
+ decisionId,
166
+ action: hasRisk ? "block" : "allow",
167
+ votes: personas.map((p) => ({
168
+ personaId: p.id,
169
+ personaName: p.name,
170
+ vote: hasRisk ? "NO" : "YES",
171
+ confidence: hasRisk ? 0.7 : 0.3,
172
+ rationale: hasRisk
173
+ ? `Fast-path regex: ${regexSignals.join("; ")}`
174
+ : "Fast-path: low-risk tool, no regex signals",
175
+ source: "regex_fallback",
176
+ })),
177
+ policy: "fast_path",
178
+ consensusTrace: { tier: "low", regexSignals },
179
+ aggregateScore: hasRisk ? 0.0 : 1.0,
180
+ };
181
+ }
182
+ // 3. Parallel LLM calls per persona (with timeout + fallback)
183
+ const voteResults = await Promise.all(personas.map(async (persona) => {
184
+ const messages = buildPersonaPrompt(persona, toolName, args, regexSignals);
185
+ try {
186
+ const response = await callLlmWithTimeout(config.model, messages, config.timeoutMs);
187
+ const parsed = parseVoteFromLlm(response);
188
+ if (parsed) {
189
+ return {
190
+ personaId: persona.id,
191
+ personaName: persona.name,
192
+ ...parsed,
193
+ source: "llm",
194
+ };
195
+ }
196
+ const fallback = regexFallbackVote(persona, toolName, args, guards);
197
+ return {
198
+ personaId: persona.id,
199
+ personaName: persona.name,
200
+ ...fallback,
201
+ source: "regex_fallback",
202
+ };
203
+ }
204
+ catch {
205
+ const fallback = regexFallbackVote(persona, toolName, args, guards);
206
+ return {
207
+ personaId: persona.id,
208
+ personaName: persona.name,
209
+ ...fallback,
210
+ source: "regex_fallback",
211
+ };
212
+ }
213
+ }));
214
+ // 4. Synthesize ConsensusInput for resolveConsensus()
215
+ //
216
+ // FIXED: Use a SINGLE "allow" submission. All personas vote on it.
217
+ // YES voters score +1, NO voters score -1, REWRITE voters score 0.
218
+ // This way resolveConsensus sees N votes on 1 submission, not N
219
+ // submissions with 1 vote each.
220
+ const now = new Date().toISOString();
221
+ const jobId = `job_facade_${decisionId}`;
222
+ const submissionId = `sub_${decisionId}_allow`;
223
+ const job = {
224
+ id: jobId,
225
+ boardId: "",
226
+ status: "SUBMITTED",
227
+ title: `Deliberation: ${toolName}`,
228
+ description: "",
229
+ createdByAgentId: "facade",
230
+ createdAt: now,
231
+ updatedAt: now,
232
+ mode: "VOTING",
233
+ consensusPolicy: { type: config.policyType },
234
+ stakeRequired: 0,
235
+ reward: 0,
236
+ maxParticipants: personas.length,
237
+ minParticipants: 1,
238
+ };
239
+ // Single submission representing "allow this tool call"
240
+ const submissions = [{
241
+ id: submissionId,
242
+ jobId,
243
+ agentId: "facade",
244
+ submittedAt: now,
245
+ summary: `Allow ${toolName}`,
246
+ artifacts: {},
247
+ confidence: 1.0,
248
+ requestedPayout: 0,
249
+ status: "SUBMITTED",
250
+ }];
251
+ // Each persona votes on the single submission
252
+ const votes = voteResults.map((v, i) => ({
253
+ id: `vote_${decisionId}_${i}`,
254
+ jobId,
255
+ agentId: v.personaId,
256
+ submissionId,
257
+ score: v.vote === "YES" ? 1 : v.vote === "NO" ? -1 : 0,
258
+ weight: v.confidence,
259
+ rationale: v.rationale,
260
+ createdAt: now,
261
+ }));
262
+ const reputation = (agentId) => config.reputationManager.getReputation(agentId);
263
+ // 5. Resolve consensus
264
+ const consensusInput = {
265
+ job: job,
266
+ submissions: submissions,
267
+ votes: votes,
268
+ reputation,
269
+ };
270
+ let consensusTrace;
271
+ try {
272
+ const result = resolveConsensus(consensusInput);
273
+ consensusTrace = result.consensusTrace;
274
+ // Extract the actual weighted score from the consensus trace.
275
+ // resolveConsensus always returns a "winner" (the single submission),
276
+ // but the score may be negative (more NO than YES votes).
277
+ const traceScores = consensusTrace?.scores;
278
+ const submissionScore = traceScores?.[submissionId] ?? 0;
279
+ consensusTrace = { ...consensusTrace, submissionScore };
280
+ }
281
+ catch {
282
+ consensusTrace = { policy: "fallback_majority", reason: "resolve_error" };
283
+ }
284
+ // 6. Determine action from vote distribution (direct counting)
285
+ // resolveConsensus provides the audit trace; vote counting determines the action.
286
+ // This avoids the "always-a-winner" problem where resolveConsensus returns
287
+ // a winner even when the score is negative.
288
+ const yesCount = voteResults.filter((v) => v.vote === "YES").length;
289
+ const noCount = voteResults.filter((v) => v.vote === "NO").length;
290
+ const rewriteCount = voteResults.filter((v) => v.vote === "REWRITE").length;
291
+ let action;
292
+ if (rewriteCount > voteResults.length / 2) {
293
+ action = "escalate";
294
+ }
295
+ else if (yesCount > noCount) {
296
+ action = "allow";
297
+ }
298
+ else {
299
+ action = "block";
300
+ }
301
+ // Compute aggregate score
302
+ const totalConfidence = voteResults.reduce((s, v) => s + v.confidence, 0);
303
+ const yesConfidence = voteResults
304
+ .filter((v) => v.vote === "YES")
305
+ .reduce((s, v) => s + v.confidence, 0);
306
+ const aggregateScore = totalConfidence > 0 ? yesConfidence / totalConfidence : 0.5;
307
+ const result = {
308
+ decisionId,
309
+ action,
310
+ votes: voteResults,
311
+ policy: config.policyType,
312
+ consensusTrace,
313
+ aggregateScore,
314
+ };
315
+ config.reputationManager.recordDecision(result);
316
+ return result;
317
+ }
318
+ //# sourceMappingURL=persona-reviewer-factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persona-reviewer-factory.js","sourceRoot":"","sources":["../src/persona-reviewer-factory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { PersonaConfig, ReputationChange } from "@consensus-tools/personas";
2
+ import type { IStorage } from "@consensus-tools/storage";
3
+ import type { FeedbackSignal, LlmDecisionResult } from "./types.js";
4
+ export interface RespawnEvent {
5
+ oldPersona: PersonaConfig;
6
+ newPersona: PersonaConfig;
7
+ reputation: number;
8
+ reason: string;
9
+ }
10
+ export declare class ReputationManager {
11
+ private personas;
12
+ private threshold;
13
+ private store?;
14
+ private scores;
15
+ private decisions;
16
+ private decisionHistory;
17
+ private onRespawn?;
18
+ constructor(personas: PersonaConfig[], threshold?: number, store?: IStorage | undefined);
19
+ /** Set callback for respawn events. */
20
+ setRespawnHandler(handler: (event: RespawnEvent) => void): void;
21
+ /** Get current reputation for a persona. */
22
+ getReputation(personaId: string): number;
23
+ /** Get all current reputation scores. */
24
+ getAllReputations(): Map<string, number>;
25
+ /** Record a decision for later feedback correlation. */
26
+ recordDecision(result: LlmDecisionResult): void;
27
+ /** Process human feedback signal and update reputation. */
28
+ processFeedback(signal: FeedbackSignal): ReputationChange[];
29
+ /** Check if any persona needs respawn. Collects replacements first to avoid mutation during iteration. */
30
+ private checkRespawn;
31
+ /** Get current persona list (may include respawned successors). */
32
+ getPersonas(): PersonaConfig[];
33
+ /** Persist reputation scores to store (if configured). */
34
+ private persist;
35
+ /** Load reputation scores from store (if configured). */
36
+ load(): Promise<void>;
37
+ }
38
+ //# sourceMappingURL=reputation-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reputation-manager.d.ts","sourceRoot":"","sources":["../src/reputation-manager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACjF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAUpE,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,aAAa,CAAC;IAC1B,UAAU,EAAE,aAAa,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,iBAAiB;IAO1B,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,KAAK,CAAC;IARhB,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,eAAe,CAA2B;IAClD,OAAO,CAAC,SAAS,CAAC,CAAgC;gBAGxC,QAAQ,EAAE,aAAa,EAAE,EACzB,SAAS,GAAE,MAAa,EACxB,KAAK,CAAC,EAAE,QAAQ,YAAA;IAO1B,uCAAuC;IACvC,iBAAiB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,IAAI;IAI/D,4CAA4C;IAC5C,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAIxC,yCAAyC;IACzC,iBAAiB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAIxC,wDAAwD;IACxD,cAAc,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI;IAe/C,2DAA2D;IAC3D,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,gBAAgB,EAAE;IAoC3D,0GAA0G;IAC1G,OAAO,CAAC,YAAY;IAuCpB,mEAAmE;IACnE,WAAW,IAAI,aAAa,EAAE;IAI9B,0DAA0D;IAC1D,OAAO,CAAC,OAAO;IAcf,yDAAyD;IACnD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAgB5B"}
@@ -0,0 +1,154 @@
1
+ import { updateReputation } from "@consensus-tools/personas";
2
+ import { buildLearningSummary, mutatePersona } from "@consensus-tools/personas";
3
+ // ── Reputation Manager ───────────────────────────────────────────────
4
+ // In-memory reputation tracking with optional persistence.
5
+ // Updates from human feedback signals (onFeedback), not self-consensus.
6
+ // Triggers persona respawn when reputation drops below threshold.
7
+ const MAX_DECISION_LOOKBACK = 100;
8
+ const MAX_FEEDBACK_LOOKBACK = 500;
9
+ export class ReputationManager {
10
+ personas;
11
+ threshold;
12
+ store;
13
+ scores = new Map();
14
+ decisions = new Map();
15
+ decisionHistory = [];
16
+ onRespawn;
17
+ constructor(personas, threshold = 0.15, store) {
18
+ this.personas = personas;
19
+ this.threshold = threshold;
20
+ this.store = store;
21
+ for (const p of personas) {
22
+ this.scores.set(p.id, p.reputation ?? 0.55);
23
+ }
24
+ }
25
+ /** Set callback for respawn events. */
26
+ setRespawnHandler(handler) {
27
+ this.onRespawn = handler;
28
+ }
29
+ /** Get current reputation for a persona. */
30
+ getReputation(personaId) {
31
+ return this.scores.get(personaId) ?? 0.55;
32
+ }
33
+ /** Get all current reputation scores. */
34
+ getAllReputations() {
35
+ return new Map(this.scores);
36
+ }
37
+ /** Record a decision for later feedback correlation. */
38
+ recordDecision(result) {
39
+ this.decisions.set(result.decisionId, result);
40
+ this.decisionHistory.push(result);
41
+ // Cap both collections to prevent memory leaks
42
+ if (this.decisionHistory.length >= MAX_DECISION_LOOKBACK) {
43
+ this.decisionHistory.shift();
44
+ }
45
+ // Trim the feedback correlation map (keep most recent N entries)
46
+ if (this.decisions.size > MAX_FEEDBACK_LOOKBACK) {
47
+ const oldest = this.decisions.keys().next().value;
48
+ if (oldest)
49
+ this.decisions.delete(oldest);
50
+ }
51
+ }
52
+ /** Process human feedback signal and update reputation. */
53
+ processFeedback(signal) {
54
+ const decision = this.decisions.get(signal.decisionId);
55
+ if (!decision)
56
+ return [];
57
+ // Map feedback to a "ground truth" final decision
58
+ // override_block = human says the block was wrong, action should have been ALLOW
59
+ // flag_miss = human says the allow was wrong, action should have been BLOCK
60
+ const groundTruth = signal.type === "override_block" ? "ALLOW" : "BLOCK";
61
+ const votes = decision.votes.map((v) => ({
62
+ persona_id: v.personaId,
63
+ vote: v.vote,
64
+ confidence: v.confidence,
65
+ }));
66
+ const personaConfigs = this.personas.map((p) => ({
67
+ ...p,
68
+ reputation: this.scores.get(p.id) ?? 0.55,
69
+ }));
70
+ const result = updateReputation(votes, groundTruth, personaConfigs);
71
+ // Apply changes
72
+ for (const change of result.changes) {
73
+ this.scores.set(change.persona_id, change.reputation_after);
74
+ }
75
+ // Check for respawn (collect respawns, then apply)
76
+ this.checkRespawn();
77
+ // Persist if store configured
78
+ this.persist();
79
+ return result.changes;
80
+ }
81
+ /** Check if any persona needs respawn. Collects replacements first to avoid mutation during iteration. */
82
+ checkRespawn() {
83
+ const replacements = [];
84
+ // Collect personas that need respawn (don't mutate during scan)
85
+ for (let i = 0; i < this.personas.length; i++) {
86
+ const persona = this.personas[i];
87
+ const rep = this.scores.get(persona.id) ?? 0.55;
88
+ if (rep < this.threshold) {
89
+ replacements.push({ index: i, old: persona, rep });
90
+ }
91
+ }
92
+ // Apply replacements after scan
93
+ for (const { index, old, rep } of replacements) {
94
+ const decisionRecords = this.decisionHistory.map((d) => ({
95
+ final_decision: d.action === "allow" ? "ALLOW" : "BLOCK",
96
+ votes: d.votes.map((v) => ({
97
+ persona_id: v.personaId,
98
+ vote: v.vote,
99
+ confidence: v.confidence,
100
+ })),
101
+ }));
102
+ const learning = buildLearningSummary(old.id, decisionRecords);
103
+ const successor = mutatePersona(old, learning);
104
+ this.personas[index] = successor;
105
+ this.scores.delete(old.id);
106
+ this.scores.set(successor.id, successor.reputation ?? 0.55);
107
+ this.onRespawn?.({
108
+ oldPersona: old,
109
+ newPersona: successor,
110
+ reputation: rep,
111
+ reason: `Reputation ${rep.toFixed(3)} below threshold ${this.threshold}`,
112
+ });
113
+ }
114
+ }
115
+ /** Get current persona list (may include respawned successors). */
116
+ getPersonas() {
117
+ return [...this.personas];
118
+ }
119
+ /** Persist reputation scores to store (if configured). */
120
+ persist() {
121
+ if (!this.store)
122
+ return;
123
+ const data = {};
124
+ for (const [id, score] of this.scores) {
125
+ data[id] = score;
126
+ }
127
+ this.store.update((state) => {
128
+ state.reputation = data;
129
+ }).catch((err) => {
130
+ // Log persistence failures instead of silently swallowing
131
+ console.warn("[consensus] Reputation persistence failed:", err); // eslint-disable-line no-console
132
+ });
133
+ }
134
+ /** Load reputation scores from store (if configured). */
135
+ async load() {
136
+ if (!this.store)
137
+ return;
138
+ try {
139
+ const state = await this.store.getState();
140
+ const saved = state?.reputation;
141
+ if (saved) {
142
+ for (const [id, score] of Object.entries(saved)) {
143
+ if (this.scores.has(id)) {
144
+ this.scores.set(id, score);
145
+ }
146
+ }
147
+ }
148
+ }
149
+ catch {
150
+ // Load failure is non-fatal, start with defaults
151
+ }
152
+ }
153
+ }
154
+ //# sourceMappingURL=reputation-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reputation-manager.js","sourceRoot":"","sources":["../src/reputation-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=reputation-manager.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reputation-manager.test.d.ts","sourceRoot":"","sources":["../src/reputation-manager.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,111 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { ReputationManager } from "./reputation-manager.js";
3
+ function makePersonas() {
4
+ return [
5
+ { id: "p1", name: "Security", role: "security", reputation: 0.55 },
6
+ { id: "p2", name: "Compliance", role: "compliance", reputation: 0.55 },
7
+ { id: "p3", name: "Operations", role: "operations", reputation: 0.55 },
8
+ ];
9
+ }
10
+ function makeDecision(overrides) {
11
+ return {
12
+ decisionId: "dec_test",
13
+ action: "block",
14
+ votes: [
15
+ { personaId: "p1", personaName: "Security", vote: "NO", confidence: 0.9, rationale: "risky", source: "llm" },
16
+ { personaId: "p2", personaName: "Compliance", vote: "YES", confidence: 0.7, rationale: "ok", source: "llm" },
17
+ { personaId: "p3", personaName: "Operations", vote: "NO", confidence: 0.8, rationale: "risky", source: "llm" },
18
+ ],
19
+ policy: "MAJORITY_VOTE",
20
+ consensusTrace: {},
21
+ aggregateScore: 0.3,
22
+ ...overrides,
23
+ };
24
+ }
25
+ describe("ReputationManager", () => {
26
+ it("initializes all personas at 0.55", () => {
27
+ const mgr = new ReputationManager(makePersonas());
28
+ expect(mgr.getReputation("p1")).toBe(0.55);
29
+ expect(mgr.getReputation("p2")).toBe(0.55);
30
+ expect(mgr.getReputation("p3")).toBe(0.55);
31
+ });
32
+ it("returns 0.55 for unknown persona IDs", () => {
33
+ const mgr = new ReputationManager(makePersonas());
34
+ expect(mgr.getReputation("unknown")).toBe(0.55);
35
+ });
36
+ it("records decisions for feedback correlation", () => {
37
+ const mgr = new ReputationManager(makePersonas());
38
+ const decision = makeDecision();
39
+ mgr.recordDecision(decision);
40
+ // processFeedback should find this decision
41
+ const changes = mgr.processFeedback({
42
+ decisionId: "dec_test",
43
+ type: "override_block",
44
+ });
45
+ expect(changes.length).toBe(3);
46
+ });
47
+ it("updates reputation on override_block feedback", () => {
48
+ const mgr = new ReputationManager(makePersonas());
49
+ const decision = makeDecision();
50
+ mgr.recordDecision(decision);
51
+ // override_block means the block was wrong, ground truth is ALLOW
52
+ // p1 voted NO (misaligned with ALLOW), p2 voted YES (aligned), p3 voted NO (misaligned)
53
+ const changes = mgr.processFeedback({
54
+ decisionId: "dec_test",
55
+ type: "override_block",
56
+ });
57
+ // p2 (voted YES, aligned with ALLOW) should gain reputation
58
+ const p2Change = changes.find((c) => c.persona_id === "p2");
59
+ expect(p2Change.delta).toBeGreaterThan(0);
60
+ // p1 (voted NO, misaligned with ALLOW) should lose reputation
61
+ const p1Change = changes.find((c) => c.persona_id === "p1");
62
+ expect(p1Change.delta).toBeLessThan(0);
63
+ });
64
+ it("updates reputation on flag_miss feedback", () => {
65
+ const mgr = new ReputationManager(makePersonas());
66
+ const decision = makeDecision({ action: "allow" });
67
+ mgr.recordDecision(decision);
68
+ // flag_miss means the allow was wrong, ground truth is BLOCK
69
+ const changes = mgr.processFeedback({
70
+ decisionId: "dec_test",
71
+ type: "flag_miss",
72
+ });
73
+ // p1 and p3 (voted NO, aligned with BLOCK) should gain reputation
74
+ const p1Change = changes.find((c) => c.persona_id === "p1");
75
+ expect(p1Change.delta).toBeGreaterThan(0);
76
+ });
77
+ it("returns empty changes for unknown decision IDs", () => {
78
+ const mgr = new ReputationManager(makePersonas());
79
+ const changes = mgr.processFeedback({
80
+ decisionId: "nonexistent",
81
+ type: "override_block",
82
+ });
83
+ expect(changes).toEqual([]);
84
+ });
85
+ it("triggers respawn when reputation drops below threshold", () => {
86
+ const personas = makePersonas();
87
+ personas[0].reputation = 0.10; // Already below default threshold of 0.15
88
+ const mgr = new ReputationManager(personas, 0.15);
89
+ const respawnHandler = vi.fn();
90
+ mgr.setRespawnHandler(respawnHandler);
91
+ // Record + feedback to trigger checkRespawn
92
+ const decision = makeDecision();
93
+ mgr.recordDecision(decision);
94
+ mgr.processFeedback({ decisionId: "dec_test", type: "override_block" });
95
+ // p1 started at 0.10, misaligned feedback pushes it further down
96
+ // Respawn should have been triggered
97
+ if (mgr.getReputation("p1") < 0.15 || respawnHandler.mock.calls.length === 0) {
98
+ // Persona was replaced, check new personas list
99
+ const currentPersonas = mgr.getPersonas();
100
+ // Either the original was respawned or it's below threshold
101
+ expect(currentPersonas.length).toBe(3);
102
+ }
103
+ });
104
+ it("getPersonas returns copies", () => {
105
+ const mgr = new ReputationManager(makePersonas());
106
+ const p1 = mgr.getPersonas();
107
+ const p2 = mgr.getPersonas();
108
+ expect(p1).not.toBe(p2);
109
+ });
110
+ });
111
+ //# sourceMappingURL=reputation-manager.test.js.map