thevoidforge 21.0.11 → 21.0.12

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 (107) hide show
  1. package/dist/.claude/commands/ai.md +69 -0
  2. package/dist/.claude/commands/architect.md +121 -0
  3. package/dist/.claude/commands/assemble.md +201 -0
  4. package/dist/.claude/commands/assess.md +75 -0
  5. package/dist/.claude/commands/blueprint.md +135 -0
  6. package/dist/.claude/commands/build.md +116 -0
  7. package/dist/.claude/commands/campaign.md +201 -0
  8. package/dist/.claude/commands/cultivation.md +166 -0
  9. package/dist/.claude/commands/current.md +128 -0
  10. package/dist/.claude/commands/dangerroom.md +74 -0
  11. package/dist/.claude/commands/debrief.md +178 -0
  12. package/dist/.claude/commands/deploy.md +99 -0
  13. package/dist/.claude/commands/devops.md +143 -0
  14. package/dist/.claude/commands/gauntlet.md +140 -0
  15. package/dist/.claude/commands/git.md +104 -0
  16. package/dist/.claude/commands/grow.md +146 -0
  17. package/dist/.claude/commands/imagine.md +126 -0
  18. package/dist/.claude/commands/portfolio.md +50 -0
  19. package/dist/.claude/commands/prd.md +113 -0
  20. package/dist/.claude/commands/qa.md +107 -0
  21. package/dist/.claude/commands/review.md +151 -0
  22. package/dist/.claude/commands/security.md +100 -0
  23. package/dist/.claude/commands/test.md +96 -0
  24. package/dist/.claude/commands/thumper.md +116 -0
  25. package/dist/.claude/commands/treasury.md +100 -0
  26. package/dist/.claude/commands/ux.md +118 -0
  27. package/dist/.claude/commands/vault.md +189 -0
  28. package/dist/.claude/commands/void.md +108 -0
  29. package/dist/CHANGELOG.md +1918 -0
  30. package/dist/CLAUDE.md +250 -0
  31. package/dist/HOLOCRON.md +856 -0
  32. package/dist/VERSION.md +123 -0
  33. package/dist/docs/NAMING_REGISTRY.md +478 -0
  34. package/dist/docs/methods/AI_INTELLIGENCE.md +276 -0
  35. package/dist/docs/methods/ASSEMBLER.md +142 -0
  36. package/dist/docs/methods/BACKEND_ENGINEER.md +165 -0
  37. package/dist/docs/methods/BUILD_JOURNAL.md +185 -0
  38. package/dist/docs/methods/BUILD_PROTOCOL.md +426 -0
  39. package/dist/docs/methods/CAMPAIGN.md +568 -0
  40. package/dist/docs/methods/CONTEXT_MANAGEMENT.md +189 -0
  41. package/dist/docs/methods/DEEP_CURRENT.md +184 -0
  42. package/dist/docs/methods/DEVOPS_ENGINEER.md +295 -0
  43. package/dist/docs/methods/FIELD_MEDIC.md +261 -0
  44. package/dist/docs/methods/FORGE_ARTIST.md +108 -0
  45. package/dist/docs/methods/FORGE_KEEPER.md +268 -0
  46. package/dist/docs/methods/GAUNTLET.md +344 -0
  47. package/dist/docs/methods/GROWTH_STRATEGIST.md +466 -0
  48. package/dist/docs/methods/HEARTBEAT.md +168 -0
  49. package/dist/docs/methods/MCP_INTEGRATION.md +139 -0
  50. package/dist/docs/methods/MUSTER.md +148 -0
  51. package/dist/docs/methods/PRD_GENERATOR.md +186 -0
  52. package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +250 -0
  53. package/dist/docs/methods/QA_ENGINEER.md +337 -0
  54. package/dist/docs/methods/RELEASE_MANAGER.md +145 -0
  55. package/dist/docs/methods/SECURITY_AUDITOR.md +320 -0
  56. package/dist/docs/methods/SUB_AGENTS.md +335 -0
  57. package/dist/docs/methods/SYSTEMS_ARCHITECT.md +171 -0
  58. package/dist/docs/methods/TESTING.md +359 -0
  59. package/dist/docs/methods/THUMPER.md +175 -0
  60. package/dist/docs/methods/TIME_VAULT.md +120 -0
  61. package/dist/docs/methods/TREASURY.md +184 -0
  62. package/dist/docs/methods/TROUBLESHOOTING.md +265 -0
  63. package/dist/docs/patterns/README.md +52 -0
  64. package/dist/docs/patterns/ad-billing-adapter.ts +537 -0
  65. package/dist/docs/patterns/ad-platform-adapter.ts +421 -0
  66. package/dist/docs/patterns/ai-classifier.ts +195 -0
  67. package/dist/docs/patterns/ai-eval.ts +272 -0
  68. package/dist/docs/patterns/ai-orchestrator.ts +341 -0
  69. package/dist/docs/patterns/ai-router.ts +194 -0
  70. package/dist/docs/patterns/ai-tool-schema.ts +237 -0
  71. package/dist/docs/patterns/api-route.ts +241 -0
  72. package/dist/docs/patterns/backtest-engine.ts +499 -0
  73. package/dist/docs/patterns/browser-review.ts +292 -0
  74. package/dist/docs/patterns/combobox.tsx +300 -0
  75. package/dist/docs/patterns/component.tsx +262 -0
  76. package/dist/docs/patterns/daemon-process.ts +338 -0
  77. package/dist/docs/patterns/data-pipeline.ts +297 -0
  78. package/dist/docs/patterns/database-migration.ts +466 -0
  79. package/dist/docs/patterns/e2e-test.ts +629 -0
  80. package/dist/docs/patterns/error-handling.ts +312 -0
  81. package/dist/docs/patterns/execution-safety.ts +601 -0
  82. package/dist/docs/patterns/financial-transaction.ts +342 -0
  83. package/dist/docs/patterns/funding-plan.ts +462 -0
  84. package/dist/docs/patterns/game-entity.ts +137 -0
  85. package/dist/docs/patterns/game-loop.ts +113 -0
  86. package/dist/docs/patterns/game-state.ts +143 -0
  87. package/dist/docs/patterns/job-queue.ts +225 -0
  88. package/dist/docs/patterns/kongo-integration.ts +164 -0
  89. package/dist/docs/patterns/middleware.ts +363 -0
  90. package/dist/docs/patterns/mobile-screen.tsx +139 -0
  91. package/dist/docs/patterns/mobile-service.ts +167 -0
  92. package/dist/docs/patterns/multi-tenant.ts +382 -0
  93. package/dist/docs/patterns/oauth-token-lifecycle.ts +223 -0
  94. package/dist/docs/patterns/outbound-rate-limiter.ts +260 -0
  95. package/dist/docs/patterns/prompt-template.ts +195 -0
  96. package/dist/docs/patterns/revenue-source-adapter.ts +311 -0
  97. package/dist/docs/patterns/service.ts +224 -0
  98. package/dist/docs/patterns/sse-endpoint.ts +118 -0
  99. package/dist/docs/patterns/stablecoin-adapter.ts +511 -0
  100. package/dist/docs/patterns/third-party-script.ts +68 -0
  101. package/dist/scripts/thumper/gom-jabbar.sh +241 -0
  102. package/dist/scripts/thumper/relay.sh +610 -0
  103. package/dist/scripts/thumper/scan.sh +359 -0
  104. package/dist/scripts/thumper/thumper.sh +190 -0
  105. package/dist/scripts/thumper/water-rings.sh +76 -0
  106. package/package.json +1 -1
  107. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Pattern: Fixed-Timestep Game Loop
