@ak--47/dungeon-master 1.0.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 (66) hide show
  1. package/README.md +518 -0
  2. package/dungeons/array-of-object-lookup-schema.json +327 -0
  3. package/dungeons/array-of-object-lookup.js +220 -0
  4. package/dungeons/ecommerce-schema.json +462 -0
  5. package/dungeons/ecommerce.js +447 -0
  6. package/dungeons/education-schema.json +2409 -0
  7. package/dungeons/education.js +768 -0
  8. package/dungeons/fintech-schema.json +14034 -0
  9. package/dungeons/fintech.js +696 -0
  10. package/dungeons/foobar-schema.json +403 -0
  11. package/dungeons/foobar.js +296 -0
  12. package/dungeons/food-delivery-schema.json +192 -0
  13. package/dungeons/food-delivery.js +602 -0
  14. package/dungeons/food-schema.json +1152 -0
  15. package/dungeons/food.js +754 -0
  16. package/dungeons/gaming-schema.json +1270 -0
  17. package/dungeons/gaming.js +508 -0
  18. package/dungeons/insurance-application-schema.json +204 -0
  19. package/dungeons/insurance-application.js +605 -0
  20. package/dungeons/media-schema.json +906 -0
  21. package/dungeons/media.js +790 -0
  22. package/dungeons/retention-cadence-schema.json +78 -0
  23. package/dungeons/retention-cadence.js +244 -0
  24. package/dungeons/rpg-schema.json +4526 -0
  25. package/dungeons/rpg.js +919 -0
  26. package/dungeons/sanity-schema.json +255 -0
  27. package/dungeons/sanity.js +152 -0
  28. package/dungeons/sass-schema.json +1291 -0
  29. package/dungeons/sass.js +795 -0
  30. package/dungeons/scd-schema.json +919 -0
  31. package/dungeons/scd.js +277 -0
  32. package/dungeons/simple-schema.json +608 -0
  33. package/dungeons/simple.js +285 -0
  34. package/dungeons/simplest-schema.json +1418 -0
  35. package/dungeons/simplest.js +392 -0
  36. package/dungeons/social-schema.json +1118 -0
  37. package/dungeons/social.js +686 -0
  38. package/dungeons/text-generation-schema.json +3096 -0
  39. package/dungeons/text-generation.js +812 -0
  40. package/index.js +567 -0
  41. package/lib/core/config-validator.js +395 -0
  42. package/lib/core/context.js +204 -0
  43. package/lib/core/dungeon-loader.js +337 -0
  44. package/lib/core/storage.js +379 -0
  45. package/lib/generators/adspend.js +132 -0
  46. package/lib/generators/events.js +271 -0
  47. package/lib/generators/funnels.js +407 -0
  48. package/lib/generators/mirror.js +167 -0
  49. package/lib/generators/product-lookup.js +262 -0
  50. package/lib/generators/product-names.js +195 -0
  51. package/lib/generators/profiles.js +93 -0
  52. package/lib/generators/scd.js +124 -0
  53. package/lib/generators/text.js +1192 -0
  54. package/lib/orchestrators/mixpanel-sender.js +266 -0
  55. package/lib/orchestrators/user-loop.js +335 -0
  56. package/lib/templates/abbreviated.d.ts +169 -0
  57. package/lib/templates/defaults.js +1405 -0
  58. package/lib/templates/phrases.js +2526 -0
  59. package/lib/templates/schema.d.ts +173 -0
  60. package/lib/templates/soup-presets.js +188 -0
  61. package/lib/utils/function-registry.js +302 -0
  62. package/lib/utils/json-evaluator.js +172 -0
  63. package/lib/utils/logger.js +34 -0
  64. package/lib/utils/utils.js +1490 -0
  65. package/package.json +89 -0
  66. package/types.d.ts +865 -0
