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
@@ -2,7 +2,7 @@
2
2
  // QNCE Core Engine - Framework Agnostic
3
3
  // Quantum Narrative Convergence Engine
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.QNCEEngine = void 0;
5
+ exports.QNCEEngine = exports.ChoiceValidationError = exports.QNCENavigationError = void 0;
6
6
  exports.createQNCEEngine = createQNCEEngine;
7
7
  exports.loadStoryData = loadStoryData;
8
8
  const ObjectPool_1 = require("../performance/ObjectPool");
@@ -10,6 +10,17 @@ const ThreadPool_1 = require("../performance/ThreadPool");
10
10
  const PerfReporter_1 = require("../performance/PerfReporter");
11
11
  // Branching system imports
12
12
  const branching_1 = require("../narrative/branching");
13
+ // Choice validation system - Sprint 3.2
14
+ const validation_1 = require("./validation");
15
+ const errors_1 = require("./errors");
16
+ // Re-export error classes for backward compatibility
17
+ var errors_2 = require("./errors");
18
+ Object.defineProperty(exports, "QNCENavigationError", { enumerable: true, get: function () { return errors_2.QNCENavigationError; } });
19
+ Object.defineProperty(exports, "ChoiceValidationError", { enumerable: true, get: function () { return errors_2.ChoiceValidationError; } });
20
+ // State persistence imports - Sprint 3.3
21
+ const types_1 = require("./types");
22
+ // Conditional choice evaluator - Sprint 3.4
23
+ const condition_1 = require("./condition");
13
24
  // Demo narrative data moved to demo-story.ts
