opencode-swarm-plugin 0.15.0 → 0.17.0

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.
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Mandate Promotion Engine
3
+ *
4
+ * Handles state transitions for mandate entries based on vote scores:
5
+ * - candidate → established: net_votes >= 2
6
+ * - established → mandate: net_votes >= 5 AND vote_ratio >= 0.7
7
+ * - any → rejected: net_votes <= -3
8
+ *
9
+ * Integrates with pattern-maturity.ts decay calculations and state machine patterns.
10
+ */
11
+
12
+ import { DEFAULT_MANDATE_DECAY_CONFIG } from "./schemas/mandate";
13
+ import type {
14
+ MandateDecayConfig,
15
+ MandateEntry,
16
+ MandateScore,
17
+ MandateStatus,
18
+ } from "./schemas/mandate";
19
+
20
+ // ============================================================================
21
+ // Types
22
+ // ============================================================================
23
+
24
+ /**
25
+ * Result of a promotion evaluation
26
+ */
27
+ export interface PromotionResult {
28
+ /** The mandate entry ID */
29
+ mandate_id: string;
30
+ /** Status before evaluation */
31
+ previous_status: MandateStatus;
32
+ /** Status after evaluation */
33
+ new_status: MandateStatus;
34
+ /** Calculated score */
35
+ score: MandateScore;
36
+ /** Whether status changed */
37
+ promoted: boolean;
38
+ /** Human-readable reason for the transition (or lack thereof) */
39
+ reason: string;
40
+ }
41
+
42
+ // ============================================================================
43
+ // Core Functions
44
+ // ============================================================================
45
+
46
+ /**
47
+ * Determine new status based on score and current status
48
+ *
49
+ * State machine:
50
+ * - candidate → established: net_votes >= establishedNetVotesThreshold (2)
51
+ * - established → mandate: net_votes >= mandateNetVotesThreshold (5) AND vote_ratio >= mandateVoteRatioThreshold (0.7)
52
+ * - any → rejected: net_votes <= rejectedNetVotesThreshold (-3)
53
+ * - mandate stays mandate (no demotion)
54
+ * - rejected stays rejected (permanent)
55
+ *
56
+ * @param score - Calculated mandate score with decayed votes
57
+ * @param currentStatus - Current status of the mandate entry
58
+ * @param config - Threshold configuration
59
+ * @returns New status after applying transition rules
60
+ */
61
+ export function shouldPromote(
62
+ score: MandateScore,
63
+ currentStatus: MandateStatus,
64
+ config: MandateDecayConfig = DEFAULT_MANDATE_DECAY_CONFIG,
65
+ ): MandateStatus {
66
+ // Edge case: already rejected, stays rejected (permanent)
67
+ if (currentStatus === "rejected") {
68
+ return "rejected";
69
+ }
70
+
71
+ // Edge case: already mandate, stays mandate (no demotion)
72
+ if (currentStatus === "mandate") {
73
+ return "mandate";
74
+ }
75
+
76
+ // Now we know status is either "candidate" or "established"
77
+ // Check rejection threshold first
78
+ if (score.net_votes <= config.rejectedNetVotesThreshold) {
79
+ return "rejected";
80
+ }
81
+
82
+ // Check mandate promotion (from established only)
83
+ if (currentStatus === "established") {
84
+ if (
85
+ score.net_votes >= config.mandateNetVotesThreshold &&
86
+ score.vote_ratio >= config.mandateVoteRatioThreshold
87
+ ) {
88
+ return "mandate";
89
+ }
90
+ return "established"; // Stays established
91
+ }
92
+
93
+ // Now we know status is "candidate"
94
+ // Check established promotion
95
+ if (score.net_votes >= config.establishedNetVotesThreshold) {
96
+ return "established";
97
+ }
98
+
99
+ return "candidate"; // Stays candidate
100
+ }
101
+
102
+ /**
103
+ * Evaluate promotion for a mandate entry
104
+ *
105
+ * Main entry point for promotion logic. Calculates new status and provides
106
+ * detailed reasoning for the decision.
107
+ *
108
+ * @param entry - The mandate entry to evaluate
109
+ * @param score - Calculated score with decayed votes
110
+ * @param config - Threshold configuration (optional)
111
+ * @returns Promotion result with status change and reasoning
112
+ */
113
+ export function evaluatePromotion(
114
+ entry: MandateEntry,
115
+ score: MandateScore,
116
+ config: MandateDecayConfig = DEFAULT_MANDATE_DECAY_CONFIG,
117
+ ): PromotionResult {
118
+ const previousStatus = entry.status;
119
+ const newStatus = shouldPromote(score, previousStatus, config);
120
+ const promoted = newStatus !== previousStatus;
121
+
122
+ // Generate reason based on transition
123
+ let reason: string;
124
+
125
+ if (newStatus === "rejected" && previousStatus === "rejected") {
126
+ reason = `Remains rejected (permanent)`;
127
+ } else if (newStatus === "rejected") {
128
+ reason = `Rejected due to negative consensus (net_votes: ${score.net_votes.toFixed(2)} ≤ ${config.rejectedNetVotesThreshold})`;
129
+ } else if (newStatus === "mandate" && previousStatus === "mandate") {
130
+ reason = `Remains mandate (no demotion)`;
131
+ } else if (newStatus === "mandate" && previousStatus === "established") {
132
+ reason = `Promoted to mandate (net_votes: ${score.net_votes.toFixed(2)} ≥ ${config.mandateNetVotesThreshold}, ratio: ${score.vote_ratio.toFixed(2)} ≥ ${config.mandateVoteRatioThreshold})`;
133
+ } else if (newStatus === "established" && previousStatus === "established") {
134
+ reason = `Remains established (net_votes: ${score.net_votes.toFixed(2)}, ratio: ${score.vote_ratio.toFixed(2)} below mandate threshold)`;
135
+ } else if (newStatus === "established" && previousStatus === "candidate") {
136
+ reason = `Promoted to established (net_votes: ${score.net_votes.toFixed(2)} ≥ ${config.establishedNetVotesThreshold})`;
137
+ } else if (newStatus === "candidate") {
138
+ reason = `Remains candidate (net_votes: ${score.net_votes.toFixed(2)} below threshold)`;
139
+ } else {
140
+ reason = `No status change (current: ${previousStatus})`;
141
+ }
142
+
143
+ return {
144
+ mandate_id: entry.id,
145
+ previous_status: previousStatus,
146
+ new_status: newStatus,
147
+ score,
148
+ promoted,
149
+ reason,
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Format promotion result for logging or display
155
+ *
156
+ * @param result - Promotion result
157
+ * @returns Formatted string
158
+ */
159
+ export function formatPromotionResult(result: PromotionResult): string {
160
+ const arrow = result.promoted
161
+ ? `${result.previous_status} → ${result.new_status}`
162
+ : result.new_status;
163
+
164
+ return `[${result.mandate_id}] ${arrow}: ${result.reason}`;
165
+ }
166
+
167
+ /**
168
+ * Batch evaluate promotions for multiple entries
169
+ *
170
+ * Useful for periodic recalculation of all mandate statuses.
171
+ *
172
+ * @param entries - Map of mandate IDs to entries
173
+ * @param scores - Map of mandate IDs to scores
174
+ * @param config - Threshold configuration (optional)
175
+ * @returns Array of promotion results
176
+ */
177
+ export function evaluateBatchPromotions(
178
+ entries: Map<string, MandateEntry>,
179
+ scores: Map<string, MandateScore>,
180
+ config: MandateDecayConfig = DEFAULT_MANDATE_DECAY_CONFIG,
181
+ ): PromotionResult[] {
182
+ const results: PromotionResult[] = [];
183
+
184
+ for (const [id, entry] of entries) {
185
+ const score = scores.get(id);
186
+ if (!score) {
187
+ // Skip entries without scores
188
+ continue;
189
+ }
190
+
191
+ const result = evaluatePromotion(entry, score, config);
192
+ results.push(result);
193
+ }
194
+
195
+ return results;
196
+ }
197
+
198
+ /**
199
+ * Get entries that changed status (promoted or demoted)
200
+ *
201
+ * Useful for filtering batch results to only show changes.
202
+ *
203
+ * @param results - Promotion results
204
+ * @returns Only the results where status changed
205
+ */
206
+ export function getStatusChanges(
207
+ results: PromotionResult[],
208
+ ): PromotionResult[] {
209
+ return results.filter((r) => r.promoted);
210
+ }
211
+
212
+ /**
213
+ * Group promotion results by status transition
214
+ *
215
+ * Useful for analytics and reporting.
216
+ *
217
+ * @param results - Promotion results
218
+ * @returns Map of transition keys (e.g., "candidate→established") to results
219
+ */
220
+ export function groupByTransition(
221
+ results: PromotionResult[],
222
+ ): Map<string, PromotionResult[]> {
223
+ const groups = new Map<string, PromotionResult[]>();
224
+
225
+ for (const result of results) {
226
+ const key = result.promoted
227
+ ? `${result.previous_status}→${result.new_status}`
228
+ : result.new_status;
229
+
230
+ const existing = groups.get(key);
231
+ if (existing) {
232
+ existing.push(result);
233
+ } else {
234
+ groups.set(key, [result]);
235
+ }
236
+ }
237
+
238
+ return groups;
239
+ }