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.
- package/README.md +713 -7
- package/dist/cli/audit.js +0 -0
- package/dist/cli/init.js +0 -0
- package/dist/cli/perf.d.ts.map +1 -1
- package/dist/cli/perf.js +2 -1
- package/dist/cli/perf.js.map +1 -1
- package/dist/cli/play.d.ts +4 -0
- package/dist/cli/play.d.ts.map +1 -0
- package/dist/cli/play.js +259 -0
- package/dist/cli/play.js.map +1 -0
- package/dist/engine/condition.d.ts +69 -0
- package/dist/engine/condition.d.ts.map +1 -0
- package/dist/engine/condition.js +195 -0
- package/dist/engine/condition.js.map +1 -0
- package/dist/engine/core.d.ts +274 -3
- package/dist/engine/core.d.ts.map +1 -1
- package/dist/engine/core.js +1148 -9
- package/dist/engine/core.js.map +1 -1
- package/dist/engine/demo-story.d.ts.map +1 -1
- package/dist/engine/demo-story.js +99 -13
- package/dist/engine/demo-story.js.map +1 -1
- package/dist/engine/errors.d.ts +76 -0
- package/dist/engine/errors.d.ts.map +1 -0
- package/dist/engine/errors.js +178 -0
- package/dist/engine/errors.js.map +1 -0
- package/dist/engine/types.d.ts +445 -0
- package/dist/engine/types.d.ts.map +1 -0
- package/dist/engine/types.js +9 -0
- package/dist/engine/types.js.map +1 -0
- package/dist/engine/validation.d.ts +110 -0
- package/dist/engine/validation.d.ts.map +1 -0
- package/dist/engine/validation.js +261 -0
- package/dist/engine/validation.js.map +1 -0
- package/dist/examples/examples/autosave-undo-demo.js +248 -0
- package/dist/examples/examples/persistence-demo.js +63 -0
- package/dist/examples/src/engine/condition.js +194 -0
- package/dist/examples/src/engine/core.js +1382 -0
- package/dist/examples/src/engine/demo-story.js +200 -0
- package/dist/examples/src/engine/types.js +8 -0
- package/dist/examples/src/index.js +35 -0
- package/dist/examples/src/integrations/react.js +322 -0
- package/dist/examples/src/narrative/branching/engine-simple.js +348 -0
- package/dist/examples/src/narrative/branching/index.js +55 -0
- package/dist/examples/src/narrative/branching/models.js +5 -0
- package/dist/examples/src/performance/ObjectPool.js +296 -0
- package/dist/examples/src/performance/PerfReporter.js +280 -0
- package/dist/examples/src/performance/ThreadPool.js +347 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/react.d.ts +200 -0
- package/dist/integrations/react.d.ts.map +1 -0
- package/dist/integrations/react.js +365 -0
- package/dist/integrations/react.js.map +1 -0
- package/dist/narrative/branching/engine-simple.js +3 -3
- package/dist/narrative/branching/engine-simple.js.map +1 -1
- package/dist/narrative/branching/engine.d.ts +1 -0
- package/dist/narrative/branching/engine.d.ts.map +1 -0
- package/dist/narrative/branching/engine.js +2 -0
- package/dist/narrative/branching/engine.js.map +1 -0
- package/dist/narrative/branching/models.d.ts.map +1 -1
- package/dist/performance/HotReloadDelta.d.ts +25 -8
- package/dist/performance/HotReloadDelta.d.ts.map +1 -1
- package/dist/performance/HotReloadDelta.js +10 -15
- package/dist/performance/HotReloadDelta.js.map +1 -1
- package/dist/ui/__tests__/AutosaveIndicator.test.d.ts +2 -0
- package/dist/ui/__tests__/AutosaveIndicator.test.d.ts.map +1 -0
- package/dist/ui/__tests__/AutosaveIndicator.test.js +329 -0
- package/dist/ui/__tests__/AutosaveIndicator.test.js.map +1 -0
- package/dist/ui/__tests__/UndoRedoControls.test.d.ts +2 -0
- package/dist/ui/__tests__/UndoRedoControls.test.d.ts.map +1 -0
- package/dist/ui/__tests__/UndoRedoControls.test.js +245 -0
- package/dist/ui/__tests__/UndoRedoControls.test.js.map +1 -0
- package/dist/ui/__tests__/autosave-simple.test.d.ts +2 -0
- package/dist/ui/__tests__/autosave-simple.test.d.ts.map +1 -0
- package/dist/ui/__tests__/autosave-simple.test.js +29 -0
- package/dist/ui/__tests__/autosave-simple.test.js.map +1 -0
- package/dist/ui/__tests__/setup.d.ts +2 -0
- package/dist/ui/__tests__/setup.d.ts.map +1 -0
- package/dist/ui/__tests__/setup.js +40 -0
- package/dist/ui/__tests__/setup.js.map +1 -0
- package/dist/ui/__tests__/smoke-test.d.ts +2 -0
- package/dist/ui/__tests__/smoke-test.d.ts.map +1 -0
- package/dist/ui/__tests__/smoke-test.js +18 -0
- package/dist/ui/__tests__/smoke-test.js.map +1 -0
- package/dist/ui/__tests__/smoke-test.test.d.ts +2 -0
- package/dist/ui/__tests__/smoke-test.test.d.ts.map +1 -0
- package/dist/ui/__tests__/smoke-test.test.js +18 -0
- package/dist/ui/__tests__/smoke-test.test.js.map +1 -0
- package/dist/ui/__tests__/useKeyboardShortcuts.test.d.ts +2 -0
- package/dist/ui/__tests__/useKeyboardShortcuts.test.d.ts.map +1 -0
- package/dist/ui/__tests__/useKeyboardShortcuts.test.js +374 -0
- package/dist/ui/__tests__/useKeyboardShortcuts.test.js.map +1 -0
- package/dist/ui/components/AutosaveIndicator.d.ts +18 -0
- package/dist/ui/components/AutosaveIndicator.d.ts.map +1 -0
- package/dist/ui/components/AutosaveIndicator.js +175 -0
- package/dist/ui/components/AutosaveIndicator.js.map +1 -0
- package/dist/ui/components/UndoRedoControls.d.ts +16 -0
- package/dist/ui/components/UndoRedoControls.d.ts.map +1 -0
- package/dist/ui/components/UndoRedoControls.js +144 -0
- package/dist/ui/components/UndoRedoControls.js.map +1 -0
- package/dist/ui/hooks/useKeyboardShortcuts.d.ts +22 -0
- package/dist/ui/hooks/useKeyboardShortcuts.d.ts.map +1 -0
- package/dist/ui/hooks/useKeyboardShortcuts.js +162 -0
- package/dist/ui/hooks/useKeyboardShortcuts.js.map +1 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +14 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/types.d.ts +141 -0
- package/dist/ui/types.d.ts.map +1 -0
- package/dist/ui/types.js +51 -0
- package/dist/ui/types.js.map +1 -0
- package/examples/autosave-undo-demo.ts +306 -0
- package/examples/branching-demo-simple.ts +0 -0
- package/examples/branching-demo.ts +0 -0
- package/examples/persistence-demo.ts +84 -0
- package/examples/tsconfig.json +13 -0
- package/examples/ui-components-demo.tsx +320 -0
- package/examples/validation-demo-story.json +177 -0
- package/examples/validation-demo.js +163 -0
- package/package.json +24 -4
- package/docs/branching/PDM.md +0 -443
- package/docs/branching/RELEASE-v1.2.0.md +0 -169
package/dist/engine/core.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
/**
|