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.
- package/README.md +268 -0
- package/dist/graphs/index.cjs +1511 -0
- package/dist/graphs/index.cjs.map +1 -0
- package/dist/graphs/index.d.cts +2 -0
- package/dist/graphs/index.d.ts +2 -0
- package/dist/graphs/index.js +1468 -0
- package/dist/graphs/index.js.map +1 -0
- package/dist/index-Btxk3nQm.d.cts +430 -0
- package/dist/index-CgXXxuIH.d.ts +430 -0
- package/dist/index-CweT-D3c.d.cts +122 -0
- package/dist/index-D-zWH42e.d.cts +66 -0
- package/dist/index-D71kh3nE.d.cts +213 -0
- package/dist/index-DApls3w2.d.ts +66 -0
- package/dist/index-UamXITgg.d.ts +122 -0
- package/dist/index-v9AlRC0M.d.ts +213 -0
- package/dist/index.cjs +2753 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2654 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/index.cjs +654 -0
- package/dist/integrations/index.cjs.map +1 -0
- package/dist/integrations/index.d.cts +2 -0
- package/dist/integrations/index.d.ts +2 -0
- package/dist/integrations/index.js +614 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/ncp-tXS9Jr9e.d.cts +132 -0
- package/dist/ncp-tXS9Jr9e.d.ts +132 -0
- package/dist/nodes/index.cjs +226 -0
- package/dist/nodes/index.cjs.map +1 -0
- package/dist/nodes/index.d.cts +2 -0
- package/dist/nodes/index.d.ts +2 -0
- package/dist/nodes/index.js +196 -0
- package/dist/nodes/index.js.map +1 -0
- package/dist/schemas/index.cjs +550 -0
- package/dist/schemas/index.cjs.map +1 -0
- package/dist/schemas/index.d.cts +2 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +484 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/unified_state_bridge-CIDm1kuf.d.cts +266 -0
- package/dist/unified_state_bridge-CIDm1kuf.d.ts +266 -0
- package/package.json +91 -0
- package/src/graphs/coherence_engine.ts +1027 -0
- package/src/graphs/index.ts +47 -0
- package/src/graphs/three_universe_processor.ts +1136 -0
- package/src/index.ts +181 -0
- package/src/integrations/index.ts +17 -0
- package/src/integrations/redis_state.ts +691 -0
- package/src/nodes/emotional_classifier.ts +289 -0
- package/src/nodes/index.ts +17 -0
- package/src/schemas/index.ts +75 -0
- package/src/schemas/ncp.ts +312 -0
- package/src/schemas/unified_state_bridge.ts +681 -0
- package/src/tests/coherence_engine.test.ts +273 -0
- package/src/tests/three_universe_processor.test.ts +309 -0
- package/src/tests/unified_state_bridge.test.ts +360 -0
|
@@ -0,0 +1,1027 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrative Coherence Engine
|
|
3
|
+
*
|
|
4
|
+
* Analyzes narrative coherence and identifies gaps.
|
|
5
|
+
* This is a core dependency for the Editor Anvil app.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Gap identification (structural, thematic, character, sensory, continuity)
|
|
9
|
+
* - Coherence scoring across multiple dimensions
|
|
10
|
+
* - Enrichment routing suggestions
|
|
11
|
+
* - Trinity perspective integration (Mia/Miette/Ava8)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
StoryBeat,
|
|
16
|
+
CharacterState,
|
|
17
|
+
ThematicThread,
|
|
18
|
+
} from "../schemas/unified_state_bridge.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Types of narrative gaps that can be identified.
|
|
22
|
+
*/
|
|
23
|
+
export enum GapType {
|
|
24
|
+
STRUCTURAL = "structural", // Missing beats, incomplete arcs
|
|
25
|
+
THEMATIC = "thematic", // Promised themes underdelivered
|
|
26
|
+
CHARACTER = "character", // Traits mentioned but not demonstrated
|
|
27
|
+
SENSORY = "sensory", // Scenes lacking grounding detail
|
|
28
|
+
CONTINUITY = "continuity", // Timeline/detail inconsistencies
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Severity levels for identified gaps.
|
|
33
|
+
*/
|
|
34
|
+
export enum GapSeverity {
|
|
35
|
+
CRITICAL = "critical", // Must fix before publication
|
|
36
|
+
MODERATE = "moderate", // Should address in next pass
|
|
37
|
+
MINOR = "minor", // Nice to have, low priority
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Where to route gaps for remediation.
|
|
42
|
+
*/
|
|
43
|
+
export enum RoutingTarget {
|
|
44
|
+
STORYTELLER = "storyteller", // Needs prose refinement
|
|
45
|
+
STRUCTURIST = "structurist", // Needs structural repair
|
|
46
|
+
ARCHITECT = "architect", // Schema inconsistency
|
|
47
|
+
AUTHOR = "author", // Human decision required
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Component score status.
|
|
52
|
+
*/
|
|
53
|
+
export type ComponentStatus = "good" | "warning" | "critical";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A narrative gap identified in the story.
|
|
57
|
+
*/
|
|
58
|
+
export interface Gap {
|
|
59
|
+
id: string;
|
|
60
|
+
gapType: GapType;
|
|
61
|
+
severity: GapSeverity;
|
|
62
|
+
description: string;
|
|
63
|
+
location: Record<string, unknown>; // beat_id, chapter_id, position
|
|
64
|
+
suggestedRoute: RoutingTarget;
|
|
65
|
+
resolved: boolean;
|
|
66
|
+
resolution?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a Gap with defaults
|
|
71
|
+
*/
|
|
72
|
+
export function createGap(
|
|
73
|
+
id: string,
|
|
74
|
+
gapType: GapType,
|
|
75
|
+
severity: GapSeverity,
|
|
76
|
+
description: string,
|
|
77
|
+
suggestedRoute: RoutingTarget,
|
|
78
|
+
options: Partial<Gap> = {}
|
|
79
|
+
): Gap {
|
|
80
|
+
return {
|
|
81
|
+
id,
|
|
82
|
+
gapType,
|
|
83
|
+
severity,
|
|
84
|
+
description,
|
|
85
|
+
location: options.location ?? {},
|
|
86
|
+
suggestedRoute,
|
|
87
|
+
resolved: options.resolved ?? false,
|
|
88
|
+
resolution: options.resolution,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Score for a single coherence component.
|
|
94
|
+
*/
|
|
95
|
+
export interface ComponentScore {
|
|
96
|
+
score: number; // 0-100
|
|
97
|
+
status: ComponentStatus;
|
|
98
|
+
issues: string[];
|
|
99
|
+
suggestions: string[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Create a ComponentScore with defaults
|
|
104
|
+
*/
|
|
105
|
+
export function createComponentScore(
|
|
106
|
+
score: number,
|
|
107
|
+
status: ComponentStatus,
|
|
108
|
+
options: Partial<ComponentScore> = {}
|
|
109
|
+
): ComponentScore {
|
|
110
|
+
return {
|
|
111
|
+
score,
|
|
112
|
+
status,
|
|
113
|
+
issues: options.issues ?? [],
|
|
114
|
+
suggestions: options.suggestions ?? [],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Complete coherence score for a narrative.
|
|
120
|
+
*/
|
|
121
|
+
export interface CoherenceScore {
|
|
122
|
+
overall: number;
|
|
123
|
+
narrativeFlow: ComponentScore;
|
|
124
|
+
characterConsistency: ComponentScore;
|
|
125
|
+
pacing: ComponentScore;
|
|
126
|
+
themeSaturation: ComponentScore;
|
|
127
|
+
continuity: ComponentScore;
|
|
128
|
+
analyzedAt: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create a CoherenceScore
|
|
133
|
+
*/
|
|
134
|
+
export function createCoherenceScore(
|
|
135
|
+
overall: number,
|
|
136
|
+
narrativeFlow: ComponentScore,
|
|
137
|
+
characterConsistency: ComponentScore,
|
|
138
|
+
pacing: ComponentScore,
|
|
139
|
+
themeSaturation: ComponentScore,
|
|
140
|
+
continuity: ComponentScore
|
|
141
|
+
): CoherenceScore {
|
|
142
|
+
return {
|
|
143
|
+
overall,
|
|
144
|
+
narrativeFlow,
|
|
145
|
+
characterConsistency,
|
|
146
|
+
pacing,
|
|
147
|
+
themeSaturation,
|
|
148
|
+
continuity,
|
|
149
|
+
analyzedAt: new Date().toISOString(),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Assessment from three narrative perspectives (Mia/Miette/Ava8).
|
|
155
|
+
*/
|
|
156
|
+
export interface TrinityAssessment {
|
|
157
|
+
mia: string; // Structural quality (🧠 logical, analytical)
|
|
158
|
+
miette: string; // Emotional effectiveness (🌸 feeling, resonance)
|
|
159
|
+
ava8: string; // Atmospheric/sensory (🎨 visual, immersive)
|
|
160
|
+
priorities: string[];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Create a TrinityAssessment
|
|
165
|
+
*/
|
|
166
|
+
export function createTrinityAssessment(
|
|
167
|
+
mia: string,
|
|
168
|
+
miette: string,
|
|
169
|
+
ava8: string,
|
|
170
|
+
priorities: string[] = []
|
|
171
|
+
): TrinityAssessment {
|
|
172
|
+
return { mia, miette, ava8, priorities };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Type alias for the coherence engine state.
|
|
177
|
+
*/
|
|
178
|
+
export interface CoherenceEngineState {
|
|
179
|
+
beats: StoryBeat[];
|
|
180
|
+
characters: CharacterState[];
|
|
181
|
+
themes: ThematicThread[];
|
|
182
|
+
|
|
183
|
+
// Component scores
|
|
184
|
+
narrativeFlowScore?: ComponentScore;
|
|
185
|
+
characterConsistencyScore?: ComponentScore;
|
|
186
|
+
pacingScore?: ComponentScore;
|
|
187
|
+
themeSaturationScore?: ComponentScore;
|
|
188
|
+
continuityScore?: ComponentScore;
|
|
189
|
+
|
|
190
|
+
// Overall
|
|
191
|
+
overallScore?: number;
|
|
192
|
+
gaps?: Gap[];
|
|
193
|
+
trinityAssessment?: TrinityAssessment;
|
|
194
|
+
coherenceScore?: CoherenceScore;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Result of coherence analysis.
|
|
199
|
+
*/
|
|
200
|
+
export interface CoherenceResult {
|
|
201
|
+
coherenceScore: CoherenceScore;
|
|
202
|
+
gaps: Gap[];
|
|
203
|
+
trinityAssessment: TrinityAssessment;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Analyzes narrative coherence and identifies gaps.
|
|
208
|
+
*
|
|
209
|
+
* This is a core component for the Editor Anvil app, providing:
|
|
210
|
+
* - Comprehensive coherence scoring across 5 dimensions
|
|
211
|
+
* - Gap identification with severity and routing
|
|
212
|
+
* - Trinity perspective assessment (Mia/Miette/Ava8)
|
|
213
|
+
* - Actionable improvement suggestions
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* const engine = new NarrativeCoherenceEngine();
|
|
217
|
+
* const result = engine.analyze(beats, characters, themes);
|
|
218
|
+
*
|
|
219
|
+
* // Access scores
|
|
220
|
+
* console.log(`Overall coherence: ${result.coherenceScore.overall}`);
|
|
221
|
+
*
|
|
222
|
+
* // Access gaps
|
|
223
|
+
* for (const gap of result.gaps) {
|
|
224
|
+
* console.log(`Gap: ${gap.description} (${gap.severity})`);
|
|
225
|
+
* }
|
|
226
|
+
*
|
|
227
|
+
* // Access Trinity assessment
|
|
228
|
+
* console.log(`Mia says: ${result.trinityAssessment.mia}`);
|
|
229
|
+
*/
|
|
230
|
+
export class NarrativeCoherenceEngine {
|
|
231
|
+
private strictMode: boolean;
|
|
232
|
+
private gapCounter: number;
|
|
233
|
+
|
|
234
|
+
constructor(options: { strictMode?: boolean } = {}) {
|
|
235
|
+
this.strictMode = options.strictMode ?? false;
|
|
236
|
+
this.gapCounter = 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private generateGapId(): string {
|
|
240
|
+
this.gapCounter += 1;
|
|
241
|
+
return `gap_${this.gapCounter}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Analyze narrative flow - how smoothly the story progresses.
|
|
246
|
+
*
|
|
247
|
+
* Checks:
|
|
248
|
+
* - Beat transitions (jarring vs smooth)
|
|
249
|
+
* - Logical causality between beats
|
|
250
|
+
* - Pacing consistency
|
|
251
|
+
*/
|
|
252
|
+
private analyzeNarrativeFlow(state: CoherenceEngineState): CoherenceEngineState {
|
|
253
|
+
const beats = state.beats;
|
|
254
|
+
const issues: string[] = [];
|
|
255
|
+
const suggestions: string[] = [];
|
|
256
|
+
let score: number;
|
|
257
|
+
let status: ComponentStatus;
|
|
258
|
+
|
|
259
|
+
if (beats.length < 2) {
|
|
260
|
+
score = 50.0;
|
|
261
|
+
issues.push("Too few beats to assess flow");
|
|
262
|
+
suggestions.push("Add more story beats to establish narrative rhythm");
|
|
263
|
+
} else {
|
|
264
|
+
// Check for logical function progression
|
|
265
|
+
const functions = beats.map((b) => b.narrativeFunction);
|
|
266
|
+
|
|
267
|
+
// Penalize if no setup before confrontation
|
|
268
|
+
let hasProperStructure = false;
|
|
269
|
+
for (let i = 0; i < functions.length; i++) {
|
|
270
|
+
const func = functions[i];
|
|
271
|
+
if (["setup", "introduction", "discovery"].includes(func)) {
|
|
272
|
+
hasProperStructure = true;
|
|
273
|
+
break;
|
|
274
|
+
} else if (["confrontation", "crisis", "climax"].includes(func)) {
|
|
275
|
+
if (!hasProperStructure) {
|
|
276
|
+
issues.push(`Beat ${i + 1} escalates without proper setup`);
|
|
277
|
+
hasProperStructure = true; // Only report once
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check emotional continuity
|
|
283
|
+
let prevTone: string | undefined;
|
|
284
|
+
let jarringTransitions = 0;
|
|
285
|
+
|
|
286
|
+
for (let i = 0; i < beats.length; i++) {
|
|
287
|
+
const beat = beats[i];
|
|
288
|
+
if (prevTone && beat.emotionalTone) {
|
|
289
|
+
// Simple check: devastation followed by joy is jarring
|
|
290
|
+
const jarringPairs: [string, string][] = [
|
|
291
|
+
["devastating", "joyful"],
|
|
292
|
+
["fearful", "peaceful"],
|
|
293
|
+
["triumphant", "devastating"],
|
|
294
|
+
];
|
|
295
|
+
for (const [p1, p2] of jarringPairs) {
|
|
296
|
+
if (
|
|
297
|
+
(prevTone.toLowerCase().includes(p1) &&
|
|
298
|
+
beat.emotionalTone.toLowerCase().includes(p2)) ||
|
|
299
|
+
(prevTone.toLowerCase().includes(p2) &&
|
|
300
|
+
beat.emotionalTone.toLowerCase().includes(p1))
|
|
301
|
+
) {
|
|
302
|
+
jarringTransitions += 1;
|
|
303
|
+
issues.push(`Jarring emotional transition at Beat ${i + 1}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
prevTone = beat.emotionalTone;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Calculate score
|
|
311
|
+
let baseScore = 85.0;
|
|
312
|
+
baseScore -= jarringTransitions * 10;
|
|
313
|
+
baseScore -=
|
|
314
|
+
issues.filter((i) => i.includes("without proper setup")).length * 15;
|
|
315
|
+
|
|
316
|
+
score = Math.max(0.0, Math.min(100.0, baseScore));
|
|
317
|
+
|
|
318
|
+
if (jarringTransitions > 0) {
|
|
319
|
+
suggestions.push("Add transitional beats to smooth emotional shifts");
|
|
320
|
+
}
|
|
321
|
+
if (!hasProperStructure) {
|
|
322
|
+
suggestions.push(
|
|
323
|
+
"Consider adding setup beats before major confrontations"
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Determine status
|
|
329
|
+
if (score >= 70) {
|
|
330
|
+
status = "good";
|
|
331
|
+
} else if (score >= 50) {
|
|
332
|
+
status = "warning";
|
|
333
|
+
} else {
|
|
334
|
+
status = "critical";
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
state.narrativeFlowScore = createComponentScore(score, status, {
|
|
338
|
+
issues,
|
|
339
|
+
suggestions,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return state;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Analyze character consistency across the narrative.
|
|
347
|
+
*
|
|
348
|
+
* Checks:
|
|
349
|
+
* - Character voice consistency
|
|
350
|
+
* - Arc progression logic
|
|
351
|
+
* - Relationship evolution coherence
|
|
352
|
+
*/
|
|
353
|
+
private analyzeCharacterConsistency(
|
|
354
|
+
state: CoherenceEngineState
|
|
355
|
+
): CoherenceEngineState {
|
|
356
|
+
const beats = state.beats;
|
|
357
|
+
const characters = state.characters;
|
|
358
|
+
const issues: string[] = [];
|
|
359
|
+
const suggestions: string[] = [];
|
|
360
|
+
let score: number;
|
|
361
|
+
let status: ComponentStatus;
|
|
362
|
+
|
|
363
|
+
if (characters.length === 0) {
|
|
364
|
+
score = 50.0;
|
|
365
|
+
issues.push("No character data provided");
|
|
366
|
+
suggestions.push("Define character states to enable consistency analysis");
|
|
367
|
+
} else {
|
|
368
|
+
// Track character appearances across beats
|
|
369
|
+
const characterBeats: Record<string, number[]> = {};
|
|
370
|
+
for (const char of characters) {
|
|
371
|
+
characterBeats[char.id] = [];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
for (let i = 0; i < beats.length; i++) {
|
|
375
|
+
const beat = beats[i];
|
|
376
|
+
if (beat.characterId && characterBeats[beat.characterId]) {
|
|
377
|
+
characterBeats[beat.characterId].push(i);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Check for characters with large gaps
|
|
382
|
+
for (const [charId, appearances] of Object.entries(characterBeats)) {
|
|
383
|
+
if (appearances.length >= 2) {
|
|
384
|
+
for (let i = 1; i < appearances.length; i++) {
|
|
385
|
+
const gap = appearances[i] - appearances[i - 1];
|
|
386
|
+
if (gap > 5) {
|
|
387
|
+
// More than 5 beats between appearances
|
|
388
|
+
const char = characters.find((c) => c.id === charId);
|
|
389
|
+
const name = char?.name || charId;
|
|
390
|
+
issues.push(`Character '${name}' disappears for ${gap} beats`);
|
|
391
|
+
suggestions.push(
|
|
392
|
+
`Consider adding '${name}' to beats between ${appearances[i - 1] + 1} and ${appearances[i] + 1}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Check arc progression
|
|
400
|
+
for (const char of characters) {
|
|
401
|
+
if (char.arcPosition < 0.1 && beats.length > 5) {
|
|
402
|
+
issues.push(`Character '${char.name}' has minimal arc progression`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Calculate score based on issues
|
|
407
|
+
let baseScore = 90.0;
|
|
408
|
+
baseScore -= issues.filter((i) => i.includes("disappears")).length * 8;
|
|
409
|
+
baseScore -= issues.filter((i) => i.includes("minimal arc")).length * 12;
|
|
410
|
+
|
|
411
|
+
score = Math.max(0.0, Math.min(100.0, baseScore));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Determine status
|
|
415
|
+
if (score >= 70) {
|
|
416
|
+
status = "good";
|
|
417
|
+
} else if (score >= 50) {
|
|
418
|
+
status = "warning";
|
|
419
|
+
} else {
|
|
420
|
+
status = "critical";
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
state.characterConsistencyScore = createComponentScore(score, status, {
|
|
424
|
+
issues,
|
|
425
|
+
suggestions,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
return state;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Analyze narrative pacing.
|
|
433
|
+
*
|
|
434
|
+
* Checks:
|
|
435
|
+
* - Tension/relief distribution
|
|
436
|
+
* - Beat density per section
|
|
437
|
+
* - Climax positioning
|
|
438
|
+
*/
|
|
439
|
+
private analyzePacing(state: CoherenceEngineState): CoherenceEngineState {
|
|
440
|
+
const beats = state.beats;
|
|
441
|
+
const issues: string[] = [];
|
|
442
|
+
const suggestions: string[] = [];
|
|
443
|
+
let score: number;
|
|
444
|
+
let status: ComponentStatus;
|
|
445
|
+
|
|
446
|
+
if (beats.length < 3) {
|
|
447
|
+
score = 50.0;
|
|
448
|
+
issues.push("Too few beats to assess pacing");
|
|
449
|
+
suggestions.push("Add more beats to establish proper pacing rhythm");
|
|
450
|
+
} else {
|
|
451
|
+
// Analyze function distribution
|
|
452
|
+
const functions = beats.map((b) => b.narrativeFunction.toLowerCase());
|
|
453
|
+
|
|
454
|
+
// Check for climax positioning (should be in last third)
|
|
455
|
+
const climaxPositions = functions
|
|
456
|
+
.map((f, i) => (f.includes("climax") ? i : -1))
|
|
457
|
+
.filter((i) => i >= 0);
|
|
458
|
+
|
|
459
|
+
if (climaxPositions.length === 0) {
|
|
460
|
+
issues.push("No climax beat identified");
|
|
461
|
+
suggestions.push("Ensure at least one beat has a climax function");
|
|
462
|
+
} else {
|
|
463
|
+
// Check if climax is too early
|
|
464
|
+
const lastClimax = climaxPositions[climaxPositions.length - 1];
|
|
465
|
+
const total = beats.length;
|
|
466
|
+
if (lastClimax < total * 0.5) {
|
|
467
|
+
issues.push("Climax occurs too early in the narrative");
|
|
468
|
+
suggestions.push(
|
|
469
|
+
"Move climax to later in the story or add post-climax resolution beats"
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Check for consecutive high-tension beats
|
|
475
|
+
const highTensionFuncs = [
|
|
476
|
+
"confrontation",
|
|
477
|
+
"crisis",
|
|
478
|
+
"climax",
|
|
479
|
+
"revelation",
|
|
480
|
+
];
|
|
481
|
+
let consecutiveHigh = 0;
|
|
482
|
+
let maxConsecutive = 0;
|
|
483
|
+
|
|
484
|
+
for (const func of functions) {
|
|
485
|
+
if (highTensionFuncs.some((ht) => func.includes(ht))) {
|
|
486
|
+
consecutiveHigh += 1;
|
|
487
|
+
maxConsecutive = Math.max(maxConsecutive, consecutiveHigh);
|
|
488
|
+
} else {
|
|
489
|
+
consecutiveHigh = 0;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (maxConsecutive > 3) {
|
|
494
|
+
issues.push(
|
|
495
|
+
`Found ${maxConsecutive} consecutive high-tension beats`
|
|
496
|
+
);
|
|
497
|
+
suggestions.push(
|
|
498
|
+
"Add breathing room with quieter beats between intense moments"
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Calculate score
|
|
503
|
+
let baseScore = 85.0;
|
|
504
|
+
if (climaxPositions.length === 0) {
|
|
505
|
+
baseScore -= 20;
|
|
506
|
+
} else if (climaxPositions[climaxPositions.length - 1] < beats.length * 0.5) {
|
|
507
|
+
baseScore -= 15;
|
|
508
|
+
}
|
|
509
|
+
baseScore -= Math.min(20, maxConsecutive * 5);
|
|
510
|
+
|
|
511
|
+
score = Math.max(0.0, Math.min(100.0, baseScore));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Determine status
|
|
515
|
+
if (score >= 70) {
|
|
516
|
+
status = "good";
|
|
517
|
+
} else if (score >= 50) {
|
|
518
|
+
status = "warning";
|
|
519
|
+
} else {
|
|
520
|
+
status = "critical";
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
state.pacingScore = createComponentScore(score, status, {
|
|
524
|
+
issues,
|
|
525
|
+
suggestions,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
return state;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Analyze how well themes permeate the narrative.
|
|
533
|
+
*
|
|
534
|
+
* Checks:
|
|
535
|
+
* - Theme presence across beats
|
|
536
|
+
* - Theme introduction and payoff
|
|
537
|
+
* - Theme strength consistency
|
|
538
|
+
*/
|
|
539
|
+
private analyzeThemeSaturation(
|
|
540
|
+
state: CoherenceEngineState
|
|
541
|
+
): CoherenceEngineState {
|
|
542
|
+
const beats = state.beats;
|
|
543
|
+
const themes = state.themes;
|
|
544
|
+
const issues: string[] = [];
|
|
545
|
+
const suggestions: string[] = [];
|
|
546
|
+
let score: number;
|
|
547
|
+
let status: ComponentStatus;
|
|
548
|
+
|
|
549
|
+
if (themes.length === 0) {
|
|
550
|
+
score = 50.0;
|
|
551
|
+
issues.push("No themes defined");
|
|
552
|
+
suggestions.push(
|
|
553
|
+
"Define thematic threads to enable saturation analysis"
|
|
554
|
+
);
|
|
555
|
+
} else {
|
|
556
|
+
// Track theme presence
|
|
557
|
+
const themeCoverage: Record<string, number> = {};
|
|
558
|
+
|
|
559
|
+
for (const theme of themes) {
|
|
560
|
+
// Calculate theme presence across beats
|
|
561
|
+
let beatsWithTheme = 0;
|
|
562
|
+
for (const beat of beats) {
|
|
563
|
+
if (beat.thematicTags?.includes(theme.id)) {
|
|
564
|
+
beatsWithTheme += 1;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const coverage = beatsWithTheme / Math.max(beats.length, 1);
|
|
569
|
+
themeCoverage[theme.name] = coverage;
|
|
570
|
+
|
|
571
|
+
// Check for underdeveloped themes
|
|
572
|
+
if (coverage < 0.2 && theme.strength > 0.5) {
|
|
573
|
+
issues.push(
|
|
574
|
+
`Theme '${theme.name}' is important but appears rarely`
|
|
575
|
+
);
|
|
576
|
+
suggestions.push(
|
|
577
|
+
`Weave '${theme.name}' into more beats to fulfill its promise`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Check for theme that appears but never pays off
|
|
582
|
+
if (coverage > 0.3 && theme.strength < 0.3) {
|
|
583
|
+
issues.push(
|
|
584
|
+
`Theme '${theme.name}' appears often but lacks impact`
|
|
585
|
+
);
|
|
586
|
+
suggestions.push(
|
|
587
|
+
`Strengthen the thematic weight of '${theme.name}' in key beats`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Calculate average coverage
|
|
593
|
+
const coverageValues = Object.values(themeCoverage);
|
|
594
|
+
const avgCoverage =
|
|
595
|
+
coverageValues.reduce((a, b) => a + b, 0) /
|
|
596
|
+
Math.max(coverageValues.length, 1);
|
|
597
|
+
|
|
598
|
+
// Score based on coverage and issues
|
|
599
|
+
let baseScore = Math.min(100.0, avgCoverage * 100 + 20); // Base on coverage + buffer
|
|
600
|
+
baseScore -= issues.filter((i) => i.includes("rarely")).length * 10;
|
|
601
|
+
baseScore -= issues.filter((i) => i.includes("lacks impact")).length * 8;
|
|
602
|
+
|
|
603
|
+
score = Math.max(0.0, Math.min(100.0, baseScore));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Determine status
|
|
607
|
+
if (score >= 70) {
|
|
608
|
+
status = "good";
|
|
609
|
+
} else if (score >= 50) {
|
|
610
|
+
status = "warning";
|
|
611
|
+
} else {
|
|
612
|
+
status = "critical";
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
state.themeSaturationScore = createComponentScore(score, status, {
|
|
616
|
+
issues,
|
|
617
|
+
suggestions,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
return state;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Analyze narrative continuity.
|
|
625
|
+
*
|
|
626
|
+
* Checks:
|
|
627
|
+
* - Timeline consistency
|
|
628
|
+
* - Detail consistency across beats
|
|
629
|
+
* - Setting/location coherence
|
|
630
|
+
*/
|
|
631
|
+
private analyzeContinuity(state: CoherenceEngineState): CoherenceEngineState {
|
|
632
|
+
const beats = state.beats;
|
|
633
|
+
const issues: string[] = [];
|
|
634
|
+
const suggestions: string[] = [];
|
|
635
|
+
let score: number;
|
|
636
|
+
let status: ComponentStatus;
|
|
637
|
+
|
|
638
|
+
if (beats.length < 2) {
|
|
639
|
+
score = 70.0; // Default to passing if not enough to analyze
|
|
640
|
+
issues.push("Too few beats for continuity analysis");
|
|
641
|
+
} else {
|
|
642
|
+
// Check sequence ordering
|
|
643
|
+
const sequences = beats.map((b) => b.sequence);
|
|
644
|
+
const sortedSequences = [...sequences].sort((a, b) => a - b);
|
|
645
|
+
|
|
646
|
+
if (JSON.stringify(sequences) !== JSON.stringify(sortedSequences)) {
|
|
647
|
+
issues.push("Beat sequences are not in order");
|
|
648
|
+
suggestions.push("Reorder beats to ensure logical sequence progression");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Check for duplicate sequences
|
|
652
|
+
if (sequences.length !== new Set(sequences).size) {
|
|
653
|
+
issues.push("Duplicate beat sequence numbers found");
|
|
654
|
+
suggestions.push("Ensure each beat has a unique sequence number");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Check for gaps in sequence
|
|
658
|
+
const maxSeq = Math.max(...sequences);
|
|
659
|
+
const expected = new Set(
|
|
660
|
+
Array.from({ length: maxSeq }, (_, i) => i + 1)
|
|
661
|
+
);
|
|
662
|
+
const actual = new Set(sequences);
|
|
663
|
+
const missing = [...expected].filter((x) => !actual.has(x));
|
|
664
|
+
|
|
665
|
+
if (missing.length > 0 && missing.length <= 3) {
|
|
666
|
+
// Small gaps are issues
|
|
667
|
+
issues.push(`Missing beat sequences: ${missing.sort().join(", ")}`);
|
|
668
|
+
suggestions.push("Fill in missing beat sequences or renumber");
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Calculate score
|
|
672
|
+
let baseScore = 90.0;
|
|
673
|
+
baseScore -= issues.filter((i) => i.includes("not in order")).length * 20;
|
|
674
|
+
baseScore -= issues.filter((i) => i.includes("Duplicate")).length * 15;
|
|
675
|
+
baseScore -= issues.filter((i) => i.includes("Missing")).length * 5;
|
|
676
|
+
|
|
677
|
+
score = Math.max(0.0, Math.min(100.0, baseScore));
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Determine status
|
|
681
|
+
if (score >= 70) {
|
|
682
|
+
status = "good";
|
|
683
|
+
} else if (score >= 50) {
|
|
684
|
+
status = "warning";
|
|
685
|
+
} else {
|
|
686
|
+
status = "critical";
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
state.continuityScore = createComponentScore(score, status, {
|
|
690
|
+
issues,
|
|
691
|
+
suggestions,
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
return state;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Calculate the overall coherence score from components.
|
|
699
|
+
*/
|
|
700
|
+
private calculateOverallScore(
|
|
701
|
+
state: CoherenceEngineState
|
|
702
|
+
): CoherenceEngineState {
|
|
703
|
+
const components = [
|
|
704
|
+
state.narrativeFlowScore,
|
|
705
|
+
state.characterConsistencyScore,
|
|
706
|
+
state.pacingScore,
|
|
707
|
+
state.themeSaturationScore,
|
|
708
|
+
state.continuityScore,
|
|
709
|
+
];
|
|
710
|
+
|
|
711
|
+
const validScores = components
|
|
712
|
+
.filter((c): c is ComponentScore => c !== undefined)
|
|
713
|
+
.map((c) => c.score);
|
|
714
|
+
|
|
715
|
+
if (validScores.length > 0) {
|
|
716
|
+
// Weighted average (narrative flow and character consistency weighted higher)
|
|
717
|
+
const weights = [1.2, 1.2, 1.0, 1.0, 0.8]; // Matches component order
|
|
718
|
+
const weightedSum = validScores.reduce(
|
|
719
|
+
(sum, s, i) => sum + s * weights[i],
|
|
720
|
+
0
|
|
721
|
+
);
|
|
722
|
+
const totalWeight = weights
|
|
723
|
+
.slice(0, validScores.length)
|
|
724
|
+
.reduce((a, b) => a + b, 0);
|
|
725
|
+
state.overallScore = weightedSum / totalWeight;
|
|
726
|
+
} else {
|
|
727
|
+
state.overallScore = 50.0;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return state;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Identify narrative gaps from component analyses.
|
|
735
|
+
*/
|
|
736
|
+
private identifyGaps(state: CoherenceEngineState): CoherenceEngineState {
|
|
737
|
+
const gaps: Gap[] = [];
|
|
738
|
+
|
|
739
|
+
// Extract issues from each component
|
|
740
|
+
const componentMappings: [keyof CoherenceEngineState, GapType][] = [
|
|
741
|
+
["narrativeFlowScore", GapType.STRUCTURAL],
|
|
742
|
+
["characterConsistencyScore", GapType.CHARACTER],
|
|
743
|
+
["pacingScore", GapType.STRUCTURAL],
|
|
744
|
+
["themeSaturationScore", GapType.THEMATIC],
|
|
745
|
+
["continuityScore", GapType.CONTINUITY],
|
|
746
|
+
];
|
|
747
|
+
|
|
748
|
+
for (const [componentKey, gapType] of componentMappings) {
|
|
749
|
+
const component = state[componentKey] as ComponentScore | undefined;
|
|
750
|
+
if (component?.issues) {
|
|
751
|
+
for (const issue of component.issues) {
|
|
752
|
+
// Determine severity
|
|
753
|
+
let severity: GapSeverity;
|
|
754
|
+
if (component.status === "critical") {
|
|
755
|
+
severity = GapSeverity.CRITICAL;
|
|
756
|
+
} else if (issue.includes("rarely") || issue.includes("disappears")) {
|
|
757
|
+
severity = GapSeverity.MODERATE;
|
|
758
|
+
} else {
|
|
759
|
+
severity = GapSeverity.MINOR;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Determine routing
|
|
763
|
+
let route: RoutingTarget;
|
|
764
|
+
if (gapType === GapType.STRUCTURAL) {
|
|
765
|
+
route = RoutingTarget.STRUCTURIST;
|
|
766
|
+
} else if (gapType === GapType.CHARACTER) {
|
|
767
|
+
route = RoutingTarget.STORYTELLER;
|
|
768
|
+
} else if (gapType === GapType.THEMATIC) {
|
|
769
|
+
route = RoutingTarget.STRUCTURIST;
|
|
770
|
+
} else if (gapType === GapType.SENSORY) {
|
|
771
|
+
route = RoutingTarget.STORYTELLER;
|
|
772
|
+
} else {
|
|
773
|
+
// CONTINUITY
|
|
774
|
+
route = RoutingTarget.AUTHOR;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
gaps.push(
|
|
778
|
+
createGap(
|
|
779
|
+
this.generateGapId(),
|
|
780
|
+
gapType,
|
|
781
|
+
severity,
|
|
782
|
+
issue,
|
|
783
|
+
route,
|
|
784
|
+
{ location: { component: componentKey } }
|
|
785
|
+
)
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Sort by severity (critical first)
|
|
792
|
+
const severityOrder: Record<GapSeverity, number> = {
|
|
793
|
+
[GapSeverity.CRITICAL]: 0,
|
|
794
|
+
[GapSeverity.MODERATE]: 1,
|
|
795
|
+
[GapSeverity.MINOR]: 2,
|
|
796
|
+
};
|
|
797
|
+
gaps.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
798
|
+
|
|
799
|
+
state.gaps = gaps;
|
|
800
|
+
|
|
801
|
+
return state;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Generate Trinity perspective assessment (Mia/Miette/Ava8).
|
|
806
|
+
*
|
|
807
|
+
* Each persona provides feedback aligned with their perspective:
|
|
808
|
+
* - Mia 🧠: Structural/logical analysis
|
|
809
|
+
* - Miette 🌸: Emotional/resonance analysis
|
|
810
|
+
* - Ava8 🎨: Atmospheric/sensory analysis
|
|
811
|
+
*/
|
|
812
|
+
private generateTrinityAssessment(
|
|
813
|
+
state: CoherenceEngineState
|
|
814
|
+
): CoherenceEngineState {
|
|
815
|
+
const gaps = state.gaps || [];
|
|
816
|
+
|
|
817
|
+
// Component scores
|
|
818
|
+
const flow = state.narrativeFlowScore;
|
|
819
|
+
const character = state.characterConsistencyScore;
|
|
820
|
+
const pacing = state.pacingScore;
|
|
821
|
+
const theme = state.themeSaturationScore;
|
|
822
|
+
const continuity = state.continuityScore;
|
|
823
|
+
|
|
824
|
+
// Build Mia's assessment (structural)
|
|
825
|
+
const miaParts: string[] = [];
|
|
826
|
+
if (flow) {
|
|
827
|
+
miaParts.push(`Structure is ${flow.score.toFixed(0)}% sound.`);
|
|
828
|
+
if (flow.issues.length > 0) {
|
|
829
|
+
miaParts.push(`Key structural gap: ${flow.issues[0]}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (pacing && pacing.score < 70) {
|
|
833
|
+
miaParts.push(`Pacing needs attention (${pacing.score.toFixed(0)}%).`);
|
|
834
|
+
if (pacing.suggestions.length > 0) {
|
|
835
|
+
miaParts.push(pacing.suggestions[0]);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (continuity && continuity.score < 80) {
|
|
839
|
+
miaParts.push(
|
|
840
|
+
`Continuity has ${continuity.issues.length} issues to address.`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const mia =
|
|
845
|
+
miaParts.length > 0
|
|
846
|
+
? miaParts.join(" ")
|
|
847
|
+
: "Structure analysis unavailable.";
|
|
848
|
+
|
|
849
|
+
// Build Miette's assessment (emotional)
|
|
850
|
+
const mietteParts: string[] = [];
|
|
851
|
+
if (character) {
|
|
852
|
+
if (character.score >= 80) {
|
|
853
|
+
mietteParts.push("Character arcs are resonating well.");
|
|
854
|
+
} else {
|
|
855
|
+
mietteParts.push(
|
|
856
|
+
`Character consistency is ${character.score.toFixed(0)}%.`
|
|
857
|
+
);
|
|
858
|
+
if (character.issues.length > 0) {
|
|
859
|
+
mietteParts.push(`The emotional gap: ${character.issues[0]}`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (theme) {
|
|
864
|
+
if (theme.score >= 70) {
|
|
865
|
+
mietteParts.push("Themes are landing with emotional weight.");
|
|
866
|
+
} else {
|
|
867
|
+
mietteParts.push("Themes need stronger emotional anchoring.");
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (flow?.issues) {
|
|
871
|
+
const jarring = flow.issues.filter((i) => i.includes("Jarring"));
|
|
872
|
+
if (jarring.length > 0) {
|
|
873
|
+
mietteParts.push("Emotional transitions feel abrupt in places.");
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const miette =
|
|
878
|
+
mietteParts.length > 0
|
|
879
|
+
? mietteParts.join(" ")
|
|
880
|
+
: "Emotional analysis unavailable.";
|
|
881
|
+
|
|
882
|
+
// Build Ava8's assessment (atmospheric)
|
|
883
|
+
const ava8Parts: string[] = [];
|
|
884
|
+
const sensoryGaps = gaps.filter((g) => g.gapType === GapType.SENSORY);
|
|
885
|
+
if (sensoryGaps.length > 0) {
|
|
886
|
+
ava8Parts.push(`Found ${sensoryGaps.length} sensory gaps to address.`);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (pacing && pacing.score >= 70) {
|
|
890
|
+
ava8Parts.push("Atmospheric rhythm feels balanced.");
|
|
891
|
+
} else {
|
|
892
|
+
ava8Parts.push("Atmosphere could use more grounding moments.");
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Check for consecutive high-tension (affects atmosphere)
|
|
896
|
+
if (
|
|
897
|
+
pacing?.issues.some((i) => i.includes("consecutive high-tension"))
|
|
898
|
+
) {
|
|
899
|
+
ava8Parts.push(
|
|
900
|
+
"The dense tension sections may benefit from visual breathing room."
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const ava8 =
|
|
905
|
+
ava8Parts.length > 0
|
|
906
|
+
? ava8Parts.join(" ")
|
|
907
|
+
: "Atmospheric analysis unavailable.";
|
|
908
|
+
|
|
909
|
+
// Determine priorities
|
|
910
|
+
const priorities: string[] = [];
|
|
911
|
+
const criticalGaps = gaps.filter(
|
|
912
|
+
(g) => g.severity === GapSeverity.CRITICAL
|
|
913
|
+
);
|
|
914
|
+
if (criticalGaps.length > 0) {
|
|
915
|
+
priorities.push(...criticalGaps.slice(0, 3).map((g) => g.description));
|
|
916
|
+
} else {
|
|
917
|
+
const moderateGaps = gaps.filter(
|
|
918
|
+
(g) => g.severity === GapSeverity.MODERATE
|
|
919
|
+
);
|
|
920
|
+
if (moderateGaps.length > 0) {
|
|
921
|
+
priorities.push(
|
|
922
|
+
...moderateGaps.slice(0, 3).map((g) => g.description)
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (priorities.length === 0) {
|
|
928
|
+
priorities.push("Minor polish items only - narrative is coherent");
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
state.trinityAssessment = createTrinityAssessment(
|
|
932
|
+
mia,
|
|
933
|
+
miette,
|
|
934
|
+
ava8,
|
|
935
|
+
priorities
|
|
936
|
+
);
|
|
937
|
+
|
|
938
|
+
return state;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Build the final coherence result object.
|
|
943
|
+
*/
|
|
944
|
+
private buildCoherenceResult(
|
|
945
|
+
state: CoherenceEngineState
|
|
946
|
+
): CoherenceEngineState {
|
|
947
|
+
state.coherenceScore = createCoherenceScore(
|
|
948
|
+
state.overallScore || 50.0,
|
|
949
|
+
state.narrativeFlowScore ||
|
|
950
|
+
createComponentScore(50, "warning"),
|
|
951
|
+
state.characterConsistencyScore ||
|
|
952
|
+
createComponentScore(50, "warning"),
|
|
953
|
+
state.pacingScore || createComponentScore(50, "warning"),
|
|
954
|
+
state.themeSaturationScore ||
|
|
955
|
+
createComponentScore(50, "warning"),
|
|
956
|
+
state.continuityScore ||
|
|
957
|
+
createComponentScore(50, "warning")
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
return state;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Analyze narrative coherence.
|
|
965
|
+
*
|
|
966
|
+
* @param beats List of story beats to analyze
|
|
967
|
+
* @param characters Optional list of character states
|
|
968
|
+
* @param themes Optional list of thematic threads
|
|
969
|
+
* @param includeMetadata Whether to include full analysis state
|
|
970
|
+
* @returns CoherenceResult with coherenceScore, gaps, and trinityAssessment
|
|
971
|
+
*/
|
|
972
|
+
analyze(
|
|
973
|
+
beats: StoryBeat[],
|
|
974
|
+
characters: CharacterState[] = [],
|
|
975
|
+
themes: ThematicThread[] = [],
|
|
976
|
+
includeMetadata: boolean = false
|
|
977
|
+
): CoherenceResult | CoherenceEngineState {
|
|
978
|
+
// Initialize state
|
|
979
|
+
let state: CoherenceEngineState = {
|
|
980
|
+
beats,
|
|
981
|
+
characters,
|
|
982
|
+
themes,
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
// Run analysis pipeline
|
|
986
|
+
state = this.analyzeNarrativeFlow(state);
|
|
987
|
+
state = this.analyzeCharacterConsistency(state);
|
|
988
|
+
state = this.analyzePacing(state);
|
|
989
|
+
state = this.analyzeThemeSaturation(state);
|
|
990
|
+
state = this.analyzeContinuity(state);
|
|
991
|
+
state = this.calculateOverallScore(state);
|
|
992
|
+
state = this.identifyGaps(state);
|
|
993
|
+
state = this.generateTrinityAssessment(state);
|
|
994
|
+
state = this.buildCoherenceResult(state);
|
|
995
|
+
|
|
996
|
+
if (includeMetadata) {
|
|
997
|
+
return state;
|
|
998
|
+
} else {
|
|
999
|
+
return {
|
|
1000
|
+
coherenceScore: state.coherenceScore!,
|
|
1001
|
+
gaps: state.gaps || [],
|
|
1002
|
+
trinityAssessment: state.trinityAssessment!,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Group gaps by their routing target.
|
|
1009
|
+
*
|
|
1010
|
+
* @param gaps List of identified gaps
|
|
1011
|
+
* @returns Dictionary mapping routing target to list of gaps
|
|
1012
|
+
*/
|
|
1013
|
+
getRoutingSuggestions(gaps: Gap[]): Record<RoutingTarget, Gap[]> {
|
|
1014
|
+
const routing: Record<RoutingTarget, Gap[]> = {
|
|
1015
|
+
[RoutingTarget.STORYTELLER]: [],
|
|
1016
|
+
[RoutingTarget.STRUCTURIST]: [],
|
|
1017
|
+
[RoutingTarget.ARCHITECT]: [],
|
|
1018
|
+
[RoutingTarget.AUTHOR]: [],
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
for (const gap of gaps) {
|
|
1022
|
+
routing[gap.suggestedRoute].push(gap);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
return routing;
|
|
1026
|
+
}
|
|
1027
|
+
}
|