14
25
  function findNode(nodes, id) {
15
26
  const node = nodes.find(n => n.id === id);
@@ -23,15 +34,58 @@ function findNode(nodes, id) {
23
34
  */
24
35
  class QNCEEngine {
25
36
  state;
26
- storyData;
37
+ storyData; // Made public for hot-reload compatibility
27
38
  activeFlowEvents = [];
28
39
  performanceMode = false;
29
40
  enableProfiling = false;
30
41
  branchingEngine;
42
+ choiceValidator; // Sprint 3.2: Choice validation
43
+ // Sprint 3.3: State persistence and checkpoints
44
+ checkpoints = new Map();
45
+ maxCheckpoints = 50;
46
+ autoCheckpointEnabled = false;
47
+ // Sprint 3.3: State persistence properties
48
+ checkpointManager;
49
+ // Sprint 3.5: Autosave and Undo/Redo properties
50
+ undoStack = [];
51
+ redoStack = [];
52
+ autosaveConfig = {
53
+ enabled: false,
54
+ triggers: ['choice', 'flag-change'],
55
+ maxEntries: 10,
56
+ throttleMs: 1000,
57
+ includeMetadata: true
58
+ };
59
+ undoRedoConfig = {
60
+ enabled: true,
61
+ maxUndoEntries: 50,
62
+ maxRedoEntries: 20,
63
+ trackFlagChanges: true,
64
+ trackChoiceText: true,
65
+ trackActions: ['choice', 'flag-change', 'state-load']
66
+ };
67
+ lastAutosaveTime = 0;
68
+ isUndoRedoOperation = false;
69
+ get flags() {
70
+ return this.state.flags;
71
+ }
72
+ get history() {
73
+ return this.state.history;
74
+ }
75
+ get isComplete() {
76
+ try {
77
+ return this.getCurrentNode().choices.length === 0;
78
+ }
79
+ catch {
80
+ return true; // If current node not found, consider it complete
81
+ }
82
+ }
31
83
  constructor(storyData, initialState, performanceMode = false, threadPoolConfig) {
32
84
  this.storyData = storyData;
33
85
  this.performanceMode = performanceMode;
34
86
  this.enableProfiling = performanceMode; // Enable profiling with performance mode
87
+ // Initialize choice validator (Sprint 3.2)
88
+ this.choiceValidator = (0, validation_1.createChoiceValidator)();
35
89
  // Initialize ThreadPool if in performance mode
36
90
  if (this.performanceMode && threadPoolConfig) {
37
91
  (0, ThreadPool_1.getThreadPool)(threadPoolConfig);
@@ -42,6 +96,48 @@ class QNCEEngine {
42
96
  history: initialState?.history || [storyData.initialNodeId],
43
97
  };
44
98
  }
99
+ // Sprint 3.1 - API Consistency Methods
100
+ /**
101
+ * Navigate directly to a specific node by ID
102
+ * @param nodeId - The ID of the node to navigate to
103
+ * @throws {QNCENavigationError} When nodeId is invalid or not found
104
+ */
105
+ goToNodeById(nodeId) {
106
+ // Validate node exists
107
+ const targetNode = this.storyData.nodes.find(n => n.id === nodeId);
108
+ if (!targetNode) {
109
+ throw new errors_1.QNCENavigationError(`Node not found: ${nodeId}`);
110
+ }
111
+ // Performance profiling for direct navigation
112
+ const navigationSpanId = this.enableProfiling
113
+ ? (0, PerfReporter_1.getPerfReporter)().startSpan('custom', {
114
+ fromNodeId: this.state.currentNodeId,
115
+ toNodeId: nodeId,
116
+ navigationType: 'direct'
117
+ })
118
+ : null;
119
+ const fromNodeId = this.state.currentNodeId;
120
+ // Update state
121
+ this.state.currentNodeId = nodeId;
122
+ this.state.history.push(nodeId);
123
+ // Record navigation event for analytics
124
+ if (this.performanceMode) {
125
+ const flowEvent = this.createFlowEvent(fromNodeId, nodeId, { navigationType: 'direct' });
126
+ this.recordFlowEvent(flowEvent);
127
+ ObjectPool_1.poolManager.returnFlow(flowEvent);
128
+ }
129
+ // End profiling span
130
+ if (navigationSpanId && this.enableProfiling) {
131
+ (0, PerfReporter_1.getPerfReporter)().endSpan(navigationSpanId, {
132
+ success: true,
133
+ targetNodeId: nodeId
134
+ });
135
+ }
136
+ }
137
+ /**
138
+ * Get the current narrative node
139
+ * @returns The current node object
140
+ */
45
141
  getCurrentNode() {
46
142
  const cacheKey = `node-${this.state.currentNodeId}`;
47
143
  // Profiling: Record cache operation
@@ -64,16 +160,98 @@ class QNCEEngine {
64
160
  }
65
161
  return findNode(this.storyData.nodes, this.state.currentNodeId);
66
162
  }
163
+ /**
164
+ * Get available choices from the current node with validation and conditional filtering
165
+ * @returns Array of available choices
166
+ */
167
+ getAvailableChoices() {
168
+ const currentNode = this.getCurrentNode();
169
+ const context = {
170
+ state: this.state,
171
+ timestamp: Date.now(),
172
+ customData: {}
173
+ };
174
+ // First apply conditional filtering (Sprint 3.4)
175
+ const conditionallyAvailable = currentNode.choices.filter((choice) => {
176
+ // If no condition is specified, choice is always available
177
+ if (!choice.condition) {
178
+ return true;
179
+ }
180
+ try {
181
+ // Evaluate the condition using the condition evaluator
182
+ return condition_1.conditionEvaluator.evaluate(choice.condition, context);
183
+ }
184
+ catch (error) {
185
+ // Log condition evaluation errors but don't block other choices
186
+ if (error instanceof condition_1.ConditionEvaluationError) {
187
+ console.warn(`[QNCE] Choice condition evaluation failed: ${error.message}`, {
188
+ choiceText: choice.text,
189
+ condition: choice.condition,
190
+ nodeId: this.state.currentNodeId
191
+ });
192
+ }
193
+ else {
194
+ console.warn(`[QNCE] Unexpected error evaluating choice condition:`, error);
195
+ }
196
+ // Return false for invalid conditions (choice won't be shown)
197
+ return false;
198
+ }
199
+ });
200
+ // Then apply choice validation (Sprint 3.2)
201
+ const validationContext = (0, validation_1.createValidationContext)(currentNode, this.state, conditionallyAvailable // Pass the conditionally filtered choices
202
+ );
203
+ return this.choiceValidator.getAvailableChoices(validationContext);
204
+ }
205
+ makeChoice(choiceIndex) {
206
+ const choices = this.getAvailableChoices();
207
+ if (choiceIndex < 0 || choiceIndex >= choices.length) {
208
+ throw new errors_1.QNCENavigationError(`Invalid choice index: ${choiceIndex}. Please select a number between 1 and ${choices.length}.`);
209
+ }
210
+ const selectedChoice = choices[choiceIndex];
211
+ // Sprint 3.2: Validate choice before execution
212
+ const currentNode = this.getCurrentNode();
213
+ const context = (0, validation_1.createValidationContext)(currentNode, this.state, choices);
214
+ const validationResult = this.choiceValidator.validate(selectedChoice, context);
215
+ if (!validationResult.isValid) {
216
+ throw new errors_1.ChoiceValidationError(selectedChoice, validationResult, choices);
217
+ }
218
+ // Execute the validated choice
219
+ this.selectChoice(selectedChoice);
220
+ }
67
221
  getState() {
68
222
  return { ...this.state };
69
223
  }
70
224
  getFlags() {
71
225
  return { ...this.state.flags };
72
226
  }
227
+ setFlag(key, value) {
228
+ // Sprint 3.5: Save state for undo before making changes
229
+ const preChangeState = this.deepCopy(this.state);
230
+ this.state.flags[key] = value;
231
+ // Sprint 3.5: Track state change for undo/redo
232
+ if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation &&
233
+ this.undoRedoConfig.trackActions.includes('flag-change')) {
234
+ this.pushToUndoStack(preChangeState, 'flag-change', {
235
+ flagsChanged: [key],
236
+ flagValue: value
237
+ });
238
+ }
239
+ // Sprint 3.5: Trigger autosave if enabled
240
+ if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('flag-change')) {
241
+ this.triggerAutosave('flag-change', {
242
+ flagKey: key,
243
+ flagValue: value
244
+ }).catch((error) => {
245
+ console.warn('[QNCE] Autosave failed:', error.message);
246
+ });
247
+ }
248
+ }
73
249
  getHistory() {
74
250
  return [...this.state.history];
75
251
  }
76
252
  selectChoice(choice) {
253
+ // Sprint 3.5: Save state for undo before making changes
254
+ const preChangeState = this.deepCopy(this.state);
77
255
  // S2-T4: Add state transition profiling
78
256
  const transitionSpanId = this.enableProfiling
79
257
  ? (0, PerfReporter_1.getPerfReporter)().startSpan('state-transition', {
@@ -111,6 +289,25 @@ class QNCEEngine {
111
289
  });
112
290
  }
113
291
  }
292
+ // Sprint 3.5: Track state change for undo/redo
293
+ if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation &&
294
+ this.undoRedoConfig.trackActions.includes('choice')) {
295
+ this.pushToUndoStack(preChangeState, 'choice', {
296
+ nodeId: fromNodeId,
297
+ choiceText: this.undoRedoConfig.trackChoiceText ? choice.text : undefined,
298
+ flagsChanged: choice.flagEffects ? Object.keys(choice.flagEffects) : undefined
299
+ });
300
+ }
301
+ // Sprint 3.5: Trigger autosave if enabled
302
+ if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('choice')) {
303
+ this.triggerAutosave('choice', {
304
+ fromNodeId,
305
+ toNodeId,
306
+ choiceText: choice.text
307
+ }).catch(error => {
308
+ console.warn('[QNCE] Autosave failed:', error.message);
309
+ });
310
+ }
114
311
  // Complete state transition span