@@ -0,0 +1,1192 @@
1
+ /**
2
+ * Organic Text Generation Module
3
+ * Generates genuinely human-feeling unstructured text
4
+ * @module text
5
+ */
6
+
7
+ // ============= Type Imports =============
8
+ // Types are defined in types.d.ts
9
+
10
+ /**
11
+ * @typedef {import("../../types").TextTone} TextTone
12
+ * @typedef {import("../../types").TextStyle} TextStyle
13
+ * @typedef {import("../../types").TextIntensity} TextIntensity
14
+ * @typedef {import("../../types").TextFormality} TextFormality
15
+ * @typedef {import("../../types").TextReturnType} TextReturnType
16
+ * @typedef {import("../../types").TextKeywordSet} TextKeywordSet
17
+ * @typedef {import("../../types").TextGeneratorConfig} TextGeneratorConfig
18
+ * @typedef {import("../../types").TextMetadata} TextMetadata
19
+ * @typedef {import("../../types").GeneratedText} GeneratedText
20
+ * @typedef {import("../../types").SimpleGeneratedText} SimpleGeneratedText
21
+ * @typedef {import("../../types").TextBatchOptions} TextBatchOptions
22
+ * @typedef {import("../../types").TextGeneratorStats} TextGeneratorStats
23
+ */
24
+
25
+ import tracery from 'tracery-grammar';
26
+ import seedrandom from 'seedrandom';
27
+ import crypto from 'crypto';
28
+ import SentimentPkg from 'sentiment';
29
+ import { PHRASE_BANK, GENERATION_PATTERNS, ORGANIC_PATTERNS } from '../templates/phrases.js';
30
+ import { getChance } from '../utils/utils.js';
31
+
32
+ const Sentiment = typeof SentimentPkg === 'function' ? SentimentPkg : SentimentPkg.default;
33
+ const sentiment = new Sentiment();
34
+
35
+ // ============= Helper Functions =============
36
+
37
+ function seededRandom() {
38
+ const c = getChance();
39
+ return c.floating({ min: 0, max: 1 });
40
+ }
41
+
42
+ function chance(probability) {
43
+ return seededRandom() < probability;
44
+ }
45
+
46
+ function pick(array) {
47
+ return array[Math.floor(seededRandom() * array.length)];
48
+ }
49
+
50
+ function pickWeighted(items, weights) {
51
+ const total = weights.reduce((a, b) => a + b, 0);
52
+ let random = seededRandom() * total;
53
+ for (let i = 0; i < items.length; i++) {
54
+ random -= weights[i];
55
+ if (random <= 0) return items[i];
56
+ }
57
+ return items[0];
58
+ }
59
+
60
+ // ============= Thought Stream Generator =============
61
+
62
+ class ThoughtStream {
63
+ constructor() {
64
+ this.momentum = {
65
+ emotional: 0,
66
+ technical: 0,
67
+ frustration: 0,
68
+ excitement: 0
69
+ };
70
+ this.lastTopic = null;
71
+ this.thoughtHistory = [];
72
+ }
73
+
74
+ reset() {
75
+ this.momentum = { emotional: 0, technical: 0, frustration: 0, excitement: 0 };
76
+ this.lastTopic = null;
77
+ this.thoughtHistory = [];
78
+ }
79
+
80
+ generateThought(tone, context = {}) {
81
+ const patterns = ORGANIC_PATTERNS.thoughtPatterns[tone] || ORGANIC_PATTERNS.thoughtPatterns.neu;
82
+ const pattern = pick(patterns);
83
+
84
+ // Replace placeholders with context-aware content
85
+ let thought = this.fillPattern(pattern, context);
86
+
87
+ // Add natural variations
88
+ if (this.momentum.frustration > 0.5 && chance(0.3)) {
89
+ thought = thought.toUpperCase();
90
+ } else if (this.momentum.emotional > 0.7 && chance(0.4)) {
91
+ thought = this.addEmphasis(thought);
92
+ }
93
+
94
+ // Update momentum
95
+ this.updateMomentum(thought, tone);
96
+
97
+ return thought;
98
+ }
99
+
100
+ fillPattern(pattern, context) {
101
+ // Simple template filling - in production this would be more sophisticated
102
+ return pattern
103
+ .replace(/{product}/g, () => pick(PHRASE_BANK.products))
104
+ .replace(/{feature}/g, () => pick(PHRASE_BANK.features))
105
+ .replace(/{issue}/g, () => pick(PHRASE_BANK.issues))
106
+ .replace(/{emotion}/g, () => pick(PHRASE_BANK.emotions[context.tone || 'neu']));
107
+ }
108
+
109
+ addEmphasis(text) {
110
+ const methods = [
111
+ t => t.replace(/\s+/g, '. ').toUpperCase() + '.',
112
+ t => t.split(' ').map(w => w.length > 3 && chance(0.3) ? w.toUpperCase() : w).join(' '),
113
+ t => t + '!!!',
114
+ t => '...' + t + '...',
115
+ t => t.split(' ').join(' 👏 ')
116
+ ];
117
+ return pick(methods)(text);
118
+ }
119
+
120
+ updateMomentum(thought, tone) {
121
+ // Emotional momentum
122
+ if (tone === 'neg') {
123
+ this.momentum.frustration = Math.min(1, this.momentum.frustration + 0.2);
124
+ this.momentum.excitement *= 0.8;
125
+ } else if (tone === 'pos') {
126
+ this.momentum.excitement = Math.min(1, this.momentum.excitement + 0.2);
127
+ this.momentum.frustration *= 0.8;
128
+ }
129
+
130
+ // Technical momentum
131
+ if (/\b(API|backend|database|server|deployment)\b/i.test(thought)) {
132
+ this.momentum.technical = Math.min(1, this.momentum.technical + 0.3);
133
+ } else {
134
+ this.momentum.technical *= 0.9;
135
+ }
136
+
137
+ // General emotional buildup
138
+ if (/[!?]{2,}|[A-Z]{5,}/.test(thought)) {
139
+ this.momentum.emotional = Math.min(1, this.momentum.emotional + 0.3);
140
+ } else {
141
+ this.momentum.emotional *= 0.95;
142
+ }
143
+
144
+ this.thoughtHistory.push(thought);
145
+ }
146
+
147
+ connect(thoughts) {
148
+ if (thoughts.length === 0) return '';
149
+ if (thoughts.length === 1) return thoughts[0];
150
+
151
+ const connected = [];
152
+ for (let i = 0; i < thoughts.length; i++) {
153
+ connected.push(thoughts[i]);
154
+
155
+ if (i < thoughts.length - 1) {
156
+ // Choose connector based on momentum
157
+ if (this.momentum.frustration > 0.6) {
158
+ connected.push(pick(['. AND ', '. ALSO ', '. Plus ', '. Oh and ', '... ']));
159
+ } else if (this.momentum.excitement > 0.6) {
160
+ connected.push(pick(['! And ', '! Also ', '!! ', '! Oh and ', '! ']));
161
+ } else {
162
+ connected.push(pick(['. ', ', ', '... ', ' - ', '. So ', '. But ', '. Well, ', '. Now, ', '. Then, ', '. Still, ']));
163
+ }
164
+ }
165
+ }
166
+
167
+ return connected.join('');
168
+ }
169
+ }
170
+
171
+ // ============= Context Tracker =============
172
+
173
+ class ContextTracker {
174
+ constructor() {
175
+ this.reset();
176
+ }
177
+
178
+ reset() {
179
+ this.topics = [];
180
+ this.sentimentHistory = [];
181
+ this.currentVoice = null;
182
+ this.technicalLevel = 0;
183
+ this.formalityLevel = 0;
184
+ }
185
+
186
+ update(text) {
187
+ // Extract topics
188
+ const topicMatches = text.match(/\b(dashboard|feature|API|app|system|tool|platform)\b/gi);
189
+ if (topicMatches) {
190
+ this.topics.push(...topicMatches);
191
+ }
192
+
193
+ // Track sentiment
194
+ const score = sentiment.analyze(text).score;
195
+ this.sentimentHistory.push(score);
196
+
197
+ // Detect voice
198
+ if (/\b(gonna|wanna|kinda|y'all)\b/i.test(text)) {
199
+ this.currentVoice = 'casual';
200
+ } else if (/\b(furthermore|therefore|consequently)\b/i.test(text)) {
201
+ this.currentVoice = 'formal';
202
+ }
203
+
204
+ // Technical level
205
+ const techTerms = (text.match(/\b(API|JSON|SQL|HTTP|CSS|HTML|JavaScript|Python)\b/gi) || []).length;
206
+ this.technicalLevel = Math.min(1, this.technicalLevel * 0.8 + techTerms * 0.1);
207
+
208
+ // Formality level
209
+ const formalTerms = (text.match(/\b(regarding|concerning|therefore|furthermore)\b/gi) || []).length;
210
+ const casualTerms = (text.match(/\b(like|kinda|sorta|stuff|thing)\b/gi) || []).length;
211
+ this.formalityLevel = Math.max(0, Math.min(1, this.formalityLevel + (formalTerms - casualTerms) * 0.1));
212
+ }
213
+
214
+ getContext() {
215
+ return {
216
+ lastTopic: this.topics[this.topics.length - 1] || null,
217
+ averageSentiment: this.sentimentHistory.length > 0 ?
218
+ this.sentimentHistory.reduce((a, b) => a + b, 0) / this.sentimentHistory.length : 0,
219
+ voice: this.currentVoice,
220
+ technicalLevel: this.technicalLevel,
221
+ formalityLevel: this.formalityLevel
222
+ };
223
+ }
224
+ }
225
+
226
+ // ============= Natural Deduplicator =============
227
+
228
+ class NaturalDeduplicator {
229
+ constructor() {
230
+ this.recentTexts = [];
231
+ this.semanticFingerprints = new Map();
232
+ this.maxRecent = 100;
233
+ }
234
+
235
+ wouldBeDuplicate(text) {
236
+ // Allow natural repetitions
237
+ if (this.isNaturalRepetition(text)) {
238
+ return false;
239
+ }
240
+
241
+ // Check semantic similarity
242
+ const fingerprint = this.getFingerprint(text);
243
+ const similar = this.findSimilar(fingerprint);
244
+
245
+ // Allow up to 2 very similar texts (humans repeat themselves)
246
+ return similar.length > 2;
247
+ }
248
+
249
+ add(text) {
250
+ this.recentTexts.push(text);
251
+ if (this.recentTexts.length > this.maxRecent) {
252
+ this.recentTexts.shift();
253
+ }
254
+
255
+ const fingerprint = this.getFingerprint(text);
256
+ const key = `${fingerprint.topic}-${fingerprint.sentiment}`;
257
+
258
+ if (!this.semanticFingerprints.has(key)) {
259
+ this.semanticFingerprints.set(key, []);
260
+ }
261
+ this.semanticFingerprints.get(key).push(text);
262
+ }
263
+
264
+ getFingerprint(text) {
265
+ const words = text.toLowerCase().split(/\s+/);
266
+ const topic = this.extractMainTopic(text);
267
+
268
+ // Use simple heuristics instead of full sentiment analysis for speed
269
+ const negWords = (text.match(/\b(broken|terrible|awful|bad|crash|error|fail|bug)\b/gi) || []).length;
270
+ const posWords = (text.match(/\b(great|excellent|amazing|good|love|perfect|works)\b/gi) || []).length;
271
+ const sentiment = negWords > posWords ? 'neg' : posWords > negWords ? 'pos' : 'neu';
272
+
273
+ return {
274
+ topic,
275
+ sentiment,
276
+ length: words.length,
277
+ structure: this.getStructure(text),
278
+ uniqueWords: new Set(words).size
279
+ };
280
+ }
281
+
282
+ extractMainTopic(text) {
283
+ const topics = text.match(/\b(dashboard|API|feature|app|system|bug|error|issue)\b/i);
284
+ return topics ? topics[0].toLowerCase() : 'general';
285
+ }
286
+
287
+ getStructure(text) {
288
+ const sentences = text.split(/[.!?]+/).length;
289
+ const hasQuestion = text.includes('?');
290
+ const hasExclamation = text.includes('!');
291
+
292
+ return `${sentences}s${hasQuestion ? 'Q' : ''}${hasExclamation ? 'E' : ''}`;
293
+ }
294
+
295
+ findSimilar(fingerprint) {
296
+ const key = `${fingerprint.topic}-${fingerprint.sentiment}`;
297
+ const candidates = this.semanticFingerprints.get(key) || [];
298
+
299
+ return candidates.filter(text => {
300
+ const similarity = this.calculateSimilarity(text, fingerprint);
301
+ return similarity > 0.7;
302
+ });
303
+ }
304
+
305
+ calculateSimilarity(text, targetFingerprint) {
306
+ const textFingerprint = this.getFingerprint(text);
307
+
308
+ let score = 0;
309
+ if (textFingerprint.topic === targetFingerprint.topic) score += 0.3;
310
+ if (textFingerprint.sentiment === targetFingerprint.sentiment) score += 0.2;
311
+ if (Math.abs(textFingerprint.length - targetFingerprint.length) < 10) score += 0.2;
312
+ if (textFingerprint.structure === targetFingerprint.structure) score += 0.3;
313
+
314
+ return score;
315
+ }
316
+
317
+ isNaturalRepetition(text) {
318
+ // Common phrases that naturally repeat
319
+ const naturalPhrases = [
320
+ /^(hi|hey|hello|thanks|thank you)/i,
321
+ /^(yes|no|okay|sure|got it)/i,
322
+ /^(any update|following up|still waiting)/i,
323
+ /^(this is ridiculous|come on|seriously)/i
324
+ ];
325
+
326
+ return naturalPhrases.some(pattern => pattern.test(text)) && text.length < 50;
327
+ }
328
+ }
329
+
330
+ // ============= Voice Consistency Engine =============
331
+
332
+ class VoiceConsistency {
333
+ constructor(formality) {
334
+ this.formality = formality;
335
+ this.vocabulary = this.selectVocabulary(formality);
336
+ }
337
+
338
+ selectVocabulary(formality) {
339
+ const vocabularies = {
340
+ casual: {
341
+ connectors: ['and', 'but', 'so', 'like', 'anyway'],
342
+ intensifiers: ['really', 'super', 'totally', 'so', 'pretty'],
343
+ hedges: ['kinda', 'sorta', 'I guess', 'maybe', 'probably']
344
+ },
345
+ business: {
346
+ connectors: ['additionally', 'however', 'therefore', 'furthermore', 'consequently'],
347
+ intensifiers: ['very', 'quite', 'extremely', 'particularly', 'especially'],
348
+ hedges: ['perhaps', 'potentially', 'possibly', 'it appears', 'it seems']
349
+ },
350
+ technical: {
351
+ connectors: ['additionally', 'moreover', 'specifically', 'namely', 'particularly'],
352
+ intensifiers: ['significantly', 'substantially', 'considerably', 'markedly'],
353
+ hedges: ['approximately', 'roughly', 'estimated', 'projected', 'calculated']
354
+ }
355
+ };
356
+
357
+ return vocabularies[formality] || vocabularies.casual;
358
+ }
359
+
360
+ maintain(text) {
361
+ // Apply consistent voice
362
+ let consistent = text;
363
+
364
+ // Replace connectors
365
+ consistent = consistent.replace(/\b(and|but|so)\b/gi, () =>
366
+ pick(this.vocabulary.connectors)
367
+ );
368
+
369
+ // Apply appropriate contractions
370
+ if (this.formality === 'casual') {
371
+ consistent = consistent
372
+ .replace(/\bcannot\b/g, "can't")
373
+ .replace(/\bwill not\b/g, "won't")
374
+ .replace(/\bdo not\b/g, "don't");
375
+ } else if (this.formality === 'business' || this.formality === 'technical') {
376
+ consistent = consistent
377
+ .replace(/\bcan't\b/g, "cannot")
378
+ .replace(/\bwon't\b/g, "will not")
379
+ .replace(/\bdon't\b/g, "do not");
380
+ }
381
+
382
+ return consistent;
383
+ }
384
+ }
385
+
386
+ // ============= Natural Typo Engine =============
387
+
388
+ class NaturalTypoEngine {
389
+ constructor() {
390
+ this.patterns = {
391
+ emotional: [
392
+ { pattern: /\bthe\b/g, errors: ['teh', 'th', 'hte'] },
393
+ { pattern: /\byou\b/g, errors: ['u', 'yuo'] },
394
+ { pattern: /\bbecause\b/g, errors: ['becuase', 'bc', 'cuz'] },
395
+ { pattern: /\bdefinitely\b/g, errors: ['definately', 'defiantly'] }
396
+ ],
397
+ mobile: [
398
+ { pattern: /\s+/g, errors: [''] }, // Missing spaces
399
+ { pattern: /([a-z])\1/g, errors: ['$1'] } // Missing double letters
400
+ ],
401
+ rushing: [
402
+ { pattern: /ing\b/g, errors: ['ign', 'in'] },
403
+ { pattern: /tion\b/g, errors: ['toin', 'tion'] }
404
+ ]
405
+ };
406
+ }
407
+
408
+ apply(text, rate, context = {}) {
409
+ if (!rate || rate === 0) return text;
410
+
411
+ const words = text.split(/(\s+)/);
412
+ const typoClusterProbability = 0.3; // Typos tend to cluster
413
+ let inCluster = false;
414
+
415
+ return words.map(word => {
416
+ if (!/\w/.test(word)) return word; // Skip non-words
417
+
418
+ const shouldTypo = inCluster ?
419
+ chance(rate * 3) : // Higher chance in cluster
420
+ chance(rate);
421
+
422
+ if (shouldTypo) {
423
+ inCluster = chance(typoClusterProbability);
424
+ return this.createTypo(word, context);
425
+ }
426
+
427
+ inCluster = false;
428
+ return word;
429
+ }).join('');
430
+ }
431
+
432
+ createTypo(word, context) {
433
+ // Select typo type based on context
434
+ const patterns = context.emotional > 0.5 ? this.patterns.emotional :
435
+ context.mobile ? this.patterns.mobile :
436
+ this.patterns.rushing;
437
+
438
+ for (const { pattern, errors } of patterns) {
439
+ if (pattern.test(word)) {
440
+ return word.replace(pattern, pick(errors));
441
+ }
442
+ }
443
+
444
+ // Fallback: transpose letters
445
+ if (word.length > 3) {
446
+ const pos = Math.floor(seededRandom() * (word.length - 2)) + 1;
447
+ return word.slice(0, pos) + word[pos + 1] + word[pos] + word.slice(pos + 2);
448
+ }
449
+
450
+ return word;
451
+ }
452
+ }
453
+
454
+ // ============= Keyword Injector =============
455
+
456
+ class KeywordInjector {
457
+ constructor(keywords) {
458
+ this.keywords = keywords || {};
459
+ this.injected = [];
460
+ }
461
+
462
+ inject(text, density = 0.15) {
463
+ if (!this.keywords || Object.keys(this.keywords).length === 0) return text;
464
+
465
+ this.injected = [];
466
+ const sentences = text.split(/([.!?]+\s*)/);
467
+ const result = [];
468
+
469
+ for (let i = 0; i < sentences.length; i += 2) {
470
+ let sentence = sentences[i];
471
+ const punctuation = sentences[i + 1] || '';
472
+
473
+ if (sentence && chance(density)) {
474
+ sentence = this.injectIntoSentence(sentence);
475
+ }
476
+
477
+ result.push(sentence + punctuation);
478
+ }
479
+
480
+ return result.join('');
481
+ }
482
+
483
+ injectIntoSentence(sentence) {
484
+ const categories = Object.keys(this.keywords).filter(cat =>
485
+ this.keywords[cat] && this.keywords[cat].length > 0
486
+ );
487
+
488
+ if (categories.length === 0) return sentence;
489
+
490
+ const category = pick(categories);
491
+ const keyword = pick(this.keywords[category]);
492
+
493
+ if (!keyword) return sentence;
494
+
495
+ this.injected.push(keyword);
496
+
497
+ // Natural injection patterns
498
+ const patterns = [
499
+ s => s.replace(/\b(the|this|that)\s+\w+/i, `$1 ${keyword}`),
500
+ s => s.replace(/\b(error|issue|problem|bug)/i, `${keyword} $1`),
501
+ s => s + ` (I mean ${keyword})`,
502
+ s => s.replace(/\b(broken|working|slow|fast)/i, `$1 with ${keyword}`),
503
+ s => `${keyword} - ` + s
504
+ ];
505
+
506
+ return pick(patterns)(sentence);
507
+ }
508
+
509
+ getInjected() {
510
+ return [...new Set(this.injected)];
511
+ }
512
+ }
513
+
514
+ // ============= Main Generator Class =============
515
+
516
+ class OrganicTextGenerator {
517
+ /**
518
+ * @param {TextGeneratorConfig} [config={}] - Configuration options
519
+ */
520
+ constructor(config = {}) {
521
+ this.config = {
522
+ tone: 'neu',
523
+ style: 'feedback',
524
+ intensity: 'medium',
525
+ formality: 'casual',
526
+ min: 100,
527
+ max: 500,
528
+ seed: null,
529
+ keywords: null,
530
+ keywordDensity: 0.15,
531
+ typos: false,
532
+ typoRate: 0.02,
533
+ mixedSentiment: true,
534
+ authenticityLevel: 0.3,
535
+ timestamps: false,
536
+ userPersona: false,
537
+ sentimentDrift: 0.2,
538
+ includeMetadata: true,
539
+ specificityLevel: 0.5,
540
+ enableDeduplication: true,
541
+ maxAttempts: 50,
542
+ ...config
543
+ };
544
+
545
+ // Initialize seed if provided
546
+ if (this.config.seed) {
547
+ seedrandom(this.config.seed, { global: true });
548
+ }
549
+
550
+ // Initialize subsystems
551
+ this.thoughtStream = new ThoughtStream();
552
+ this.contextTracker = new ContextTracker();
553
+ this.deduplicator = new NaturalDeduplicator();
554
+ this.voiceConsistency = new VoiceConsistency(this.config.formality);
555
+ this.typoEngine = new NaturalTypoEngine();
556
+ this.keywordInjector = new KeywordInjector(this.config.keywords);
557
+
558
+ // Initialize Tracery grammar as fallback
559
+ this.grammar = tracery.createGrammar(PHRASE_BANK);
560
+ this.grammar.addModifiers(tracery.baseEngModifiers);
561
+
562
+ // Track statistics
563
+ this.stats = {
564
+ generated: 0,
565
+ attempts: 0,
566
+ duplicates: 0,
567
+ failures: 0,
568
+ totalTime: 0
569
+ };
570
+
571
+ // Current generation state
572
+ this.currentTone = this.config.tone;
573
+ }
574
+
575
+ next() {
576
+ return this.generateOne();
577
+ }
578
+
579
+ /**
580
+ * Generate a single text item
581
+ * @returns {string|GeneratedText|null} Generated text or null if failed
582
+ */
583
+ generateOne() {
584
+ const startTime = Date.now();
585
+
586
+ for (let attempt = 0; attempt < this.config.maxAttempts; attempt++) {
587
+ this.stats.attempts++;
588
+
589
+ // Allow sentiment drift
590
+ if (this.config.sentimentDrift > 0 && chance(this.config.sentimentDrift)) {
591
+ this.currentTone = this.driftTone(this.currentTone);
592
+ }
593
+
594
+ // Reset context for new generation
595
+ this.thoughtStream.reset();
596
+ this.contextTracker.reset();
597
+
598
+ // Choose generation strategy
599
+ const strategy = this.selectStrategy();
600
+ let text = null;
601
+
602
+ try {
603
+ switch (strategy) {
604
+ case 'stream':
605
+ text = this.generateStreamOfConsciousness();
606
+ break;
607
+ case 'burst':
608
+ text = this.generateEmotionalBurst();
609
+ break;
610
+ case 'structured':
611
+ text = this.generateStructuredThought();
612
+ break;
613
+ case 'fragment':
614
+ text = this.generateFragmented();
615
+ break;
616
+ case 'narrative':
617
+ text = this.generateNarrative();
618
+ break;
619
+ default:
620
+ text = this.generateHybrid();
621
+ }
622
+ } catch (e) {
623
+ this.stats.failures++;
624
+ continue;
625
+ }
626
+
627
+ if (!text || text.length < this.config.min) continue;
628
+
629
+ // Apply enhancements
630
+ text = this.enhance(text);
631
+
632
+ // Fast duplicate check - only check if we have few generations
633
+ if (this.config.enableDeduplication && this.stats.generated < 100 && this.deduplicator.wouldBeDuplicate(text)) {
634
+ this.stats.duplicates++;
635
+ continue;
636
+ }
637
+
638
+ // Length validation
639
+ if (text.length > this.config.max) {
640
+ text = this.smartTruncate(text);
641
+ }
642
+
643
+ if (text.length >= this.config.min && text.length <= this.config.max) {
644
+ // Success!
645
+ this.stats.generated++;
646
+ this.stats.totalTime += Date.now() - startTime;
647
+
648
+ // Only add to deduplicator if we're still doing duplicate checking
649
+ if (this.config.enableDeduplication && this.stats.generated < 100) {
650
+ this.deduplicator.add(text);
651
+ }
652
+
653
+ // Return based on metadata preference
654
+ if (this.config.includeMetadata) {
655
+ return this.createTextObject(text);
656
+ }
657
+
658
+ return text;
659
+ }
660
+ }
661
+
662
+ // Fallback
663
+ this.stats.failures++;
664
+ return this.generateFallback();
665
+ }
666
+
667
+ selectStrategy() {
668
+ const strategies = {
669
+ support: {
670
+ high: ['burst', 'stream', 'burst'],
671
+ medium: ['structured', 'stream', 'narrative'],
672
+ low: ['structured', 'narrative', 'structured']
673
+ },
674
+ review: {
675
+ high: ['narrative', 'burst', 'stream'],
676
+ medium: ['narrative', 'structured', 'stream'],
677
+ low: ['structured', 'narrative', 'structured']
678
+ },
679
+ chat: {
680
+ high: ['fragment', 'burst', 'stream'],
681
+ medium: ['fragment', 'stream', 'fragment'],
682
+ low: ['fragment', 'structured', 'fragment']
683
+ },
684
+ feedback: {
685
+ high: ['stream', 'burst', 'narrative'],
686
+ medium: ['structured', 'narrative', 'stream'],
687
+ low: ['structured', 'structured', 'narrative']
688
+ },
689
+ search: {
690
+ high: ['fragment', 'fragment', 'burst'],
691
+ medium: ['fragment', 'fragment', 'fragment'],
692
+ low: ['fragment', 'fragment', 'fragment']
693
+ },
694
+ email: {
695
+ high: ['structured', 'narrative', 'stream'],
696
+ medium: ['structured', 'narrative', 'structured'],
697
+ low: ['structured', 'structured', 'narrative']
698
+ },
699
+ forum: {
700
+ high: ['stream', 'narrative', 'burst'],
701
+ medium: ['narrative', 'structured', 'stream'],
702
+ low: ['structured', 'narrative', 'structured']
703
+ }
704
+ };
705
+
706
+ const styleStrategies = strategies[this.config.style] || strategies.feedback;
707
+ const intensityStrategies = styleStrategies[this.config.intensity] || styleStrategies.medium;
708
+
709
+ return pick(intensityStrategies);
710
+ }
711
+
712
+ generateStreamOfConsciousness() {
713
+ const thoughts = [];
714
+ const numThoughts = 2 + Math.floor(seededRandom() * 4);
715
+
716
+ for (let i = 0; i < numThoughts; i++) {
717
+ const thought = this.thoughtStream.generateThought(
718
+ this.currentTone,
719
+ this.contextTracker.getContext()
720
+ );
721
+
722
+ if (thought) {
723
+ thoughts.push(thought);
724
+ this.contextTracker.update(thought);
725
+
726
+ // Add interruptions
727
+ if (chance(0.3) && i < numThoughts - 1) {
728
+ thoughts.push(pick(ORGANIC_PATTERNS.interruptions));
729
+ }
730
+ }
731
+ }
732
+
733
+ return this.thoughtStream.connect(thoughts);
734
+ }
735
+
736
+ generateEmotionalBurst() {
737
+ const emotion = this.currentTone === 'pos' ? 'excitement' :
738
+ this.currentTone === 'neg' ? 'frustration' : 'confusion';
739
+
740
+ const burst = [];
741
+
742
+ // Opening
743
+ burst.push(pick(ORGANIC_PATTERNS.bursts[emotion].openings));
744
+
745
+ // Core message
746
+ const core = this.grammar.flatten(`#${this.currentTone}_core#`);
747
+ if (emotion === 'frustration' && chance(0.5)) {
748
+ burst.push(core.toUpperCase());
749
+ } else {
750
+ burst.push(core);
751
+ }
752
+
753
+ // Emphasis
754
+ if (chance(0.6)) {
755
+ burst.push(pick(ORGANIC_PATTERNS.bursts[emotion].emphasis));
756
+ }
757
+
758
+ // Closer
759
+ burst.push(pick(ORGANIC_PATTERNS.bursts[emotion].closers));
760
+
761
+ return burst.filter(Boolean).join(' ');
762
+ }
763
+
764
+ generateStructuredThought() {
765
+ const structure = GENERATION_PATTERNS.structures[this.config.style] ||
766
+ GENERATION_PATTERNS.structures.default;
767
+
768
+ const parts = [];
769
+
770
+ for (const element of structure) {
771
+ if (chance(element.probability)) {
772
+ const content = this.grammar.flatten(element.pattern);
773
+ parts.push(content);
774
+
775
+ if (element.transition && chance(0.4)) {
776
+ parts.push(pick(ORGANIC_PATTERNS.transitions));
777
+ }
778
+ }
779
+ }
780
+
781
+ return parts.join(' ');
782
+ }
783
+
784
+ generateFragmented() {
785
+ const fragments = [];
786
+ const numFragments = this.config.style === 'search' ?
787
+ 1 + Math.floor(seededRandom() * 3) :
788
+ 2 + Math.floor(seededRandom() * 4);
789
+
790
+ for (let i = 0; i < numFragments; i++) {
791
+ const type = seededRandom();
792
+
793
+ if (type < 0.3) {
794
+ fragments.push(pick(ORGANIC_PATTERNS.fragments.incomplete));
795
+ } else if (type < 0.6) {
796
+ fragments.push(pick(ORGANIC_PATTERNS.fragments.short));
797
+ } else {
798
+ const full = this.grammar.flatten(`#${this.currentTone}_core#`);
799
+ fragments.push(this.breakSentence(full));
800
+ }
801
+ }
802
+
803
+ // Connect fragments naturally
804
+ return this.connectFragments(fragments);
805
+ }
806
+
807
+ generateNarrative() {
808
+ const narrative = GENERATION_PATTERNS.narratives[this.config.style];
809
+ if (!narrative) return this.generateHybrid();
810
+
811
+ const story = [];
812
+
813
+ for (const beat of narrative) {
814
+ if (chance(beat.optional ? 0.6 : 0.9)) {
815
+ const content = this.grammar.flatten(beat.template);
816
+ story.push(content);
817
+ }
818
+ }
819
+
820
+ return story.join(' ');
821
+ }
822
+
823
+ generateHybrid() {
824
+ // Mix multiple strategies
825
+ const parts = [];
826
+
827
+ // Opening
828
+ if (chance(0.7)) {
829
+ parts.push(pick(ORGANIC_PATTERNS.openings[this.config.style] || ORGANIC_PATTERNS.openings.default));
830
+ }
831
+
832
+ // Main content
833
+ const main = this.grammar.flatten(`#origin_${this.config.style}_${this.currentTone}#`);
834
+ parts.push(main);
835
+
836
+ // Additional thoughts
837
+ if (chance(0.4)) {
838
+ parts.push(pick(ORGANIC_PATTERNS.addons[this.currentTone]));
839
+ }
840
+
841
+ // Closing
842
+ if (chance(0.6)) {
843
+ parts.push(pick(ORGANIC_PATTERNS.closings[this.currentTone]));
844
+ }
845
+
846
+ return parts.filter(Boolean).join(' ');
847
+ }
848
+
849
+ generateFallback() {
850
+ // Simple fallback when all else fails
851
+ const simple = this.grammar.flatten(`#origin_${this.currentTone}#`);
852
+ return this.enhance(simple);
853
+ }
854
+
855
+ stripUnfilledPlaceholders(text) {
856
+ // Remove tracery's unfilled placeholders: ((pattern))
857
+ // These occur when a pattern like #TITLE# is used but not defined
858
+ return text.replace(/\(\([a-zA-Z_]+\)\)/g, '').replace(/\s{2,}/g, ' ').trim();
859
+ }
860
+
861
+ enhance(text) {
862
+ // Layer 0: Remove unfilled placeholders (from tracery)
863
+ // Tracery wraps undefined patterns in (( )) - strip these out
864
+ text = this.stripUnfilledPlaceholders(text);
865
+
866
+ // Layer 1: Mixed sentiment
867
+ if (this.config.mixedSentiment && chance(0.3)) {
868
+ text = this.addMixedSentiment(text);
869
+ }
870
+
871
+ // Layer 2: Keywords
872
+ if (this.config.keywords) {
873
+ text = this.keywordInjector.inject(text, this.config.keywordDensity);
874
+ }
875
+
876
+ // Layer 3: Authenticity markers
877
+ if (this.config.authenticityLevel > 0) {
878
+ text = this.addAuthenticityMarkers(text);
879
+ }
880
+
881
+ // Layer 4: Specificity
882
+ if (this.config.specificityLevel > 0.5) {
883
+ text = this.addSpecificDetails(text);
884
+ }
885
+
886
+ // Layer 5: Voice consistency
887
+ text = this.voiceConsistency.maintain(text);
888
+
889
+ // Layer 6: Typos
890
+ if (this.config.typos) {
891
+ const context = {
892
+ emotional: this.thoughtStream.momentum.emotional,
893
+ mobile: this.config.style === 'chat'
894
+ };
895
+ text = this.typoEngine.apply(text, this.config.typoRate, context);
896
+ }
897
+
898
+ // Layer 7: Timestamps
899
+ if (this.config.timestamps && chance(0.3)) {
900
+ text = this.addTimestamp(text);
901
+ }
902
+
903
+ // Layer 8: User persona
904
+ if (this.config.userPersona && chance(0.4)) {
905
+ text = this.addPersonaMarker(text);
906
+ }
907
+
908
+ return text;
909
+ }
910
+
911
+ addMixedSentiment(text) {
912
+ const counter = this.currentTone === 'pos' ? 'neg' :
913
+ this.currentTone === 'neg' ? 'pos' :
914
+ pick(['pos', 'neg']);
915
+
916
+ const addition = pick([
917
+ `That said, ${this.grammar.flatten(`#${counter}_point#`)}`,
918
+ `Although ${this.grammar.flatten(`#${counter}_clause#`)}`,
919
+ `But ${this.grammar.flatten(`#${counter}_short#`)}`
920
+ ]);
921
+
922
+ return text + '. ' + addition;
923
+ }
924
+
925
+ addAuthenticityMarkers(text) {
926
+ const markers = [];
927
+
928
+ if (chance(this.config.authenticityLevel * 0.3)) {
929
+ markers.push(pick(ORGANIC_PATTERNS.authenticity.selfCorrections));
930
+ }
931
+
932
+ if (chance(this.config.authenticityLevel * 0.3)) {
933
+ markers.push(pick(ORGANIC_PATTERNS.authenticity.fillers));
934
+ }
935
+
936
+ if (chance(this.config.authenticityLevel * 0.2)) {
937
+ markers.push(pick(ORGANIC_PATTERNS.authenticity.asides));
938
+ }
939
+
940
+ // Insert markers naturally
941
+ for (const marker of markers) {
942
+ const insertPoint = Math.floor(seededRandom() * text.length);
943
+ text = text.slice(0, insertPoint) + ' ' + marker + ' ' + text.slice(insertPoint);
944
+ }
945
+
946
+ return text;
947
+ }
948
+
949
+ addSpecificDetails(text) {
950
+ const details = {
951
+ pos: ['saved 2 hours daily', 'reduced costs by 40%', 'loads in under 100ms'],
952
+ neg: ['crashes 3-4 times per day', 'takes 30+ seconds to load', 'error 404 constantly'],
953
+ neu: ['works most of the time', 'about average performance', 'standard functionality']
954
+ };
955
+
956
+ const relevantDetails = details[this.currentTone] || details.neu;
957
+
958
+ if (chance(this.config.specificityLevel)) {
959
+ const detail = pick(relevantDetails);
960
+ text = text.replace(/\.$/, ` - ${detail}.`);
961
+ }
962
+
963
+ return text;
964
+ }
965
+
966
+ addTimestamp(text) {
967
+ const hour = Math.floor(seededRandom() * 24);
968
+ const min = Math.floor(seededRandom() * 60);
969
+ const timestamp = `[${hour}:${min.toString().padStart(2, '0')}]`;
970
+
971
+ return timestamp + ' ' + text;
972
+ }
973
+
974
+ addPersonaMarker(text) {
975
+ const personas = [
976
+ 'As a developer, ',
977
+ 'As someone who uses this daily, ',
978
+ 'Speaking from experience, ',
979
+ 'In my 10+ years in tech, ',
980
+ 'From a user perspective, '
981
+ ];
982
+
983
+ return pick(personas) + text.charAt(0).toLowerCase() + text.slice(1);
984
+ }
985
+
986
+ breakSentence(sentence) {
987
+ const breakPoint = Math.floor(sentence.length * (0.3 + seededRandom() * 0.4));
988
+ return sentence.slice(0, breakPoint) + '...';
989
+ }
990
+
991
+ connectFragments(fragments) {
992
+ const connectors = ['... ', ' ', ', ', ' - ', '? ', '... wait ', '.. '];
993
+ return fragments.join(pick(connectors));
994
+ }
995
+
996
+ smartTruncate(text) {
997
+ // Truncate at natural boundary
998
+ const truncated = text.slice(0, this.config.max);
999
+ const lastPeriod = truncated.lastIndexOf('.');
1000
+ const lastQuestion = truncated.lastIndexOf('?');
1001
+ const lastExclamation = truncated.lastIndexOf('!');
1002
+
1003
+ const lastPunct = Math.max(lastPeriod, lastQuestion, lastExclamation);
1004
+
1005
+ if (lastPunct > this.config.min * 0.8) {
1006
+ return truncated.slice(0, lastPunct + 1);
1007
+ }
1008
+
1009
+ return truncated.slice(0, this.config.max - 3) + '...';
1010
+ }
1011
+
1012
+ driftTone(currentTone) {
1013
+ const drifts = {
1014
+ pos: chance(0.7) ? 'pos' : chance(0.8) ? 'neu' : 'neg',
1015
+ neg: chance(0.7) ? 'neg' : chance(0.8) ? 'neu' : 'pos',
1016
+ neu: chance(0.5) ? 'neu' : chance(0.5) ? 'pos' : 'neg'
1017
+ };
1018
+
1019
+ return drifts[currentTone] || currentTone;
1020
+ }
1021
+
1022
+ createTextObject(text) {
1023
+ const metadata = {
1024
+ style: this.config.style,
1025
+ intensity: this.config.intensity,
1026
+ formality: this.config.formality
1027
+ };
1028
+
1029
+ if (this.config.includeMetadata) {
1030
+ // Use fast sentiment estimation instead of full analysis
1031
+ const negWords = (text.match(/\b(broken|terrible|awful|bad|crash|error|fail|bug)\b/gi) || []).length;
1032
+ const posWords = (text.match(/\b(great|excellent|amazing|good|love|perfect|works)\b/gi) || []).length;
1033
+ metadata.sentimentScore = negWords > posWords ? -1 : posWords > negWords ? 1 : 0;
1034
+
1035
+ if (this.config.timestamps) {
1036
+ metadata.timestamp = new Date().toISOString();
1037
+ }
1038
+
1039
+ if (this.config.userPersona) {
1040
+ metadata.persona = {
1041
+ role: pick(['developer', 'designer', 'manager', 'user']),
1042
+ experience: pick(['junior', 'senior', 'expert'])
1043
+ };
1044
+ }
1045
+
1046
+ const injected = this.keywordInjector.getInjected();
1047
+ if (injected.length > 0) {
1048
+ metadata.injectedKeywords = injected;
1049
+ }
1050
+
1051
+ metadata.readabilityScore = this.calculateReadability(text);
1052
+ }
1053
+
1054
+ return {
1055
+ text,
1056
+ tone: this.currentTone,
1057
+ metadata
1058
+ };
1059
+ }
1060
+
1061
+ calculateReadability(text) {
1062
+ const words = text.split(/\s+/).length;
1063
+ const sentences = text.split(/[.!?]+/).length;
1064
+ const syllables = text.split(/\s+/).reduce((sum, word) =>
1065
+ sum + this.countSyllables(word), 0);
1066
+
1067
+ const score = 206.835 - 1.015 * (words / sentences) - 84.6 * (syllables / words);
1068
+ return Math.max(0, Math.min(100, Math.round(score)));
1069
+ }
1070
+
1071
+ countSyllables(word) {
1072
+ word = word.toLowerCase().replace(/[^a-z]/g, '');
1073
+ const vowels = word.match(/[aeiou]/g);
1074
+ return vowels ? Math.max(1, vowels.length) : 1;
1075
+ }
1076
+
1077
+ /**
1078
+ * Generate multiple text items in batch
1079
+ * @param {TextBatchOptions} options - Batch generation options
1080
+ * @returns {(string|GeneratedText|SimpleGeneratedText)[]} Array of generated text items
1081
+ */
1082
+ generateBatch(options) {
1083
+ const {
1084
+ n = 10,
1085
+ returnType = 'strings',
1086
+ tone = this.config.tone,
1087
+ related = false,
1088
+ sharedContext = null
1089
+ } = options;
1090
+
1091
+ const results = [];
1092
+ const startTime = Date.now();
1093
+
1094
+ // Reset for new batch
1095
+ this.currentTone = tone;
1096
+
1097
+ // Generate shared context if related
1098
+ let context = sharedContext;
1099
+ if (related && !context) {
1100
+ const contexts = ['new feature', 'recent update', 'pricing change', 'UI redesign', 'performance issues'];
1101
+ context = pick(contexts);
1102
+ }
1103
+
1104
+ for (let i = 0; i < n; i++) {
1105
+ let item = this.generateOne();
1106
+
1107
+ if (!item) {
1108
+ this.stats.failures++;
1109
+ continue;
1110
+ }
1111
+
1112
+ // Add shared context if related
1113
+ if (related && context) {
1114
+ const text = typeof item === 'string' ? item : item.text;
1115
+ const contextualText = this.addSharedContext(text, context);
1116
+
1117
+ if (typeof item === 'string') {
1118
+ item = contextualText;
1119
+ } else {
1120
+ item.text = contextualText;
1121
+ }
1122
+ }
1123
+
1124
+ // Format based on return type
1125
+ if (returnType === 'strings') {
1126
+ results.push(typeof item === 'string' ? item : item.text);
1127
+ } else {
1128
+ results.push(typeof item === 'string' ? { text: item, tone } : item);
1129
+ }
1130
+ }
1131
+
1132
+ this.stats.totalTime += Date.now() - startTime;
1133
+
1134
+ return results;
1135
+ }
1136
+
1137
+ addSharedContext(text, context) {
1138
+ const templates = [
1139
+ `About the ${context}: ${text}`,
1140
+ `${text} (regarding the ${context})`,
1141
+ `Re: ${context} - ${text}`,
1142
+ `${text}. This is about the ${context}.`
1143
+ ];
1144
+
1145
+ return pick(templates);
1146
+ }
1147
+
1148
+ /**
1149
+ * Get generation statistics
1150
+ * @returns {TextGeneratorStats} Performance statistics
1151
+ */
1152
+ getStats() {
1153
+ const avgTime = this.stats.generated > 0 ?
1154
+ this.stats.totalTime / this.stats.generated : 0;
1155
+
1156
+ return {
1157
+ config: /** @type {import('../../types.d.ts').TextGeneratorConfig} */ (this.config),
1158
+ generatedCount: this.stats.generated,
1159
+ duplicateCount: this.stats.duplicates,
1160
+ failedCount: this.stats.failures,
1161
+ avgGenerationTime: avgTime,
1162
+ totalGenerationTime: this.stats.totalTime
1163
+ };
1164
+ }
1165
+ }
1166
+
1167
+ // ============= Public API =============
1168
+
1169
+ /**
1170
+ * Creates a new text generator instance
1171
+ * @param {TextGeneratorConfig} [config={}] - Configuration options for the generator
1172
+ * @returns {OrganicTextGenerator} Text generator instance
1173
+ */
1174
+ export function createTextGenerator(config = {}) {
1175
+ return new OrganicTextGenerator(config);
1176
+ }
1177
+
1178
+ // Alias for backwards compatibility
1179
+ export const createGenerator = createTextGenerator;
1180
+
1181
+ /**
1182
+ * Generate a batch of text items directly (standalone function)
1183
+ * @param {TextGeneratorConfig & TextBatchOptions} options - Combined generator config and batch options
1184
+ * @returns {(string|GeneratedText|SimpleGeneratedText)[]} Array of generated text items
1185
+ */
1186
+ export function generateBatch(options) {
1187
+ const { n, returnType, tone, related, sharedContext, ...config } = options;
1188
+ const generator = new OrganicTextGenerator(config);
1189
+ return generator.generateBatch({ n, returnType, tone, related, sharedContext });
1190
+ }
1191
+
1192
+ export default OrganicTextGenerator;