qnce-engine 1.2.0 → 1.2.2

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 (125) hide show
  1. package/README.md +713 -7
  2. package/dist/cli/audit.js +0 -0
  3. package/dist/cli/init.js +0 -0
  4. package/dist/cli/perf.d.ts.map +1 -1
  5. package/dist/cli/perf.js +2 -1
  6. package/dist/cli/perf.js.map +1 -1
  7. package/dist/cli/play.d.ts +4 -0
  8. package/dist/cli/play.d.ts.map +1 -0
  9. package/dist/cli/play.js +259 -0
  10. package/dist/cli/play.js.map +1 -0
  11. package/dist/engine/condition.d.ts +69 -0
  12. package/dist/engine/condition.d.ts.map +1 -0
  13. package/dist/engine/condition.js +195 -0
  14. package/dist/engine/condition.js.map +1 -0
  15. package/dist/engine/core.d.ts +274 -3
  16. package/dist/engine/core.d.ts.map +1 -1
  17. package/dist/engine/core.js +1148 -9
  18. package/dist/engine/core.js.map +1 -1
  19. package/dist/engine/demo-story.d.ts.map +1 -1
  20. package/dist/engine/demo-story.js +99 -13
  21. package/dist/engine/demo-story.js.map +1 -1
  22. package/dist/engine/errors.d.ts +76 -0
  23. package/dist/engine/errors.d.ts.map +1 -0
  24. package/dist/engine/errors.js +178 -0
  25. package/dist/engine/errors.js.map +1 -0
  26. package/dist/engine/types.d.ts +445 -0
  27. package/dist/engine/types.d.ts.map +1 -0
  28. package/dist/engine/types.js +9 -0
  29. package/dist/engine/types.js.map +1 -0
  30. package/dist/engine/validation.d.ts +110 -0
  31. package/dist/engine/validation.d.ts.map +1 -0
  32. package/dist/engine/validation.js +261 -0
  33. package/dist/engine/validation.js.map +1 -0
  34. package/dist/examples/examples/autosave-undo-demo.js +248 -0
  35. package/dist/examples/examples/persistence-demo.js +63 -0
  36. package/dist/examples/src/engine/condition.js +194 -0
  37. package/dist/examples/src/engine/core.js +1382 -0
  38. package/dist/examples/src/engine/demo-story.js +200 -0
  39. package/dist/examples/src/engine/types.js +8 -0
  40. package/dist/examples/src/index.js +35 -0
  41. package/dist/examples/src/integrations/react.js +322 -0
  42. package/dist/examples/src/narrative/branching/engine-simple.js +348 -0
  43. package/dist/examples/src/narrative/branching/index.js +55 -0
  44. package/dist/examples/src/narrative/branching/models.js +5 -0
  45. package/dist/examples/src/performance/ObjectPool.js +296 -0
  46. package/dist/examples/src/performance/PerfReporter.js +280 -0
  47. package/dist/examples/src/performance/ThreadPool.js +347 -0
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +12 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/integrations/react.d.ts +200 -0
  53. package/dist/integrations/react.d.ts.map +1 -0
  54. package/dist/integrations/react.js +365 -0
  55. package/dist/integrations/react.js.map +1 -0
  56. package/dist/narrative/branching/engine-simple.js +3 -3
  57. package/dist/narrative/branching/engine-simple.js.map +1 -1
  58. package/dist/narrative/branching/engine.d.ts +1 -0
  59. package/dist/narrative/branching/engine.d.ts.map +1 -0
  60. package/dist/narrative/branching/engine.js +2 -0
  61. package/dist/narrative/branching/engine.js.map +1 -0
  62. package/dist/narrative/branching/models.d.ts.map +1 -1
  63. package/dist/performance/HotReloadDelta.d.ts +25 -8
  64. package/dist/performance/HotReloadDelta.d.ts.map +1 -1
  65. package/dist/performance/HotReloadDelta.js +10 -15
  66. package/dist/performance/HotReloadDelta.js.map +1 -1
  67. package/dist/ui/__tests__/AutosaveIndicator.test.d.ts +2 -0
  68. package/dist/ui/__tests__/AutosaveIndicator.test.d.ts.map +1 -0
  69. package/dist/ui/__tests__/AutosaveIndicator.test.js +329 -0
  70. package/dist/ui/__tests__/AutosaveIndicator.test.js.map +1 -0
  71. package/dist/ui/__tests__/UndoRedoControls.test.d.ts +2 -0
  72. package/dist/ui/__tests__/UndoRedoControls.test.d.ts.map +1 -0
  73. package/dist/ui/__tests__/UndoRedoControls.test.js +245 -0
  74. package/dist/ui/__tests__/UndoRedoControls.test.js.map +1 -0
  75. package/dist/ui/__tests__/autosave-simple.test.d.ts +2 -0
  76. package/dist/ui/__tests__/autosave-simple.test.d.ts.map +1 -0
  77. package/dist/ui/__tests__/autosave-simple.test.js +29 -0
  78. package/dist/ui/__tests__/autosave-simple.test.js.map +1 -0
  79. package/dist/ui/__tests__/setup.d.ts +2 -0
  80. package/dist/ui/__tests__/setup.d.ts.map +1 -0
  81. package/dist/ui/__tests__/setup.js +40 -0
  82. package/dist/ui/__tests__/setup.js.map +1 -0
  83. package/dist/ui/__tests__/smoke-test.d.ts +2 -0
  84. package/dist/ui/__tests__/smoke-test.d.ts.map +1 -0
  85. package/dist/ui/__tests__/smoke-test.js +18 -0
  86. package/dist/ui/__tests__/smoke-test.js.map +1 -0
  87. package/dist/ui/__tests__/smoke-test.test.d.ts +2 -0
  88. package/dist/ui/__tests__/smoke-test.test.d.ts.map +1 -0
  89. package/dist/ui/__tests__/smoke-test.test.js +18 -0
  90. package/dist/ui/__tests__/smoke-test.test.js.map +1 -0
  91. package/dist/ui/__tests__/useKeyboardShortcuts.test.d.ts +2 -0
  92. package/dist/ui/__tests__/useKeyboardShortcuts.test.d.ts.map +1 -0
  93. package/dist/ui/__tests__/useKeyboardShortcuts.test.js +374 -0
  94. package/dist/ui/__tests__/useKeyboardShortcuts.test.js.map +1 -0
  95. package/dist/ui/components/AutosaveIndicator.d.ts +18 -0
  96. package/dist/ui/components/AutosaveIndicator.d.ts.map +1 -0
  97. package/dist/ui/components/AutosaveIndicator.js +175 -0
  98. package/dist/ui/components/AutosaveIndicator.js.map +1 -0
  99. package/dist/ui/components/UndoRedoControls.d.ts +16 -0
  100. package/dist/ui/components/UndoRedoControls.d.ts.map +1 -0
  101. package/dist/ui/components/UndoRedoControls.js +144 -0
  102. package/dist/ui/components/UndoRedoControls.js.map +1 -0
  103. package/dist/ui/hooks/useKeyboardShortcuts.d.ts +22 -0
  104. package/dist/ui/hooks/useKeyboardShortcuts.d.ts.map +1 -0
  105. package/dist/ui/hooks/useKeyboardShortcuts.js +162 -0
  106. package/dist/ui/hooks/useKeyboardShortcuts.js.map +1 -0
  107. package/dist/ui/index.d.ts +9 -0
  108. package/dist/ui/index.d.ts.map +1 -0
  109. package/dist/ui/index.js +14 -0
  110. package/dist/ui/index.js.map +1 -0
  111. package/dist/ui/types.d.ts +141 -0
  112. package/dist/ui/types.d.ts.map +1 -0
  113. package/dist/ui/types.js +51 -0
  114. package/dist/ui/types.js.map +1 -0
  115. package/examples/autosave-undo-demo.ts +306 -0
  116. package/examples/branching-demo-simple.ts +0 -0
  117. package/examples/branching-demo.ts +0 -0
  118. package/examples/persistence-demo.ts +84 -0
  119. package/examples/tsconfig.json +13 -0
  120. package/examples/ui-components-demo.tsx +320 -0
  121. package/examples/validation-demo-story.json +177 -0
  122. package/examples/validation-demo.js +163 -0
  123. package/package.json +24 -4
  124. package/docs/branching/PDM.md +0 -443
  125. package/docs/branching/RELEASE-v1.2.0.md +0 -169