115
312
  if (transitionSpanId) {
116
313
  (0, PerfReporter_1.getPerfReporter)().endSpan(transitionSpanId, {
@@ -136,6 +333,8 @@ class QNCEEngine {
136
333
  }
137
334
  }
138
335
  resetNarrative() {
336
+ // Sprint 3.5: Save state for undo before reset
337
+ const preChangeState = this.deepCopy(this.state);
139
338
  // Clean up pooled objects before reset
140
339
  if (this.performanceMode) {
141
340
  this.cleanupPools();
@@ -143,9 +342,318 @@ class QNCEEngine {
143
342
  this.state.currentNodeId = this.storyData.initialNodeId;
144
343
  this.state.flags = {};
145
344
  this.state.history = [this.storyData.initialNodeId];
345
+ // Sprint 3.5: Track state change for undo/redo
346
+ if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation &&
347
+ this.undoRedoConfig.trackActions.includes('reset')) {
348
+ this.pushToUndoStack(preChangeState, 'reset', {
349
+ nodeId: this.storyData.initialNodeId
350
+ });
351
+ }
146
352
  }
147
- loadState(state) {
353
+ /**
354
+ * Load simple state (legacy method for backward compatibility)
355
+ * @param state - QNCEState to load
356
+ */
357
+ loadSimpleState(state) {
358
+ // Sprint 3.5: Save state for undo before loading
359
+ const preChangeState = this.deepCopy(this.state);
148
360
  this.state = { ...state };
361
+ // Sprint 3.5: Track state change for undo/redo
362
+ if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation &&
363
+ this.undoRedoConfig.trackActions.includes('state-load')) {
364
+ this.pushToUndoStack(preChangeState, 'state-load', {
365
+ nodeId: state.currentNodeId
366
+ });
367
+ }
368
+ // Sprint 3.5: Trigger autosave if enabled
369
+ if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('state-load')) {
370
+ this.triggerAutosave('state-load', {
371
+ nodeId: state.currentNodeId
372
+ }).catch((error) => {
373
+ console.warn('[QNCE] Autosave failed:', error.message);
374
+ });
375
+ }
376
+ }
377
+ // Sprint 3.3: State Persistence & Checkpoints Implementation
378
+ /**
379
+ * Save current engine state to a serialized format
380
+ * @param options - Serialization options
381
+ * @returns Promise resolving to serialized state
382
+ */
383
+ async saveState(options = {}) {
384
+ const startTime = performance.now();
385
+ if (!this.state.currentNodeId) {
386
+ throw new Error('Invalid state: currentNodeId is missing.');
387
+ }
388
+ try {
389
+ // Create serialization metadata
390
+ const metadata = {
391
+ engineVersion: types_1.PERSISTENCE_VERSION,
392
+ timestamp: new Date().toISOString(),
393
+ storyId: this.generateStoryHash(),
394
+ customMetadata: options.customMetadata || {},
395
+ compression: options.compression || 'none'
396
+ };
397
+ // Build serialized state
398
+ const serializedState = {
399
+ state: this.deepCopy(this.state),
400
+ flowEvents: options.includeFlowEvents !== false ?
401
+ this.deepCopy(this.activeFlowEvents) : [],
402
+ metadata
403
+ };
404
+ // Add optional data based on options
405
+ if (options.includePerformanceData && this.performanceMode) {
406
+ serializedState.performanceState = {
407
+ performanceMode: this.performanceMode,
408
+ enableProfiling: this.enableProfiling,
409
+ backgroundTasks: [], // Would be populated with actual background task IDs
410
+ telemetryData: [] // Would be populated with telemetry history
411
+ };
412
+ serializedState.poolStats = this.getPoolStats() || {};
413
+ }
414
+ if (options.includeBranchingContext && this.branchingEngine) {
415
+ serializedState.branchingContext = {
416
+ activeBranches: [], // Would be populated from branching engine
417
+ branchStates: {},
418
+ convergencePoints: []
419
+ };
420
+ }
421
+ if (options.includeValidationState) {
422
+ serializedState.validationState = {
423
+ disabledRules: [],
424
+ customRules: {},
425
+ validationErrors: []
426
+ };
427
+ }
428
+ // Generate checksum if requested
429
+ if (options.generateChecksum) {
430
+ const stateToHash = { ...serializedState };
431
+ delete stateToHash.metadata.checksum; // Exclude checksum from hash
432
+ const stateString = JSON.stringify(stateToHash);
433
+ metadata.checksum = await this.generateChecksum(stateString);
434
+ serializedState.metadata = metadata;
435
+ }
436
+ // Performance tracking
437
+ if (this.enableProfiling) {
438
+ const duration = performance.now() - startTime;
439
+ PerfReporter_1.perf.record('custom', {
440
+ eventType: 'state-serialized',
441
+ duration,
442
+ stateSize: JSON.stringify(serializedState).length,
443
+ includePerformance: !!options.includePerformanceData,
444
+ includeFlowEvents: options.includeFlowEvents !== false
445
+ });
446
+ }
447
+ return serializedState;
448
+ }
449
+ catch (error) {
450
+ const errorMessage = error instanceof Error ? error.message : 'Unknown serialization error';
451
+ if (this.enableProfiling) {
452
+ PerfReporter_1.perf.record('custom', {
453
+ eventType: 'state-serialization-failed',
454
+ error: errorMessage,
455
+ duration: performance.now() - startTime
456
+ });
457
+ }
458
+ throw new Error(`Failed to save state: ${errorMessage}`);
459
+ }
460
+ }
461
+ /**
462
+ * Load engine state from serialized format
463
+ * @param serializedState - The serialized state to load
464
+ * @param options - Load options
465
+ * @returns Promise resolving to persistence result
466
+ */
467
+ async loadState(serializedState, options = {}) {
468
+ const startTime = performance.now();
469
+ try {
470
+ // Validate serialized state structure
471
+ const validationResult = this.validateSerializedState(serializedState);
472
+ if (!validationResult.isValid) {
473
+ return {
474
+ success: false,
475
+ error: `Invalid serialized state: ${validationResult.errors.join(', ')}`,
476
+ warnings: validationResult.warnings
477
+ };
478
+ }
479
+ // Check compatibility
480
+ if (!options.skipCompatibilityCheck) {
481
+ const compatibilityCheck = this.checkCompatibility(serializedState.metadata);
482
+ if (!compatibilityCheck.compatible) {
483
+ return {
484
+ success: false,
485
+ error: `Incompatible state version: ${compatibilityCheck.reason}`,
486
+ warnings: compatibilityCheck.suggestions
487
+ };
488
+ }
489
+ }
490
+ // Verify checksum if available and requested
491
+ if (options.verifyChecksum && serializedState.metadata.checksum) {
492
+ const isValid = await this.verifyChecksum(serializedState);
493
+ if (!isValid) {
494
+ return {
495
+ success: false,
496
+ error: 'Checksum verification failed - state may be corrupted'
497
+ };
498
+ }
499
+ }
500
+ // Story hash validation
501
+ if (this.generateStoryHash() !== serializedState.metadata.storyId) {
502
+ return {
503
+ success: false,
504
+ error: 'Story hash mismatch. The state belongs to a different narrative.'
505
+ };
506
+ }
507
+ // Apply migration if needed
508
+ let stateToLoad = serializedState;
509
+ if (options.migrationFunction) {
510
+ stateToLoad = options.migrationFunction(serializedState);
511
+ }
512
+ // Load core state
513
+ this.state = this.deepCopy(stateToLoad.state);
514
+ // Restore optional data based on options
515
+ if (options.restoreFlowEvents && stateToLoad.flowEvents) {
516
+ this.activeFlowEvents = this.deepCopy(stateToLoad.flowEvents);
517
+ }
518
+ if (options.restorePerformanceState && stateToLoad.performanceState) {
519
+ this.performanceMode = stateToLoad.performanceState.performanceMode;
520
+ this.enableProfiling = stateToLoad.performanceState.enableProfiling;
521
+ // Background tasks would be restored here
522
+ }
523
+ if (options.restoreBranchingContext && stateToLoad.branchingContext && this.branchingEngine) {
524
+ // Restore branching context - would be implemented with actual branching engine
525
+ }
526
+ // Performance tracking
527
+ const duration = performance.now() - startTime;
528
+ if (this.enableProfiling) {
529
+ PerfReporter_1.perf.record('custom', {
530
+ eventType: 'state-loaded',
531
+ duration,
532
+ stateSize: JSON.stringify(stateToLoad).length,
533
+ restoredFlowEvents: !!options.restoreFlowEvents,
534
+ restoredPerformance: !!options.restorePerformanceState
535
+ });
536
+ }
537
+ return {
538
+ success: true,
539
+ data: {
540
+ size: JSON.stringify(stateToLoad).length,
541
+ duration,
542
+ checksum: stateToLoad.metadata.checksum
543
+ }
544
+ };
545
+ }
546
+ catch (error) {
547
+ const errorMessage = error instanceof Error ? error.message : 'Unknown loading error';
548
+ const duration = performance.now() - startTime;
549
+ if (this.enableProfiling) {
550
+ PerfReporter_1.perf.record('custom', {
551
+ eventType: 'state-loading-failed',
552
+ error: errorMessage,
553
+ duration
554
+ });
555
+ }
556
+ return {
557
+ success: false,
558
+ error: `Failed to load state: ${errorMessage}`,
559
+ data: { duration }
560
+ };
561
+ }
562
+ }
563
+ /**
564
+ * Create a lightweight checkpoint of current state
565
+ * @param name - Optional checkpoint name
566
+ * @param options - Checkpoint options
567
+ * @returns Promise resolving to created checkpoint
568
+ */
569
+ async createCheckpoint(name, options = {}) {
570
+ const checkpointId = this.generateCheckpointId();
571
+ const timestamp = new Date().toISOString();
572
+ const checkpoint = {
573
+ id: checkpointId,
574
+ name: name || `Checkpoint ${checkpointId.slice(-8)}`,
575
+ state: this.deepCopy(this.state),
576
+ timestamp,
577
+ description: options.includeMetadata ? `Auto-checkpoint at node ${this.state.currentNodeId}` : undefined,
578
+ tags: options.autoTags || [],
579
+ metadata: options.includeMetadata ? {
580
+ nodeTitle: this.getCurrentNode().text.slice(0, 50),
581
+ choiceCount: this.getCurrentNode().choices.length,
582
+ flagCount: Object.keys(this.state.flags).length,
583
+ historyLength: this.state.history.length
584
+ } : undefined
585
+ };
586
+ // Store checkpoint
587
+ this.checkpoints.set(checkpointId, checkpoint);
588
+ // Cleanup old checkpoints if needed
589
+ const maxCheckpoints = options.maxCheckpoints || this.maxCheckpoints;
590
+ if (this.checkpoints.size > maxCheckpoints) {
591
+ await this.cleanupCheckpoints(options.cleanupStrategy || 'lru', maxCheckpoints);
592
+ }
593
+ // Performance tracking
594
+ if (this.enableProfiling) {
595
+ PerfReporter_1.perf.record('custom', {
596
+ eventType: 'checkpoint-created',
597
+ checkpointId,
598
+ checkpointCount: this.checkpoints.size,
599
+ stateSize: JSON.stringify(checkpoint.state).length
600
+ });
601
+ }
602
+ return checkpoint;
603
+ }
604
+ /**
605
+ * Restore engine state from a checkpoint
606
+ * @param checkpointId - ID of checkpoint to restore
607
+ * @returns Promise resolving to persistence result
608
+ */
609
+ async restoreFromCheckpoint(checkpointId) {
610
+ const startTime = performance.now();
611
+ try {
612
+ const checkpoint = this.checkpoints.get(checkpointId);
613
+ if (!checkpoint) {
614
+ return {
615
+ success: false,
616
+ error: `Checkpoint not found: ${checkpointId}`
617
+ };
618
+ }
619
+ // Restore state
620
+ this.state = this.deepCopy(checkpoint.state);
621
+ const duration = performance.now() - startTime;
622
+ // Performance tracking
623
+ if (this.enableProfiling) {
624
+ PerfReporter_1.perf.record('custom', {
625
+ eventType: 'checkpoint-restored',
626
+ checkpointId,
627
+ duration,
628
+ stateSize: JSON.stringify(checkpoint.state).length
629
+ });
630
+ }
631
+ return {
632
+ success: true,
633
+ data: {
634
+ size: JSON.stringify(checkpoint.state).length,
635
+ duration,
636
+ checksum: checkpointId
637
+ }
638
+ };
639
+ }
640
+ catch (error) {
641
+ const errorMessage = error instanceof Error ? error.message : 'Unknown restore error';
642
+ const duration = performance.now() - startTime;
643
+ if (this.enableProfiling) {
644
+ PerfReporter_1.perf.record('custom', {
645
+ eventType: 'checkpoint-restore-failed',
646
+ checkpointId,
647
+ error: errorMessage,
648
+ duration
649
+ });
650
+ }
651
+ return {
652
+ success: false,
653
+ error: `Failed to restore checkpoint: ${errorMessage}`,
654
+ data: { duration }
655
+ };
656
+ }
149
657
  }
150
658
  // Utility method for checking flag conditions
151
659
  checkFlag(flagName, expectedValue) {
@@ -154,13 +662,33 @@ class QNCEEngine {
154
662
  }
155
663
  return this.state.flags[flagName] === expectedValue;
156
664
  }
157
- // Get available choices (with potential flag-based filtering)
158
- getAvailableChoices() {
665
+ // Sprint 3.2: Choice Validation Methods
666
+ /**
667
+ * Get the choice validator instance
668
+ * @returns The current choice validator
669
+ */
670
+ getChoiceValidator() {
671
+ return this.choiceValidator;
672
+ }
673
+ /**
674
+ * Validate a specific choice without executing it
675
+ * @param choice - The choice to validate
676
+ * @returns Validation result with details
677
+ */
678
+ validateChoice(choice) {
159
679
  const currentNode = this.getCurrentNode();
160
- return currentNode.choices.filter(() => {
161
- // Future: Add flag-based choice filtering logic here
162
- return true;
163
- });
680
+ const availableChoices = this.getAvailableChoices();
681
+ const context = (0, validation_1.createValidationContext)(currentNode, this.state, availableChoices);
682
+ return this.choiceValidator.validate(choice, context);
683
+ }
684
+ /**
685
+ * Check if a specific choice is valid without executing it
686
+ * @param choice - The choice to check
687
+ * @returns True if the choice is valid, false otherwise
688
+ */
689
+ isChoiceValid(choice) {
690
+ const result = this.validateChoice(choice);
691
+ return result.isValid;
164
692
  }
165
693
  // Performance and object pooling methods
166
694
  /**
@@ -344,6 +872,617 @@ class QNCEEngine {
344
872
  }
345
873
  });
346
874
  }
875
+ // Sprint 3.3: State persistence utility methods
876
+ /**
877
+ * Deep copy utility for state objects
878
+ * @param obj - Object to deep copy
879
+ * @returns Deep copied object
880
+ */
881
+ deepCopy(obj) {
882
+ if (obj === null || typeof obj !== 'object') {
883
+ return obj;
884
+ }
885
+ if (obj instanceof Date) {
886
+ return new Date(obj.getTime());
887
+ }
888
+ if (obj instanceof Array) {
889
+ return obj.map(item => this.deepCopy(item));
890
+ }
891
+ if (typeof obj === 'object') {
892
+ const copy = {};
893
+ for (const key in obj) {
894
+ if (obj.hasOwnProperty(key)) {
895
+ copy[key] = this.deepCopy(obj[key]);
896
+ }
897
+ }
898
+ return copy;
899
+ }
900
+ return obj;
901
+ }
902
+ /**
903
+ * Generate a hash for the current story data
904
+ * @returns Story hash string
905
+ */
906
+ generateStoryHash() {
907
+ const storyString = JSON.stringify({
908
+ initialNodeId: this.storyData.initialNodeId,
909
+ nodeCount: this.storyData.nodes.length,
910
+ nodeIds: this.storyData.nodes.map(n => n.id).sort()
911
+ });
912
+ // Simple hash function (in production, use crypto.subtle.digest)
913
+ let hash = 0;
914
+ for (let i = 0; i < storyString.length; i++) {
915
+ const char = storyString.charCodeAt(i);
916
+ hash = ((hash << 5) - hash) + char;
917
+ hash = hash & hash; // Convert to 32-bit integer
918
+ }
919
+ return hash.toString(16);
920
+ }
921
+ /**
922
+ * Generate a unique checkpoint ID
923
+ * @returns Unique checkpoint ID
924
+ */
925
+ generateCheckpointId() {
926
+ const timestamp = Date.now();
927
+ const random = Math.random().toString(36).substr(2, 9);
928
+ return `checkpoint_${timestamp}_${random}`;
929
+ }
930
+ /**
931
+ * Generate checksum for data integrity
932
+ * @param data - Data to generate checksum for
933
+ * @returns Promise resolving to checksum string
934
+ */
935
+ async generateChecksum(data) {
936
+ // Simple checksum implementation (in production, use crypto.subtle.digest)
937
+ let checksum = 0;
938
+ for (let i = 0; i < data.length; i++) {
939
+ checksum = ((checksum << 5) - checksum) + data.charCodeAt(i);
940
+ checksum = checksum & checksum;
941
+ }
942
+ return checksum.toString(16);
943
+ }
944
+ /**
945
+ * Verify checksum integrity
946
+ * @param serializedState - State with checksum to verify
947
+ * @returns Promise resolving to verification result
948
+ */
949
+ async verifyChecksum(serializedState) {
950
+ const receivedChecksum = serializedState.metadata.checksum;
951
+ if (!receivedChecksum)
952
+ return false;
953
+ const stateToHash = { ...serializedState };
954
+ delete stateToHash.metadata.checksum;
955
+ const stateString = JSON.stringify(stateToHash);
956
+ const expectedChecksum = await this.generateChecksum(stateString);
957
+ return receivedChecksum === expectedChecksum;
958
+ }
959
+ /**
960
+ * Validate the structure of the serialized state object
961
+ * @param serializedState - State to validate
962
+ * @returns Validation result
963
+ */
964
+ validateSerializedState(serializedState) {
965
+ const errors = [];
966
+ const warnings = [];
967
+ // Check required fields
968
+ if (!serializedState.state) {
969
+ errors.push('Missing state field');
970
+ }
971
+ else {
972
+ if (!serializedState.state.currentNodeId) {
973
+ errors.push('Missing currentNodeId in state');
974
+ }
975
+ if (!serializedState.state.flags) {
976
+ warnings.push('Missing flags in state');
977
+ }
978
+ if (!serializedState.state.history) {
979
+ warnings.push('Missing history in state');
980
+ }
981
+ }
982
+ if (!serializedState.metadata) {
983
+ errors.push('Missing metadata field');
984
+ }
985
+ else {
986
+ if (!serializedState.metadata.engineVersion) {
987
+ warnings.push('Missing engine version in metadata');
988
+ }
989
+ if (!serializedState.metadata.timestamp) {
990
+ warnings.push('Missing timestamp in metadata');
991
+ }
992
+ }
993
+ return {
994
+ isValid: errors.length === 0,
995
+ errors,
996
+ warnings
997
+ };
998
+ }
999
+ /**
1000
+ * Check version compatibility
1001
+ * @param metadata - Serialization metadata
1002
+ * @returns Compatibility check result
1003
+ */
1004
+ checkCompatibility(metadata) {
1005
+ const currentVersion = types_1.PERSISTENCE_VERSION;
1006
+ const stateVersion = metadata.engineVersion;
1007
+ if (!stateVersion) {
1008
+ return {
1009
+ compatible: false,
1010
+ reason: 'Unknown state version',
1011
+ suggestions: ['State may be from an older engine version']
1012
+ };
1013
+ }
1014
+ if (stateVersion === currentVersion) {
1015
+ return { compatible: true };
1016
+ }
1017
+ // Simple version comparison (in production, use semver)
1018
+ const [currentMajor, currentMinor] = currentVersion.split('.').map(Number);
1019
+ const [stateMajor, stateMinor] = stateVersion.split('.').map(Number);
1020
+ if (stateMajor > currentMajor ||
1021
+ (stateMajor === currentMajor && stateMinor > currentMinor)) {
1022
+ return {
1023
+ compatible: false,
1024
+ reason: `State from newer engine version (${stateVersion} > ${currentVersion})`,
1025
+ suggestions: ['Update the engine to a newer version']
1026
+ };
1027
+ }
1028
+ if (stateMajor < currentMajor) {
1029
+ return {
1030
+ compatible: false,
1031
+ reason: `State from incompatible major version (${stateVersion} vs ${currentVersion})`,
1032
+ suggestions: ['Use migration function to upgrade state format']
1033
+ };
1034
+ }
1035
+ // Minor version differences are usually compatible
1036
+ return {
1037
+ compatible: true,
1038
+ suggestions: [`State from older minor version (${stateVersion})`]
1039
+ };
1040
+ }
1041
+ /**
1042
+ * Cleanup old checkpoints based on strategy
1043
+ * @param strategy - Cleanup strategy
1044
+ * @param maxCheckpoints - Maximum number of checkpoints to keep
1045
+ * @returns Promise resolving to number of cleaned checkpoints
1046
+ */
1047
+ async cleanupCheckpoints(strategy, maxCheckpoints) {
1048
+ const checkpoints = Array.from(this.checkpoints.entries());
1049
+ if (checkpoints.length <= maxCheckpoints) {
1050
+ return 0;
1051
+ }
1052
+ const toRemove = checkpoints.length - maxCheckpoints;
1053
+ let checkpointsToDelete = [];
1054
+ switch (strategy) {
1055
+ case 'fifo':
1056
+ // Remove oldest by creation order (assuming IDs contain timestamp)
1057
+ checkpointsToDelete = checkpoints
1058
+ .sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp))
1059
+ .slice(0, toRemove)
1060
+ .map(([id]) => id);
1061
+ break;
1062
+ case 'timestamp':
1063
+ // Remove oldest by timestamp
1064
+ checkpointsToDelete = checkpoints
1065
+ .sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp))
1066
+ .slice(0, toRemove)
1067
+ .map(([id]) => id);
1068
+ break;
1069
+ case 'lru':
1070
+ default:
1071
+ // For now, same as FIFO (would need access tracking in production)
1072
+ checkpointsToDelete = checkpoints
1073
+ .sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp))
1074
+ .slice(0, toRemove)
1075
+ .map(([id]) => id);
1076
+ break;
1077
+ }
1078
+ // Remove checkpoints
1079
+ for (const id of checkpointsToDelete) {
1080
+ this.checkpoints.delete(id);
1081
+ }
1082
+ if (this.enableProfiling) {
1083
+ PerfReporter_1.perf.record('custom', {
1084
+ eventType: 'checkpoints-cleaned',
1085
+ strategy,
1086
+ removedCount: checkpointsToDelete.length,
1087
+ remainingCount: this.checkpoints.size
1088
+ });
1089
+ }
1090
+ return checkpointsToDelete.length;
1091
+ }
1092
+ /**
1093
+ * Get all checkpoints
1094
+ * @returns Array of all checkpoints
1095
+ */
1096
+ getCheckpoints() {
1097
+ return Array.from(this.checkpoints.values());
1098
+ }
1099
+ /**
1100
+ * Get specific checkpoint by ID
1101
+ * @param id - Checkpoint ID
1102
+ * @returns Checkpoint or undefined
1103
+ */
1104
+ getCheckpoint(id) {
1105
+ return this.checkpoints.get(id);
1106
+ }
1107
+ /**
1108
+ * Delete a checkpoint
1109
+ * @param id - Checkpoint ID to delete
1110
+ * @returns Whether checkpoint was deleted
1111
+ */
1112
+ deleteCheckpoint(id) {
1113
+ return this.checkpoints.delete(id);
1114
+ }
1115
+ /**
1116
+ * Enable/disable automatic checkpointing
1117
+ * @param enabled - Whether to enable auto-checkpointing
1118
+ */
1119
+ setAutoCheckpoint(enabled) {
1120
+ this.autoCheckpointEnabled = enabled;
1121
+ }
1122
+ // Sprint 3.5: Autosave and Undo/Redo Implementation
1123
+ /**
1124
+ * Configure autosave settings
1125
+ * @param config - Autosave configuration
1126
+ */
1127
+ configureAutosave(config) {
1128
+ this.autosaveConfig = { ...this.autosaveConfig, ...config };
1129
+ }
1130
+ /**
1131
+ * Configure undo/redo settings
1132
+ * @param config - Undo/redo configuration
1133
+ */
1134
+ configureUndoRedo(config) {
1135
+ this.undoRedoConfig = { ...this.undoRedoConfig, ...config };
1136
+ }
1137
+ /**
1138
+ * Push a state to the undo stack
1139
+ * @param state - State to save
1140
+ * @param action - Action that caused this state change
1141
+ * @param metadata - Optional metadata about the change
1142
+ */
1143
+ pushToUndoStack(state, action, metadata) {
1144
+ const startTime = performance.now();
1145
+ const entry = {
1146
+ id: this.generateHistoryId(),
1147
+ state: this.deepCopy(state),
1148
+ timestamp: new Date().toISOString(),
1149
+ action,
1150
+ metadata
1151
+ };
1152
+ this.undoStack.push(entry);
1153
+ // Clear redo stack when new change is made
1154
+ this.redoStack = [];
1155
+ // Enforce max undo entries
1156
+ if (this.undoStack.length > this.undoRedoConfig.maxUndoEntries) {
1157
+ this.undoStack.shift(); // Remove oldest entry
1158
+ }
1159
+ // Performance tracking
1160
+ if (this.enableProfiling) {
1161
+ const duration = performance.now() - startTime;
1162
+ PerfReporter_1.perf.record('custom', {
1163
+ eventType: 'undo-stack-push',
1164
+ duration,
1165
+ undoCount: this.undoStack.length,
1166
+ action
1167
+ });
1168
+ }
1169
+ }
1170
+ /**
1171
+ * Undo the last operation
1172
+ * @returns Result of undo operation
1173
+ */
1174
+ undo() {
1175
+ const startTime = performance.now();
1176
+ if (this.undoStack.length === 0) {
1177
+ return {
1178
+ success: false,
1179
+ error: 'No operations to undo',
1180
+ stackSizes: {
1181
+ undoCount: this.undoStack.length,
1182
+ redoCount: this.redoStack.length
1183
+ }
1184
+ };
1185
+ }
1186
+ try {
1187
+ // Save current state to redo stack
1188
+ const currentEntry = {
1189
+ id: this.generateHistoryId(),
1190
+ state: this.deepCopy(this.state),
1191
+ timestamp: new Date().toISOString(),
1192
+ action: 'redo-point'
1193
+ };
1194
+ this.redoStack.push(currentEntry);
1195
+ // Enforce max redo entries
1196
+ if (this.redoStack.length > this.undoRedoConfig.maxRedoEntries) {
1197
+ this.redoStack.shift(); // Remove oldest entry
1198
+ }
1199
+ // Restore previous state
1200
+ const entryToRestore = this.undoStack.pop();
1201
+ // Set flag to prevent triggering undo/redo tracking during restore
1202
+ this.isUndoRedoOperation = true;
1203
+ this.state = this.deepCopy(entryToRestore.state);
1204
+ this.isUndoRedoOperation = false;
1205
+ const duration = performance.now() - startTime;
1206
+ // Performance tracking
1207
+ if (this.enableProfiling) {
1208
+ PerfReporter_1.perf.record('custom', {
1209
+ eventType: 'undo-operation',
1210
+ duration,
1211
+ undoCount: this.undoStack.length,
1212
+ redoCount: this.redoStack.length
1213
+ });
1214
+ }
1215
+ return {
1216
+ success: true,
1217
+ restoredState: this.deepCopy(this.state),
1218
+ entry: {
1219
+ id: entryToRestore.id,
1220
+ timestamp: entryToRestore.timestamp,
1221
+ action: entryToRestore.action,
1222
+ nodeId: entryToRestore.metadata?.nodeId
1223
+ },
1224
+ stackSizes: {
1225
+ undoCount: this.undoStack.length,
1226
+ redoCount: this.redoStack.length
1227
+ }
1228
+ };
1229
+ }
1230
+ catch (error) {
1231
+ this.isUndoRedoOperation = false; // Ensure flag is reset on error
1232
+ const errorMessage = error instanceof Error ? error.message : 'Unknown undo error';
1233
+ return {
1234
+ success: false,
1235
+ error: `Undo failed: ${errorMessage}`,
1236
+ stackSizes: {
1237
+ undoCount: this.undoStack.length,
1238
+ redoCount: this.redoStack.length
1239
+ }
1240
+ };
1241
+ }
1242
+ }
1243
+ /**
1244
+ * Redo the last undone operation
1245
+ * @returns Result of redo operation
1246
+ */
1247
+ redo() {
1248
+ const startTime = performance.now();
1249
+ if (this.redoStack.length === 0) {
1250
+ return {
1251
+ success: false,
1252
+ error: 'No operations to redo',
1253
+ stackSizes: {
1254
+ undoCount: this.undoStack.length,
1255
+ redoCount: this.redoStack.length
1256
+ }
1257
+ };
1258
+ }
1259
+ try {
1260
+ // Save current state to undo stack
1261
+ const currentEntry = {
1262
+ id: this.generateHistoryId(),
1263
+ state: this.deepCopy(this.state),
1264
+ timestamp: new Date().toISOString(),
1265
+ action: 'undo-point'
1266
+ };
1267
+ this.undoStack.push(currentEntry);
1268
+ // Enforce max undo entries
1269
+ if (this.undoStack.length > this.undoRedoConfig.maxUndoEntries) {
1270
+ this.undoStack.shift(); // Remove oldest entry
1271
+ }
1272
+ // Restore redo state
1273
+ const entryToRestore = this.redoStack.pop();
1274
+ // Set flag to prevent triggering undo/redo tracking during restore
1275
+ this.isUndoRedoOperation = true;
1276
+ this.state = this.deepCopy(entryToRestore.state);
1277
+ this.isUndoRedoOperation = false;
1278
+ const duration = performance.now() - startTime;
1279
+ // Performance tracking
1280
+ if (this.enableProfiling) {
1281
+ PerfReporter_1.perf.record('custom', {
1282
+ eventType: 'redo-operation',
1283
+ duration,
1284
+ undoCount: this.undoStack.length,
1285
+ redoCount: this.redoStack.length
1286
+ });
1287
+ }
1288
+ return {
1289
+ success: true,
1290
+ restoredState: this.deepCopy(this.state),
1291
+ entry: {
1292
+ id: entryToRestore.id,
1293
+ timestamp: entryToRestore.timestamp,
1294
+ action: entryToRestore.action,
1295
+ nodeId: entryToRestore.metadata?.nodeId
1296
+ },
1297
+ stackSizes: {
1298
+ undoCount: this.undoStack.length,
1299
+ redoCount: this.redoStack.length
1300
+ }
1301
+ };
1302
+ }
1303
+ catch (error) {
1304
+ this.isUndoRedoOperation = false; // Ensure flag is reset on error
1305
+ const errorMessage = error instanceof Error ? error.message : 'Unknown redo error';
1306
+ return {
1307
+ success: false,
1308
+ error: `Redo failed: ${errorMessage}`,
1309
+ stackSizes: {
1310
+ undoCount: this.undoStack.length,
1311
+ redoCount: this.redoStack.length
1312
+ }
1313
+ };
1314
+ }
1315
+ }
1316
+ /**
1317
+ * Check if undo is available
1318
+ * @returns True if undo is possible
1319
+ */
1320
+ canUndo() {
1321
+ return this.undoRedoConfig.enabled && this.undoStack.length > 0;
1322
+ }
1323
+ /**
1324
+ * Check if redo is available
1325
+ * @returns True if redo is possible
1326
+ */
1327
+ canRedo() {
1328
+ return this.undoRedoConfig.enabled && this.redoStack.length > 0;
1329
+ }
1330
+ /**
1331
+ * Get the number of available undo operations
1332
+ * @returns Number of undo entries
1333
+ */
1334
+ getUndoCount() {
1335
+ return this.undoStack.length;
1336
+ }
1337
+ /**
1338
+ * Get the number of available redo operations
1339
+ * @returns Number of redo entries
1340
+ */
1341
+ getRedoCount() {
1342
+ return this.redoStack.length;
1343
+ }
1344
+ /**
1345
+ * Clear all undo/redo history
1346
+ */
1347
+ clearHistory() {
1348
+ this.undoStack = [];
1349
+ this.redoStack = [];
1350
+ if (this.enableProfiling) {
1351
+ PerfReporter_1.perf.record('custom', {
1352
+ eventType: 'history-cleared'
1353
+ });
1354
+ }
1355
+ }
1356
+ /**
1357
+ * Get history summary for debugging
1358
+ * @returns Summary of undo/redo history
1359
+ */
1360
+ getHistorySummary() {
1361
+ return {
1362
+ undoEntries: this.undoStack.map(entry => ({
1363
+ id: entry.id,
1364
+ timestamp: entry.timestamp,
1365
+ action: entry.action
1366
+ })),
1367
+ redoEntries: this.redoStack.map(entry => ({
1368
+ id: entry.id,
1369
+ timestamp: entry.timestamp,
1370
+ action: entry.action
1371
+ }))
1372
+ };
1373
+ }
1374
+ /**
1375
+ * Trigger an autosave operation
1376
+ * @param trigger - What triggered the autosave
1377
+ * @param metadata - Optional metadata about the trigger
1378
+ * @returns Promise resolving to autosave result
1379
+ */
1380
+ async triggerAutosave(trigger, metadata) {
1381
+ const startTime = performance.now();
1382
+ // Check if autosave is enabled
1383
+ if (!this.autosaveConfig.enabled) {
1384
+ return { success: false, error: 'Autosave is disabled' };
1385
+ }
1386
+ // Check throttling
1387
+ const now = performance.now();
1388
+ if (now - this.lastAutosaveTime < this.autosaveConfig.throttleMs) {
1389
+ return {
1390
+ success: false,
1391
+ error: 'Autosave throttled',
1392
+ trigger
1393
+ };
1394
+ }
1395
+ try {
1396
+ // Create autosave checkpoint
1397
+ const checkpointName = `autosave-${trigger}-${Date.now()}`;
1398
+ const checkpoint = await this.createCheckpoint(checkpointName, {
1399
+ includeMetadata: this.autosaveConfig.includeMetadata,
1400
+ autoTags: ['autosave', trigger],
1401
+ maxCheckpoints: this.autosaveConfig.maxEntries
1402
+ });
1403
+ this.lastAutosaveTime = now;
1404
+ const duration = performance.now() - startTime;
1405
+ // Performance tracking
1406
+ if (this.enableProfiling) {
1407
+ PerfReporter_1.perf.record('custom', {
1408
+ eventType: 'autosave-completed',
1409
+ duration,
1410
+ trigger,
1411
+ checkpointId: checkpoint.id
1412
+ });
1413
+ }
1414
+ return {
1415
+ success: true,
1416
+ checkpointId: checkpoint.id,
1417
+ trigger,
1418
+ duration,
1419
+ size: JSON.stringify(checkpoint.state).length
1420
+ };
1421
+ }
1422
+ catch (error) {
1423
+ const errorMessage = error instanceof Error ? error.message : 'Unknown autosave error';
1424
+ const duration = performance.now() - startTime;
1425
+ if (this.enableProfiling) {
1426
+ PerfReporter_1.perf.record('custom', {
1427
+ eventType: 'autosave-failed',
1428
+ duration,
1429
+ trigger,
1430
+ error: errorMessage
1431
+ });
1432
+ }
1433
+ return {
1434
+ success: false,
1435
+ error: `Autosave failed: ${errorMessage}`,
1436
+ trigger,
1437
+ duration
1438
+ };
1439
+ }
1440
+ }
1441
+ /**
1442
+ * Manually trigger an autosave
1443
+ * @param metadata - Optional metadata
1444
+ * @returns Promise resolving to autosave result
1445
+ */
1446
+ async manualAutosave(metadata) {
1447
+ return this.triggerAutosave('manual', metadata);
1448
+ }
1449
+ /**
1450
+ * Generate a unique history entry ID
1451
+ * @returns Unique ID string
1452
+ */
1453
+ generateHistoryId() {
1454
+ return `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1455
+ }
1456
+ // Sprint 3.4: Conditional Choice Display API
1457
+ /**
1458
+ * Set a custom condition evaluator function for complex choice logic
1459
+ * @param evaluator - Custom evaluator function
1460
+ */
1461
+ setConditionEvaluator(evaluator) {
1462
+ condition_1.conditionEvaluator.setCustomEvaluator(evaluator);
1463
+ }
1464
+ /**
1465
+ * Clear the custom condition evaluator
1466
+ */
1467
+ clearConditionEvaluator() {
1468
+ condition_1.conditionEvaluator.clearCustomEvaluator();
1469
+ }
1470
+ /**
1471
+ * Validate a condition expression without evaluating it
1472
+ * @param expression - Condition expression to validate
1473
+ * @returns Validation result
1474
+ */
1475
+ validateCondition(expression) {
1476
+ return condition_1.conditionEvaluator.validateExpression(expression);
1477
+ }
1478
+ /**
1479
+ * Get flags referenced in a condition expression
1480
+ * @param expression - Condition expression to analyze
1481
+ * @returns Array of flag names referenced
1482
+ */
1483
+ getConditionFlags(expression) {
1484
+ return condition_1.conditionEvaluator.getReferencedFlags(expression);
1485
+ }
347
1486
  }
348
1487
  exports.QNCEEngine = QNCEEngine;
349
1488
  /**