qnce-engine 1.3.1 → 1.4.1

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 (168) hide show
  1. package/README.md +136 -2
  2. package/dist/adapters/contracts.d.ts +1 -1
  3. package/dist/adapters/contracts.d.ts.map +1 -1
  4. package/dist/adapters/storage/MockAdapters.d.ts +89 -0
  5. package/dist/adapters/storage/MockAdapters.d.ts.map +1 -0
  6. package/dist/adapters/storage/MockAdapters.js +109 -0
  7. package/dist/adapters/storage/MockAdapters.js.map +1 -0
  8. package/dist/adapters/story/CustomJSONAdapter.d.ts +1 -1
  9. package/dist/adapters/story/CustomJSONAdapter.d.ts.map +1 -1
  10. package/dist/adapters/story/CustomJSONAdapter.js +20 -14
  11. package/dist/adapters/story/CustomJSONAdapter.js.map +1 -1
  12. package/dist/adapters/story/InkAdapter.d.ts +1 -1
  13. package/dist/adapters/story/InkAdapter.d.ts.map +1 -1
  14. package/dist/adapters/story/InkAdapter.js +7 -10
  15. package/dist/adapters/story/InkAdapter.js.map +1 -1
  16. package/dist/adapters/story/TwisonAdapter.d.ts +1 -1
  17. package/dist/adapters/story/TwisonAdapter.d.ts.map +1 -1
  18. package/dist/adapters/story/TwisonAdapter.js +2 -2
  19. package/dist/adapters/story/TwisonAdapter.js.map +1 -1
  20. package/dist/cli/audit.js +1 -0
  21. package/dist/cli/audit.js.map +1 -1
  22. package/dist/cli/import.d.ts.map +1 -1
  23. package/dist/cli/import.js +56 -20
  24. package/dist/cli/import.js.map +1 -1
  25. package/dist/cli/init.js +1 -0
  26. package/dist/cli/init.js.map +1 -1
  27. package/dist/cli/perf.d.ts.map +1 -1
  28. package/dist/cli/perf.js +30 -0
  29. package/dist/cli/perf.js.map +1 -1
  30. package/dist/cli/play.d.ts.map +1 -1
  31. package/dist/cli/play.js +135 -60
  32. package/dist/cli/play.js.map +1 -1
  33. package/dist/engine/condition.d.ts +25 -3
  34. package/dist/engine/condition.d.ts.map +1 -1
  35. package/dist/engine/condition.js +358 -64
  36. package/dist/engine/condition.js.map +1 -1
  37. package/dist/engine/core.d.ts +109 -3
  38. package/dist/engine/core.d.ts.map +1 -1
  39. package/dist/engine/core.js +486 -36
  40. package/dist/engine/core.js.map +1 -1
  41. package/dist/engine/demo-story.d.ts +1 -0
  42. package/dist/engine/demo-story.d.ts.map +1 -1
  43. package/dist/engine/demo-story.js +1 -0
  44. package/dist/engine/demo-story.js.map +1 -1
  45. package/dist/engine/error-factory.d.ts +86 -0
  46. package/dist/engine/error-factory.d.ts.map +1 -0
  47. package/dist/engine/error-factory.js +87 -0
  48. package/dist/engine/error-factory.js.map +1 -0
  49. package/dist/engine/errors.d.ts +3 -0
  50. package/dist/engine/errors.d.ts.map +1 -1
  51. package/dist/engine/errors.js +3 -0
  52. package/dist/engine/errors.js.map +1 -1
  53. package/dist/engine/validation.js +1 -1
  54. package/dist/engine/validation.js.map +1 -1
  55. package/dist/index.d.ts +22 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +34 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/integrations/react.d.ts +20 -3
  60. package/dist/integrations/react.d.ts.map +1 -1
  61. package/dist/integrations/react.js +15 -2
  62. package/dist/integrations/react.js.map +1 -1
  63. package/dist/narrative/branching/models.d.ts +1 -0
  64. package/dist/narrative/branching/models.d.ts.map +1 -1
  65. package/dist/narrative/branching/models.js.map +1 -1
  66. package/dist/performance/AdaptiveControllers.d.ts +51 -0
  67. package/dist/performance/AdaptiveControllers.d.ts.map +1 -0
  68. package/dist/performance/AdaptiveControllers.js +87 -0
  69. package/dist/performance/AdaptiveControllers.js.map +1 -0
  70. package/dist/performance/ContextPool.d.ts +1 -0
  71. package/dist/performance/ContextPool.d.ts.map +1 -0
  72. package/dist/performance/ContextPool.js +2 -0
  73. package/dist/performance/ContextPool.js.map +1 -0
  74. package/dist/performance/HotReloadDelta.d.ts +5 -0
  75. package/dist/performance/HotReloadDelta.d.ts.map +1 -1
  76. package/dist/performance/HotReloadDelta.js +18 -4
  77. package/dist/performance/HotReloadDelta.js.map +1 -1
  78. package/dist/performance/ObjectPool.d.ts.map +1 -1
  79. package/dist/performance/ObjectPool.js +4 -1
  80. package/dist/performance/ObjectPool.js.map +1 -1
  81. package/dist/performance/PerfReporter.d.ts +69 -0
  82. package/dist/performance/PerfReporter.d.ts.map +1 -1
  83. package/dist/performance/PerfReporter.js +314 -28
  84. package/dist/performance/PerfReporter.js.map +1 -1
  85. package/dist/performance/ThreadPool.d.ts +5 -0
  86. package/dist/performance/ThreadPool.d.ts.map +1 -1
  87. package/dist/performance/ThreadPool.js +33 -5
  88. package/dist/performance/ThreadPool.js.map +1 -1
  89. package/dist/persistence/StorageAdapters.d.ts.map +1 -1
  90. package/dist/persistence/StorageAdapters.js +13 -19
  91. package/dist/persistence/StorageAdapters.js.map +1 -1
  92. package/dist/qnce-engine.d.ts +2258 -0
  93. package/dist/quantum/entangler.d.ts +13 -0
  94. package/dist/quantum/entangler.d.ts.map +1 -0
  95. package/dist/quantum/entangler.js +24 -0
  96. package/dist/quantum/entangler.js.map +1 -0
  97. package/dist/quantum/flags.d.ts +24 -0
  98. package/dist/quantum/flags.d.ts.map +1 -0
  99. package/dist/quantum/flags.js +29 -0
  100. package/dist/quantum/flags.js.map +1 -0
  101. package/dist/quantum/integration.d.ts +26 -0
  102. package/dist/quantum/integration.d.ts.map +1 -0
  103. package/dist/quantum/integration.js +38 -0
  104. package/dist/quantum/integration.js.map +1 -0
  105. package/dist/quantum/measurement.d.ts +22 -0
  106. package/dist/quantum/measurement.d.ts.map +1 -0
  107. package/dist/quantum/measurement.js +44 -0
  108. package/dist/quantum/measurement.js.map +1 -0
  109. package/dist/quantum/phase.d.ts +20 -0
  110. package/dist/quantum/phase.d.ts.map +1 -0
  111. package/dist/quantum/phase.js +22 -0
  112. package/dist/quantum/phase.js.map +1 -0
  113. package/dist/quantum/types.d.ts +12 -0
  114. package/dist/quantum/types.d.ts.map +1 -0
  115. package/dist/quantum/types.js +5 -0
  116. package/dist/quantum/types.js.map +1 -0
  117. package/dist/schemas/validateStoryData.d.ts.map +1 -1
  118. package/dist/schemas/validateStoryData.js +8 -2
  119. package/dist/schemas/validateStoryData.js.map +1 -1
  120. package/dist/telemetry/core.d.ts +88 -0
  121. package/dist/telemetry/core.d.ts.map +1 -0
  122. package/dist/telemetry/core.js +303 -0
  123. package/dist/telemetry/core.js.map +1 -0
  124. package/dist/telemetry/types.d.ts +76 -0
  125. package/dist/telemetry/types.d.ts.map +1 -0
  126. package/dist/telemetry/types.js +4 -0
  127. package/dist/telemetry/types.js.map +1 -0
  128. package/dist/tsdoc-metadata.json +11 -0
  129. package/dist/ui/__tests__/AutosaveIndicator.test.js +10 -13
  130. package/dist/ui/__tests__/AutosaveIndicator.test.js.map +1 -1
  131. package/dist/ui/__tests__/UndoRedoControls.test.js +7 -9
  132. package/dist/ui/__tests__/UndoRedoControls.test.js.map +1 -1
  133. package/dist/ui/components/AutosaveIndicator.d.ts +1 -0
  134. package/dist/ui/components/AutosaveIndicator.d.ts.map +1 -1
  135. package/dist/ui/components/AutosaveIndicator.js +2 -1
  136. package/dist/ui/components/AutosaveIndicator.js.map +1 -1
  137. package/dist/ui/components/UndoRedoControls.d.ts +1 -0
  138. package/dist/ui/components/UndoRedoControls.d.ts.map +1 -1
  139. package/dist/ui/components/UndoRedoControls.js +1 -0
  140. package/dist/ui/components/UndoRedoControls.js.map +1 -1
  141. package/dist/ui/hooks/useKeyboardShortcuts.d.ts +1 -0
  142. package/dist/ui/hooks/useKeyboardShortcuts.d.ts.map +1 -1
  143. package/dist/ui/hooks/useKeyboardShortcuts.js +17 -5
  144. package/dist/ui/hooks/useKeyboardShortcuts.js.map +1 -1
  145. package/dist/ui/types.d.ts +6 -0
  146. package/dist/ui/types.d.ts.map +1 -1
  147. package/dist/ui/types.js +1 -0
  148. package/dist/ui/types.js.map +1 -1
  149. package/dist/utils/debug-logger.d.ts +20 -0
  150. package/dist/utils/debug-logger.d.ts.map +1 -0
  151. package/dist/utils/debug-logger.js +24 -0
  152. package/dist/utils/debug-logger.js.map +1 -0
  153. package/dist/utils/hot-profiler.d.ts +21 -0
  154. package/dist/utils/hot-profiler.d.ts.map +1 -0
  155. package/dist/utils/hot-profiler.js +36 -0
  156. package/dist/utils/hot-profiler.js.map +1 -0
  157. package/dist/utils/intern.d.ts +11 -0
  158. package/dist/utils/intern.d.ts.map +1 -0
  159. package/dist/utils/intern.js +45 -0
  160. package/dist/utils/intern.js.map +1 -0
  161. package/dist/utils/logger.d.ts +34 -0
  162. package/dist/utils/logger.d.ts.map +1 -0
  163. package/dist/utils/logger.js +115 -0
  164. package/dist/utils/logger.js.map +1 -0
  165. package/docs/PERFORMANCE.md +330 -0
  166. package/examples/fluent-builder-prototype.ts +71 -0
  167. package/examples/quantum-integration-demo.ts +51 -0
  168. package/package.json +25 -9
