ava-langgraph-narrative-intelligence 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.
Files changed (58) hide show
  1. package/README.md +268 -0
  2. package/dist/graphs/index.cjs +1511 -0
  3. package/dist/graphs/index.cjs.map +1 -0
  4. package/dist/graphs/index.d.cts +2 -0
  5. package/dist/graphs/index.d.ts +2 -0
  6. package/dist/graphs/index.js +1468 -0
  7. package/dist/graphs/index.js.map +1 -0
  8. package/dist/index-Btxk3nQm.d.cts +430 -0
  9. package/dist/index-CgXXxuIH.d.ts +430 -0
  10. package/dist/index-CweT-D3c.d.cts +122 -0
  11. package/dist/index-D-zWH42e.d.cts +66 -0
  12. package/dist/index-D71kh3nE.d.cts +213 -0
  13. package/dist/index-DApls3w2.d.ts +66 -0
  14. package/dist/index-UamXITgg.d.ts +122 -0
  15. package/dist/index-v9AlRC0M.d.ts +213 -0
  16. package/dist/index.cjs +2753 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.cts +6 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +2654 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/integrations/index.cjs +654 -0
  23. package/dist/integrations/index.cjs.map +1 -0
  24. package/dist/integrations/index.d.cts +2 -0
  25. package/dist/integrations/index.d.ts +2 -0
  26. package/dist/integrations/index.js +614 -0
  27. package/dist/integrations/index.js.map +1 -0
  28. package/dist/ncp-tXS9Jr9e.d.cts +132 -0
  29. package/dist/ncp-tXS9Jr9e.d.ts +132 -0
  30. package/dist/nodes/index.cjs +226 -0
  31. package/dist/nodes/index.cjs.map +1 -0
  32. package/dist/nodes/index.d.cts +2 -0
  33. package/dist/nodes/index.d.ts +2 -0
  34. package/dist/nodes/index.js +196 -0
  35. package/dist/nodes/index.js.map +1 -0
  36. package/dist/schemas/index.cjs +550 -0
  37. package/dist/schemas/index.cjs.map +1 -0
  38. package/dist/schemas/index.d.cts +2 -0
  39. package/dist/schemas/index.d.ts +2 -0
  40. package/dist/schemas/index.js +484 -0
  41. package/dist/schemas/index.js.map +1 -0
  42. package/dist/unified_state_bridge-CIDm1kuf.d.cts +266 -0
  43. package/dist/unified_state_bridge-CIDm1kuf.d.ts +266 -0
  44. package/package.json +91 -0
  45. package/src/graphs/coherence_engine.ts +1027 -0
  46. package/src/graphs/index.ts +47 -0
  47. package/src/graphs/three_universe_processor.ts +1136 -0
  48. package/src/index.ts +181 -0
  49. package/src/integrations/index.ts +17 -0
  50. package/src/integrations/redis_state.ts +691 -0
  51. package/src/nodes/emotional_classifier.ts +289 -0
  52. package/src/nodes/index.ts +17 -0
  53. package/src/schemas/index.ts +75 -0
  54. package/src/schemas/ncp.ts +312 -0
  55. package/src/schemas/unified_state_bridge.ts +681 -0
  56. package/src/tests/coherence_engine.test.ts +273 -0
  57. package/src/tests/three_universe_processor.test.ts +309 -0
  58. package/src/tests/unified_state_bridge.test.ts +360 -0
