opencode-swarm-plugin 0.1.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,430 @@
1
+ /**
2
+ * Anti-Pattern Learning Module
3
+ *
4
+ * Tracks failed decomposition patterns and auto-inverts them to anti-patterns.
5
+ * When a pattern consistently fails, it gets flagged as something to avoid.
6
+ *
7
+ * @see https://github.com/Dicklesworthstone/cass_memory_system/blob/main/src/curate.ts#L95-L117
8
+ */
9
+ import { z } from "zod";
10
+
11
+ // ============================================================================
12
+ // Schemas
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Pattern kind - whether this is a positive pattern or an anti-pattern
17
+ */
18
+ export const PatternKindSchema = z.enum(["pattern", "anti_pattern"]);
19
+ export type PatternKind = z.infer<typeof PatternKindSchema>;
20
+
21
+ /**
22
+ * A decomposition pattern that has been observed
23
+ *
24
+ * Patterns are extracted from successful/failed decompositions and
25
+ * tracked over time to learn what works and what doesn't.
26
+ */
27
+ export const DecompositionPatternSchema = z.object({
28
+ /** Unique ID for this pattern */
29
+ id: z.string(),
30
+ /** Human-readable description of the pattern */
31
+ content: z.string(),
32
+ /** Whether this is a positive pattern or anti-pattern */
33
+ kind: PatternKindSchema,
34
+ /** Whether this pattern should be avoided (true for anti-patterns) */
35
+ is_negative: z.boolean(),
36
+ /** Number of times this pattern succeeded */
37
+ success_count: z.number().int().min(0).default(0),
38
+ /** Number of times this pattern failed */
39
+ failure_count: z.number().int().min(0).default(0),
40
+ /** When this pattern was first observed */
41
+ created_at: z.string(), // ISO-8601
42
+ /** When this pattern was last updated */
43
+ updated_at: z.string(), // ISO-8601
44
+ /** Context about why this pattern was created/inverted */
45
+ reason: z.string().optional(),
46
+ /** Tags for categorization (e.g., "file-splitting", "dependency-ordering") */
47
+ tags: z.array(z.string()).default([]),
48
+ /** Example bead IDs where this pattern was observed */
49
+ example_beads: z.array(z.string()).default([]),
50
+ });
51
+ export type DecompositionPattern = z.infer<typeof DecompositionPatternSchema>;
52
+
53
+ /**
54
+ * Result of pattern inversion
55
+ */
56
+ export const PatternInversionResultSchema = z.object({
57
+ /** The original pattern */
58
+ original: DecompositionPatternSchema,
59
+ /** The inverted anti-pattern */
60
+ inverted: DecompositionPatternSchema,
61
+ /** Why the inversion happened */
62
+ reason: z.string(),
63
+ });
64
+ export type PatternInversionResult = z.infer<
65
+ typeof PatternInversionResultSchema
66
+ >;
67
+
68
+ // ============================================================================
69
+ // Configuration
70
+ // ============================================================================
71
+
72
+ /**
73
+ * Configuration for anti-pattern detection
74
+ */
75
+ export interface AntiPatternConfig {
76
+ /** Minimum observations before considering inversion */
77
+ minObservations: number;
78
+ /** Failure ratio threshold for inversion (0-1) */
79
+ failureRatioThreshold: number;
80
+ /** Prefix for anti-pattern content */
81
+ antiPatternPrefix: string;
82
+ }
83
+
84
+ export const DEFAULT_ANTI_PATTERN_CONFIG: AntiPatternConfig = {
85
+ minObservations: 3,
86
+ failureRatioThreshold: 0.6, // 60% failure rate triggers inversion
87
+ antiPatternPrefix: "AVOID: ",
88
+ };
89
+
90
+ // ============================================================================
91
+ // Core Functions
92
+ // ============================================================================
93
+
94
+ /**
95
+ * Check if a pattern should be inverted to an anti-pattern
96
+ *
97
+ * A pattern is inverted when:
98
+ * 1. It has enough observations (minObservations)
99
+ * 2. Its failure ratio exceeds the threshold
100
+ *
101
+ * @param pattern - The pattern to check
102
+ * @param config - Anti-pattern configuration
103
+ * @returns Whether the pattern should be inverted
104
+ */
105
+ export function shouldInvertPattern(
106
+ pattern: DecompositionPattern,
107
+ config: AntiPatternConfig = DEFAULT_ANTI_PATTERN_CONFIG,
108
+ ): boolean {
109
+ // Already an anti-pattern
110
+ if (pattern.kind === "anti_pattern") {
111
+ return false;
112
+ }
113
+
114
+ const total = pattern.success_count + pattern.failure_count;
115
+
116
+ // Not enough observations
117
+ if (total < config.minObservations) {
118
+ return false;
119
+ }
120
+
121
+ const failureRatio = pattern.failure_count / total;
122
+ return failureRatio >= config.failureRatioThreshold;
123
+ }
124
+
125
+ /**
126
+ * Invert a pattern to an anti-pattern
127
+ *
128
+ * Creates a new anti-pattern from a failing pattern.
129
+ * The content is prefixed with "AVOID: " and the kind is changed.
130
+ *
131
+ * @param pattern - The pattern to invert
132
+ * @param reason - Why the inversion is happening
133
+ * @param config - Anti-pattern configuration
134
+ * @returns The inverted anti-pattern
135
+ */
136
+ export function invertToAntiPattern(
137
+ pattern: DecompositionPattern,
138
+ reason: string,
139
+ config: AntiPatternConfig = DEFAULT_ANTI_PATTERN_CONFIG,
140
+ ): PatternInversionResult {
141
+ // Clean the content (remove any existing prefix)
142
+ const cleaned = pattern.content
143
+ .replace(/^AVOID:\s*/i, "")
144
+ .replace(/^DO NOT:\s*/i, "")
145
+ .replace(/^NEVER:\s*/i, "");
146
+
147
+ const inverted: DecompositionPattern = {
148
+ ...pattern,
149
+ id: `anti-${pattern.id}`,
150
+ content: `${config.antiPatternPrefix}${cleaned}. ${reason}`,
151
+ kind: "anti_pattern",
152
+ is_negative: true,
153
+ reason,
154
+ updated_at: new Date().toISOString(),
155
+ };
156
+
157
+ return {
158
+ original: pattern,
159
+ inverted,
160
+ reason,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Record a pattern observation (success or failure)
166
+ *
167
+ * Updates the pattern's success/failure counts and checks if
168
+ * it should be inverted to an anti-pattern.
169
+ *
170
+ * @param pattern - The pattern to update
171
+ * @param success - Whether this observation was successful
172
+ * @param beadId - Optional bead ID to record as example
173
+ * @param config - Anti-pattern configuration
174
+ * @returns Updated pattern and optional inversion result
175
+ */
176
+ export function recordPatternObservation(
177
+ pattern: DecompositionPattern,
178
+ success: boolean,
179
+ beadId?: string,
180
+ config: AntiPatternConfig = DEFAULT_ANTI_PATTERN_CONFIG,
181
+ ): { pattern: DecompositionPattern; inversion?: PatternInversionResult } {
182
+ // Update counts
183
+ const updated: DecompositionPattern = {
184
+ ...pattern,
185
+ success_count: success ? pattern.success_count + 1 : pattern.success_count,
186
+ failure_count: success ? pattern.failure_count : pattern.failure_count + 1,
187
+ updated_at: new Date().toISOString(),
188
+ example_beads: beadId
189
+ ? [...pattern.example_beads.slice(-9), beadId] // Keep last 10
190
+ : pattern.example_beads,
191
+ };
192
+
193
+ // Check if should invert
194
+ if (shouldInvertPattern(updated, config)) {
195
+ const total = updated.success_count + updated.failure_count;
196
+ const failureRatio = updated.failure_count / total;
197
+ const reason = `Failed ${updated.failure_count}/${total} times (${Math.round(failureRatio * 100)}% failure rate)`;
198
+
199
+ return {
200
+ pattern: updated,
201
+ inversion: invertToAntiPattern(updated, reason, config),
202
+ };
203
+ }
204
+
205
+ return { pattern: updated };
206
+ }
207
+
208
+ /**
209
+ * Extract patterns from a decomposition description
210
+ *
211
+ * Looks for common decomposition strategies in the text.
212
+ *
213
+ * @param description - Decomposition description or reasoning
214
+ * @returns Extracted pattern descriptions
215
+ */
216
+ export function extractPatternsFromDescription(description: string): string[] {
217
+ const patterns: string[] = [];
218
+
219
+ // Common decomposition strategies to detect
220
+ const strategyPatterns = [
221
+ {
222
+ regex: /split(?:ting)?\s+by\s+file\s+type/i,
223
+ pattern: "Split by file type",
224
+ },
225
+ {
226
+ regex: /split(?:ting)?\s+by\s+component/i,
227
+ pattern: "Split by component",
228
+ },
229
+ {
230
+ regex: /split(?:ting)?\s+by\s+layer/i,
231
+ pattern: "Split by layer (UI/logic/data)",
232
+ },
233
+ { regex: /split(?:ting)?\s+by\s+feature/i, pattern: "Split by feature" },
234
+ {
235
+ regex: /one\s+file\s+per\s+(?:sub)?task/i,
236
+ pattern: "One file per subtask",
237
+ },
238
+ { regex: /shared\s+types?\s+first/i, pattern: "Handle shared types first" },
239
+ { regex: /api\s+(?:routes?)?\s+separate/i, pattern: "Separate API routes" },
240
+ {
241
+ regex: /tests?\s+(?:with|alongside)\s+(?:code|implementation)/i,
242
+ pattern: "Tests alongside implementation",
243
+ },
244
+ {
245
+ regex: /tests?\s+(?:in\s+)?separate\s+(?:sub)?task/i,
246
+ pattern: "Tests in separate subtask",
247
+ },
248
+ {
249
+ regex: /parallel(?:ize)?\s+(?:all|everything)/i,
250
+ pattern: "Maximize parallelization",
251
+ },
252
+ {
253
+ regex: /sequential\s+(?:order|execution)/i,
254
+ pattern: "Sequential execution order",
255
+ },
256
+ {
257
+ regex: /dependency\s+(?:chain|order)/i,
258
+ pattern: "Respect dependency chain",
259
+ },
260
+ ];
261
+
262
+ for (const { regex, pattern } of strategyPatterns) {
263
+ if (regex.test(description)) {
264
+ patterns.push(pattern);
265
+ }
266
+ }
267
+
268
+ return patterns;
269
+ }
270
+
271
+ /**
272
+ * Create a new pattern from a description
273
+ *
274
+ * @param content - Pattern description
275
+ * @param tags - Optional tags for categorization
276
+ * @returns New pattern
277
+ */
278
+ export function createPattern(
279
+ content: string,
280
+ tags: string[] = [],
281
+ ): DecompositionPattern {
282
+ const now = new Date().toISOString();
283
+ return {
284
+ id: `pattern-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
285
+ content,
286
+ kind: "pattern",
287
+ is_negative: false,
288
+ success_count: 0,
289
+ failure_count: 0,
290
+ created_at: now,
291
+ updated_at: now,
292
+ tags,
293
+ example_beads: [],
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Format anti-patterns for inclusion in decomposition prompts
299
+ *
300
+ * @param patterns - Anti-patterns to format
301
+ * @returns Formatted string for prompt inclusion
302
+ */
303
+ export function formatAntiPatternsForPrompt(
304
+ patterns: DecompositionPattern[],
305
+ ): string {
306
+ const antiPatterns = patterns.filter((p) => p.kind === "anti_pattern");
307
+
308
+ if (antiPatterns.length === 0) {
309
+ return "";
310
+ }
311
+
312
+ const lines = [
313
+ "## Anti-Patterns to Avoid",
314
+ "",
315
+ "Based on past failures, avoid these decomposition strategies:",
316
+ "",
317
+ ...antiPatterns.map((p) => `- ${p.content}`),
318
+ "",
319
+ ];
320
+
321
+ return lines.join("\n");
322
+ }
323
+
324
+ /**
325
+ * Format successful patterns for inclusion in decomposition prompts
326
+ *
327
+ * @param patterns - Patterns to format
328
+ * @param minSuccessRate - Minimum success rate to include (0-1)
329
+ * @returns Formatted string for prompt inclusion
330
+ */
331
+ export function formatSuccessfulPatternsForPrompt(
332
+ patterns: DecompositionPattern[],
333
+ minSuccessRate: number = 0.7,
334
+ ): string {
335
+ const successful = patterns.filter((p) => {
336
+ if (p.kind === "anti_pattern") return false;
337
+ const total = p.success_count + p.failure_count;
338
+ if (total < 2) return false;
339
+ return p.success_count / total >= minSuccessRate;
340
+ });
341
+
342
+ if (successful.length === 0) {
343
+ return "";
344
+ }
345
+
346
+ const lines = [
347
+ "## Successful Patterns",
348
+ "",
349
+ "These decomposition strategies have worked well in the past:",
350
+ "",
351
+ ...successful.map((p) => {
352
+ const total = p.success_count + p.failure_count;
353
+ const rate = Math.round((p.success_count / total) * 100);
354
+ return `- ${p.content} (${rate}% success rate)`;
355
+ }),
356
+ "",
357
+ ];
358
+
359
+ return lines.join("\n");
360
+ }
361
+
362
+ // ============================================================================
363
+ // Storage Interface
364
+ // ============================================================================
365
+
366
+ /**
367
+ * Storage interface for decomposition patterns
368
+ */
369
+ export interface PatternStorage {
370
+ /** Store or update a pattern */
371
+ store(pattern: DecompositionPattern): Promise<void>;
372
+ /** Get a pattern by ID */
373
+ get(id: string): Promise<DecompositionPattern | null>;
374
+ /** Get all patterns */
375
+ getAll(): Promise<DecompositionPattern[]>;
376
+ /** Get all anti-patterns */
377
+ getAntiPatterns(): Promise<DecompositionPattern[]>;
378
+ /** Get patterns by tag */
379
+ getByTag(tag: string): Promise<DecompositionPattern[]>;
380
+ /** Find patterns matching content */
381
+ findByContent(content: string): Promise<DecompositionPattern[]>;
382
+ }
383
+
384
+ /**
385
+ * In-memory pattern storage (for testing and short-lived sessions)
386
+ */
387
+ export class InMemoryPatternStorage implements PatternStorage {
388
+ private patterns: Map<string, DecompositionPattern> = new Map();
389
+
390
+ async store(pattern: DecompositionPattern): Promise<void> {
391
+ this.patterns.set(pattern.id, pattern);
392
+ }
393
+
394
+ async get(id: string): Promise<DecompositionPattern | null> {
395
+ return this.patterns.get(id) ?? null;
396
+ }
397
+
398
+ async getAll(): Promise<DecompositionPattern[]> {
399
+ return Array.from(this.patterns.values());
400
+ }
401
+
402
+ async getAntiPatterns(): Promise<DecompositionPattern[]> {
403
+ return Array.from(this.patterns.values()).filter(
404
+ (p) => p.kind === "anti_pattern",
405
+ );
406
+ }
407
+
408
+ async getByTag(tag: string): Promise<DecompositionPattern[]> {
409
+ return Array.from(this.patterns.values()).filter((p) =>
410
+ p.tags.includes(tag),
411
+ );
412
+ }
413
+
414
+ async findByContent(content: string): Promise<DecompositionPattern[]> {
415
+ const lower = content.toLowerCase();
416
+ return Array.from(this.patterns.values()).filter((p) =>
417
+ p.content.toLowerCase().includes(lower),
418
+ );
419
+ }
420
+ }
421
+
422
+ // ============================================================================
423
+ // Exports
424
+ // ============================================================================
425
+
426
+ export const antiPatternSchemas = {
427
+ PatternKindSchema,
428
+ DecompositionPatternSchema,
429
+ PatternInversionResultSchema,
430
+ };