@@ -0,0 +1,1382 @@
1
+ "use strict";
2
+ // QNCE Core Engine - Framework Agnostic
3
+ // Quantum Narrative Convergence Engine
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.QNCEEngine = void 0;
6
+ exports.createQNCEEngine = createQNCEEngine;
7
+ exports.loadStoryData = loadStoryData;
8
+ const ObjectPool_1 = require("../performance/ObjectPool");
9
+ const ThreadPool_1 = require("../performance/ThreadPool");
10
+ const PerfReporter_1 = require("../performance/PerfReporter");
11
+ // Branching system imports
12
+ const branching_1 = require("../narrative/branching");
13
+ // State persistence imports - Sprint 3.3
14
+ const types_1 = require("./types");
15
+ // Conditional choice evaluator - Sprint 3.4
16
+ const condition_1 = require("./condition");
17
+ // Demo narrative data moved to demo-story.ts
18
+ function findNode(nodes, id) {
19
+ const node = nodes.find(n => n.id === id);
20
+ if (!node)
21
+ throw new Error(`Node not found: ${id}`);
22
+ return node;
23
+ }
24
+ /**
25
+ * QNCE Engine - Core narrative state management
26
+ * Framework agnostic implementation with object pooling integration
27
+ */
28
+ class QNCEEngine {
29
+ state;
30
+ storyData;
31
+ activeFlowEvents = [];
32
+ performanceMode = false;
33
+ enableProfiling = false;
34
+ branchingEngine;
35
+ // Sprint 3.3: State persistence and checkpoints
36
+ checkpoints = new Map();
37
+ maxCheckpoints = 50;
38
+ autoCheckpointEnabled = false;
39
+ // Sprint 3.3: State persistence properties
40
+ checkpointManager;
41
+ // Sprint 3.5: Autosave and Undo/Redo properties
42
+ undoStack = [];
43
+ redoStack = [];
44
+ autosaveConfig = {
45
+ enabled: false,
46
+ triggers: ['choice', 'flag-change'],
47
+ maxEntries: 10,
48
+ throttleMs: 1000,
49
+ includeMetadata: true
50
+ };
51
+ undoRedoConfig = {
52
+ enabled: true,
53
+ maxUndoEntries: 50,
54
+ maxRedoEntries: 20,
55
+ trackFlagChanges: true,
56
+ trackChoiceText: true,
57
+ trackActions: ['choice', 'flag-change', 'state-load']
58
+ };
59
+ lastAutosaveTime = 0;
60
+ isUndoRedoOperation = false;
61
+ constructor(storyData, initialState, performanceMode = false, threadPoolConfig) {
62
+ this.storyData = storyData;
63
+ this.performanceMode = performanceMode;
64
+ this.enableProfiling = performanceMode; // Enable profiling with performance mode
65
+ // Initialize ThreadPool if in performance mode
66
+ if (this.performanceMode && threadPoolConfig) {
67
+ (0, ThreadPool_1.getThreadPool)(threadPoolConfig);
68
+ }
69
+ this.state = {
70
+ currentNodeId: initialState?.currentNodeId || storyData.initialNodeId,
71
+ flags: initialState?.flags || {},
72
+ history: initialState?.history || [storyData.initialNodeId],
73
+ };
74
+ }
75
+ getCurrentNode() {
76
+ const cacheKey = `node-${this.state.currentNodeId}`;
77
+ // Profiling: Record cache operation
78
+ if (this.enableProfiling) {
79
+ const found = findNode(this.storyData.nodes, this.state.currentNodeId);
80
+ PerfReporter_1.perf.cacheHit(cacheKey, { nodeId: this.state.currentNodeId });
81
+ return found;
82
+ }
83
+ if (this.performanceMode) {
84
+ // Use pooled node for enhanced node data
85
+ const pooledNode = ObjectPool_1.poolManager.borrowNode();
86
+ const coreNode = findNode(this.storyData.nodes, this.state.currentNodeId);
87
+ pooledNode.initialize(coreNode.id, coreNode.text, coreNode.choices);
88
+ // Return the pooled node (caller should return it when done)
89
+ return {
90
+ id: pooledNode.id,
91
+ text: pooledNode.text,
92
+ choices: pooledNode.choices
93
+ };
94
+ }
95
+ return findNode(this.storyData.nodes, this.state.currentNodeId);
96
+ }
97
+ getState() {
98
+ return { ...this.state };
99
+ }
100
+ getFlags() {
101
+ return { ...this.state.flags };
102
+ }
103
+ setFlag(key, value) {
104
+ // Sprint 3.5: Save state for undo before making changes
105
+ const preChangeState = this.deepCopy(this.state);
106
+ this.state.flags[key] = value;
107
+ // Sprint 3.5: Track state change for undo/redo
108
+ if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation &&
109
+ this.undoRedoConfig.trackActions.includes('flag-change')) {
110
+ this.pushToUndoStack(preChangeState, 'flag-change', {
111
+ flagsChanged: [key],
112
+ flagValue: value
113
+ });
114
+ }
115
+ // Sprint 3.5: Trigger autosave if enabled
116
+ if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('flag-change')) {
117
+ this.triggerAutosave('flag-change', {
118
+ flagKey: key,
119
+ flagValue: value
120
+ }).catch((error) => {
121
+ console.warn('[QNCE] Autosave failed:', error.message);
122
+ });
123
+ }
124
+ }
125
+ getHistory() {
126
+ return [...this.state.history];
127
+ }
128
+ selectChoice(choice) {
129
+ // Sprint 3.5: Save state for undo before making changes
130
+ const preChangeState = this.deepCopy(this.state);
131
+ // S2-T4: Add state transition profiling
132
+ const transitionSpanId = this.enableProfiling
133
+ ? (0, PerfReporter_1.getPerfReporter)().startSpan('state-transition', {
134
+ fromNodeId: this.state.currentNodeId,
135
+ toNodeId: choice.nextNodeId,
136
+ hasEffects: !!choice.flagEffects
137
+ })
138
+ : null;
139
+ const fromNodeId = this.state.currentNodeId;
140
+ const toNodeId = choice.nextNodeId;
141
+ // Create flow event for tracking narrative progression
142
+ if (this.performanceMode) {
143
+ const flowSpanId = this.enableProfiling
144
+ ? PerfReporter_1.perf.flowStart(fromNodeId, { toNodeId })
145
+ : null;
146
+ const flowEvent = this.createFlowEvent(fromNodeId, toNodeId, choice.flagEffects);
147
+ this.recordFlowEvent(flowEvent);
148
+ // Return the flow immediately after recording (we don't need to keep it)
149
+ // This ensures proper pool recycling
150
+ ObjectPool_1.poolManager.returnFlow(flowEvent);
151
+ if (flowSpanId) {
152
+ PerfReporter_1.perf.flowComplete(flowSpanId, toNodeId, { transitionType: 'choice' });
153
+ }
154
+ }
155
+ this.state.currentNodeId = choice.nextNodeId;
156
+ this.state.history.push(choice.nextNodeId);
157
+ if (choice.flagEffects) {
158
+ this.state.flags = { ...this.state.flags, ...choice.flagEffects };
159
+ // S2-T4: Track flag updates
160
+ if (this.enableProfiling) {
161
+ PerfReporter_1.perf.record('custom', {
162
+ flagCount: Object.keys(choice.flagEffects).length,
163
+ nodeId: toNodeId,
164
+ eventType: 'flag-update'
165
+ });
166
+ }
167
+ }
168
+ // Sprint 3.5: Track state change for undo/redo
169
+ if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation &&
170
+ this.undoRedoConfig.trackActions.includes('choice')) {
171
+ this.pushToUndoStack(preChangeState, 'choice', {
172
+ nodeId: fromNodeId,
173
+ choiceText: this.undoRedoConfig.trackChoiceText ? choice.text : undefined,
174
+ flagsChanged: choice.flagEffects ? Object.keys(choice.flagEffects) : undefined
175
+ });
176
+ }
177
+ // Sprint 3.5: Trigger autosave if enabled
178
+ if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('choice')) {
179
+ this.triggerAutosave('choice', {
180
+ fromNodeId,
181
+ toNodeId,
182
+ choiceText: choice.text
183
+ }).catch(error => {
184
+ console.warn('[QNCE] Autosave failed:', error.message);
185
+ });
186
+ }
187
+ // Complete state transition span
188
+ if (transitionSpanId) {
189
+ (0, PerfReporter_1.getPerfReporter)().endSpan(transitionSpanId, {
190
+ historyLength: this.state.history.length,
191
+ flagCount: Object.keys(this.state.flags).length
192
+ });
193
+ }
194
+ // S2-T2: Background operations after state change
195
+ if (this.performanceMode) {
196
+ // Preload next possible nodes in background
197
+ this.preloadNextNodes().catch(error => {
198
+ console.warn('[QNCE] Background preload failed:', error.message);
199
+ });
200
+ // Write telemetry data in background
201
+ this.backgroundTelemetryWrite({
202
+ action: 'choice-selected',
203
+ fromNodeId,
204
+ toNodeId,
205
+ choiceText: choice.text.slice(0, 50) // First 50 chars for privacy
206
+ }).catch(error => {
207
+ console.warn('[QNCE] Background telemetry failed:', error.message);
208
+ });
209
+ }
210
+ }
211
+ resetNarrative() {
212
+ // Sprint 3.5: Save state for undo before reset
213
+ const preChangeState = this.deepCopy(this.state);
214
+ // Clean up pooled objects before reset
215
+ if (this.performanceMode) {
216
+ this.cleanupPools();
217
+ }
218
+ this.state.currentNodeId = this.storyData.initialNodeId;
219
+ this.state.flags = {};
220
+ this.state.history = [this.storyData.initialNodeId];
221
+ // Sprint 3.5: Track state change for undo/redo
222
+ if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation &&
223
+ this.undoRedoConfig.trackActions.includes('reset')) {
224
+ this.pushToUndoStack(preChangeState, 'reset', {
225
+ nodeId: this.storyData.initialNodeId
226
+ });
227
+ }
228
+ }
229
+ /**
230
+ * Load simple state (legacy method for backward compatibility)
231
+ * @param state - QNCEState to load
232
+ */
233
+ loadSimpleState(state) {
234
+ // Sprint 3.5: Save state for undo before loading
235
+ const preChangeState = this.deepCopy(this.state);
236
+ this.state = { ...state };
237
+ // Sprint 3.5: Track state change for undo/redo
238
+ if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation &&
239
+ this.undoRedoConfig.trackActions.includes('state-load')) {
240
+ this.pushToUndoStack(preChangeState, 'state-load', {
241
+ nodeId: state.currentNodeId
242
+ });
243
+ }
244
+ // Sprint 3.5: Trigger autosave if enabled
245
+ if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('state-load')) {
246
+ this.triggerAutosave('state-load', {
247
+ nodeId: state.currentNodeId
248
+ }).catch((error) => {
249
+ console.warn('[QNCE] Autosave failed:', error.message);
250
+ });
251
+ }
252
+ }
253
+ // Sprint 3.3: State Persistence & Checkpoints Implementation
254
+ /**
255
+ * Save current engine state to a serialized format
256
+ * @param options - Serialization options
257
+ * @returns Promise resolving to serialized state
258
+ */
259
+ async saveState(options = {}) {
260
+ const startTime = performance.now();
261
+ if (!this.state.currentNodeId) {
262
+ throw new Error('Invalid state: currentNodeId is missing.');
263
+ }
264
+ try {
265
+ // Create serialization metadata
266
+ const metadata = {
267
+ engineVersion: types_1.PERSISTENCE_VERSION,
268
+ timestamp: new Date().toISOString(),
269
+ storyId: this.generateStoryHash(),
270
+ customMetadata: options.customMetadata || {},
271
+ compression: options.compression || 'none'
272
+ };
273
+ // Build serialized state
274
+ const serializedState = {
275
+ state: this.deepCopy(this.state),
276
+ flowEvents: options.includeFlowEvents !== false ?
277
+ this.deepCopy(this.activeFlowEvents) : [],
278
+ metadata
279
+ };
280
+ // Add optional data based on options
281
+ if (options.includePerformanceData && this.performanceMode) {
282
+ serializedState.performanceState = {
283
+ performanceMode: this.performanceMode,
284
+ enableProfiling: this.enableProfiling,
285
+ backgroundTasks: [], // Would be populated with actual background task IDs
286
+ telemetryData: [] // Would be populated with telemetry history
287
+ };
288
+ serializedState.poolStats = this.getPoolStats() || {};
289
+ }
290
+ if (options.includeBranchingContext && this.branchingEngine) {
291
+ serializedState.branchingContext = {
292
+ activeBranches: [], // Would be populated from branching engine
293
+ branchStates: {},
294
+ convergencePoints: []
295
+ };
296
+ }
297
+ if (options.includeValidationState) {
298
+ serializedState.validationState = {
299
+ disabledRules: [],
300
+ customRules: {},
301
+ validationErrors: []
302
+ };
303
+ }
304
+ // Generate checksum if requested
305
+ if (options.generateChecksum) {
306
+ const stateToHash = { ...serializedState };
307
+ delete stateToHash.metadata.checksum; // Exclude checksum from hash
308
+ const stateString = JSON.stringify(stateToHash);
309
+ metadata.checksum = await this.generateChecksum(stateString);
310
+ serializedState.metadata = metadata;
311
+ }
312
+ // Performance tracking
313
+ if (this.enableProfiling) {
314
+ const duration = performance.now() - startTime;
315
+ PerfReporter_1.perf.record('custom', {
316
+ eventType: 'state-serialized',
317
+ duration,
318
+ stateSize: JSON.stringify(serializedState).length,
319
+ includePerformance: !!options.includePerformanceData,
320
+ includeFlowEvents: options.includeFlowEvents !== false
321
+ });
322
+ }
323
+ return serializedState;
324
+ }
325
+ catch (error) {
326
+ const errorMessage = error instanceof Error ? error.message : 'Unknown serialization error';
327
+ if (this.enableProfiling) {
328
+ PerfReporter_1.perf.record('custom', {
329
+ eventType: 'state-serialization-failed',
330
+ error: errorMessage,
331
+ duration: performance.now() - startTime
332
+ });
333
+ }
334
+ throw new Error(`Failed to save state: ${errorMessage}`);
335
+ }
336
+ }
337
+ /**
338
+ * Load engine state from serialized format
339
+ * @param serializedState - The serialized state to load
340
+ * @param options - Load options
341
+ * @returns Promise resolving to persistence result
342
+ */
343
+ async loadState(serializedState, options = {}) {
344
+ const startTime = performance.now();
345
+ try {
346
+ // Validate serialized state structure
347
+ const validationResult = this.validateSerializedState(serializedState);
348
+ if (!validationResult.isValid) {
349
+ return {
350
+ success: false,
351
+ error: `Invalid serialized state: ${validationResult.errors.join(', ')}`,
352
+ warnings: validationResult.warnings
353
+ };
354
+ }
355
+ // Check compatibility
356
+ if (!options.skipCompatibilityCheck) {
357
+ const compatibilityCheck = this.checkCompatibility(serializedState.metadata);
358
+ if (!compatibilityCheck.compatible) {
359
+ return {
360
+ success: false,
361
+ error: `Incompatible state version: ${compatibilityCheck.reason}`,
362
+ warnings: compatibilityCheck.suggestions
363
+ };
364
+ }
365
+ }
366
+ // Verify checksum if available and requested
367
+ if (options.verifyChecksum && serializedState.metadata.checksum) {
368
+ const isValid = await this.verifyChecksum(serializedState);
369
+ if (!isValid) {
370
+ return {
371
+ success: false,
372
+ error: 'Checksum verification failed - state may be corrupted'
373
+ };
374
+ }
375
+ }
376
+ // Story hash validation
377
+ if (this.generateStoryHash() !== serializedState.metadata.storyId) {
378
+ return {
379
+ success: false,
380
+ error: 'Story hash mismatch. The state belongs to a different narrative.'
381
+ };
382
+ }
383
+ // Apply migration if needed
384
+ let stateToLoad = serializedState;
385
+ if (options.migrationFunction) {
386
+ stateToLoad = options.migrationFunction(serializedState);
387
+ }
388
+ // Load core state
389
+ this.state = this.deepCopy(stateToLoad.state);
390
+ // Restore optional data based on options
391
+ if (options.restoreFlowEvents && stateToLoad.flowEvents) {
392
+ this.activeFlowEvents = this.deepCopy(stateToLoad.flowEvents);
393
+ }
394
+ if (options.restorePerformanceState && stateToLoad.performanceState) {
395
+ this.performanceMode = stateToLoad.performanceState.performanceMode;
396
+ this.enableProfiling = stateToLoad.performanceState.enableProfiling;
397
+ // Background tasks would be restored here
398
+ }
399
+ if (options.restoreBranchingContext && stateToLoad.branchingContext && this.branchingEngine) {
400
+ // Restore branching context - would be implemented with actual branching engine
401
+ }
402
+ // Performance tracking
403
+ const duration = performance.now() - startTime;
404
+ if (this.enableProfiling) {
405
+ PerfReporter_1.perf.record('custom', {
406
+ eventType: 'state-loaded',
407
+ duration,
408
+ stateSize: JSON.stringify(stateToLoad).length,
409
+ restoredFlowEvents: !!options.restoreFlowEvents,
410
+ restoredPerformance: !!options.restorePerformanceState
411
+ });
412
+ }
413
+ return {
414
+ success: true,
415
+ data: {
416
+ size: JSON.stringify(stateToLoad).length,
417
+ duration,
418
+ checksum: stateToLoad.metadata.checksum
419
+ }
420
+ };
421
+ }
422
+ catch (error) {
423
+ const errorMessage = error instanceof Error ? error.message : 'Unknown loading error';
424
+ const duration = performance.now() - startTime;
425
+ if (this.enableProfiling) {
426
+ PerfReporter_1.perf.record('custom', {
427
+ eventType: 'state-loading-failed',
428
+ error: errorMessage,
429
+ duration
430
+ });
431
+ }
432
+ return {
433
+ success: false,
434
+ error: `Failed to load state: ${errorMessage}`,
435
+ data: { duration }
436
+ };
437
+ }
438
+ }
439
+ /**
440
+ * Create a lightweight checkpoint of current state
441
+ * @param name - Optional checkpoint name
442
+ * @param options - Checkpoint options
443
+ * @returns Promise resolving to created checkpoint
444
+ */
445
+ async createCheckpoint(name, options = {}) {
446
+ const checkpointId = this.generateCheckpointId();
447
+ const timestamp = new Date().toISOString();
448
+ const checkpoint = {
449
+ id: checkpointId,
450
+ name: name || `Checkpoint ${checkpointId.slice(-8)}`,
451
+ state: this.deepCopy(this.state),
452
+ timestamp,
453
+ description: options.includeMetadata ? `Auto-checkpoint at node ${this.state.currentNodeId}` : undefined,
454
+ tags: options.autoTags || [],
455
+ metadata: options.includeMetadata ? {
456
+ nodeTitle: this.getCurrentNode().text.slice(0, 50),
457
+ choiceCount: this.getCurrentNode().choices.length,
458
+ flagCount: Object.keys(this.state.flags).length,
459
+ historyLength: this.state.history.length
460
+ } : undefined
461
+ };
462
+ // Store checkpoint
463
+ this.checkpoints.set(checkpointId, checkpoint);
464
+ // Cleanup old checkpoints if needed
465
+ const maxCheckpoints = options.maxCheckpoints || this.maxCheckpoints;
466
+ if (this.checkpoints.size > maxCheckpoints) {
467
+ await this.cleanupCheckpoints(options.cleanupStrategy || 'lru', maxCheckpoints);
468
+ }
469
+ // Performance tracking
470
+ if (this.enableProfiling) {
471
+ PerfReporter_1.perf.record('custom', {
472
+ eventType: 'checkpoint-created',
473
+ checkpointId,
474
+ checkpointCount: this.checkpoints.size,
475
+ stateSize: JSON.stringify(checkpoint.state).length
476
+ });
477
+ }
478
+ return checkpoint;
479
+ }
480
+ /**
481
+ * Restore engine state from a checkpoint
482
+ * @param checkpointId - ID of checkpoint to restore
483
+ * @returns Promise resolving to persistence result
484
+ */
485
+ async restoreFromCheckpoint(checkpointId) {
486
+ const startTime = performance.now();
487
+ try {
488
+ const checkpoint = this.checkpoints.get(checkpointId);
489
+ if (!checkpoint) {
490
+ return {
491
+ success: false,
492
+ error: `Checkpoint not found: ${checkpointId}`
493
+ };
494
+ }
495
+ // Restore state
496
+ this.state = this.deepCopy(checkpoint.state);
497
+ const duration = performance.now() - startTime;
498
+ // Performance tracking
499
+ if (this.enableProfiling) {
500
+ PerfReporter_1.perf.record('custom', {
501
+ eventType: 'checkpoint-restored',
502
+ checkpointId,
503
+ duration,
504
+ stateSize: JSON.stringify(checkpoint.state).length
505
+ });
506
+ }
507
+ return {
508
+ success: true,
509
+ data: {
510
+ size: JSON.stringify(checkpoint.state).length,
511
+ duration,
512
+ checksum: checkpointId
513
+ }
514
+ };
515
+ }
516
+ catch (error) {
517
+ const errorMessage = error instanceof Error ? error.message : 'Unknown restore error';
518
+ const duration = performance.now() - startTime;
519
+ if (this.enableProfiling) {
520
+ PerfReporter_1.perf.record('custom', {
521
+ eventType: 'checkpoint-restore-failed',
522
+ checkpointId,
523
+ error: errorMessage,
524
+ duration
525
+ });
526
+ }
527
+ return {
528
+ success: false,
529
+ error: `Failed to restore checkpoint: ${errorMessage}`,
530
+ data: { duration }
531
+ };
532
+ }
533
+ }
534
+ // Utility method for checking flag conditions
535
+ checkFlag(flagName, expectedValue) {
536
+ if (expectedValue === undefined) {
537
+ return this.state.flags[flagName] !== undefined;
538
+ }
539
+ return this.state.flags[flagName] === expectedValue;
540
+ }
541
+ // Get available choices (with conditional filtering)
542
+ getAvailableChoices() {
543
+ const currentNode = this.getCurrentNode();
544
+ const context = {
545
+ state: this.state,
546
+ timestamp: Date.now(),
547
+ customData: {}
548
+ };
549
+ return currentNode.choices.filter((choice) => {
550
+ // If no condition is specified, choice is always available
551
+ if (!choice.condition) {
552
+ return true;
553
+ }
554
+ try {
555
+ // Evaluate the condition using the condition evaluator
556
+ return condition_1.conditionEvaluator.evaluate(choice.condition, context);
557
+ }
558
+ catch (error) {
559
+ // Log condition evaluation errors but don't block other choices
560
+ if (error instanceof condition_1.ConditionEvaluationError) {
561
+ console.warn(`[QNCE] Choice condition evaluation failed: ${error.message}`, {
562
+ choiceText: choice.text,
563
+ condition: choice.condition,
564
+ nodeId: this.state.currentNodeId
565
+ });
566
+ }
567
+ else {
568
+ console.warn(`[QNCE] Unexpected error evaluating choice condition:`, error);
569
+ }
570
+ // Return false for invalid conditions (choice won't be shown)
571
+ return false;
572
+ }
573
+ });
574
+ }
575
+ // Performance and object pooling methods
576
+ /**
577
+ * Create a flow event using pooled objects for performance tracking
578
+ */
579
+ createFlowEvent(fromNodeId, toNodeId, metadata) {
580
+ const flow = ObjectPool_1.poolManager.borrowFlow();
581
+ flow.initialize(fromNodeId, metadata);
582
+ flow.addTransition(fromNodeId, toNodeId);
583
+ return flow;
584
+ }
585
+ /**
586
+ * Record and manage flow events
587
+ */
588
+ recordFlowEvent(flow) {
589
+ const flowEvent = {
590
+ id: `${flow.nodeId}-${Date.now()}`,
591
+ fromNodeId: flow.nodeId,
592
+ toNodeId: flow.transitions[flow.transitions.length - 1]?.split('->')[1] || '',
593
+ timestamp: flow.timestamp,
594
+ metadata: flow.metadata
595
+ };
596
+ this.activeFlowEvents.push(flowEvent);
597
+ // Clean up old flow events (basic LRU-style cleanup)
598
+ if (this.activeFlowEvents.length > 10) {
599
+ this.activeFlowEvents.shift(); // Remove oldest
600
+ }
601
+ }
602
+ /**
603
+ * Get current flow events (for debugging/monitoring)
604
+ */
605
+ getActiveFlows() {
606
+ return [...this.activeFlowEvents];
607
+ }
608
+ /**
609
+ * Get object pool statistics for performance monitoring
610
+ */
611
+ getPoolStats() {
612
+ return this.performanceMode ? ObjectPool_1.poolManager.getAllStats() : null;
613
+ }
614
+ /**
615
+ * Return all pooled objects (cleanup method)
616
+ */
617
+ cleanupPools() {
618
+ // Clear flow events (no pooled objects to return since we return them immediately)
619
+ this.activeFlowEvents.length = 0;
620
+ }
621
+ // S2-T2: Background ThreadPool Operations
622
+ /**
623
+ * Preload next possible nodes in background using ThreadPool
624
+ */
625
+ async preloadNextNodes(choice) {
626
+ if (!this.performanceMode)
627
+ return;
628
+ const threadPool = (0, ThreadPool_1.getThreadPool)();
629
+ const currentNode = this.getCurrentNode();
630
+ const choicesToPreload = choice ? [choice] : currentNode.choices;
631
+ // Submit background jobs for each node to preload
632
+ for (const ch of choicesToPreload) {
633
+ threadPool.submitJob('cache-load', { nodeId: ch.nextNodeId }, 'normal').catch(error => {
634
+ if (this.enableProfiling) {
635
+ PerfReporter_1.perf.record('cache-miss', {
636
+ nodeId: ch.nextNodeId,
637
+ error: error.message,
638
+ jobType: 'preload'
639
+ });
640
+ }
641
+ });
642
+ }
643
+ }
644
+ /**
645
+ * Write telemetry data in background using ThreadPool
646
+ */
647
+ async backgroundTelemetryWrite(eventData) {
648
+ if (!this.performanceMode || !this.enableProfiling)
649
+ return;
650
+ const threadPool = (0, ThreadPool_1.getThreadPool)();
651
+ const telemetryData = {
652
+ timestamp: performance.now(),
653
+ sessionId: this.state.history[0], // Use first node as session ID
654
+ eventData,
655
+ stateSnapshot: {
656
+ nodeId: this.state.currentNodeId,
657
+ flagCount: Object.keys(this.state.flags).length,
658
+ historyLength: this.state.history.length
659
+ }
660
+ };
661
+ threadPool.submitJob('telemetry-write', telemetryData, 'low').catch(error => {
662
+ console.warn('[QNCE] Telemetry write failed:', error.message);
663
+ });
664
+ }
665
+ // ================================
666
+ // Sprint #3: Advanced Branching System Integration
667
+ // ================================
668
+ /**
669
+ * Enable advanced branching capabilities for this story
670
+ * Integrates the QNCE Branching API with the core engine
671
+ */
672
+ enableBranching(story) {
673
+ if (this.branchingEngine) {
674
+ console.warn('[QNCE] Branching already enabled for this engine instance');
675
+ return this.branchingEngine;
676
+ }
677
+ // Create branching engine with current state
678
+ this.branchingEngine = (0, branching_1.createBranchingEngine)(story, this.state);
679
+ if (this.enableProfiling) {
680
+ PerfReporter_1.perf.record('custom', {
681
+ eventType: 'branching-enabled',
682
+ storyId: story.id,
683
+ chapterCount: story.chapters.length,
684
+ performanceMode: this.performanceMode
685
+ });
686
+ }
687
+ return this.branchingEngine;
688
+ }
689
+ /**
690
+ * Get the branching engine if enabled
691
+ */
692
+ getBranchingEngine() {
693
+ return this.branchingEngine;
694
+ }
695
+ /**
696
+ * Check if branching is enabled
697
+ */
698
+ isBranchingEnabled() {
699
+ return !!this.branchingEngine;
700
+ }
701
+ /**
702
+ * Sync core engine state with branching engine
703
+ * Call this when core state changes to keep branching engine updated
704
+ */
705
+ syncBranchingState() {
706
+ if (this.branchingEngine) {
707
+ // The branching engine maintains its own state copy
708
+ // This method could be extended to sync state changes
709
+ if (this.enableProfiling) {
710
+ PerfReporter_1.perf.record('custom', {
711
+ eventType: 'branching-state-synced',
712
+ currentNodeId: this.state.currentNodeId
713
+ });
714
+ }
715
+ }
716
+ }
717
+ /**
718
+ * Disable branching and cleanup resources
719
+ */
720
+ disableBranching() {
721
+ if (this.branchingEngine) {
722
+ this.branchingEngine = undefined;
723
+ if (this.enableProfiling) {
724
+ PerfReporter_1.perf.record('custom', {
725
+ eventType: 'branching-disabled'
726
+ });
727
+ }
728
+ }
729
+ }
730
+ /**
731
+ * Background cache warming for story data
732
+ */
733
+ async warmCache() {
734
+ if (!this.performanceMode)
735
+ return;
736
+ const threadPool = (0, ThreadPool_1.getThreadPool)();
737
+ // Cache all nodes in background
738
+ const cacheWarmData = {
739
+ nodeIds: this.storyData.nodes.map(n => n.id),
740
+ storyId: this.storyData.initialNodeId
741
+ };
742
+ if (this.enableProfiling) {
743
+ PerfReporter_1.perf.record('custom', {
744
+ eventType: 'cache-warm-start',
745
+ nodeCount: this.storyData.nodes.length
746
+ });
747
+ }
748
+ threadPool.submitJob('cache-load', cacheWarmData, 'low').catch(error => {
749
+ if (this.enableProfiling) {
750
+ PerfReporter_1.perf.record('custom', {
751
+ eventType: 'cache-warm-failed',
752
+ error: error.message
753
+ });
754
+ }
755
+ });
756
+ }
757
+ // Sprint 3.3: State persistence utility methods
758
+ /**
759
+ * Deep copy utility for state objects
760
+ * @param obj - Object to deep copy
761
+ * @returns Deep copied object
762
+ */
763
+ deepCopy(obj) {
764
+ if (obj === null || typeof obj !== 'object') {
765
+ return obj;
766
+ }
767
+ if (obj instanceof Date) {
768
+ return new Date(obj.getTime());
769
+ }
770
+ if (obj instanceof Array) {
771
+ return obj.map(item => this.deepCopy(item));
772
+ }
773
+ if (typeof obj === 'object') {
774
+ const copy = {};
775
+ for (const key in obj) {
776
+ if (obj.hasOwnProperty(key)) {
777
+ copy[key] = this.deepCopy(obj[key]);
778
+ }
779
+ }
780
+ return copy;
781
+ }
782
+ return obj;
783
+ }
784
+ /**
785
+ * Generate a hash for the current story data
786
+ * @returns Story hash string
787
+ */
788
+ generateStoryHash() {
789
+ const storyString = JSON.stringify({
790
+ initialNodeId: this.storyData.initialNodeId,
791
+ nodeCount: this.storyData.nodes.length,
792
+ nodeIds: this.storyData.nodes.map(n => n.id).sort()
793
+ });
794
+ // Simple hash function (in production, use crypto.subtle.digest)
795
+ let hash = 0;
796
+ for (let i = 0; i < storyString.length; i++) {
797
+ const char = storyString.charCodeAt(i);
798
+ hash = ((hash << 5) - hash) + char;
799
+ hash = hash & hash; // Convert to 32-bit integer
800
+ }
801
+ return hash.toString(16);
802
+ }
803
+ /**
804
+ * Generate a unique checkpoint ID
805
+ * @returns Unique checkpoint ID
806
+ */
807
+ generateCheckpointId() {
808
+ const timestamp = Date.now();
809
+ const random = Math.random().toString(36).substr(2, 9);
810
+ return `checkpoint_${timestamp}_${random}`;
811
+ }
812
+ /**
813
+ * Generate checksum for data integrity
814
+ * @param data - Data to generate checksum for
815
+ * @returns Promise resolving to checksum string
816
+ */
817
+ async generateChecksum(data) {
818
+ // Simple checksum implementation (in production, use crypto.subtle.digest)
819
+ let checksum = 0;
820
+ for (let i = 0; i < data.length; i++) {
821
+ checksum = ((checksum << 5) - checksum) + data.charCodeAt(i);
822
+ checksum = checksum & checksum;
823
+ }
824
+ return checksum.toString(16);
825
+ }
826
+ /**
827
+ * Verify checksum integrity
828
+ * @param serializedState - State with checksum to verify
829
+ * @returns Promise resolving to verification result
830
+ */
831
+ async verifyChecksum(serializedState) {
832
+ const receivedChecksum = serializedState.metadata.checksum;
833
+ if (!receivedChecksum)
834
+ return false;
835
+ const stateToHash = { ...serializedState };
836
+ delete stateToHash.metadata.checksum;
837
+ const stateString = JSON.stringify(stateToHash);
838
+ const expectedChecksum = await this.generateChecksum(stateString);
839
+ return receivedChecksum === expectedChecksum;
840
+ }
841
+ /**
842
+ * Validate the structure of the serialized state object
843
+ * @param serializedState - State to validate
844
+ * @returns Validation result
845
+ */
846
+ validateSerializedState(serializedState) {
847
+ const errors = [];
848
+ const warnings = [];
849
+ // Check required fields
850
+ if (!serializedState.state) {
851
+ errors.push('Missing state field');
852
+ }
853
+ else {
854
+ if (!serializedState.state.currentNodeId) {
855
+ errors.push('Missing currentNodeId in state');
856
+ }
857
+ if (!serializedState.state.flags) {
858
+ warnings.push('Missing flags in state');
859
+ }
860
+ if (!serializedState.state.history) {
861
+ warnings.push('Missing history in state');
862
+ }
863
+ }
864
+ if (!serializedState.metadata) {
865
+ errors.push('Missing metadata field');
866
+ }
867
+ else {
868
+ if (!serializedState.metadata.engineVersion) {
869
+ warnings.push('Missing engine version in metadata');
870
+ }
871
+ if (!serializedState.metadata.timestamp) {
872
+ warnings.push('Missing timestamp in metadata');
873
+ }
874
+ }
875
+ return {
876
+ isValid: errors.length === 0,
877
+ errors,
878
+ warnings
879
+ };
880
+ }
881
+ /**
882
+ * Check version compatibility
883
+ * @param metadata - Serialization metadata
884
+ * @returns Compatibility check result
885
+ */
886
+ checkCompatibility(metadata) {
887
+ const currentVersion = types_1.PERSISTENCE_VERSION;
888
+ const stateVersion = metadata.engineVersion;
889
+ if (!stateVersion) {
890
+ return {
891
+ compatible: false,
892
+ reason: 'Unknown state version',
893
+ suggestions: ['State may be from an older engine version']
894
+ };
895
+ }
896
+ if (stateVersion === currentVersion) {
897
+ return { compatible: true };
898
+ }
899
+ // Simple version comparison (in production, use semver)
900
+ const [currentMajor, currentMinor] = currentVersion.split('.').map(Number);
901
+ const [stateMajor, stateMinor] = stateVersion.split('.').map(Number);
902
+ if (stateMajor > currentMajor ||
903
+ (stateMajor === currentMajor && stateMinor > currentMinor)) {
904
+ return {
905
+ compatible: false,
906
+ reason: `State from newer engine version (${stateVersion} > ${currentVersion})`,
907
+ suggestions: ['Update the engine to a newer version']
908
+ };
909
+ }
910
+ if (stateMajor < currentMajor) {
911
+ return {
912
+ compatible: false,
913
+ reason: `State from incompatible major version (${stateVersion} vs ${currentVersion})`,
914
+ suggestions: ['Use migration function to upgrade state format']
915
+ };
916
+ }
917
+ // Minor version differences are usually compatible
918
+ return {
919
+ compatible: true,
920
+ suggestions: [`State from older minor version (${stateVersion})`]
921
+ };
922
+ }
923
+ /**
924
+ * Cleanup old checkpoints based on strategy
925
+ * @param strategy - Cleanup strategy
926
+ * @param maxCheckpoints - Maximum number of checkpoints to keep
927
+ * @returns Promise resolving to number of cleaned checkpoints
928
+ */
929
+ async cleanupCheckpoints(strategy, maxCheckpoints) {
930
+ const checkpoints = Array.from(this.checkpoints.entries());
931
+ if (checkpoints.length <= maxCheckpoints) {
932
+ return 0;
933
+ }
934
+ const toRemove = checkpoints.length - maxCheckpoints;
935
+ let checkpointsToDelete = [];
936
+ switch (strategy) {
937
+ case 'fifo':
938
+ // Remove oldest by creation order (assuming IDs contain timestamp)
939
+ checkpointsToDelete = checkpoints
940
+ .sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp))
941
+ .slice(0, toRemove)
942
+ .map(([id]) => id);
943
+ break;
944
+ case 'timestamp':
945
+ // Remove oldest by timestamp
946
+ checkpointsToDelete = checkpoints
947
+ .sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp))
948
+ .slice(0, toRemove)
949
+ .map(([id]) => id);
950
+ break;
951
+ case 'lru':
952
+ default:
953
+ // For now, same as FIFO (would need access tracking in production)
954
+ checkpointsToDelete = checkpoints
955
+ .sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp))
956
+ .slice(0, toRemove)
957
+ .map(([id]) => id);
958
+ break;
959
+ }
960
+ // Remove checkpoints
961
+ for (const id of checkpointsToDelete) {
962
+ this.checkpoints.delete(id);
963
+ }
964
+ if (this.enableProfiling) {
965
+ PerfReporter_1.perf.record('custom', {
966
+ eventType: 'checkpoints-cleaned',
967
+ strategy,
968
+ removedCount: checkpointsToDelete.length,
969
+ remainingCount: this.checkpoints.size
970
+ });
971
+ }
972
+ return checkpointsToDelete.length;
973
+ }
974
+ /**
975
+ * Get all checkpoints
976
+ * @returns Array of all checkpoints
977
+ */
978
+ getCheckpoints() {
979
+ return Array.from(this.checkpoints.values());
980
+ }
981
+ /**
982
+ * Get specific checkpoint by ID
983
+ * @param id - Checkpoint ID
984
+ * @returns Checkpoint or undefined
985
+ */
986
+ getCheckpoint(id) {
987
+ return this.checkpoints.get(id);
988
+ }
989
+ /**
990
+ * Delete a checkpoint
991
+ * @param id - Checkpoint ID to delete
992
+ * @returns Whether checkpoint was deleted
993
+ */
994
+ deleteCheckpoint(id) {
995
+ return this.checkpoints.delete(id);
996
+ }
997
+ /**
998
+ * Enable/disable automatic checkpointing
999
+ * @param enabled - Whether to enable auto-checkpointing
1000
+ */
1001
+ setAutoCheckpoint(enabled) {
1002
+ this.autoCheckpointEnabled = enabled;
1003
+ }
1004
+ // Sprint 3.5: Autosave and Undo/Redo Implementation
1005
+ /**
1006
+ * Configure autosave settings
1007
+ * @param config - Autosave configuration
1008
+ */
1009
+ configureAutosave(config) {
1010
+ this.autosaveConfig = { ...this.autosaveConfig, ...config };
1011
+ }
1012
+ /**
1013
+ * Configure undo/redo settings
1014
+ * @param config - Undo/redo configuration
1015
+ */
1016
+ configureUndoRedo(config) {
1017
+ this.undoRedoConfig = { ...this.undoRedoConfig, ...config };
1018
+ }
1019
+ /**
1020
+ * Push a state to the undo stack
1021
+ * @param state - State to save
1022
+ * @param action - Action that caused this state change
1023
+ * @param metadata - Optional metadata about the change
1024
+ */
1025
+ pushToUndoStack(state, action, metadata) {
1026
+ const startTime = performance.now();
1027
+ const entry = {
1028
+ id: this.generateHistoryId(),
1029
+ state: this.deepCopy(state),
1030
+ timestamp: new Date().toISOString(),
1031
+ action,
1032
+ metadata
1033
+ };
1034
+ this.undoStack.push(entry);
1035
+ // Clear redo stack when new change is made
1036
+ this.redoStack = [];
1037
+ // Enforce max undo entries
1038
+ if (this.undoStack.length > this.undoRedoConfig.maxUndoEntries) {
1039
+ this.undoStack.shift(); // Remove oldest entry
1040
+ }
1041
+ // Performance tracking
1042
+ if (this.enableProfiling) {
1043
+ const duration = performance.now() - startTime;
1044
+ PerfReporter_1.perf.record('custom', {
1045
+ eventType: 'undo-stack-push',
1046
+ duration,
1047
+ undoCount: this.undoStack.length,
1048
+ action
1049
+ });
1050
+ }
1051
+ }
1052
+ /**
1053
+ * Undo the last operation
1054
+ * @returns Result of undo operation
1055
+ */
1056
+ undo() {
1057
+ const startTime = performance.now();
1058
+ if (this.undoStack.length === 0) {
1059
+ return {
1060
+ success: false,
1061
+ error: 'No operations to undo',
1062
+ stackSizes: {
1063
+ undoCount: this.undoStack.length,
1064
+ redoCount: this.redoStack.length
1065
+ }
1066
+ };
1067
+ }
1068
+ try {
1069
+ // Save current state to redo stack
1070
+ const currentEntry = {
1071
+ id: this.generateHistoryId(),
1072
+ state: this.deepCopy(this.state),
1073
+ timestamp: new Date().toISOString(),
1074
+ action: 'redo-point'
1075
+ };
1076
+ this.redoStack.push(currentEntry);
1077
+ // Enforce max redo entries
1078
+ if (this.redoStack.length > this.undoRedoConfig.maxRedoEntries) {
1079
+ this.redoStack.shift(); // Remove oldest entry
1080
+ }
1081
+ // Restore previous state
1082
+ const entryToRestore = this.undoStack.pop();
1083
+ // Set flag to prevent triggering undo/redo tracking during restore
1084
+ this.isUndoRedoOperation = true;
1085
+ this.state = this.deepCopy(entryToRestore.state);
1086
+ this.isUndoRedoOperation = false;
1087
+ const duration = performance.now() - startTime;
1088
+ // Performance tracking
1089
+ if (this.enableProfiling) {
1090
+ PerfReporter_1.perf.record('custom', {
1091
+ eventType: 'undo-operation',
1092
+ duration,
1093
+ undoCount: this.undoStack.length,
1094
+ redoCount: this.redoStack.length
1095
+ });
1096
+ }
1097
+ return {
1098
+ success: true,
1099
+ restoredState: this.deepCopy(this.state),
1100
+ entry: {
1101
+ id: entryToRestore.id,
1102
+ timestamp: entryToRestore.timestamp,
1103
+ action: entryToRestore.action,
1104
+ nodeId: entryToRestore.metadata?.nodeId
1105
+ },
1106
+ stackSizes: {
1107
+ undoCount: this.undoStack.length,
1108
+ redoCount: this.redoStack.length
1109
+ }
1110
+ };
1111
+ }
1112
+ catch (error) {
1113
+ this.isUndoRedoOperation = false; // Ensure flag is reset on error
1114
+ const errorMessage = error instanceof Error ? error.message : 'Unknown undo error';
1115
+ return {
1116
+ success: false,
1117
+ error: `Undo failed: ${errorMessage}`,
1118
+ stackSizes: {
1119
+ undoCount: this.undoStack.length,
1120
+ redoCount: this.redoStack.length
1121
+ }
1122
+ };
1123
+ }
1124
+ }
1125
+ /**
1126
+ * Redo the last undone operation
1127
+ * @returns Result of redo operation
1128
+ */
1129
+ redo() {
1130
+ const startTime = performance.now();
1131
+ if (this.redoStack.length === 0) {
1132
+ return {
1133
+ success: false,
1134
+ error: 'No operations to redo',
1135
+ stackSizes: {
1136
+ undoCount: this.undoStack.length,
1137
+ redoCount: this.redoStack.length
1138
+ }
1139
+ };
1140
+ }
1141
+ try {
1142
+ // Save current state to undo stack
1143
+ const currentEntry = {
1144
+ id: this.generateHistoryId(),
1145
+ state: this.deepCopy(this.state),
1146
+ timestamp: new Date().toISOString(),
1147
+ action: 'undo-point'
1148
+ };
1149
+ this.undoStack.push(currentEntry);
1150
+ // Enforce max undo entries
1151
+ if (this.undoStack.length > this.undoRedoConfig.maxUndoEntries) {
1152
+ this.undoStack.shift(); // Remove oldest entry
1153
+ }
1154
+ // Restore redo state
1155
+ const entryToRestore = this.redoStack.pop();
1156
+ // Set flag to prevent triggering undo/redo tracking during restore
1157
+ this.isUndoRedoOperation = true;
1158
+ this.state = this.deepCopy(entryToRestore.state);
1159
+ this.isUndoRedoOperation = false;
1160
+ const duration = performance.now() - startTime;
1161
+ // Performance tracking
1162
+ if (this.enableProfiling) {
1163
+ PerfReporter_1.perf.record('custom', {
1164
+ eventType: 'redo-operation',
1165
+ duration,
1166
+ undoCount: this.undoStack.length,
1167
+ redoCount: this.redoStack.length
1168
+ });
1169
+ }
1170
+ return {
1171
+ success: true,
1172
+ restoredState: this.deepCopy(this.state),
1173
+ entry: {
1174
+ id: entryToRestore.id,
1175
+ timestamp: entryToRestore.timestamp,
1176
+ action: entryToRestore.action,
1177
+ nodeId: entryToRestore.metadata?.nodeId
1178
+ },
1179
+ stackSizes: {
1180
+ undoCount: this.undoStack.length,
1181
+ redoCount: this.redoStack.length
1182
+ }
1183
+ };
1184
+ }
1185
+ catch (error) {
1186
+ this.isUndoRedoOperation = false; // Ensure flag is reset on error
1187
+ const errorMessage = error instanceof Error ? error.message : 'Unknown redo error';
1188
+ return {
1189
+ success: false,
1190
+ error: `Redo failed: ${errorMessage}`,
1191
+ stackSizes: {
1192
+ undoCount: this.undoStack.length,
1193
+ redoCount: this.redoStack.length
1194
+ }
1195
+ };
1196
+ }
1197
+ }
1198
+ /**
1199
+ * Check if undo is available
1200
+ * @returns True if undo is possible
1201
+ */
1202
+ canUndo() {
1203
+ return this.undoRedoConfig.enabled && this.undoStack.length > 0;
1204
+ }
1205
+ /**
1206
+ * Check if redo is available
1207
+ * @returns True if redo is possible
1208
+ */
1209
+ canRedo() {
1210
+ return this.undoRedoConfig.enabled && this.redoStack.length > 0;
1211
+ }
1212
+ /**
1213
+ * Get the number of available undo operations
1214
+ * @returns Number of undo entries
1215
+ */
1216
+ getUndoCount() {
1217
+ return this.undoStack.length;
1218
+ }
1219
+ /**
1220
+ * Get the number of available redo operations
1221
+ * @returns Number of redo entries
1222
+ */
1223
+ getRedoCount() {
1224
+ return this.redoStack.length;
1225
+ }
1226
+ /**
1227
+ * Clear all undo/redo history
1228
+ */
1229
+ clearHistory() {
1230
+ this.undoStack = [];
1231
+ this.redoStack = [];
1232
+ if (this.enableProfiling) {
1233
+ PerfReporter_1.perf.record('custom', {
1234
+ eventType: 'history-cleared'
1235
+ });
1236
+ }
1237
+ }
1238
+ /**
1239
+ * Get history summary for debugging
1240
+ * @returns Summary of undo/redo history
1241
+ */
1242
+ getHistorySummary() {
1243
+ return {
1244
+ undoEntries: this.undoStack.map(entry => ({
1245
+ id: entry.id,
1246
+ timestamp: entry.timestamp,
1247
+ action: entry.action
1248
+ })),
1249
+ redoEntries: this.redoStack.map(entry => ({
1250
+ id: entry.id,
1251
+ timestamp: entry.timestamp,
1252
+ action: entry.action
1253
+ }))
1254
+ };
1255
+ }
1256
+ /**
1257
+ * Trigger an autosave operation
1258
+ * @param trigger - What triggered the autosave
1259
+ * @param metadata - Optional metadata about the trigger
1260
+ * @returns Promise resolving to autosave result
1261
+ */
1262
+ async triggerAutosave(trigger, metadata) {
1263
+ const startTime = performance.now();
1264
+ // Check if autosave is enabled
1265
+ if (!this.autosaveConfig.enabled) {
1266
+ return { success: false, error: 'Autosave is disabled' };
1267
+ }
1268
+ // Check throttling
1269
+ const now = performance.now();
1270
+ if (now - this.lastAutosaveTime < this.autosaveConfig.throttleMs) {
1271
+ return {
1272
+ success: false,
1273
+ error: 'Autosave throttled',
1274
+ trigger
1275
+ };
1276
+ }
1277
+ try {
1278
+ // Create autosave checkpoint
1279
+ const checkpointName = `autosave-${trigger}-${Date.now()}`;
1280
+ const checkpoint = await this.createCheckpoint(checkpointName, {
1281
+ includeMetadata: this.autosaveConfig.includeMetadata,
1282
+ autoTags: ['autosave', trigger],
1283
+ maxCheckpoints: this.autosaveConfig.maxEntries
1284
+ });
1285
+ this.lastAutosaveTime = now;
1286
+ const duration = performance.now() - startTime;
1287
+ // Performance tracking
1288
+ if (this.enableProfiling) {
1289
+ PerfReporter_1.perf.record('custom', {
1290
+ eventType: 'autosave-completed',
1291
+ duration,
1292
+ trigger,
1293
+ checkpointId: checkpoint.id
1294
+ });
1295
+ }
1296
+ return {
1297
+ success: true,
1298
+ checkpointId: checkpoint.id,
1299
+ trigger,
1300
+ duration,
1301
+ size: JSON.stringify(checkpoint.state).length
1302
+ };
1303
+ }
1304
+ catch (error) {
1305
+ const errorMessage = error instanceof Error ? error.message : 'Unknown autosave error';
1306
+ const duration = performance.now() - startTime;
1307
+ if (this.enableProfiling) {
1308
+ PerfReporter_1.perf.record('custom', {
1309
+ eventType: 'autosave-failed',
1310
+ duration,
1311
+ trigger,
1312
+ error: errorMessage
1313
+ });
1314
+ }
1315
+ return {
1316
+ success: false,
1317
+ error: `Autosave failed: ${errorMessage}`,
1318
+ trigger,
1319
+ duration
1320
+ };
1321
+ }
1322
+ }
1323
+ /**
1324
+ * Manually trigger an autosave
1325
+ * @param metadata - Optional metadata
1326
+ * @returns Promise resolving to autosave result
1327
+ */
1328
+ async manualAutosave(metadata) {
1329
+ return this.triggerAutosave('manual', metadata);
1330
+ }
1331
+ /**
1332
+ * Generate a unique history entry ID
1333
+ * @returns Unique ID string
1334
+ */
1335
+ generateHistoryId() {
1336
+ return `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1337
+ }
1338
+ // Sprint 3.4: Conditional Choice Display API
1339
+ /**
1340
+ * Set a custom condition evaluator function for complex choice logic
1341
+ * @param evaluator - Custom evaluator function
1342
+ */
1343
+ setConditionEvaluator(evaluator) {
1344
+ condition_1.conditionEvaluator.setCustomEvaluator(evaluator);
1345
+ }
1346
+ /**
1347
+ * Clear the custom condition evaluator
1348
+ */
1349
+ clearConditionEvaluator() {
1350
+ condition_1.conditionEvaluator.clearCustomEvaluator();
1351
+ }
1352
+ /**
1353
+ * Validate a condition expression without evaluating it
1354
+ * @param expression - Condition expression to validate
1355
+ * @returns Validation result
1356
+ */
1357
+ validateCondition(expression) {
1358
+ return condition_1.conditionEvaluator.validateExpression(expression);
1359
+ }
1360
+ /**
1361
+ * Get flags referenced in a condition expression
1362
+ * @param expression - Condition expression to analyze
1363
+ * @returns Array of flag names referenced
1364
+ */
1365
+ getConditionFlags(expression) {
1366
+ return condition_1.conditionEvaluator.getReferencedFlags(expression);
1367
+ }
1368
+ }
1369
+ exports.QNCEEngine = QNCEEngine;
1370
+ /**
1371
+ * Factory function to create a QNCE engine instance
1372
+ */
1373
+ function createQNCEEngine(storyData, initialState, performanceMode = false) {
1374
+ return new QNCEEngine(storyData, initialState, performanceMode);
1375
+ }
1376
+ /**
1377
+ * Load story data from JSON
1378
+ */
1379
+ function loadStoryData(jsonData) {
1380
+ // Add validation here in the future
1381
+ return jsonData;
1382
+ }