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