@@ -1,6 +1,29 @@
1
1
  "use strict";
2
2
  // QNCE Core Engine - Framework Agnostic
3
3
  // Quantum Narrative Convergence Engine
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || function (mod) {
21
+ if (mod && mod.__esModule) return mod;
22
+ var result = {};
23
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
24
+ __setModuleDefault(result, mod);
25
+ return result;
26
+ };
4
27
  Object.defineProperty(exports, "__esModule", { value: true });
5
28
  exports.QNCEEngine = exports.ChoiceValidationError = exports.QNCENavigationError = void 0;
6
29
  exports.createQNCEEngine = createQNCEEngine;
@@ -19,8 +42,13 @@ Object.defineProperty(exports, "QNCENavigationError", { enumerable: true, get: f
19
42
  Object.defineProperty(exports, "ChoiceValidationError", { enumerable: true, get: function () { return errors_2.ChoiceValidationError; } });
20
43
  // State persistence imports - Sprint 3.3
21
44
  const types_1 = require("./types");
45
+ const logger_1 = require("../utils/logger");
22
46
  // Conditional choice evaluator - Sprint 3.4
23
47
  const condition_1 = require("./condition");
48
+ const error_factory_1 = require("./error-factory");
49
+ const debug_logger_1 = require("../utils/debug-logger");
50
+ const hot_profiler_1 = require("../utils/hot-profiler");
51
+ const intern_1 = require("../utils/intern");
24
52
  // Demo narrative data moved to demo-story.ts
25
53
  function findNode(nodes, id) {
26
54
  const node = nodes.find(n => n.id === id);
@@ -32,18 +60,45 @@ function findNode(nodes, id) {
32
60
  * QNCE Engine - Core narrative state management
33
61
  * Framework agnostic implementation with object pooling integration
34
62
  */
63
+ /**
64
+ * Main engine class.
65
+ * @public
66
+ */
35
67
  class QNCEEngine {
36
68
  state;
37
69
  storyData; // Made public for hot-reload compatibility
38
70
  activeFlowEvents = [];
39
71
  performanceMode = false;
40
72
  enableProfiling = false;
73
+ debugMode = false;
41
74
  branchingEngine;
42
75
  choiceValidator; // Sprint 3.2: Choice validation
43
76
  // Sprint 3.3: State persistence and checkpoints
44
77
  checkpoints = new Map();
45
78
  maxCheckpoints = 50;
46
79
  autoCheckpointEnabled = false;
80
+ // Flag pooling to reduce duplicate string allocations
81
+ flagKeyPool = new Map();
82
+ flagValuePool = new Map();
83
+ internFlagKey(raw) {
84
+ let pooled = this.flagKeyPool.get(raw);
85
+ if (!pooled) {
86
+ this.flagKeyPool.set(raw, raw);
87
+ pooled = raw;
88
+ }
89
+ return pooled;
90
+ }
91
+ internFlagValue(val) {
92
+ if (typeof val === 'string' && val.length <= 64) { // limit to modest strings
93
+ let pooled = this.flagValuePool.get(val);
94
+ if (!pooled) {
95
+ this.flagValuePool.set(val, val);
96
+ pooled = val;
97
+ }
98
+ return pooled;
99
+ }
100
+ return val;
101
+ }
47
102
  // Sprint 3.3: State persistence properties
48
103
  checkpointManager;
49
104
  // Sprint 3.5: Autosave and Undo/Redo properties
@@ -67,12 +122,32 @@ class QNCEEngine {
67
122
  lastAutosaveTime = 0;
68
123
  isUndoRedoOperation = false;
69
124
  storageAdapter;
125
+ // Storage retry policy (initial simple implementation)
126
+ storageRetryPolicy = {
127
+ maxAttempts: 3,
128
+ baseDelayMs: 5,
129
+ factor: 2,
130
+ maxDelayMs: 100,
131
+ jitter: true
132
+ };
133
+ // Sprint 4.1: Telemetry support
134
+ telemetry;
135
+ defaultTelemetryCtx;
136
+ // Hooks
137
+ preChoiceHooks = [];
138
+ postChoiceHooks = [];
139
+ hookCounter = 0;
140
+ logger = (0, logger_1.createLogger)({ level: 'warn' });
141
+ engineOptions;
142
+ minimalTelemetry = false;
70
143
  get flags() {
71
144
  return this.state.flags;
72
145
  }
73
146
  get history() {
74
147
  return this.state.history;
75
148
  }
149
+ /** Access the engine logger (public read-only) */
150
+ getLogger() { return this.logger; }
76
151
  get isComplete() {
77
152
  try {
78
153
  return this.getCurrentNode().choices.length === 0;
@@ -81,10 +156,16 @@ class QNCEEngine {
81
156
  return true; // If current node not found, consider it complete
82
157
  }
83
158
  }
84
- constructor(storyData, initialState, performanceMode = false, threadPoolConfig) {
159
+ constructor(storyData, initialState, performanceMode = false, threadPoolConfig, options) {
85
160
  this.storyData = storyData;
86
161
  this.performanceMode = performanceMode;
87
162
  this.enableProfiling = performanceMode; // Enable profiling with performance mode
163
+ if (this.performanceMode) {
164
+ try {
165
+ condition_1.conditionEvaluator.enablePooling(true);
166
+ }
167
+ catch { }
168
+ }
88
169
  // Initialize choice validator (Sprint 3.2)
89
170
  this.choiceValidator = (0, validation_1.createChoiceValidator)();
90
171
  // Initialize ThreadPool if in performance mode
@@ -96,39 +177,138 @@ class QNCEEngine {
96
177
  flags: initialState?.flags || {},
97
178
  history: initialState?.history || [storyData.initialNodeId],
98
179
  };
180
+ // Telemetry wiring
181
+ this.telemetry = options?.telemetry;
182
+ if (options?.logger)
183
+ this.logger = options.logger;
184
+ this.engineOptions = options;
185
+ this.minimalTelemetry = options?.minimalTelemetry ?? false;
186
+ if (this.telemetry) {
187
+ const sessionId = options?.sessionId || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
188
+ this.defaultTelemetryCtx = {
189
+ sessionId,
190
+ storyId: undefined,
191
+ appVersion: options?.appVersion,
192
+ engineVersion: types_1.PERSISTENCE_VERSION,
193
+ env: options?.env
194
+ };
195
+ try {
196
+ this.telemetry.emit({ type: 'session.start', payload: { initialNodeId: this.state.currentNodeId }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
197
+ }
198
+ catch { }
199
+ }
200
+ }
201
+ /**
202
+ * Dispose engine resources (telemetry, performance reporter, background thread pool in perf mode)
203
+ * @public
204
+ */
205
+ async dispose() {
206
+ try {
207
+ if (this.telemetry)
208
+ await this.telemetry.dispose();
209
+ }
210
+ catch { }
211
+ try {
212
+ (0, PerfReporter_1.getPerfReporter)().dispose?.();
213
+ }
214
+ catch { }
215
+ if (this.performanceMode) {
216
+ try {
217
+ const { shutdownThreadPool } = await Promise.resolve().then(() => __importStar(require('../performance/ThreadPool')));
218
+ await shutdownThreadPool();
219
+ }
220
+ catch { }
221
+ }
99
222
  }
100
223
  // Lane B: StorageAdapter integration
101
224
  /** Attach a storage adapter for persistence */
102
225
  attachStorageAdapter(adapter) {
103
226
  this.storageAdapter = adapter;
104
227
  }
228
+ /**
229
+ * Configure storage retry policy for transient save failures.
230
+ * @param policy - Partial policy overriding defaults. Set maxAttempts to 1 to disable retries.
231
+ * @beta
232
+ */
233
+ setStorageRetryPolicy(policy) {
234
+ this.storageRetryPolicy = { ...this.storageRetryPolicy, ...policy };
235
+ }
236
+ async sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
237
+ computeBackoff(attempt) {
238
+ const { baseDelayMs, factor, maxDelayMs, jitter } = this.storageRetryPolicy;
239
+ const raw = Math.min(baseDelayMs * Math.pow(factor, attempt - 1), maxDelayMs);
240
+ if (!jitter)
241
+ return raw;
242
+ const spread = raw * 0.5;
243
+ return Math.max(0, raw - spread + Math.random() * spread);
244
+ }
105
245
  /** Save current state through the attached storage adapter */
106
246
  async saveToStorage(key, options = {}) {
107
247
  if (!this.storageAdapter)
108
248
  return { success: false, error: 'No storage adapter attached' };
249
+ const t0 = Date.now();
109
250
  const serialized = await this.saveState(options);
110
- return this.storageAdapter.save(key, serialized, options);
251
+ const policy = this.storageRetryPolicy;
252
+ let attempt = 0;
253
+ let lastResult;
254
+ while (true) {
255
+ attempt++;
256
+ try {
257
+ lastResult = await this.storageAdapter.save(key, serialized, options);
258
+ }
259
+ catch (err) {
260
+ lastResult = { success: false, error: err?.message || 'unknown-error' };
261
+ }
262
+ if (lastResult.success || attempt >= policy.maxAttempts || policy.maxAttempts <= 1)
263
+ break;
264
+ // Only retry if explicitly failed (success === false)
265
+ const delay = this.computeBackoff(attempt);
266
+ await this.sleep(delay);
267
+ }
268
+ try {
269
+ this.telemetry?.emit({ type: 'storage.op', payload: { op: 'save', key, ms: Date.now() - t0, ok: !!lastResult?.success, attempts: attempt, maxAttempts: policy.maxAttempts }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
270
+ }
271
+ catch { }
272
+ return lastResult || { success: false, error: 'unknown' };
111
273
  }
112
274
  /** Load state from the attached storage adapter */
113
275
  async loadFromStorage(key, options = {}) {
114
276
  if (!this.storageAdapter)
115
277
  return { success: false, error: 'No storage adapter attached' };
278
+ const t0 = Date.now();
116
279
  const serialized = await this.storageAdapter.load(key, options);
117
280
  if (!serialized)
118
281
  return { success: false, error: `No data for key: ${key}` };
119
- return this.loadState(serialized, options);
282
+ const res = await this.loadState(serialized, options);
283
+ try {
284
+ this.telemetry?.emit({ type: 'storage.op', payload: { op: 'load', key, ms: Date.now() - t0, ok: !!res?.success }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
285
+ }
286
+ catch { }
287
+ return res;
120
288
  }
121
289
  /** Delete a stored state via the adapter */
122
290
  async deleteFromStorage(key) {
123
291
  if (!this.storageAdapter)
124
292
  return false;
125
- return this.storageAdapter.delete(key);
293
+ const t0 = Date.now();
294
+ const ok = await this.storageAdapter.delete(key);
295
+ try {
296
+ this.telemetry?.emit({ type: 'storage.op', payload: { op: 'delete', key, ms: Date.now() - t0, ok }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
297
+ }
298
+ catch { }
299
+ return ok;
126
300
  }
127
301
  /** List keys from the adapter */
128
302
  async listStorageKeys() {
129
303
  if (!this.storageAdapter)
130
304
  return [];
131
- return this.storageAdapter.list();
305
+ const t0 = Date.now();
306
+ const keys = await this.storageAdapter.list();
307
+ try {
308
+ this.telemetry?.emit({ type: 'storage.op', payload: { op: 'list', count: keys.length, ms: Date.now() - t0, ok: true }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
309
+ }
310
+ catch { }
311
+ return keys;
132
312
  }
133
313
  /** Check if a key exists via the adapter */
134
314
  async storageExists(key) {
@@ -140,7 +320,13 @@ class QNCEEngine {
140
320
  async clearStorage() {
141
321
  if (!this.storageAdapter)
142
322
  return false;
143
- return this.storageAdapter.clear();
323
+ const t0 = Date.now();
324
+ const ok = await this.storageAdapter.clear();
325
+ try {
326
+ this.telemetry?.emit({ type: 'storage.op', payload: { op: 'clear', ms: Date.now() - t0, ok }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
327
+ }
328
+ catch { }
329
+ return ok;
144
330
  }
145
331
  /** Get storage statistics from the adapter */
146
332
  async getStorageStats() {
@@ -152,7 +338,7 @@ class QNCEEngine {
152
338
  /**
153
339
  * Navigate directly to a specific node by ID
154
340
  * @param nodeId - The ID of the node to navigate to
155
- * @throws {QNCENavigationError} When nodeId is invalid or not found
341
+ * @throws \{QNCENavigationError\} When nodeId is invalid or not found
156
342
  */
157
343
  goToNodeById(nodeId) {
158
344
  // Validate node exists
@@ -210,7 +396,12 @@ class QNCEEngine {
210
396
  choices: pooledNode.choices
211
397
  };
212
398
  }
213
- return findNode(this.storyData.nodes, this.state.currentNodeId);
399
+ const node = findNode(this.storyData.nodes, this.state.currentNodeId);
400
+ try {
401
+ this.telemetry?.emit({ type: 'node.enter', payload: this.minimalTelemetry ? node.id : { nodeId: node.id }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
402
+ }
403
+ catch { }
404
+ return node;
214
405
  }
215
406
  /**
216
407
  * Get available choices from the current node with validation and conditional filtering
@@ -223,6 +414,22 @@ class QNCEEngine {
223
414
  timestamp: Date.now(),
224
415
  customData: {}
225
416
  };
417
+ // Performance-mode choice array pooling: reuse a scratch array to avoid per-call allocations
418
+ let scratch;
419
+ if (this.performanceMode) {
420
+ // Lazily allocate a shared scratch array on first use and clear in-place
421
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
422
+ // @ts-ignore - attach hidden field for pooling (internal only)
423
+ if (!this.__choiceScratch) { /* eslint-disable-line */
424
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
425
+ // @ts-ignore
426
+ this.__choiceScratch = []; /* eslint-disable-line */
427
+ }
428
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
429
+ // @ts-ignore
430
+ scratch = this.__choiceScratch; /* eslint-disable-line */
431
+ scratch.length = 0;
432
+ }
226
433
  // First apply conditional filtering (Sprint 3.4)
227
434
  const conditionallyAvailable = currentNode.choices.filter((choice) => {
228
435
  // If no condition is specified, choice is always available
@@ -231,19 +438,58 @@ class QNCEEngine {
231
438
  }
232
439
  try {
233
440
  // Evaluate the condition using the condition evaluator
234
- return condition_1.conditionEvaluator.evaluate(choice.condition, context);
441
+ const t0 = Date.now();
442
+ const res = hot_profiler_1.globalHotProfiler.wrap('condition.evaluate', () => condition_1.conditionEvaluator.evaluate(choice.condition, context));
443
+ try {
444
+ this.telemetry?.emit({ type: 'expression.evaluate', payload: this.minimalTelemetry ? 1 : { ok: true, ms: Date.now() - t0 }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
445
+ }
446
+ catch { }
447
+ if (this.debugMode)
448
+ debug_logger_1.globalDebugLogger.log('condition.ok', { nodeId: this.state.currentNodeId, choiceText: choice.text, expr: choice.condition });
449
+ return res;
235
450
  }
236
451
  catch (error) {
237
452
  // Log condition evaluation errors but don't block other choices
238
453
  if (error instanceof condition_1.ConditionEvaluationError) {
239
- console.warn(`[QNCE] Choice condition evaluation failed: ${error.message}`, {
454
+ const struct = error_factory_1.ErrorFactory.condition('Choice condition evaluation failed', {
240
455
  choiceText: choice.text,
241
- condition: choice.condition,
242
- nodeId: this.state.currentNodeId
456
+ conditionExpression: choice.condition,
457
+ nodeId: this.state.currentNodeId,
458
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
459
+ storyId: this.storyData?.id,
460
+ cause: error
243
461
  });
462
+ // eslint-disable-next-line no-console
463
+ this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
464
+ try {
465
+ this.telemetry?.emit({ type: 'engine.structuredError', payload: (0, error_factory_1.serializeStructuredError)(struct), ts: Date.now(), ctx: this.defaultTelemetryCtx });
466
+ }
467
+ catch { }
468
+ try {
469
+ this.telemetry?.emit({ type: 'expression.evaluate', payload: this.minimalTelemetry ? 0 : { ok: false, error: 'ConditionEvaluationError' }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
470
+ }
471
+ catch { }
244
472
  }
245
473
  else {
246
- console.warn(`[QNCE] Unexpected error evaluating choice condition:`, error);
474
+ const struct = error_factory_1.ErrorFactory.condition('Unexpected error evaluating choice condition', {
475
+ choiceText: choice.text,
476
+ conditionExpression: choice.condition,
477
+ nodeId: this.state.currentNodeId,
478
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
479
+ storyId: this.storyData?.id,
480
+ cause: error
481
+ });
482
+ // eslint-disable-next-line no-console
483
+ this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
484
+ try {
485
+ this.telemetry?.emit({ type: 'engine.structuredError', payload: (0, error_factory_1.serializeStructuredError)(struct), ts: Date.now(), ctx: this.defaultTelemetryCtx });
486
+ }
487
+ catch { }
488
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
489
+ try {
490
+ this.telemetry?.emit({ type: 'engine.error', payload: { where: 'getAvailableChoices', error: error?.message || String(error) }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
491
+ }
492
+ catch { }
247
493
  }
248
494
  // Return false for invalid conditions (choice won't be shown)
249
495
  return false;
@@ -252,7 +498,14 @@ class QNCEEngine {
252
498
  // Then apply choice validation (Sprint 3.2)
253
499
  const validationContext = (0, validation_1.createValidationContext)(currentNode, this.state, conditionallyAvailable // Pass the conditionally filtered choices
254
500
  );
255
- return this.choiceValidator.getAvailableChoices(validationContext);
501
+ const validated = this.choiceValidator.getAvailableChoices(validationContext);
502
+ if (!this.performanceMode || !scratch)
503
+ return validated;
504
+ // Copy into scratch to present a stable pooled array (avoid leaking validator's array if it allocates)
505
+ for (let i = 0; i < validated.length; i++)
506
+ scratch[i] = validated[i];
507
+ scratch.length = validated.length;
508
+ return scratch;
256
509
  }
257
510
  makeChoice(choiceIndex) {
258
511
  const choices = this.getAvailableChoices();
@@ -277,9 +530,13 @@ class QNCEEngine {
277
530
  return { ...this.state.flags };
278
531
  }
279
532
  setFlag(key, value) {
533
+ if (this.debugMode)
534
+ debug_logger_1.globalDebugLogger.log('flag.set', { key, value });
280
535
  // Sprint 3.5: Save state for undo before making changes
281
536
  const preChangeState = this.deepCopy(this.state);
282
- this.state.flags[key] = value;
537
+ const pooledKey = this.internFlagKey(key);
538
+ const pooledValue = this.internFlagValue(value);
539
+ this.state.flags[pooledKey] = pooledValue;
283
540
  // Sprint 3.5: Track state change for undo/redo
284
541
  if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation &&
285
542
  this.undoRedoConfig.trackActions.includes('flag-change')) {
@@ -294,7 +551,20 @@ class QNCEEngine {
294
551
  flagKey: key,
295
552
  flagValue: value
296
553
  }).catch((error) => {
297
- console.warn('[QNCE] Autosave failed:', error.message);
554
+ const struct = error_factory_1.ErrorFactory.persistence('Autosave failed', {
555
+ operation: 'autosave',
556
+ flagKey: key,
557
+ flagValue: value,
558
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
559
+ storyId: this.storyData?.id,
560
+ cause: error
561
+ });
562
+ // eslint-disable-next-line no-console
563
+ this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
564
+ try {
565
+ this.telemetry?.emit({ type: 'engine.structuredError', payload: (0, error_factory_1.serializeStructuredError)(struct), ts: Date.now(), ctx: this.defaultTelemetryCtx });
566
+ }
567
+ catch { }
298
568
  });
299
569
  }
300
570
  }
@@ -302,6 +572,35 @@ class QNCEEngine {
302
572
  return [...this.state.history];
303
573
  }
304
574
  selectChoice(choice) {
575
+ if (this.debugMode)
576
+ debug_logger_1.globalDebugLogger.log('choice.select.start', { from: this.state.currentNodeId, to: choice.nextNodeId, choiceText: choice.text });
577
+ // Pre-choice hooks (ordered by priority desc then registration order)
578
+ if (this.preChoiceHooks.length) {
579
+ const currentNodeForHooks = this.getCurrentNode();
580
+ for (const hook of this.preChoiceHooks) {
581
+ try {
582
+ const res = hook.h({ engine: this, node: currentNodeForHooks, choice });
583
+ if (res === false)
584
+ return; // cancellation
585
+ }
586
+ catch (e) {
587
+ const struct = error_factory_1.ErrorFactory.hook('pre-choice hook error', {
588
+ hookStage: 'pre-choice',
589
+ nodeId: currentNodeForHooks.id,
590
+ choiceText: choice.text,
591
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
592
+ storyId: this.storyData?.id,
593
+ cause: e
594
+ });
595
+ // eslint-disable-next-line no-console
596
+ this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
597
+ try {
598
+ this.telemetry?.emit({ type: 'engine.structuredError', payload: (0, error_factory_1.serializeStructuredError)(struct), ts: Date.now(), ctx: this.defaultTelemetryCtx });
599
+ }
600
+ catch { }
601
+ }
602
+ }
603
+ }
305
604
  // Sprint 3.5: Save state for undo before making changes
306
605
  const preChangeState = this.deepCopy(this.state);
307
606
  // S2-T4: Add state transition profiling
@@ -314,6 +613,11 @@ class QNCEEngine {
314
613
  : null;
315
614
  const fromNodeId = this.state.currentNodeId;
316
615
  const toNodeId = choice.nextNodeId;
616
+ // Telemetry: choice.select
617
+ try {
618
+ this.telemetry?.emit({ type: 'choice.select', payload: { fromNodeId, toNodeId, choiceText: choice.text }, ts: Date.now(), ctx: this.defaultTelemetryCtx });
619
+ }
620
+ catch { }
317
621
  // Create flow event for tracking narrative progression
318
622
  if (this.performanceMode) {
319
623
  const flowSpanId = this.enableProfiling
@@ -357,9 +661,50 @@ class QNCEEngine {
357
661
  toNodeId,
358
662
  choiceText: choice.text
359
663
  }).catch(error => {
360
- console.warn('[QNCE] Autosave failed:', error.message);
664
+ const struct = error_factory_1.ErrorFactory.persistence('Autosave failed', {
665
+ operation: 'autosave',
666
+ fromNodeId,
667
+ toNodeId,
668
+ choiceText: choice.text,
669
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
670
+ storyId: this.storyData?.id,
671
+ cause: error
672
+ });
673
+ // eslint-disable-next-line no-console
674
+ this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
675
+ try {
676
+ this.telemetry?.emit({ type: 'engine.structuredError', payload: (0, error_factory_1.serializeStructuredError)(struct), ts: Date.now(), ctx: this.defaultTelemetryCtx });
677
+ }
678
+ catch { }
361
679
  });
362
680
  }
681
+ // Post-choice hooks
682
+ if (this.postChoiceHooks.length) {
683
+ const nodeAfter = this.getCurrentNode();
684
+ for (const hook of this.postChoiceHooks) {
685
+ try {
686
+ hook.h({ engine: this, node: nodeAfter, choice });
687
+ }
688
+ catch (e) {
689
+ const struct = error_factory_1.ErrorFactory.hook('post-choice hook error', {
690
+ hookStage: 'post-choice',
691
+ nodeId: nodeAfter.id,
692
+ choiceText: choice.text,
693
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
694
+ storyId: this.storyData?.id,
695
+ cause: e
696
+ });
697
+ // eslint-disable-next-line no-console
698
+ this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
699
+ try {
700
+ this.telemetry?.emit({ type: 'engine.structuredError', payload: (0, error_factory_1.serializeStructuredError)(struct), ts: Date.now(), ctx: this.defaultTelemetryCtx });
701
+ }
702
+ catch { }
703
+ }
704
+ }
705
+ }
706
+ if (this.debugMode)
707
+ debug_logger_1.globalDebugLogger.log('choice.select.end', { to: this.state.currentNodeId });
363
708
  // Complete state transition span
364
709
  if (transitionSpanId) {
365
710
  (0, PerfReporter_1.getPerfReporter)().endSpan(transitionSpanId, {
@@ -371,7 +716,19 @@ class QNCEEngine {
371
716
  if (this.performanceMode) {
372
717
  // Preload next possible nodes in background
373
718
  this.preloadNextNodes().catch(error => {
374
- console.warn('[QNCE] Background preload failed:', error.message);
719
+ const struct = error_factory_1.ErrorFactory.state('Background preload failed', {
720
+ operation: 'preloadNextNodes',
721
+ nodeId: toNodeId,
722
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
723
+ storyId: this.storyData?.id,
724
+ cause: error
725
+ });
726
+ // eslint-disable-next-line no-console
727
+ this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
728
+ try {
729
+ this.telemetry?.emit({ type: 'engine.structuredError', payload: (0, error_factory_1.serializeStructuredError)(struct), ts: Date.now(), ctx: this.defaultTelemetryCtx });
730
+ }
731
+ catch { }
375
732
  });
376
733
  // Write telemetry data in background
377
734
  this.backgroundTelemetryWrite({
@@ -380,10 +737,51 @@ class QNCEEngine {
380
737
  toNodeId,
381
738
  choiceText: choice.text.slice(0, 50) // First 50 chars for privacy
382
739
  }).catch(error => {
383
- console.warn('[QNCE] Background telemetry failed:', error.message);
740
+ const struct = error_factory_1.ErrorFactory.telemetry('Background telemetry failed', {
741
+ operation: 'backgroundTelemetryWrite',
742
+ fromNodeId,
743
+ toNodeId,
744
+ choiceText: choice.text,
745
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
746
+ storyId: this.storyData?.id,
747
+ cause: error
748
+ });
749
+ // eslint-disable-next-line no-console
750
+ this.logger.warn('[QNCE] ' + (0, error_factory_1.serializeStructuredError)(struct));
384
751
  });
385
752
  }
386
753
  }
754
+ // eslint-disable-next-line @typescript-eslint/no-inferrable-types
755
+ registerHook(type, handler, priority = 0) {
756
+ const list = type === 'pre-choice' ? this.preChoiceHooks : this.postChoiceHooks;
757
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
758
+ const entry = { h: handler, p: priority, o: this.hookCounter++ };
759
+ list.push(entry);
760
+ list.sort((a, b) => (b.p - a.p) || (a.o - b.o));
761
+ return () => {
762
+ const idx = list.indexOf(entry);
763
+ if (idx >= 0)
764
+ list.splice(idx, 1);
765
+ };
766
+ }
767
+ /** Clear all hooks (testing / reset) */
768
+ clearHooks() { this.preChoiceHooks = []; this.postChoiceHooks = []; }
769
+ /** Enable verbose debug logging (stored in ring buffer) */
770
+ enableDebug() { this.debugMode = true; debug_logger_1.globalDebugLogger.setEnabled(true); }
771
+ disableDebug() { this.debugMode = false; if (!this.enableProfiling)
772
+ debug_logger_1.globalDebugLogger.setEnabled(false); }
773
+ getDebugLogs() { return debug_logger_1.globalDebugLogger.flush(); }
774
+ clearDebugLogs() { debug_logger_1.globalDebugLogger.clear(); }
775
+ /** Enable aggregated hot path profiling */
776
+ enableHotProfiling() { hot_profiler_1.globalHotProfiler.enable(true); }
777
+ disableHotProfiling() { hot_profiler_1.globalHotProfiler.enable(false); }
778
+ getHotProfileSummary() { return hot_profiler_1.globalHotProfiler.summary(); }
779
+ resetHotProfile() { hot_profiler_1.globalHotProfiler.reset(); }
780
+ /** Flush telemetry (useful before process exit) */
781
+ async flushTelemetry() { try {
782
+ await this.telemetry?.flush();
783
+ }
784
+ catch { } }
387
785
  resetNarrative() {
388
786
  // Sprint 3.5: Save state for undo before reset
389
787
  const preChangeState = this.deepCopy(this.state);
@@ -422,7 +820,8 @@ class QNCEEngine {
422
820
  this.triggerAutosave('state-load', {
423
821
  nodeId: state.currentNodeId
424
822
  }).catch((error) => {
425
- console.warn('[QNCE] Autosave failed:', error.message);
823
+ // eslint-disable-next-line no-console
824
+ this.logger.warn('[QNCE] Autosave failed: ' + error.message);
426
825
  });
427
826
  }
428
827
  }
@@ -480,6 +879,7 @@ class QNCEEngine {
480
879
  // Generate checksum if requested
481
880
  if (options.generateChecksum) {
482
881
  const stateToHash = { ...serializedState };
882
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
483
883
  delete stateToHash.metadata.checksum; // Exclude checksum from hash
484
884
  const stateString = JSON.stringify(stateToHash);
485
885
  metadata.checksum = await this.generateChecksum(stateString);
@@ -748,7 +1148,11 @@ class QNCEEngine {
748
1148
  */
749
1149
  createFlowEvent(fromNodeId, toNodeId, metadata) {
750
1150
  const flow = ObjectPool_1.poolManager.borrowFlow();
751
- flow.initialize(fromNodeId, metadata);
1151
+ // In minimal telemetry mode, avoid attaching heavy metadata
1152
+ const meta = this.minimalTelemetry ? undefined : (0, intern_1.internShallowRecord)(metadata || {});
1153
+ // Intern node id to reduce duplicate string allocations across events
1154
+ const fromId = (0, intern_1.internString)(fromNodeId);
1155
+ flow.initialize(fromId, meta);
752
1156
  flow.addTransition(fromNodeId, toNodeId);
753
1157
  return flow;
754
1158
  }
@@ -756,16 +1160,19 @@ class QNCEEngine {
756
1160
  * Record and manage flow events
757
1161
  */
758
1162
  recordFlowEvent(flow) {
1163
+ const toId = flow.transitions[flow.transitions.length - 1]?.split('->')[1] || '';
759
1164
  const flowEvent = {
760
1165
  id: `${flow.nodeId}-${Date.now()}`,
761
1166
  fromNodeId: flow.nodeId,
762
- toNodeId: flow.transitions[flow.transitions.length - 1]?.split('->')[1] || '',
1167
+ toNodeId: (0, intern_1.internString)(toId),
763
1168
  timestamp: flow.timestamp,
764
- metadata: flow.metadata
1169
+ // In minimal mode, drop metadata entirely
1170
+ metadata: this.minimalTelemetry ? undefined : flow.metadata
765
1171
  };
766
1172
  this.activeFlowEvents.push(flowEvent);
767
1173
  // Clean up old flow events (basic LRU-style cleanup)
768
- if (this.activeFlowEvents.length > 10) {
1174
+ const cap = this.minimalTelemetry ? 5 : 10;
1175
+ if (this.activeFlowEvents.length > cap) {
769
1176
  this.activeFlowEvents.shift(); // Remove oldest
770
1177
  }
771
1178
  }
@@ -829,7 +1236,9 @@ class QNCEEngine {
829
1236
  }
830
1237
  };
831
1238
  threadPool.submitJob('telemetry-write', telemetryData, 'low').catch(error => {
832
- console.warn('[QNCE] Telemetry write failed:', error.message);
1239
+ if (this.engineOptions?.suppressTelemetryWarnings && error?.message === 'Job queue limit exceeded')
1240
+ return;
1241
+ this.logger.warn('[QNCE] Telemetry write failed: ' + error.message);
833
1242
  });
834
1243
  }
835
1244
  // ================================
@@ -841,7 +1250,8 @@ class QNCEEngine {
841
1250
  */
842
1251
  enableBranching(story) {
843
1252
  if (this.branchingEngine) {
844
- console.warn('[QNCE] Branching already enabled for this engine instance');
1253
+ // eslint-disable-next-line no-console
1254
+ this.logger.warn('[QNCE] Branching already enabled for this engine instance');
845
1255
  return this.branchingEngine;
846
1256
  }
847
1257
  // Create branching engine with current state
@@ -1003,6 +1413,7 @@ class QNCEEngine {
1003
1413
  if (!receivedChecksum)
1004
1414
  return false;
1005
1415
  const stateToHash = { ...serializedState };
1416
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1006
1417
  delete stateToHash.metadata.checksum;
1007
1418
  const stateString = JSON.stringify(stateToHash);
1008
1419
  const expectedChecksum = await this.generateChecksum(stateString);
@@ -1194,12 +1605,19 @@ class QNCEEngine {
1194
1605
  */
1195
1606
  pushToUndoStack(state, action, metadata) {
1196
1607
  const startTime = performance.now();
1608
+ // Memory optimization (lightweight history): we store a shallow snapshot of flags + currentNodeId
1609
+ // instead of deep copying full engine state. We embed it in metadata under a reserved key and keep
1610
+ // a minimal placeholder state reference to satisfy existing types.
1611
+ const LIGHT_STATE_KEY = '__lightState';
1612
+ const prevHist = state?.history;
1613
+ const lightState = { currentNodeId: state.currentNodeId, flags: { ...state.flags }, history: [...(Array.isArray(prevHist) ? prevHist : this.state.history)] };
1197
1614
  const entry = {
1198
1615
  id: this.generateHistoryId(),
1199
- state: this.deepCopy(state),
1616
+ // Keep original deep copy for compatibility OFF for now -> minimal placeholder with current fields.
1617
+ state: this.deepCopy({ currentNodeId: state.currentNodeId, flags: { ...state.flags }, history: [...(Array.isArray(prevHist) ? prevHist : this.state.history)] }),
1200
1618
  timestamp: new Date().toISOString(),
1201
1619
  action,
1202
- metadata
1620
+ metadata: { ...(metadata || {}), [LIGHT_STATE_KEY]: lightState }
1203
1621
  };
1204
1622
  this.undoStack.push(entry);
1205
1623
  // Clear redo stack when new change is made
@@ -1239,9 +1657,10 @@ class QNCEEngine {
1239
1657
  // Save current state to redo stack
1240
1658
  const currentEntry = {
1241
1659
  id: this.generateHistoryId(),
1242
- state: this.deepCopy(this.state),
1660
+ state: this.deepCopy({ currentNodeId: this.state.currentNodeId, flags: { ...this.state.flags }, history: [...this.state.history] }),
1243
1661
  timestamp: new Date().toISOString(),
1244
- action: 'redo-point'
1662
+ action: 'redo-point',
1663
+ metadata: { __lightState: { currentNodeId: this.state.currentNodeId, flags: { ...this.state.flags }, history: [...this.state.history] } }
1245
1664
  };
1246
1665
  this.redoStack.push(currentEntry);
1247
1666
  // Enforce max redo entries
@@ -1252,7 +1671,17 @@ class QNCEEngine {
1252
1671
  const entryToRestore = this.undoStack.pop();
1253
1672
  // Set flag to prevent triggering undo/redo tracking during restore
1254
1673
  this.isUndoRedoOperation = true;
1255
- this.state = this.deepCopy(entryToRestore.state);
1674
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- metadata may carry lightweight snapshot
1675
+ const light = entryToRestore.metadata?.__lightState;
1676
+ if (light) {
1677
+ this.state.currentNodeId = light.currentNodeId;
1678
+ this.state.flags = { ...light.flags };
1679
+ if (Array.isArray(light.history))
1680
+ this.state.history = [...light.history];
1681
+ }
1682
+ else {
1683
+ this.state = this.deepCopy(entryToRestore.state);
1684
+ }
1256
1685
  this.isUndoRedoOperation = false;
1257
1686
  const duration = performance.now() - startTime;
1258
1687
  // Performance tracking
@@ -1312,9 +1741,10 @@ class QNCEEngine {
1312
1741
  // Save current state to undo stack
1313
1742
  const currentEntry = {
1314
1743
  id: this.generateHistoryId(),
1315
- state: this.deepCopy(this.state),
1744
+ state: this.deepCopy({ currentNodeId: this.state.currentNodeId, flags: { ...this.state.flags }, history: [...this.state.history] }),
1316
1745
  timestamp: new Date().toISOString(),
1317
- action: 'undo-point'
1746
+ action: 'undo-point',
1747
+ metadata: { __lightState: { currentNodeId: this.state.currentNodeId, flags: { ...this.state.flags }, history: [...this.state.history] } }
1318
1748
  };
1319
1749
  this.undoStack.push(currentEntry);
1320
1750
  // Enforce max undo entries
@@ -1325,7 +1755,17 @@ class QNCEEngine {
1325
1755
  const entryToRestore = this.redoStack.pop();
1326
1756
  // Set flag to prevent triggering undo/redo tracking during restore
1327
1757
  this.isUndoRedoOperation = true;
1328
- this.state = this.deepCopy(entryToRestore.state);
1758
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- metadata may carry lightweight snapshot
1759
+ const light = entryToRestore.metadata?.__lightState;
1760
+ if (light) {
1761
+ this.state.currentNodeId = light.currentNodeId;
1762
+ this.state.flags = { ...light.flags };
1763
+ if (Array.isArray(light.history))
1764
+ this.state.history = [...light.history];
1765
+ }
1766
+ else {
1767
+ this.state = this.deepCopy(entryToRestore.state);
1768
+ }
1329
1769
  this.isUndoRedoOperation = false;
1330
1770
  const duration = performance.now() - startTime;
1331
1771
  // Performance tracking
@@ -1429,6 +1869,8 @@ class QNCEEngine {
1429
1869
  * @param metadata - Optional metadata about the trigger
1430
1870
  * @returns Promise resolving to autosave result
1431
1871
  */
1872
+ // metadata currently unused (reserved for future extended autosave telemetry)
1873
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1432
1874
  async triggerAutosave(trigger, metadata) {
1433
1875
  const startTime = performance.now();
1434
1876
  // Check if autosave is enabled
@@ -1540,12 +1982,20 @@ exports.QNCEEngine = QNCEEngine;
1540
1982
  /**
1541
1983
  * Factory function to create a QNCE engine instance
1542
1984
  */
1543
- function createQNCEEngine(storyData, initialState, performanceMode = false) {
1544
- return new QNCEEngine(storyData, initialState, performanceMode);
1985
+ /**
1986
+ * Factory to create a QNCE engine instance.
1987
+ * @public
1988
+ */
1989
+ function createQNCEEngine(storyData, initialState, performanceMode = false, threadPoolConfig, options) {
1990
+ return new QNCEEngine(storyData, initialState, performanceMode, threadPoolConfig, options);
1545
1991
  }
1546
1992
  /**
1547
1993
  * Load story data from JSON
1548
1994
  */
1995
+ /**
1996
+ * Load and validate StoryData from JSON-like input.
1997
+ * @public
1998
+ */
1549
1999
  function loadStoryData(jsonData) {
1550
2000
  // Add validation here in the future
1551
2001
  return jsonData;