3
+ *
4
+ * Key principles:
5
+ * - Fixed timestep for physics/logic (deterministic, reproducible)
6
+ * - Variable rendering (render as fast as possible, interpolate between states)
7
+ * - Frame budget tracking (detect when logic takes too long)
8
+ * - Pause/resume without state corruption
9
+ * - requestAnimationFrame for browser, custom loop for Node/native
10
+ *
11
+ * Agents: Spike-GameDev (architecture), L-Profiler (frame budget), Gimli (performance)
12
+ *
13
+ * Engine adaptations:
14
+ * Phaser: this.time.addEvent() for fixed timestep, scene.update() for variable
15
+ * Godot: _physics_process(delta) for fixed, _process(delta) for variable
16
+ * Unity: FixedUpdate() for fixed, Update() for variable
17
+ * Three.js: This file (manual loop with clock.getDelta())
18
+ */
19
+
20
+ // --- Configuration ---
21
+ const FIXED_TIMESTEP = 1 / 60 // 60 Hz physics/logic
22
+ const MAX_FRAME_TIME = 0.25 // Cap to prevent spiral of death
23
+ const FRAME_BUDGET_MS = 16.67 // Target: 60 FPS
24
+
25
+ // --- Game State ---
26
+ interface GameState {
27
+ paused: boolean
28
+ time: number // Total elapsed game time (paused time excluded)
29
+ frameCount: number
30
+ entities: Entity[]
31
+ }
32
+
33
+ interface Entity {
34
+ x: number
35
+ y: number
36
+ prevX: number // Previous position for interpolation
37
+ prevY: number
38
+ vx: number
39
+ vy: number
40
+ update(dt: number): void
41
+ }
42
+
43
+ // --- The Loop ---
44
+
45
+ let accumulator = 0
46
+ let lastTime = 0
47
+ let state: GameState = { paused: false, time: 0, frameCount: 0, entities: [] }
48
+
49
+ function gameLoop(currentTime: number): void {
50
+ if (state.paused) {
51
+ lastTime = currentTime // Reset so unpause doesn't cause a time jump
52
+ requestAnimationFrame(gameLoop)
53
+ return
54
+ }
55
+
56
+ let frameTime = (currentTime - lastTime) / 1000 // Convert ms to seconds
57
+ lastTime = currentTime
58
+
59
+ // Cap frame time to prevent spiral of death (e.g., tab was backgrounded)
60
+ if (frameTime > MAX_FRAME_TIME) frameTime = MAX_FRAME_TIME
61
+
62
+ accumulator += frameTime
63
+
64
+ // --- Fixed timestep: update logic/physics ---
65
+ while (accumulator >= FIXED_TIMESTEP) {
66
+ // Save previous positions for interpolation
67
+ for (const entity of state.entities) {
68
+ entity.prevX = entity.x
69
+ entity.prevY = entity.y
70
+ }
71
+
72
+ // Update at fixed rate
73
+ for (const entity of state.entities) {
74
+ entity.update(FIXED_TIMESTEP)
75
+ }
76
+
77
+ state.time += FIXED_TIMESTEP
78
+ accumulator -= FIXED_TIMESTEP
79
+ }
80
+
81
+ // --- Variable rendering: interpolate between states ---
82
+ const alpha = accumulator / FIXED_TIMESTEP // 0.0 to 1.0
83
+
84
+ for (const entity of state.entities) {
85
+ const renderX = entity.prevX + (entity.x - entity.prevX) * alpha
86
+ const renderY = entity.prevY + (entity.y - entity.prevY) * alpha
87
+ // render(entity, renderX, renderY)
88
+ }
89
+
90
+ // --- Frame budget tracking ---
91
+ const frameEndTime = performance.now()
92
+ const frameDuration = frameEndTime - currentTime
93
+ if (frameDuration > FRAME_BUDGET_MS) {
94
+ console.warn(`Frame ${state.frameCount} exceeded budget: ${frameDuration.toFixed(1)}ms`)
95
+ // L-Profiler flags: investigate which entities/systems took too long
96
+ }
97
+
98
+ state.frameCount++
99
+ requestAnimationFrame(gameLoop)
100
+ }
101
+
102
+ // --- Pause/Resume ---
103
+ function pause(): void { state.paused = true }
104
+ function resume(): void { state.paused = false }
105
+
106
+ // --- Start ---
107
+ function startGame(): void {
108
+ lastTime = performance.now()
109
+ requestAnimationFrame(gameLoop)
110
+ }
111
+
112
+ export { gameLoop, startGame, pause, resume, FIXED_TIMESTEP }
113
+ export type { GameState, Entity }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Pattern: Hierarchical Game State Machine
3
+ *
4
+ * Key principles:
5
+ * - Every game has states: Menu → Playing → Paused → GameOver (minimum)
6
+ * - Transitions have enter/exit hooks (load assets on enter, clean up on exit)
7
+ * - States can have sub-states (Playing → Cutscene, Playing → Dialog)
8
+ * - History: can return to previous state (Pause → resume to Playing, not Menu)
9
+ * - Serializable: save/load works by serializing the state stack
10
+ *
11
+ * Agents: Spike-GameDev (architecture), Éowyn-GameFeel (transitions), Constantine (state bugs)
12
+ *
13
+ * Engine adaptations:
14
+ * Phaser: this.scene.start('GameOver'), this.scene.pause('Playing')
15
+ * Godot: SceneTree.change_scene(), custom state machine node
16
+ * Unity: SceneManager.LoadScene(), or custom FSM MonoBehaviour
17
+ */
18
+
19
+ // --- State Definition ---
20
+ interface GameStateConfig {
21
+ name: string
22
+ enter?: () => void | Promise<void> // Called when entering this state
23
+ exit?: () => void | Promise<void> // Called when leaving this state
24
+ update?: (dt: number) => void // Called every frame while active
25
+ handleInput?: (input: GameInput) => void
26
+ }
27
+
28
+ interface GameInput {
29
+ type: 'keydown' | 'keyup' | 'click' | 'gamepad'
30
+ key?: string
31
+ button?: number
32
+ x?: number
33
+ y?: number
34
+ }
35
+
36
+ // --- State Machine ---
37
+ class StateMachine {
38
+ private states: Map<string, GameStateConfig> = new Map()
39
+ private stack: string[] = [] // State stack for history
40
+ private current: GameStateConfig | null = null
41
+
42
+ register(config: GameStateConfig): void {
43
+ this.states.set(config.name, config)
44
+ }
45
+
46
+ get currentState(): string | null {
47
+ return this.current?.name ?? null
48
+ }
49
+
50
+ async transition(stateName: string): Promise<void> {
51
+ const next = this.states.get(stateName)
52
+ if (!next) throw new Error(`Unknown state: ${stateName}`)
53
+
54
+ // Exit current state
55
+ if (this.current?.exit) await this.current.exit()
56
+
57
+ // Push to history
58
+ if (this.current) this.stack.push(this.current.name)
59
+
60
+ // Enter new state
61
+ this.current = next
62
+ if (this.current.enter) await this.current.enter()
63
+ }
64
+
65
+ async back(): Promise<void> {
66
+ if (this.stack.length === 0) return
67
+
68
+ const previousName = this.stack.pop()!
69
+ const previous = this.states.get(previousName)
70
+ if (!previous) return
71
+
72
+ if (this.current?.exit) await this.current.exit()
73
+ this.current = previous
74
+ if (this.current.enter) await this.current.enter()
75
+ }
76
+
77
+ update(dt: number): void {
78
+ if (this.current?.update) this.current.update(dt)
79
+ }
80
+
81
+ handleInput(input: GameInput): void {
82
+ if (this.current?.handleInput) this.current.handleInput(input)
83
+ }
84
+
85
+ // Serialize for save/load
86
+ serialize(): { current: string | null; stack: string[] } {
87
+ return { current: this.current?.name ?? null, stack: [...this.stack] }
88
+ }
89
+
90
+ async restore(saved: { current: string | null; stack: string[] }): Promise<void> {
91
+ this.stack = [...saved.stack]
92
+ if (saved.current) {
93
+ this.current = this.states.get(saved.current) ?? null
94
+ if (this.current?.enter) await this.current.enter()
95
+ }
96
+ }
97
+ }
98
+
99
+ // --- Usage Example ---
100
+ /*
101
+ const sm = new StateMachine()
102
+
103
+ sm.register({
104
+ name: 'menu',
105
+ enter: () => showMainMenu(),
106
+ handleInput: (input) => {
107
+ if (input.key === 'Enter') sm.transition('playing')
108
+ }
109
+ })
110
+
111
+ sm.register({
112
+ name: 'playing',
113
+ enter: () => startLevel(),
114
+ exit: () => cleanupLevel(),
115
+ update: (dt) => updateGameplay(dt),
116
+ handleInput: (input) => {
117
+ if (input.key === 'Escape') sm.transition('paused')
118
+ if (playerDead) sm.transition('gameover')
119
+ }
120
+ })
121
+
122
+ sm.register({
123
+ name: 'paused',
124
+ enter: () => showPauseMenu(),
125
+ handleInput: (input) => {
126
+ if (input.key === 'Escape') sm.back() // Resume playing, not go to menu
127
+ }
128
+ })
129
+
130
+ sm.register({
131
+ name: 'gameover',
132
+ enter: () => showGameOverScreen(),
133
+ handleInput: (input) => {
134
+ if (input.key === 'Enter') sm.transition('menu')
135
+ }
136
+ })
137
+
138
+ // Start the game
139
+ await sm.transition('menu')
140
+ */
141
+
142
+ export { StateMachine }
143
+ export type { GameStateConfig, GameInput }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Pattern: Background Job / Queue Worker
3
+ *
4
+ * Key principles:
5
+ * - Jobs are idempotent — running the same job twice produces the same result
6
+ * - Idempotency keys prevent duplicate processing
7
+ * - Retry with exponential backoff for transient failures
8
+ * - Dead letter queue for permanently failed jobs
9
+ * - Minimal payloads — pass IDs, fetch fresh data in the handler
10
+ * - Graceful shutdown — finish current job before stopping
11
+ *
12
+ * Agents: Thor (queue engineer), Barton (error handling), L (monitoring)
13
+ *
14
+ * Framework adaptations:
15
+ * Next.js/Node: BullMQ + Redis, or Inngest for serverless
16
+ * Django: Celery + Redis/RabbitMQ
17
+ * Rails: Sidekiq + Redis, or ActiveJob
18
+ * Express: BullMQ + Redis, or Agenda + MongoDB
19
+ *
20
+ * === Django + Celery Deep Dive ===
21
+ *
22
+ * # app/tasks.py
23
+ * from celery import shared_task
24
+ * from .services import EmailService
25
+ *
26
+ * @shared_task(bind=True, max_retries=3, default_retry_delay=60)
27
+ * def send_welcome_email(self, user_id: int):
28
+ * try:
29
+ * EmailService.send_welcome(user_id)
30
+ * except Exception as exc:
31
+ * self.retry(exc=exc)
32
+ *
33
+ * # Idempotency: use task_id or a dedupe key in the DB to prevent re-processing
34
+ * # Dead letter: configure task_reject_on_worker_lost=True + dlx in broker
35
+ * # Monitoring: Flower (celery monitor) or Sentry integration
36
+ * # Beat scheduler: periodic tasks via django-celery-beat
37
+ *
38
+ * === FastAPI + ARQ Deep Dive ===
39
+ *
40
+ * # app/tasks.py
41
+ * from arq import create_pool
42
+ * from arq.connections import RedisSettings
43
+ *
44
+ * async def send_welcome_email(ctx, user_id: int):
45
+ * await EmailService.send_welcome(user_id)
46
+ *
47
+ * class WorkerSettings:
48
+ * functions = [send_welcome_email]
49
+ * redis_settings = RedisSettings()
50
+ * max_tries = 3
51
+ * retry_delay = 60
52
+ *
53
+ * # ARQ is async-native (matches FastAPI). Alternative: Celery still works with FastAPI.
54
+ * # Same principles: idempotency, retry with backoff, dead letter, monitoring.
55
+ */
56
+
57
+ // --- Job definition (enqueue side) ---
58
+
59
+ import { Queue } from 'bullmq'
60
+ import { redis } from '@/lib/redis'
61
+
62
+ // One queue per job domain (not one per job type)
63
+ const emailQueue = new Queue('email', { connection: redis })
64
+
65
+ // Enqueue with idempotency key to prevent duplicates
66
+ export async function enqueueWelcomeEmail(userId: string) {
67
+ await emailQueue.add(
68
+ 'welcome-email',
69
+ { userId }, // Minimal payload — just the ID
70
+ {
71
+ jobId: `welcome-email:${userId}`, // Idempotency key
72
+ attempts: 3,
73
+ backoff: {
74
+ type: 'exponential',
75
+ delay: 1000, // 1s, 2s, 4s
76
+ },
77
+ removeOnComplete: { age: 86400 }, // Clean up after 24h
78
+ removeOnFail: { age: 604800 }, // Keep failures for 7 days
79
+ }
80
+ )
81
+ }
82
+
83
+ // --- Job handler (worker side) ---
84
+
85
+ import { Worker } from 'bullmq'
86
+ import { db } from '@/lib/db'
87
+ import { emailService } from '@/lib/services/email'
88
+
89
+ const emailWorker = new Worker(
90
+ 'email',
91
+ async (job) => {
92
+ const { userId } = job.data
93
+
94
+ // Fetch fresh data — don't trust stale payload
95
+ const user = await db.user.findUnique({ where: { id: userId } })
96
+
97
+ if (!user) {
98
+ // User was deleted between enqueue and processing — skip, don't retry
99
+ console.log(JSON.stringify({
100
+ event: 'job.skipped',
101
+ queue: 'email',
102
+ job: job.name,
103
+ jobId: job.id,
104
+ reason: 'user_not_found',
105
+ userId,
106
+ }))
107
+ return
108
+ }
109
+
110
+ // Idempotency check — has this already been sent?
111
+ const alreadySent = await db.emailLog.findFirst({
112
+ where: { userId, type: 'welcome', sentAt: { not: null } },
113
+ })
114
+
115
+ if (alreadySent) {
116
+ console.log(JSON.stringify({
117
+ event: 'job.skipped',
118
+ queue: 'email',
119
+ job: job.name,
120
+ jobId: job.id,
121
+ reason: 'already_sent',
122
+ userId,
123
+ }))
124
+ return
125
+ }
126
+
127
+ // Do the work
128
+ await emailService.sendWelcome(user)
129
+
130
+ // Record that it was sent (for idempotency on retry)
131
+ await db.emailLog.create({
132
+ data: { userId, type: 'welcome', sentAt: new Date() },
133
+ })
134
+
135
+ // Structured log for monitoring (L — observability)
136
+ console.log(JSON.stringify({
137
+ event: 'job.completed',
138
+ queue: 'email',
139
+ job: job.name,
140
+ jobId: job.id,
141
+ userId,
142
+ duration: Date.now() - job.timestamp,
143
+ }))
144
+ },
145
+ {
146
+ connection: redis,
147
+ concurrency: 5, // Max parallel jobs
148
+ limiter: {
149
+ max: 10,
150
+ duration: 1000, // Rate limit: 10 jobs/sec
151
+ },
152
+ }
153
+ )
154
+
155
+ // --- Error handling ---
156
+
157
+ emailWorker.on('failed', (job, error) => {
158
+ console.error(JSON.stringify({
159
+ event: 'job.failed',
160
+ queue: 'email',
161
+ job: job?.name,
162
+ jobId: job?.id,
163
+ attempt: job?.attemptsMade,
164
+ maxAttempts: job?.opts.attempts,
165
+ error: error.message,
166
+ // If max attempts reached, this goes to the dead letter queue
167
+ deadLettered: job?.attemptsMade === job?.opts.attempts,
168
+ }))
169
+ })
170
+
171
+ // --- Graceful shutdown ---
172
+
173
+ async function shutdown() {
174
+ console.log('Shutting down email worker...')
175
+ await emailWorker.close() // Finish current job, stop accepting new ones
176
+ process.exit(0)
177
+ }
178
+
179
+ process.on('SIGTERM', shutdown)
180
+ process.on('SIGINT', shutdown)
181
+
182
+ // --- Django equivalent (Celery) ---
183
+ /*
184
+ # tasks/email.py
185
+ from celery import shared_task
186
+ from django.core.mail import send_mail
187
+ from users.models import User, EmailLog
188
+
189
+ @shared_task(
190
+ bind=True,
191
+ max_retries=3,
192
+ default_retry_delay=60,
193
+ acks_late=True, # Acknowledge after completion, not before
194
+ )
195
+ def send_welcome_email(self, user_id: str):
196
+ try:
197
+ user = User.objects.get(id=user_id)
198
+ except User.DoesNotExist:
199
+ return # User deleted — skip
200
+
201
+ if EmailLog.objects.filter(user=user, type='welcome').exists():
202
+ return # Already sent — idempotent
203
+
204
+ send_mail(subject='Welcome', message='...', recipient_list=[user.email])
205
+ EmailLog.objects.create(user=user, type='welcome')
206
+ */
207
+
208
+ // --- Rails equivalent (Sidekiq) ---
209
+ /*
210
+ # app/jobs/welcome_email_job.rb
211
+ class WelcomeEmailJob
212
+ include Sidekiq::Job
213
+ sidekiq_options retry: 3, queue: 'email', unique: :until_executed
214
+
215
+ def perform(user_id)
216
+ user = User.find_by(id: user_id)
217
+ return unless user # Deleted — skip
218
+
219
+ return if EmailLog.exists?(user: user, email_type: 'welcome') # Idempotent
220
+
221
+ UserMailer.welcome(user).deliver_now
222
+ EmailLog.create!(user: user, email_type: 'welcome', sent_at: Time.current)
223
+ end
224
+ end
225
+ */
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Pattern: Kongo Integration
3
+ * When to use: Integrating an external landing page engine with growth/campaign systems
4
+ * Covers: authenticated client, from-PRD page generation, growth signal, webhook handling
5
+ *
6
+ * This pattern demonstrates first-party API integration for a product the team owns.
7
+ * Unlike adapter patterns (ad-platform-adapter.ts), there's no abstraction layer —
8
+ * the client talks directly to the external API with typed request/response shapes.
9
+ *
10
+ * Architecture: ADR-036
11
+ *
12
+ * Framework adaptations:
13
+ * - Next.js: Webhook route at /api/webhooks/kongo
14
+ * - Express: Router mounted at /webhooks/kongo
15
+ * - Django/DRF: View at /api/webhooks/kongo/ with raw body parsing
16
+ * - FastAPI: POST endpoint with Request.body() for raw access
17
+ */
18
+
19
+ // ── Section 1: Client Interface ──────────────────────────
20
+ //
21
+ // Authenticated HTTP client with rate limiting.
22
+ // API key stored in financial vault (never .env).
23
+
24
+ interface KongoClientConfig {
25
+ apiKey: string; // ke_live_ prefix, vault-sourced
26
+ baseUrl?: string; // Default: https://kongo.io/api/v1
27
+ timeoutMs?: number; // Default: 30_000
28
+ rateLimitPerMinute?: number; // Default: 60 (client-side safety)
29
+ }
30
+
31
+ // Client methods: get<T>, post<T>, put<T>, patch<T>, delete<T>
32
+ // All return parsed response data (envelope unwrapped).
33
+ // Automatic retry on 429/503 with exponential backoff (3 attempts).
34
+ // Rate limiter: sliding window, throws KongoApiError on exhaustion.
35
+
36
+ // Error type extends the standard ApiError pattern:
37
+ class KongoApiError extends Error {
38
+ constructor(
39
+ public code: string, // Kongo error code (UNAUTHORIZED, RATE_LIMITED, etc.)
40
+ message: string,
41
+ public status: number, // HTTP status
42
+ public retryable: boolean,
43
+ public retryAfterMs?: number,
44
+ ) {
45
+ super(message);
46
+ }
47
+ }
48
+
49
+ // ── Section 2: From-PRD Page Generation ──────────────────
50
+ //
51
+ // Maps PRD content to Kongo's page creation API.
52
+ // No custom endpoint needed — uses existing POST /engine/pages
53
+ // with the `brief` field for structured input.
54
+
55
+ interface PrdSeedContent {
56
+ projectName: string;
57
+ headline: string;
58
+ subheadline: string;
59
+ valueProps: string[]; // 3-5 bullet points
60
+ ctaText: string;
61
+ ctaUrl: string;
62
+ brandColors: { primary: string; secondary: string; accent: string };
63
+ logoUrl?: string;
64
+ socialProof?: string[];
65
+ campaignId?: string; // VoidForge campaign ID for UTM linking
66
+ platform?: string; // Target ad platform (affects page style)
67
+ }
68
+
69
+ // Seed → Kongo mapping:
70
+ // PrdSeedContent.projectName → CreatePageRequest.companyName
71
+ // PrdSeedContent.* → CreatePageRequest.brief (structured)
72
+ // PrdSeedContent.* → CreatePageRequest.content (flattened text)
73
+ // PrdSeedContent.brandColors → CreatePageRequest.style.colors
74
+ // Always: template='landing-page', hosted=true
75
+ // Metadata: source='voidforge', projectName, campaignId, platform
76
+
77
+ // Polling: awaitPage() polls GET /engine/pages/:id every 3s until READY.
78
+ // Timeout: 120s default. Throws KongoApiError on ERROR status.
79
+
80
+ // ── Section 3: Growth Signal ─────────────────────────────
81
+ //
82
+ // Kongo doesn't have a dedicated growth-signal endpoint.
83
+ // Compute it client-side from campaign analytics.
84
+
85
+ interface ComputedGrowthSignal {
86
+ campaignId: string;
87
+ winningVariantId: string | null; // null if no winner yet
88
+ confidence: number; // 0.0-1.0
89
+ conversionRateDelta: number; // Lift over control
90
+ recommendation: 'scale' | 'iterate' | 'kill' | 'wait';
91
+ reasoning: string;
92
+ sampleSize: { control: number; variant: number };
93
+ }
94
+
95
+ // Algorithm: Two-proportion z-test comparing best challenger vs control (first variant by creation order).
96
+ // Minimum thresholds: 200 total views, 100 per variant.
97
+ // Confidence mapping:
98
+ // ≥0.95 + positive delta → 'scale' (winner found)
99
+ // ≥0.80 + positive delta → 'iterate' (promising, need more data)
100
+ // ≥0.95 + negative/zero delta + 500+ views → 'kill'
101
+ // else → 'wait'
102
+
103
+ // ── Section 4: Webhook Handlers ──────────────────────────
104
+ //
105
+ // Kongo sends webhooks for page lifecycle events.
106
+ // Signature: X-Kongo-Signature: t=timestamp,v1=hmac_sha256
107
+
108
+ // Verification steps:
109
+ // 1. Parse header: split by comma → extract t= and v1= parts
110
+ // 2. Check freshness: reject if timestamp > 5 minutes old
111
+ // 3. Compute expected: HMAC-SHA256 of "timestamp.rawBody" with secret
112
+ // 4. Compare: timing-safe comparison (prevents timing attacks)
113
+
114
+ // Events:
115
+ // - page.completed → update page status in heartbeat state
116
+ // - page.failed → log error, flag for retry on next daemon cycle
117
+
118
+ // Router pattern:
119
+ // const router = createWebhookRouter();
120
+ // router.on('page.completed', async (payload) => { ... });
121
+ // router.on('page.failed', async (payload) => { ... });
122
+ // await router.handle(rawBody, signature, secret);
123
+
124
+ // ── Section 5: Campaign-Page Linkage ─────────────────────
125
+ //
126
+ // Ad campaigns point to Kongo pages with UTM parameters.
127
+
128
+ // UTM taxonomy (standardized with Vin):
129
+ // utm_source = voidforge
130
+ // utm_medium = paid | organic | email | social
131
+ // utm_campaign = {voidforge_campaign_id}
132
+ // utm_content = {kongo_variant_id}
133
+ // utm_term = {keyword} (paid search only)
134
+
135
+ // Kongo campaigns use rotation strategies:
136
+ // 'weighted' — manual traffic weights per variant
137
+ // 'equal' — even split across all variants
138
+ // 'bandit' — multi-armed bandit (auto-optimize)
139
+
140
+ // AI variant generation:
141
+ // POST /engine/campaigns/:id/variants/generate
142
+ // count: 1-20, vary: ['headline', 'tagline', 'cta_text'], context: string
143
+ // ~$0.01, ~3s for 5 variants (Claude Sonnet)
144
+
145
+ // ── Section 6: Daemon Jobs ───────────────────────────────
146
+ //
147
+ // Three Kongo-specific heartbeat jobs:
148
+
149
+ // kongo-signal (hourly):
150
+ // Poll getGrowthSignal() for all published campaigns.
151
+ // Log signals to heartbeat.json. Forward 'scale'/'kill' for daemon action.
152
+ // Fault-tolerant: continues polling other campaigns if one fails.
153
+
154
+ // kongo-seed (event-driven):
155
+ // Triggered when Wayne's A/B evaluation declares a page variant winner.
156
+ // Extracts winning variant's slotValues.
157
+ // Available as seed for next createPageFromPrd() cycle.
158
+
159
+ // kongo-webhook (event-driven):
160
+ // Receives POST from Kongo on daemon's callback port.
161
+ // Verifies HMAC signature. Routes to typed handlers.
162
+ // Conditional: only registered when Kongo API key exists in vault.
163
+
164
+ export {};