@@ -0,0 +1,1136 @@
1
+ /**
2
+ * Three-Universe Processor
3
+ *
4
+ * Processes events through all three universe lenses:
5
+ * - Engineer World (Mia) - Technical precision
6
+ * - Ceremony World (Ava8) - Relational protocols
7
+ * - Story Engine World (Miette) - Narrative patterns
8
+ *
9
+ * This produces a ThreeUniverseAnalysis with:
10
+ * - Individual perspectives from each universe
11
+ * - Lead universe determination
12
+ * - Coherence score
13
+ */
14
+
15
+ import {
16
+ Universe,
17
+ UniversePerspective,
18
+ ThreeUniverseAnalysis,
19
+ NarrativeFunction,
20
+ StoryBeat,
21
+ createUniversePerspective,
22
+ createThreeUniverseAnalysis,
23
+ createStoryBeat,
24
+ } from "../schemas/unified_state_bridge.js";
25
+
26
+ /**
27
+ * Types of events that can be processed.
28
+ */
29
+ export enum EventType {
30
+ GITHUB_PUSH = "github.push",
31
+ GITHUB_ISSUE = "github.issue",
32
+ GITHUB_PR = "github.pull_request",
33
+ GITHUB_COMMENT = "github.comment",
34
+ GITHUB_REVIEW = "github.review",
35
+ USER_INPUT = "user.input",
36
+ AGENT_ACTION = "agent.action",
37
+ SYSTEM_EVENT = "system.event",
38
+ }
39
+
40
+ /**
41
+ * An event ready for three-universe processing.
42
+ */
43
+ export interface ProcessedEvent {
44
+ eventId: string;
45
+ eventType: EventType;
46
+ content: string;
47
+ metadata: Record<string, unknown>;
48
+ timestamp: Date;
49
+ source?: string;
50
+ }
51
+
52
+ /**
53
+ * State for the three-universe processor.
54
+ */
55
+ export interface ThreeUniverseState {
56
+ // Input
57
+ event: Record<string, unknown>;
58
+ eventType: string;
59
+
60
+ // Processing state
61
+ engineerPerspective?: UniversePerspective;
62
+ ceremonyPerspective?: UniversePerspective;
63
+ storyEnginePerspective?: UniversePerspective;
64
+
65
+ // Output
66
+ analysis?: ThreeUniverseAnalysis;
67
+ leadUniverse?: Universe;
68
+ coherenceScore?: number;
69
+
70
+ // Error handling
71
+ error?: string;
72
+ }
73
+
74
+ /**
75
+ * Protocol for callbacks that receive three-universe analysis results.
76
+ */
77
+ export type AnalysisCallback = (
78
+ eventId: string,
79
+ eventContent: string,
80
+ engineerResult: Record<string, unknown>,
81
+ ceremonyResult: Record<string, unknown>,
82
+ storyEngineResult: Record<string, unknown>,
83
+ leadUniverse: string,
84
+ coherenceScore: number
85
+ ) => void;
86
+
87
+ // =============================================================================
88
+ // Engineer World (Mia) - The Builder
89
+ // =============================================================================
90
+
91
+ /**
92
+ * Keywords that indicate different engineering intents.
93
+ */
94
+ export function engineerIntentKeywords(): Record<string, string[]> {
95
+ return {
96
+ feature_implementation: [
97
+ "feat:",
98
+ "feature",
99
+ "add",
100
+ "implement",
101
+ "create",
102
+ "new",
103
+ ],
104
+ bug_fix: ["fix:", "bug", "hotfix", "patch", "resolve", "correct"],
105
+ refactor: ["refactor", "refact:", "cleanup", "restructure", "reorganize"],
106
+ documentation: ["docs:", "doc:", "documentation", "readme", "comment"],
107
+ testing: ["test:", "tests:", "testing", "spec", "coverage"],
108
+ dependency: ["deps:", "dependency", "upgrade", "update", "bump"],
109
+ configuration: ["config:", "configure", "settings", "env"],
110
+ performance: ["perf:", "performance", "optimize", "speed", "cache"],
111
+ security: ["security", "sec:", "vulnerability", "auth", "permission"],
112
+ ci_cd: ["ci:", "cd:", "pipeline", "workflow", "build"],
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Mia's perspective: The Builder (Engineer-world)
118
+ *
119
+ * Focuses on:
120
+ * - What was built/changed
121
+ * - Technical impact
122
+ * - System architecture implications
123
+ * - Flow routing for technical actions
124
+ */
125
+ export function analyzeEngineerPerspective(
126
+ state: ThreeUniverseState
127
+ ): ThreeUniverseState {
128
+ const event = state.event;
129
+ const eventType = state.eventType;
130
+
131
+ // Extract relevant content
132
+ let content = "";
133
+ const payload = event.payload as Record<string, unknown> | undefined;
134
+
135
+ if (payload) {
136
+ const commits = payload.commits as Array<Record<string, string>> | undefined;
137
+ if (commits) {
138
+ content = commits.map((c) => c.message || "").join(" ");
139
+ } else if (payload.issue) {
140
+ const issue = payload.issue as Record<string, string>;
141
+ content = (issue.title || "") + " " + (issue.body || "");
142
+ } else if (payload.pull_request) {
143
+ const pr = payload.pull_request as Record<string, string>;
144
+ content = (pr.title || "") + " " + (pr.body || "");
145
+ }
146
+ } else if (event.content) {
147
+ content = event.content as string;
148
+ }
149
+
150
+ const contentLower = content.toLowerCase();
151
+
152
+ // Analyze intent based on keywords
153
+ const keywords = engineerIntentKeywords();
154
+ const intentScores: Record<string, number> = {};
155
+
156
+ for (const [intent, terms] of Object.entries(keywords)) {
157
+ const score = terms.filter((term) =>
158
+ contentLower.includes(term.toLowerCase())
159
+ ).length;
160
+ if (score > 0) {
161
+ intentScores[intent] = score / terms.length;
162
+ }
163
+ }
164
+
165
+ // Determine primary intent
166
+ let intent: string;
167
+ let confidence: number;
168
+
169
+ if (Object.keys(intentScores).length > 0) {
170
+ intent = Object.entries(intentScores).reduce((a, b) =>
171
+ a[1] > b[1] ? a : b
172
+ )[0];
173
+ confidence = Math.min(0.95, 0.6 + intentScores[intent] * 0.4);
174
+ } else {
175
+ intent = "maintenance";
176
+ confidence = 0.5;
177
+ }
178
+
179
+ // Map intents to suggested flows
180
+ const flowMap: Record<string, string[]> = {
181
+ feature_implementation: [
182
+ "code_review",
183
+ "integration_test",
184
+ "documentation_update",
185
+ ],
186
+ bug_fix: ["regression_test", "root_cause_analysis", "changelog_update"],
187
+ refactor: ["architecture_review", "performance_test", "code_quality"],
188
+ documentation: ["doc_review", "example_validation"],
189
+ testing: ["coverage_analysis", "test_quality_review"],
190
+ dependency: ["security_scan", "compatibility_test"],
191
+ configuration: ["validation_test", "rollback_plan"],
192
+ performance: ["benchmark", "profiling", "optimization_review"],
193
+ security: ["security_audit", "penetration_test", "credential_scan"],
194
+ ci_cd: ["pipeline_validation", "deployment_test"],
195
+ maintenance: ["standard_ci"],
196
+ };
197
+
198
+ const suggestedFlows = flowMap[intent] || ["standard_ci"];
199
+
200
+ // Build context
201
+ const context: Record<string, unknown> = {
202
+ detectedKeywords: Object.entries(intentScores)
203
+ .filter(([, v]) => v > 0)
204
+ .map(([k]) => k),
205
+ contentLength: content.length,
206
+ eventType,
207
+ technicalScope: determineTechnicalScope(content, event),
208
+ estimatedComplexity: estimateComplexity(content, event),
209
+ };
210
+
211
+ const perspective = createUniversePerspective(
212
+ Universe.ENGINEER,
213
+ intent,
214
+ confidence,
215
+ { suggestedFlows, context }
216
+ );
217
+
218
+ return { ...state, engineerPerspective: perspective };
219
+ }
220
+
221
+ function determineTechnicalScope(
222
+ content: string,
223
+ _event: Record<string, unknown>
224
+ ): string {
225
+ const contentLower = content.toLowerCase();
226
+
227
+ if (["api", "endpoint", "route"].some((kw) => contentLower.includes(kw))) {
228
+ return "api_layer";
229
+ }
230
+ if (
231
+ ["database", "schema", "migration"].some((kw) => contentLower.includes(kw))
232
+ ) {
233
+ return "data_layer";
234
+ }
235
+ if (
236
+ ["ui", "component", "frontend"].some((kw) => contentLower.includes(kw))
237
+ ) {
238
+ return "presentation_layer";
239
+ }
240
+ if (["test", "spec"].some((kw) => contentLower.includes(kw))) {
241
+ return "testing";
242
+ }
243
+ if (["config", "env", "settings"].some((kw) => contentLower.includes(kw))) {
244
+ return "configuration";
245
+ }
246
+
247
+ return "general";
248
+ }
249
+
250
+ function estimateComplexity(
251
+ content: string,
252
+ event: Record<string, unknown>
253
+ ): string {
254
+ const payload = event.payload as Record<string, unknown> | undefined;
255
+ if (payload) {
256
+ const commits = payload.commits as unknown[] | undefined;
257
+ if (commits) {
258
+ if (commits.length > 5) return "high";
259
+ if (commits.length > 2) return "medium";
260
+ }
261
+ }
262
+
263
+ if (content.length > 500) return "high";
264
+ if (content.length > 100) return "medium";
265
+
266
+ return "low";
267
+ }
268
+
269
+ // =============================================================================
270
+ // Ceremony World (Ava8) - The Keeper
271
+ // =============================================================================
272
+
273
+ /**
274
+ * Keywords that indicate different ceremonial intents.
275
+ */
276
+ export function ceremonyIntentKeywords(): Record<string, string[]> {
277
+ return {
278
+ co_creation: ["we", "together", "team", "pair", "collaborate", "co-author"],
279
+ gratitude_expression: [
280
+ "thanks",
281
+ "thank you",
282
+ "grateful",
283
+ "appreciate",
284
+ "credit",
285
+ ],
286
+ witnessing: ["witness", "observe", "acknowledge", "see", "recognize"],
287
+ sacred_pause: ["pause", "reflect", "consider", "contemplate", "breathe"],
288
+ relationship_building: [
289
+ "connect",
290
+ "relationship",
291
+ "community",
292
+ "support",
293
+ ],
294
+ healing: ["heal", "restore", "repair", "reconcile", "mend"],
295
+ celebration: [
296
+ "celebrate",
297
+ "milestone",
298
+ "achievement",
299
+ "success",
300
+ "complete",
301
+ ],
302
+ offering: ["offer", "gift", "contribute", "share", "give"],
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Ava8's perspective: The Keeper (Ceremony-world)
308
+ *
309
+ * Focuses on:
310
+ * - Who contributed and their state
311
+ * - Relational dynamics (K'é)
312
+ * - Witnessing and acknowledgment
313
+ * - Seven-generation awareness
314
+ */
315
+ export function analyzeCeremonyPerspective(
316
+ state: ThreeUniverseState
317
+ ): ThreeUniverseState {
318
+ const event = state.event;
319
+
320
+ // Extract contributor information
321
+ const contributors = extractContributors(event);
322
+
323
+ // Extract content for analysis
324
+ const content = extractContent(event);
325
+ const contentLower = content.toLowerCase();
326
+
327
+ // Analyze relational intent
328
+ const keywords = ceremonyIntentKeywords();
329
+ const intentScores: Record<string, number> = {};
330
+
331
+ for (const [intent, terms] of Object.entries(keywords)) {
332
+ const score = terms.filter((term) => contentLower.includes(term)).length;
333
+ if (score > 0) {
334
+ intentScores[intent] = score / terms.length;
335
+ }
336
+ }
337
+
338
+ // Special case: multiple contributors = co_creation
339
+ if (contributors.length > 1) {
340
+ intentScores.co_creation = (intentScores.co_creation || 0) + 0.5;
341
+ }
342
+
343
+ // Determine primary intent
344
+ let intent: string;
345
+ let confidence: number;
346
+
347
+ if (Object.keys(intentScores).length > 0) {
348
+ intent = Object.entries(intentScores).reduce((a, b) =>
349
+ a[1] > b[1] ? a : b
350
+ )[0];
351
+ confidence = Math.min(0.95, 0.5 + intentScores[intent] * 0.4);
352
+ } else {
353
+ intent = "individual_offering";
354
+ confidence = 0.6;
355
+ }
356
+
357
+ // Map intents to ceremonial flows
358
+ const flowMap: Record<string, string[]> = {
359
+ co_creation: ["witness_collaboration", "honor_contributions", "amplify_voices"],
360
+ gratitude_expression: [
361
+ "amplify_acknowledgment",
362
+ "record_connection",
363
+ "reciprocity_check",
364
+ ],
365
+ witnessing: ["hold_space", "reflect_back", "presence"],
366
+ sacred_pause: ["create_silence", "contemplation_prompt", "breathing_space"],
367
+ relationship_building: [
368
+ "map_connections",
369
+ "strengthen_ties",
370
+ "introduce_support",
371
+ ],
372
+ healing: ["compassion_response", "restoration_path", "forgiveness_space"],
373
+ celebration: ["amplify_joy", "community_acknowledgment", "gratitude_circle"],
374
+ offering: ["receive_gracefully", "honor_gift", "share_forward"],
375
+ individual_offering: [
376
+ "witness_work",
377
+ "hold_space",
378
+ "gentle_acknowledgment",
379
+ ],
380
+ };
381
+
382
+ const suggestedFlows = flowMap[intent] || ["witness_work", "hold_space"];
383
+
384
+ // Build ceremonial context
385
+ const context: Record<string, unknown> = {
386
+ contributors,
387
+ isCollaborative: contributors.length > 1,
388
+ senderEnergy: assessEnergy(content),
389
+ witnessingNeeded: needsWitnessing(content, event),
390
+ relationshipDepth: assessRelationshipDepth(contributors, event),
391
+ sevenGenerationRelevance: assessLongTermImpact(content, event),
392
+ };
393
+
394
+ const perspective = createUniversePerspective(
395
+ Universe.CEREMONY,
396
+ intent,
397
+ confidence,
398
+ { suggestedFlows, context }
399
+ );
400
+
401
+ return { ...state, ceremonyPerspective: perspective };
402
+ }
403
+
404
+ function extractContributors(event: Record<string, unknown>): string[] {
405
+ const contributors: string[] = [];
406
+
407
+ if (event.sender) {
408
+ contributors.push(event.sender as string);
409
+ }
410
+
411
+ const payload = event.payload as Record<string, unknown> | undefined;
412
+ if (payload) {
413
+ // Git commits
414
+ const commits = payload.commits as Array<Record<string, unknown>> | undefined;
415
+ if (commits) {
416
+ for (const commit of commits) {
417
+ const author = (commit.author as Record<string, string> | undefined)?.name;
418
+ if (author && !contributors.includes(author)) {
419
+ contributors.push(author);
420
+ }
421
+ }
422
+ }
423
+
424
+ // Issue/PR author
425
+ const issue = payload.issue as Record<string, unknown> | undefined;
426
+ if (issue) {
427
+ const user = issue.user as Record<string, string> | undefined;
428
+ if (user?.login && !contributors.includes(user.login)) {
429
+ contributors.push(user.login);
430
+ }
431
+ }
432
+
433
+ const pr = payload.pull_request as Record<string, unknown> | undefined;
434
+ if (pr) {
435
+ const user = pr.user as Record<string, string> | undefined;
436
+ if (user?.login && !contributors.includes(user.login)) {
437
+ contributors.push(user.login);
438
+ }
439
+ }
440
+ }
441
+
442
+ return contributors.length > 0 ? contributors : ["unknown"];
443
+ }
444
+
445
+ function extractContent(event: Record<string, unknown>): string {
446
+ if (event.content) {
447
+ return event.content as string;
448
+ }
449
+
450
+ const payload = event.payload as Record<string, unknown> | undefined;
451
+ if (payload) {
452
+ const parts: string[] = [];
453
+
454
+ const commits = payload.commits as Array<Record<string, string>> | undefined;
455
+ if (commits) {
456
+ parts.push(...commits.map((c) => c.message || ""));
457
+ }
458
+
459
+ const issue = payload.issue as Record<string, string> | undefined;
460
+ if (issue) {
461
+ parts.push(issue.title || "");
462
+ parts.push(issue.body || "");
463
+ }
464
+
465
+ const pr = payload.pull_request as Record<string, string> | undefined;
466
+ if (pr) {
467
+ parts.push(pr.title || "");
468
+ parts.push(pr.body || "");
469
+ }
470
+
471
+ const comment = payload.comment as Record<string, string> | undefined;
472
+ if (comment) {
473
+ parts.push(comment.body || "");
474
+ }
475
+
476
+ return parts.filter(Boolean).join(" ");
477
+ }
478
+
479
+ return "";
480
+ }
481
+
482
+ function assessEnergy(content: string): string {
483
+ const contentLower = content.toLowerCase();
484
+
485
+ if (["urgent", "critical", "asap", "emergency"].some((w) =>
486
+ contentLower.includes(w)
487
+ )) {
488
+ return "urgent_flow";
489
+ }
490
+ if (["excited", "happy", "great", "awesome"].some((w) =>
491
+ contentLower.includes(w)
492
+ )) {
493
+ return "joyful_flow";
494
+ }
495
+ if (["stuck", "blocked", "help", "issue"].some((w) =>
496
+ contentLower.includes(w)
497
+ )) {
498
+ return "seeking_support";
499
+ }
500
+ if (["thoughtful", "consider", "reflect"].some((w) =>
501
+ contentLower.includes(w)
502
+ )) {
503
+ return "contemplative_flow";
504
+ }
505
+
506
+ return "steady_flow";
507
+ }
508
+
509
+ function needsWitnessing(
510
+ content: string,
511
+ _event: Record<string, unknown>
512
+ ): boolean {
513
+ const contentLower = content.toLowerCase();
514
+
515
+ // Vulnerable sharing needs witnessing
516
+ if (["first", "new", "trying", "learning", "help"].some((w) =>
517
+ contentLower.includes(w)
518
+ )) {
519
+ return true;
520
+ }
521
+
522
+ // Significant achievements need witnessing
523
+ if (["complete", "achieve", "milestone", "done"].some((w) =>
524
+ contentLower.includes(w)
525
+ )) {
526
+ return true;
527
+ }
528
+
529
+ return false;
530
+ }
531
+
532
+ function assessRelationshipDepth(
533
+ contributors: string[],
534
+ _event: Record<string, unknown>
535
+ ): string {
536
+ if (contributors.length > 2) return "community";
537
+ if (contributors.length > 1) return "pair";
538
+ return "individual";
539
+ }
540
+
541
+ function assessLongTermImpact(
542
+ content: string,
543
+ _event: Record<string, unknown>
544
+ ): number {
545
+ const contentLower = content.toLowerCase();
546
+ let score = 0.3; // Base score
547
+
548
+ // Infrastructure changes have long-term impact
549
+ if (["architecture", "foundation", "core", "framework"].some((w) =>
550
+ contentLower.includes(w)
551
+ )) {
552
+ score += 0.3;
553
+ }
554
+
555
+ // Documentation affects future generations
556
+ if (["document", "guide", "tutorial", "example"].some((w) =>
557
+ contentLower.includes(w)
558
+ )) {
559
+ score += 0.2;
560
+ }
561
+
562
+ // Breaking changes affect the future
563
+ if (["breaking", "migration", "deprecate"].some((w) =>
564
+ contentLower.includes(w)
565
+ )) {
566
+ score += 0.2;
567
+ }
568
+
569
+ return Math.min(1.0, score);
570
+ }
571
+
572
+ // =============================================================================
573
+ // Story Engine World (Miette) - The Weaver
574
+ // =============================================================================
575
+
576
+ /**
577
+ * Keywords that indicate different narrative functions.
578
+ */
579
+ export function storyEngineIntentKeywords(): Record<string, string[]> {
580
+ return {
581
+ inciting_incident: ["init", "start", "begin", "new", "first", "introduce"],
582
+ rising_action: ["add", "implement", "build", "develop", "progress", "continue"],
583
+ turning_point: ["feat:", "major", "significant", "pivot", "change", "transform"],
584
+ complication: ["issue", "problem", "bug", "error", "conflict", "challenge"],
585
+ crisis: ["critical", "urgent", "breaking", "emergency", "blocker"],
586
+ climax: ["complete", "finish", "final", "release", "launch", "deploy"],
587
+ resolution: ["fix", "resolve", "close", "merge", "done"],
588
+ denouement: ["cleanup", "refactor", "optimize", "polish", "improve"],
589
+ };
590
+ }
591
+
592
+ /**
593
+ * Miette's perspective: The Weaver (Story-engine-world)
594
+ *
595
+ * Focuses on:
596
+ * - Narrative position (which act/phase)
597
+ * - Dramatic function
598
+ * - Story arc progression
599
+ * - Character development
600
+ */
601
+ export function analyzeStoryEnginePerspective(
602
+ state: ThreeUniverseState
603
+ ): ThreeUniverseState {
604
+ const event = state.event;
605
+ const content = extractContent(event);
606
+ const contentLower = content.toLowerCase();
607
+
608
+ // Analyze narrative function
609
+ const keywords = storyEngineIntentKeywords();
610
+ const intentScores: Record<string, number> = {};
611
+
612
+ for (const [intent, terms] of Object.entries(keywords)) {
613
+ const score = terms.filter((term) => contentLower.includes(term)).length;
614
+ if (score > 0) {
615
+ intentScores[intent] = score / terms.length;
616
+ }
617
+ }
618
+
619
+ // Determine primary intent
620
+ let intent: string;
621
+ let confidence: number;
622
+
623
+ if (Object.keys(intentScores).length > 0) {
624
+ intent = Object.entries(intentScores).reduce((a, b) =>
625
+ a[1] > b[1] ? a : b
626
+ )[0];
627
+ confidence = Math.min(0.95, 0.55 + intentScores[intent] * 0.4);
628
+ } else {
629
+ intent = "rising_action";
630
+ confidence = 0.5;
631
+ }
632
+
633
+ // Map intent to act
634
+ const actMap: Record<string, number> = {
635
+ inciting_incident: 1,
636
+ rising_action: 2,
637
+ turning_point: 2,
638
+ complication: 2,
639
+ crisis: 2,
640
+ climax: 3,
641
+ resolution: 3,
642
+ denouement: 3,
643
+ };
644
+ const act = actMap[intent] || 2;
645
+
646
+ // Map intent to narrative function
647
+ const functionMap: Record<string, string> = {
648
+ inciting_incident: "inciting_incident",
649
+ rising_action: "rising_action",
650
+ turning_point: "turning_point",
651
+ complication: "complication",
652
+ crisis: "crisis",
653
+ climax: "climax",
654
+ resolution: "resolution",
655
+ denouement: "denouement",
656
+ };
657
+ const narrativeFunction = functionMap[intent] || "beat";
658
+
659
+ // Suggested flows for story engine
660
+ const flowMap: Record<string, string[]> = {
661
+ inciting_incident: ["establish_stakes", "introduce_characters", "set_tone"],
662
+ rising_action: ["advance_narrative", "develop_characters", "build_tension"],
663
+ turning_point: ["mark_pivot", "shift_perspective", "update_arc"],
664
+ complication: ["deepen_conflict", "raise_stakes", "add_obstacle"],
665
+ crisis: ["peak_tension", "force_decision", "approach_climax"],
666
+ climax: ["resolve_main_conflict", "character_transformation", "theme_revelation"],
667
+ resolution: ["tie_loose_ends", "show_consequences", "new_equilibrium"],
668
+ denouement: ["reflect_journey", "hint_future", "final_image"],
669
+ };
670
+
671
+ const suggestedFlows = flowMap[intent] || [
672
+ "advance_narrative",
673
+ "update_arc_position",
674
+ ];
675
+
676
+ // Calculate dramatic tension
677
+ const dramaticTension = calculateDramaticTension(intent, content);
678
+
679
+ // Build story context
680
+ const context: Record<string, unknown> = {
681
+ act,
682
+ narrativeFunction,
683
+ dramaticTension,
684
+ suggestedNextBeat: suggestNextBeat(intent),
685
+ characterImpact: assessCharacterImpact(content),
686
+ themeResonance: assessThemeResonance(content),
687
+ pacingSuggestion: suggestPacing(intent, dramaticTension),
688
+ };
689
+
690
+ const perspective = createUniversePerspective(
691
+ Universe.STORY_ENGINE,
692
+ intent,
693
+ confidence,
694
+ { suggestedFlows, context }
695
+ );
696
+
697
+ return { ...state, storyEnginePerspective: perspective };
698
+ }
699
+
700
+ function calculateDramaticTension(intent: string, content: string): number {
701
+ const baseTension: Record<string, number> = {
702
+ inciting_incident: 0.4,
703
+ rising_action: 0.5,
704
+ turning_point: 0.7,
705
+ complication: 0.6,
706
+ crisis: 0.9,
707
+ climax: 1.0,
708
+ resolution: 0.4,
709
+ denouement: 0.2,
710
+ };
711
+
712
+ let tension = baseTension[intent] || 0.5;
713
+ const contentLower = content.toLowerCase();
714
+
715
+ // Adjust based on content intensity
716
+ if (["urgent", "critical", "breaking"].some((w) => contentLower.includes(w))) {
717
+ tension = Math.min(1.0, tension + 0.2);
718
+ }
719
+ if (["minor", "small", "trivial"].some((w) => contentLower.includes(w))) {
720
+ tension = Math.max(0.1, tension - 0.2);
721
+ }
722
+
723
+ return Math.round(tension * 100) / 100;
724
+ }
725
+
726
+ function suggestNextBeat(currentIntent: string): string {
727
+ const nextBeatMap: Record<string, string> = {
728
+ inciting_incident: "rising_action",
729
+ rising_action: "complication",
730
+ turning_point: "rising_action",
731
+ complication: "crisis",
732
+ crisis: "climax",
733
+ climax: "resolution",
734
+ resolution: "denouement",
735
+ denouement: "inciting_incident", // New cycle
736
+ };
737
+ return nextBeatMap[currentIntent] || "rising_action";
738
+ }
739
+
740
+ function assessCharacterImpact(content: string): string {
741
+ const contentLower = content.toLowerCase();
742
+
743
+ if (["transform", "change", "grow", "learn"].some((w) =>
744
+ contentLower.includes(w)
745
+ )) {
746
+ return "transformative";
747
+ }
748
+ if (["challenge", "struggle", "overcome"].some((w) =>
749
+ contentLower.includes(w)
750
+ )) {
751
+ return "character_testing";
752
+ }
753
+ if (["connect", "relationship", "team"].some((w) => contentLower.includes(w))) {
754
+ return "relational";
755
+ }
756
+
757
+ return "incremental";
758
+ }
759
+
760
+ function assessThemeResonance(content: string): string {
761
+ const contentLower = content.toLowerCase();
762
+ const themes: string[] = [];
763
+
764
+ if (["together", "team", "collaborate"].some((w) => contentLower.includes(w))) {
765
+ themes.push("collaboration");
766
+ }
767
+ if (["integrate", "connect", "bridge"].some((w) => contentLower.includes(w))) {
768
+ themes.push("integration");
769
+ }
770
+ if (["coherent", "consistent", "unified"].some((w) =>
771
+ contentLower.includes(w)
772
+ )) {
773
+ themes.push("coherence");
774
+ }
775
+ if (["transform", "change", "evolve"].some((w) => contentLower.includes(w))) {
776
+ themes.push("transformation");
777
+ }
778
+
779
+ return themes.length > 0 ? themes.join(", ") : "development";
780
+ }
781
+
782
+ function suggestPacing(intent: string, tension: number): string {
783
+ if (tension > 0.8) return "accelerate";
784
+ if (tension < 0.3) return "breathe";
785
+ if (intent === "inciting_incident" || intent === "climax") return "emphasize";
786
+ return "steady";
787
+ }
788
+
789
+ // =============================================================================
790
+ // Synthesis - Combining All Three Perspectives
791
+ // =============================================================================
792
+
793
+ /**
794
+ * Combine all three universe perspectives into a unified analysis.
795
+ */
796
+ export function synthesizePerspectives(
797
+ state: ThreeUniverseState
798
+ ): ThreeUniverseState {
799
+ const engineer = state.engineerPerspective;
800
+ const ceremony = state.ceremonyPerspective;
801
+ const storyEngine = state.storyEnginePerspective;
802
+
803
+ if (!engineer || !ceremony || !storyEngine) {
804
+ return {
805
+ ...state,
806
+ error: "Missing one or more perspectives",
807
+ };
808
+ }
809
+
810
+ // Determine lead universe based on confidence and special conditions
811
+ const lead = determineLeadUniverse(engineer, ceremony, storyEngine);
812
+
813
+ // Calculate coherence
814
+ const coherence = calculateCoherence(engineer, ceremony, storyEngine);
815
+
816
+ // Build the analysis
817
+ const analysis = createThreeUniverseAnalysis(
818
+ engineer,
819
+ ceremony,
820
+ storyEngine,
821
+ lead,
822
+ coherence
823
+ );
824
+
825
+ return {
826
+ ...state,
827
+ analysis,
828
+ leadUniverse: lead,
829
+ coherenceScore: coherence,
830
+ };
831
+ }
832
+
833
+ /**
834
+ * Determine which universe should lead the response.
835
+ *
836
+ * Priority logic:
837
+ * 1. CEREMONY leads if: new contributor, sacred pause needed, relational obligation
838
+ * 2. STORY_ENGINE leads if: narrative coherence critical, character arc in focus
839
+ * 3. ENGINEER leads if: technical precision critical, schema validation required
840
+ * 4. Otherwise: highest confidence wins
841
+ */
842
+ function determineLeadUniverse(
843
+ engineer: UniversePerspective,
844
+ ceremony: UniversePerspective,
845
+ storyEngine: UniversePerspective
846
+ ): Universe {
847
+ const ceremonyContext = ceremony.context;
848
+
849
+ // Check ceremony priority conditions
850
+ if (ceremonyContext.witnessingNeeded) {
851
+ return Universe.CEREMONY;
852
+ }
853
+ if (ceremonyContext.isCollaborative) {
854
+ // Collaborative work honors the ceremony world
855
+ return Universe.CEREMONY;
856
+ }
857
+
858
+ // Check story engine priority conditions
859
+ const storyContext = storyEngine.context;
860
+ if ((storyContext.dramaticTension as number) > 0.8) {
861
+ // High drama moments are led by story engine
862
+ return Universe.STORY_ENGINE;
863
+ }
864
+ if (
865
+ storyContext.narrativeFunction === "climax" ||
866
+ storyContext.narrativeFunction === "turning_point"
867
+ ) {
868
+ return Universe.STORY_ENGINE;
869
+ }
870
+
871
+ // Check engineer priority conditions
872
+ const engineerContext = engineer.context;
873
+ if (engineerContext.estimatedComplexity === "high") {
874
+ return Universe.ENGINEER;
875
+ }
876
+ if (engineer.intent === "security" || engineer.intent === "bug_fix") {
877
+ // Technical urgency
878
+ return Universe.ENGINEER;
879
+ }
880
+
881
+ // Default: highest confidence
882
+ const perspectives: [UniversePerspective, Universe][] = [
883
+ [engineer, Universe.ENGINEER],
884
+ [ceremony, Universe.CEREMONY],
885
+ [storyEngine, Universe.STORY_ENGINE],
886
+ ];
887
+
888
+ return perspectives.reduce((a, b) =>
889
+ a[0].confidence > b[0].confidence ? a : b
890
+ )[1];
891
+ }
892
+
893
+ /**
894
+ * Calculate how well the three perspectives align.
895
+ *
896
+ * Higher coherence means the perspectives are complementary.
897
+ * Lower coherence might indicate conflicting interpretations.
898
+ */
899
+ function calculateCoherence(
900
+ engineer: UniversePerspective,
901
+ ceremony: UniversePerspective,
902
+ storyEngine: UniversePerspective
903
+ ): number {
904
+ // Base: average confidence
905
+ const avgConfidence =
906
+ (engineer.confidence + ceremony.confidence + storyEngine.confidence) / 3;
907
+
908
+ // Bonus for alignment
909
+ let bonus = 0.0;
910
+
911
+ // If all suggest similar urgency
912
+ const engineerUrgent = ["security", "bug_fix", "performance"].includes(
913
+ engineer.intent
914
+ );
915
+ const ceremonyUrgent = ceremony.context.senderEnergy === "urgent_flow";
916
+ const storyUrgent =
917
+ (storyEngine.context.dramaticTension as number) > 0.7;
918
+
919
+ if ([engineerUrgent, ceremonyUrgent, storyUrgent].filter(Boolean).length >= 2) {
920
+ bonus += 0.1; // Aligned on urgency
921
+ }
922
+
923
+ // Penalty for very different confidences (might indicate conflict)
924
+ const confidences = [
925
+ engineer.confidence,
926
+ ceremony.confidence,
927
+ storyEngine.confidence,
928
+ ];
929
+ const confidenceSpread = Math.max(...confidences) - Math.min(...confidences);
930
+ const penalty = confidenceSpread * 0.2;
931
+
932
+ const coherence = avgConfidence + bonus - penalty;
933
+ return Math.round(Math.max(0.0, Math.min(1.0, coherence)) * 100) / 100;
934
+ }
935
+
936
+ // =============================================================================
937
+ // Main Processor Class
938
+ // =============================================================================
939
+
940
+ /**
941
+ * High-level interface for three-universe event processing.
942
+ *
943
+ * @example
944
+ * const processor = new ThreeUniverseProcessor();
945
+ * const result = processor.process(event);
946
+ * console.log(result.leadUniverse); // "ceremony"
947
+ *
948
+ * @example With tracing callback
949
+ * const handler = new NarrativeTracingHandler({ storyId: "story_123" });
950
+ * const bridge = new LangGraphBridge(handler);
951
+ * const processor = new ThreeUniverseProcessor({
952
+ * tracingCallback: bridge.createThreeUniverseCallback()
953
+ * });
954
+ * const result = processor.process(event); // Automatically traced to Langfuse
955
+ */
956
+ export class ThreeUniverseProcessor {
957
+ private tracingCallback?: AnalysisCallback;
958
+
959
+ constructor(options: { tracingCallback?: AnalysisCallback } = {}) {
960
+ this.tracingCallback = options.tracingCallback;
961
+ }
962
+
963
+ /**
964
+ * Process an event through all three universes.
965
+ *
966
+ * @param event The event data (webhook payload, user input, etc.)
967
+ * @param eventType Type of event (e.g., "github.push", "user.input")
968
+ * @returns ThreeUniverseAnalysis with all perspectives and synthesis
969
+ */
970
+ process(
971
+ event: Record<string, unknown>,
972
+ eventType: string = "unknown"
973
+ ): ThreeUniverseAnalysis {
974
+ // Initialize state
975
+ let state: ThreeUniverseState = {
976
+ event,
977
+ eventType,
978
+ };
979
+
980
+ // Process through each universe
981
+ state = analyzeEngineerPerspective(state);
982
+ state = analyzeCeremonyPerspective(state);
983
+ state = analyzeStoryEnginePerspective(state);
984
+ state = synthesizePerspectives(state);
985
+
986
+ // Check for errors
987
+ if (state.error) {
988
+ throw new Error(`Processing error: ${state.error}`);
989
+ }
990
+
991
+ const analysis = state.analysis!;
992
+
993
+ // Call tracing callback if configured
994
+ if (this.tracingCallback && analysis) {
995
+ const eventId =
996
+ (event.eventId as string) ||
997
+ (event.id as string) ||
998
+ `${eventType}_${Date.now()}`;
999
+ const eventContent = this.extractEventContent(event);
1000
+
1001
+ this.tracingCallback(
1002
+ eventId,
1003
+ eventContent,
1004
+ perspectiveToRecord(analysis.engineer),
1005
+ perspectiveToRecord(analysis.ceremony),
1006
+ perspectiveToRecord(analysis.storyEngine),
1007
+ analysis.leadUniverse,
1008
+ analysis.coherenceScore
1009
+ );
1010
+ }
1011
+
1012
+ return analysis;
1013
+ }
1014
+
1015
+ private extractEventContent(event: Record<string, unknown>): string {
1016
+ // Try common content locations
1017
+ if (event.content) {
1018
+ return String(event.content).slice(0, 500);
1019
+ }
1020
+
1021
+ const payload = event.payload as Record<string, unknown> | undefined;
1022
+ if (payload && typeof payload === "object") {
1023
+ const issue = payload.issue as Record<string, string> | undefined;
1024
+ if (issue?.title) return issue.title;
1025
+
1026
+ const pr = payload.pull_request as Record<string, string> | undefined;
1027
+ if (pr?.title) return pr.title;
1028
+
1029
+ const comment = payload.comment as Record<string, string> | undefined;
1030
+ if (comment?.body) return comment.body.slice(0, 500);
1031
+ }
1032
+
1033
+ if (event.message) {
1034
+ return String(event.message).slice(0, 500);
1035
+ }
1036
+
1037
+ return `Event: ${event.eventType || "unknown"}`;
1038
+ }
1039
+
1040
+ /**
1041
+ * Convenience method for processing GitHub webhooks.
1042
+ */
1043
+ processWebhook(webhookPayload: Record<string, unknown>): ThreeUniverseAnalysis {
1044
+ // Determine event type from webhook
1045
+ let eventType = "github.push"; // Default
1046
+
1047
+ const payload = webhookPayload.payload as Record<string, unknown> | undefined;
1048
+ if (payload) {
1049
+ if (payload.issue) {
1050
+ eventType = "github.issue";
1051
+ } else if (payload.pull_request) {
1052
+ eventType = "github.pull_request";
1053
+ } else if (payload.comment) {
1054
+ eventType = "github.comment";
1055
+ }
1056
+ }
1057
+
1058
+ return this.process(webhookPayload, eventType);
1059
+ }
1060
+
1061
+ /**
1062
+ * Create a story beat from event and analysis.
1063
+ */
1064
+ createBeatFromAnalysis(
1065
+ event: Record<string, unknown>,
1066
+ analysis: ThreeUniverseAnalysis,
1067
+ sequence: number
1068
+ ): StoryBeat {
1069
+ // Map story engine intent to NarrativeFunction
1070
+ const functionMap: Record<string, NarrativeFunction> = {
1071
+ inciting_incident: NarrativeFunction.INCITING_INCIDENT,
1072
+ rising_action: NarrativeFunction.RISING_ACTION,
1073
+ turning_point: NarrativeFunction.TURNING_POINT,
1074
+ complication: NarrativeFunction.COMPLICATION,
1075
+ crisis: NarrativeFunction.CRISIS,
1076
+ climax: NarrativeFunction.CLIMAX,
1077
+ resolution: NarrativeFunction.RESOLUTION,
1078
+ denouement: NarrativeFunction.DENOUEMENT,
1079
+ };
1080
+
1081
+ const storyIntent = analysis.storyEngine.intent;
1082
+ const narrativeFunc =
1083
+ functionMap[storyIntent] || NarrativeFunction.BEAT;
1084
+
1085
+ // Extract act
1086
+ const act = (analysis.storyEngine.context.act as number) || 2;
1087
+
1088
+ // Build content
1089
+ let content = extractContent(event);
1090
+ if (!content) {
1091
+ content = String(event.eventType || "event");
1092
+ }
1093
+
1094
+ // Generate beat ID
1095
+ const timestamp = new Date().toISOString();
1096
+ const beatId = `beat_${timestamp}`;
1097
+
1098
+ // Source event ID
1099
+ let sourceEventId: string | undefined;
1100
+ const payload = event.payload as Record<string, unknown> | undefined;
1101
+ if (payload) {
1102
+ const headCommit = payload.head_commit as Record<string, string> | undefined;
1103
+ if (headCommit?.id) {
1104
+ sourceEventId = headCommit.id;
1105
+ } else if (payload.issue) {
1106
+ const issue = payload.issue as Record<string, unknown>;
1107
+ sourceEventId = String(issue.id);
1108
+ } else if (payload.pull_request) {
1109
+ const pr = payload.pull_request as Record<string, unknown>;
1110
+ sourceEventId = String(pr.id);
1111
+ }
1112
+ }
1113
+
1114
+ return createStoryBeat(beatId, sequence, content.slice(0, 500), narrativeFunc, act, {
1115
+ universeAnalysis: analysis,
1116
+ leadUniverse: analysis.leadUniverse,
1117
+ source: "processor",
1118
+ sourceEventId,
1119
+ });
1120
+ }
1121
+ }
1122
+
1123
+ /**
1124
+ * Convert a UniversePerspective to a plain record.
1125
+ */
1126
+ function perspectiveToRecord(
1127
+ perspective: UniversePerspective
1128
+ ): Record<string, unknown> {
1129
+ return {
1130
+ universe: perspective.universe,
1131
+ intent: perspective.intent,
1132
+ confidence: perspective.confidence,
1133
+ suggestedFlows: perspective.suggestedFlows,
1134
+ context: perspective.context,
1135
+ };
1136
+ }