farmon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/dist/bin/farmon.js +12 -0
- package/dist/bin/farmon.js.map +1 -0
- package/dist/execute/agents/index.js +19 -0
- package/dist/execute/agents/index.js.map +1 -0
- package/dist/execute/agents/instruction-classifier-agent.js +16 -0
- package/dist/execute/agents/instruction-classifier-agent.js.map +1 -0
- package/dist/execute/agents/mutation-agent.js +272 -0
- package/dist/execute/agents/mutation-agent.js.map +1 -0
- package/dist/execute/agents/query-agent.js +118 -0
- package/dist/execute/agents/query-agent.js.map +1 -0
- package/dist/execute/helpers/analyzers.js +8 -0
- package/dist/execute/helpers/analyzers.js.map +1 -0
- package/dist/execute/helpers/ensurers.js +1053 -0
- package/dist/execute/helpers/ensurers.js.map +1 -0
- package/dist/execute/helpers/finders.js +1454 -0
- package/dist/execute/helpers/finders.js.map +1 -0
- package/dist/execute/helpers/general.js +3736 -0
- package/dist/execute/helpers/general.js.map +1 -0
- package/dist/execute/helpers/import-helpers.js +183 -0
- package/dist/execute/helpers/import-helpers.js.map +1 -0
- package/dist/execute/helpers/parsers.js +840 -0
- package/dist/execute/helpers/parsers.js.map +1 -0
- package/dist/execute/helpers/prompt-maker.js +1163 -0
- package/dist/execute/helpers/prompt-maker.js.map +1 -0
- package/dist/execute/helpers/validators.js +40 -0
- package/dist/execute/helpers/validators.js.map +1 -0
- package/dist/execute/history/history-manager.js +1030 -0
- package/dist/execute/history/history-manager.js.map +1 -0
- package/dist/execute/history/rollback-handlers.js +2524 -0
- package/dist/execute/history/rollback-handlers.js.map +1 -0
- package/dist/execute/index.js +44 -0
- package/dist/execute/index.js.map +1 -0
- package/dist/execute/llm/call.js +103 -0
- package/dist/execute/llm/call.js.map +1 -0
- package/dist/execute/tasks/ast.js +3819 -0
- package/dist/execute/tasks/ast.js.map +1 -0
- package/dist/execute/tasks/generators.js +96 -0
- package/dist/execute/tasks/generators.js.map +1 -0
- package/dist/execute/tasks/index.js +7 -0
- package/dist/execute/tasks/index.js.map +1 -0
- package/dist/execute/tasks/mutations.js +8139 -0
- package/dist/execute/tasks/mutations.js.map +1 -0
- package/dist/execute/tasks/query.js +248 -0
- package/dist/execute/tasks/query.js.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/index.js +15 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/ollama.js +40 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/openai-compatible.js +52 -0
- package/dist/providers/openai-compatible.js.map +1 -0
- package/dist/runtime/inject.js +250 -0
- package/dist/runtime/inject.js.map +1 -0
- package/dist/schemas/agent/action.schema.js +935 -0
- package/dist/schemas/agent/action.schema.js.map +1 -0
- package/dist/schemas/agent/index.js +4 -0
- package/dist/schemas/agent/index.js.map +1 -0
- package/dist/schemas/agent/llm.schema.js +16 -0
- package/dist/schemas/agent/llm.schema.js.map +1 -0
- package/dist/schemas/agent/planner.schema.js +17 -0
- package/dist/schemas/agent/planner.schema.js.map +1 -0
- package/dist/schemas/index.js +7 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/project/context.schema.js +2 -0
- package/dist/schemas/project/context.schema.js.map +1 -0
- package/dist/schemas/project/index.js +2 -0
- package/dist/schemas/project/index.js.map +1 -0
- package/dist/schemas/runtime/index.js +4 -0
- package/dist/schemas/runtime/index.js.map +1 -0
- package/dist/schemas/runtime/injector.schema.js +11 -0
- package/dist/schemas/runtime/injector.schema.js.map +1 -0
- package/dist/schemas/runtime/runtime.schema.js +73 -0
- package/dist/schemas/runtime/runtime.schema.js.map +1 -0
- package/dist/schemas/runtime/sse.schema.js +15 -0
- package/dist/schemas/runtime/sse.schema.js.map +1 -0
- package/dist/schemas/system/index.js +2 -0
- package/dist/schemas/system/index.js.map +1 -0
- package/dist/schemas/system/logger.schema.js +56 -0
- package/dist/schemas/system/logger.schema.js.map +1 -0
- package/dist/schemas/task/index.js +9 -0
- package/dist/schemas/task/index.js.map +1 -0
- package/dist/server/app-context.js +254 -0
- package/dist/server/app-context.js.map +1 -0
- package/dist/server/config.js +22 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/error.js +22 -0
- package/dist/server/error.js.map +1 -0
- package/dist/server/event-bus.js +60 -0
- package/dist/server/event-bus.js.map +1 -0
- package/dist/server/logger.js +57 -0
- package/dist/server/logger.js.map +1 -0
- package/dist/server/run.js +265 -0
- package/dist/server/run.js.map +1 -0
- package/dist/server/sse.js +143 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/ui/assets/index-C4ydQSAw.css +2 -0
- package/dist/ui/assets/index-Dzo7S5xs.js +85 -0
- package/dist/ui/favicon.svg +1 -0
- package/dist/ui/icons.svg +24 -0
- package/dist/ui/index.html +14 -0
- package/dist/workers/prettier.js +11 -0
- package/dist/workers/prettier.js.map +1 -0
- package/package.json +114 -0
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
executeAction();
|
|
4
|
+
appendAction();
|
|
5
|
+
undo();
|
|
6
|
+
redo();
|
|
7
|
+
rollbackAction();
|
|
8
|
+
replayAction();
|
|
9
|
+
rollbackSingleOperation();
|
|
10
|
+
replaySingleOperation();
|
|
11
|
+
|
|
12
|
+
*/
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import crypto from "crypto";
|
|
16
|
+
import sse from "../../server/sse.js";
|
|
17
|
+
import rollback from "../../execute/history/rollback-handlers.js";
|
|
18
|
+
import { ERROR_CODES } from "../../schemas/index.js";
|
|
19
|
+
import { LoomaError } from "../../server/error.js";
|
|
20
|
+
/**
|
|
21
|
+
* Performs one-step undo.
|
|
22
|
+
*
|
|
23
|
+
* Steps:
|
|
24
|
+
* 1. Read undo.json.
|
|
25
|
+
* 2. Remove the latest operation from undo stack.
|
|
26
|
+
* 3. Execute rollback for that operation.
|
|
27
|
+
* 4. Read redo.json (create if missing).
|
|
28
|
+
* 5. Push the operation onto redo stack.
|
|
29
|
+
* 6. Save both files.
|
|
30
|
+
*/
|
|
31
|
+
function undo({ appContext, }) {
|
|
32
|
+
sse.emitInfo("Undoing last instruction...");
|
|
33
|
+
// ----------------------------------------------------------
|
|
34
|
+
// STEP 1:
|
|
35
|
+
// Resolve paths.
|
|
36
|
+
// ----------------------------------------------------------
|
|
37
|
+
const { undoPath, redoPath } = appContext.project;
|
|
38
|
+
// const undoPath = appContext.project.undoPath
|
|
39
|
+
// const redoPath = path.resolve(paths.redo());
|
|
40
|
+
// ----------------------------------------------------------
|
|
41
|
+
// STEP 2:
|
|
42
|
+
// If undo stack doesn't exist, there is nothing to undo.
|
|
43
|
+
// ----------------------------------------------------------
|
|
44
|
+
if (!fs.existsSync(undoPath)) {
|
|
45
|
+
throw new LoomaError(ERROR_CODES.FILE_NOT_FOUND, "Undo history does not exist");
|
|
46
|
+
// return {
|
|
47
|
+
// success: false,
|
|
48
|
+
// message: "Undo history does not exist",
|
|
49
|
+
// };
|
|
50
|
+
}
|
|
51
|
+
// ----------------------------------------------------------
|
|
52
|
+
// STEP 3:
|
|
53
|
+
// Read undo stack.
|
|
54
|
+
// ----------------------------------------------------------
|
|
55
|
+
const undoStack = JSON.parse(fs.readFileSync(undoPath, "utf8"));
|
|
56
|
+
// ----------------------------------------------------------
|
|
57
|
+
// STEP 4:
|
|
58
|
+
// If stack is empty, nothing to undo.
|
|
59
|
+
// ----------------------------------------------------------
|
|
60
|
+
if (undoStack.length === 0) {
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
message: "Nothing to undo, Stack is clean.",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// ----------------------------------------------------------
|
|
67
|
+
// STEP 5:
|
|
68
|
+
// Remove latest operation.
|
|
69
|
+
// pop() behaves like a stack.
|
|
70
|
+
// ----------------------------------------------------------
|
|
71
|
+
const action = undoStack.pop();
|
|
72
|
+
// here const logId = undoStack.pop()
|
|
73
|
+
// get logs from ./logs/logid.json
|
|
74
|
+
// ----------------------------------------------------------
|
|
75
|
+
// STEP 6:
|
|
76
|
+
// Save updated undo stack.
|
|
77
|
+
// ----------------------------------------------------------
|
|
78
|
+
fs.writeFileSync(undoPath, JSON.stringify(undoStack, null, 2));
|
|
79
|
+
// ----------------------------------------------------------
|
|
80
|
+
// STEP 7:
|
|
81
|
+
// Execute rollback.
|
|
82
|
+
// rollbackOperation() should perform the inverse
|
|
83
|
+
// of the original operation.
|
|
84
|
+
// ----------------------------------------------------------
|
|
85
|
+
for (const operation of action.operations) {
|
|
86
|
+
const taskName = operation.task;
|
|
87
|
+
const rollbackHandler = rollback.undoHandlers[taskName];
|
|
88
|
+
if (!rollbackHandler) {
|
|
89
|
+
throw new LoomaError(ERROR_CODES.FUNCTION_NOT_FOUND, `Rollback handler missing for task: ${taskName}`);
|
|
90
|
+
}
|
|
91
|
+
rollbackHandler(operation, appContext);
|
|
92
|
+
}
|
|
93
|
+
// ----------------------------------------------------------
|
|
94
|
+
// STEP 8:
|
|
95
|
+
// Ensure redo.json exists.
|
|
96
|
+
// ----------------------------------------------------------
|
|
97
|
+
if (!fs.existsSync(redoPath)) {
|
|
98
|
+
fs.mkdirSync(path.dirname(redoPath), {
|
|
99
|
+
recursive: true,
|
|
100
|
+
});
|
|
101
|
+
fs.writeFileSync(redoPath, "[]");
|
|
102
|
+
}
|
|
103
|
+
// ----------------------------------------------------------
|
|
104
|
+
// STEP 9:
|
|
105
|
+
// Read redo stack.
|
|
106
|
+
// ----------------------------------------------------------
|
|
107
|
+
const redoStack = JSON.parse(fs.readFileSync(redoPath, "utf8"));
|
|
108
|
+
// ----------------------------------------------------------
|
|
109
|
+
// STEP 10:
|
|
110
|
+
// Push the undone operation onto redo stack.
|
|
111
|
+
// ----------------------------------------------------------
|
|
112
|
+
redoStack.push(action);
|
|
113
|
+
// ----------------------------------------------------------
|
|
114
|
+
// STEP 11:
|
|
115
|
+
// Persist redo stack.
|
|
116
|
+
// ----------------------------------------------------------
|
|
117
|
+
fs.writeFileSync(redoPath, JSON.stringify(redoStack, null, 2));
|
|
118
|
+
// ----------------------------------------------------------
|
|
119
|
+
// STEP 12:
|
|
120
|
+
// Return success.
|
|
121
|
+
// ----------------------------------------------------------
|
|
122
|
+
return {
|
|
123
|
+
success: true,
|
|
124
|
+
message: `Undo complete for this instruction: ${action.command}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Performs one-step redo.
|
|
129
|
+
*
|
|
130
|
+
* Steps:
|
|
131
|
+
* 1. Read redo stack.
|
|
132
|
+
* 2. Remove the latest operation.
|
|
133
|
+
* 3. Re-execute the original task.
|
|
134
|
+
* 4. Push the operation back onto undo stack.
|
|
135
|
+
* 5. Save both files.
|
|
136
|
+
*/
|
|
137
|
+
function redo({ appContext, }) {
|
|
138
|
+
sse.emitInfo("Re Appling last instruction...");
|
|
139
|
+
// ----------------------------------------------------------
|
|
140
|
+
// STEP 1:
|
|
141
|
+
// Resolve paths to undo.json and redo.json.
|
|
142
|
+
// ----------------------------------------------------------
|
|
143
|
+
const { undoPath, redoPath } = appContext.project;
|
|
144
|
+
// const undoPath = path.resolve(LOOMA_UNDO_PATH);
|
|
145
|
+
// const redoPath = path.resolve(LOOMA_REDO_PATH);
|
|
146
|
+
// ----------------------------------------------------------
|
|
147
|
+
// STEP 2:
|
|
148
|
+
// If redo stack does not exist, there is nothing to redo.
|
|
149
|
+
// ----------------------------------------------------------
|
|
150
|
+
if (!fs.existsSync(redoPath)) {
|
|
151
|
+
throw new LoomaError(ERROR_CODES.FILE_NOT_FOUND, "Redo history does not exist");
|
|
152
|
+
// return {
|
|
153
|
+
// success: false,
|
|
154
|
+
// message: "Redo history does not exist",
|
|
155
|
+
// };
|
|
156
|
+
}
|
|
157
|
+
// ----------------------------------------------------------
|
|
158
|
+
// STEP 3:
|
|
159
|
+
// Read redo stack.
|
|
160
|
+
// ----------------------------------------------------------
|
|
161
|
+
const redoStack = JSON.parse(fs.readFileSync(redoPath, "utf8"));
|
|
162
|
+
// ----------------------------------------------------------
|
|
163
|
+
// STEP 4:
|
|
164
|
+
// If redo stack is empty, nothing to redo.
|
|
165
|
+
// ----------------------------------------------------------
|
|
166
|
+
if (redoStack.length === 0) {
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
message: "Nothing to redo",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// ----------------------------------------------------------
|
|
173
|
+
// STEP 5:
|
|
174
|
+
// Remove the latest operation from redo stack.
|
|
175
|
+
// ----------------------------------------------------------
|
|
176
|
+
const action = redoStack.pop();
|
|
177
|
+
// ----------------------------------------------------------
|
|
178
|
+
// STEP 6:
|
|
179
|
+
// Re-execute the original task.
|
|
180
|
+
//
|
|
181
|
+
// IMPORTANT:
|
|
182
|
+
// This execution should NOT append a new operation
|
|
183
|
+
// into undo history because we are merely replaying
|
|
184
|
+
// an existing operation.
|
|
185
|
+
// ----------------------------------------------------------
|
|
186
|
+
// ----------------------------------------------------------
|
|
187
|
+
// STEP 7:
|
|
188
|
+
// Execute redo.
|
|
189
|
+
// ----------------------------------------------------------
|
|
190
|
+
for (const operation of action.operations) {
|
|
191
|
+
const taskName = operation.task;
|
|
192
|
+
const redoHandler = rollback.redoHandlers[taskName];
|
|
193
|
+
if (!redoHandler) {
|
|
194
|
+
throw new LoomaError(ERROR_CODES.FUNCTION_NOT_FOUND, `Redo handler missing for task: ${taskName}`);
|
|
195
|
+
}
|
|
196
|
+
redoHandler(operation, appContext);
|
|
197
|
+
}
|
|
198
|
+
// const result = await executeAction({
|
|
199
|
+
// actions: action.operations,
|
|
200
|
+
// componentContext,
|
|
201
|
+
// });
|
|
202
|
+
// ----------------------------------------------------------
|
|
203
|
+
// STEP 7:
|
|
204
|
+
// If redo failed, restore redo stack and abort.
|
|
205
|
+
// ----------------------------------------------------------
|
|
206
|
+
// if (!result.success) {
|
|
207
|
+
// redoStack.push(action);
|
|
208
|
+
// fs.writeFileSync(redoPath, JSON.stringify(redoStack, null, 2));
|
|
209
|
+
// return {
|
|
210
|
+
// success: false,
|
|
211
|
+
// action,
|
|
212
|
+
// };
|
|
213
|
+
// }
|
|
214
|
+
// ----------------------------------------------------------
|
|
215
|
+
// STEP 8:
|
|
216
|
+
// Persist updated redo stack.
|
|
217
|
+
// ----------------------------------------------------------
|
|
218
|
+
fs.writeFileSync(redoPath, JSON.stringify(redoStack, null, 2));
|
|
219
|
+
// ----------------------------------------------------------
|
|
220
|
+
// STEP 9:
|
|
221
|
+
// Ensure undo stack exists.
|
|
222
|
+
// ----------------------------------------------------------
|
|
223
|
+
if (!fs.existsSync(undoPath)) {
|
|
224
|
+
fs.mkdirSync(path.dirname(undoPath), {
|
|
225
|
+
recursive: true,
|
|
226
|
+
});
|
|
227
|
+
fs.writeFileSync(undoPath, "[]");
|
|
228
|
+
}
|
|
229
|
+
// ----------------------------------------------------------
|
|
230
|
+
// STEP 10:
|
|
231
|
+
// Read undo stack.
|
|
232
|
+
// ----------------------------------------------------------
|
|
233
|
+
const undoStack = JSON.parse(fs.readFileSync(undoPath, "utf8"));
|
|
234
|
+
// ----------------------------------------------------------
|
|
235
|
+
// STEP 11:
|
|
236
|
+
// Push the operation back onto undo stack.
|
|
237
|
+
// ----------------------------------------------------------
|
|
238
|
+
undoStack.push(action);
|
|
239
|
+
// ----------------------------------------------------------
|
|
240
|
+
// STEP 12:
|
|
241
|
+
// Persist undo stack.
|
|
242
|
+
// ----------------------------------------------------------
|
|
243
|
+
fs.writeFileSync(undoPath, JSON.stringify(undoStack, null, 2));
|
|
244
|
+
// ----------------------------------------------------------
|
|
245
|
+
// STEP 13:
|
|
246
|
+
// Return success.
|
|
247
|
+
// ----------------------------------------------------------
|
|
248
|
+
return {
|
|
249
|
+
success: true,
|
|
250
|
+
message: `Re applied this instruction: ${action.command}`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Creates a new Looma operation log session.
|
|
255
|
+
*
|
|
256
|
+
* ------------------------------------------------------------
|
|
257
|
+
* WHY THIS FUNCTION EXISTS
|
|
258
|
+
* ------------------------------------------------------------
|
|
259
|
+
*
|
|
260
|
+
* Every mutation Looma performs should be traceable
|
|
261
|
+
* and reversible.
|
|
262
|
+
*
|
|
263
|
+
* Instead of storing all mutations in memory,
|
|
264
|
+
* Looma persists them into session log files.
|
|
265
|
+
*
|
|
266
|
+
* Each session gets its own JSONL file:
|
|
267
|
+
*
|
|
268
|
+
* .looma/operations/session_xxx.jsonl
|
|
269
|
+
*
|
|
270
|
+
* JSONL = one JSON object per line.
|
|
271
|
+
*
|
|
272
|
+
* This architecture is:
|
|
273
|
+
* - append friendly
|
|
274
|
+
* - crash resistant
|
|
275
|
+
* - scalable
|
|
276
|
+
* - streamable
|
|
277
|
+
* - easy to rollback
|
|
278
|
+
*
|
|
279
|
+
* ------------------------------------------------------------
|
|
280
|
+
* WHAT THIS FUNCTION DOES
|
|
281
|
+
* ------------------------------------------------------------
|
|
282
|
+
*
|
|
283
|
+
* - ensures .looma directory exists
|
|
284
|
+
* - ensures operations directory exists
|
|
285
|
+
* - creates a new session log file
|
|
286
|
+
* - writes session metadata
|
|
287
|
+
* - returns session information
|
|
288
|
+
*
|
|
289
|
+
* ------------------------------------------------------------
|
|
290
|
+
* RETURNS
|
|
291
|
+
* ------------------------------------------------------------
|
|
292
|
+
*
|
|
293
|
+
* {
|
|
294
|
+
* sessionId,
|
|
295
|
+
* logFilePath,
|
|
296
|
+
* createdAt
|
|
297
|
+
* }
|
|
298
|
+
*
|
|
299
|
+
*/
|
|
300
|
+
function createOperationLog(context) {
|
|
301
|
+
// ----------------------------------------------------------
|
|
302
|
+
// STEP 1:
|
|
303
|
+
// Generate unique session id
|
|
304
|
+
// ----------------------------------------------------------
|
|
305
|
+
//
|
|
306
|
+
// We combine:
|
|
307
|
+
// - timestamp
|
|
308
|
+
// - random bytes
|
|
309
|
+
//
|
|
310
|
+
// This avoids collisions between sessions.
|
|
311
|
+
//
|
|
312
|
+
// Example:
|
|
313
|
+
//
|
|
314
|
+
// session_1748450000_a82bc91f
|
|
315
|
+
//
|
|
316
|
+
// ----------------------------------------------------------
|
|
317
|
+
const sessionId = `session_${Date.now()}_${crypto
|
|
318
|
+
.randomBytes(4)
|
|
319
|
+
.toString("hex")}`;
|
|
320
|
+
// ----------------------------------------------------------
|
|
321
|
+
// STEP 2:
|
|
322
|
+
// Resolve .looma directory path
|
|
323
|
+
// ----------------------------------------------------------
|
|
324
|
+
//
|
|
325
|
+
// All Looma internal files should stay isolated
|
|
326
|
+
// from user application code.
|
|
327
|
+
//
|
|
328
|
+
// Example:
|
|
329
|
+
//
|
|
330
|
+
// /my-project/.looma
|
|
331
|
+
//
|
|
332
|
+
// ----------------------------------------------------------
|
|
333
|
+
const logsDirectoryPath = context.project.logsDir;
|
|
334
|
+
// ----------------------------------------------------------
|
|
335
|
+
// STEP 3:
|
|
336
|
+
// Resolve operations directory path
|
|
337
|
+
// ----------------------------------------------------------
|
|
338
|
+
//
|
|
339
|
+
// This directory stores all session logs.
|
|
340
|
+
//
|
|
341
|
+
// Example:
|
|
342
|
+
//
|
|
343
|
+
// /my-project/.looma/operations
|
|
344
|
+
//
|
|
345
|
+
// ----------------------------------------------------------
|
|
346
|
+
const operationsDirectoryPath = path.join(logsDirectoryPath, "operations");
|
|
347
|
+
// ----------------------------------------------------------
|
|
348
|
+
// STEP 4:
|
|
349
|
+
// Ensure .looma directory exists
|
|
350
|
+
// ----------------------------------------------------------
|
|
351
|
+
//
|
|
352
|
+
// mkdirSync with recursive:true safely creates:
|
|
353
|
+
//
|
|
354
|
+
// - parent directories
|
|
355
|
+
// - nested directories
|
|
356
|
+
//
|
|
357
|
+
// and does nothing if already existing.
|
|
358
|
+
//
|
|
359
|
+
// ----------------------------------------------------------
|
|
360
|
+
fs.mkdirSync(logsDirectoryPath, {
|
|
361
|
+
recursive: true,
|
|
362
|
+
});
|
|
363
|
+
// ----------------------------------------------------------
|
|
364
|
+
// STEP 5:
|
|
365
|
+
// Ensure operations directory exists
|
|
366
|
+
// ----------------------------------------------------------
|
|
367
|
+
fs.mkdirSync(operationsDirectoryPath, {
|
|
368
|
+
recursive: true,
|
|
369
|
+
});
|
|
370
|
+
// ----------------------------------------------------------
|
|
371
|
+
// STEP 6:
|
|
372
|
+
// Create session log file path
|
|
373
|
+
// ----------------------------------------------------------
|
|
374
|
+
//
|
|
375
|
+
// Example:
|
|
376
|
+
//
|
|
377
|
+
// session_1748450000_abcd1234.jsonl
|
|
378
|
+
//
|
|
379
|
+
// ----------------------------------------------------------
|
|
380
|
+
const logFilePath = path.join(operationsDirectoryPath, `${sessionId}.jsonl`);
|
|
381
|
+
// ----------------------------------------------------------
|
|
382
|
+
// STEP 7:
|
|
383
|
+
// Create initial session metadata object
|
|
384
|
+
// ----------------------------------------------------------
|
|
385
|
+
//
|
|
386
|
+
// This acts as the first entry in the log.
|
|
387
|
+
//
|
|
388
|
+
// Useful later for:
|
|
389
|
+
// - debugging
|
|
390
|
+
// - recovery
|
|
391
|
+
// - analytics
|
|
392
|
+
// - session restoration
|
|
393
|
+
//
|
|
394
|
+
// ----------------------------------------------------------
|
|
395
|
+
const sessionMetadata = {
|
|
396
|
+
type: "SESSION_START",
|
|
397
|
+
sessionId,
|
|
398
|
+
createdAt: new Date().toISOString(),
|
|
399
|
+
version: 1,
|
|
400
|
+
};
|
|
401
|
+
// ----------------------------------------------------------
|
|
402
|
+
// STEP 8:
|
|
403
|
+
// Write metadata as first JSONL line
|
|
404
|
+
// ----------------------------------------------------------
|
|
405
|
+
//
|
|
406
|
+
// JSONL format:
|
|
407
|
+
//
|
|
408
|
+
// {"type":"SESSION_START",...}
|
|
409
|
+
//
|
|
410
|
+
// Every line is independent JSON.
|
|
411
|
+
//
|
|
412
|
+
// We append newline at end because:
|
|
413
|
+
//
|
|
414
|
+
// - easier streaming
|
|
415
|
+
// - easier appending
|
|
416
|
+
// - easier parsing
|
|
417
|
+
//
|
|
418
|
+
// ----------------------------------------------------------
|
|
419
|
+
fs.writeFileSync(logFilePath, JSON.stringify(sessionMetadata) + "\n", "utf8");
|
|
420
|
+
// ----------------------------------------------------------
|
|
421
|
+
// STEP 9:
|
|
422
|
+
// Return session information
|
|
423
|
+
// ----------------------------------------------------------
|
|
424
|
+
return {
|
|
425
|
+
sessionId,
|
|
426
|
+
logFilePath,
|
|
427
|
+
createdAt: sessionMetadata.createdAt,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Appends a mutation operation into current session log.
|
|
432
|
+
*
|
|
433
|
+
* ------------------------------------------------------------
|
|
434
|
+
* PARAMS
|
|
435
|
+
* ------------------------------------------------------------
|
|
436
|
+
*
|
|
437
|
+
* @param {Object} params
|
|
438
|
+
*
|
|
439
|
+
* @param {string} params.logFilePath
|
|
440
|
+
* Absolute path of current session JSONL log file.
|
|
441
|
+
*
|
|
442
|
+
* Example:
|
|
443
|
+
* /project/.looma/operations/session_xxx.jsonl
|
|
444
|
+
*
|
|
445
|
+
* @param {string} params.taskName
|
|
446
|
+
* Name of executed task.
|
|
447
|
+
*
|
|
448
|
+
* Example:
|
|
449
|
+
* - updateStyles
|
|
450
|
+
* - insertJSX
|
|
451
|
+
* - renameComponent
|
|
452
|
+
*
|
|
453
|
+
* @param {Object} params.target
|
|
454
|
+
* Information about mutation target.
|
|
455
|
+
*
|
|
456
|
+
* Structure depends on task type.
|
|
457
|
+
*
|
|
458
|
+
* Example:
|
|
459
|
+
* {
|
|
460
|
+
* cssPath: "/src/Header.css"
|
|
461
|
+
* }
|
|
462
|
+
*
|
|
463
|
+
* @param {Object} params.before
|
|
464
|
+
* Snapshot BEFORE mutation.
|
|
465
|
+
*
|
|
466
|
+
* Structure depends on task type.
|
|
467
|
+
*
|
|
468
|
+
* Example:
|
|
469
|
+
* {
|
|
470
|
+
* cssCode: ".header { color:red; }"
|
|
471
|
+
* }
|
|
472
|
+
*
|
|
473
|
+
* @param {Object} params.after
|
|
474
|
+
* Snapshot AFTER mutation.
|
|
475
|
+
*
|
|
476
|
+
* Structure depends on task type.
|
|
477
|
+
*
|
|
478
|
+
* Example:
|
|
479
|
+
* {
|
|
480
|
+
* cssCode: ".header { color:blue; }"
|
|
481
|
+
* }
|
|
482
|
+
*
|
|
483
|
+
* @param {Object} params.metadata
|
|
484
|
+
* Additional optional debugging metadata.
|
|
485
|
+
*
|
|
486
|
+
* Example:
|
|
487
|
+
* {
|
|
488
|
+
* plannerTaskId,
|
|
489
|
+
* executionTime,
|
|
490
|
+
* triggeredBy
|
|
491
|
+
* }
|
|
492
|
+
*/
|
|
493
|
+
function appendOperation({ logFilePath, taskName, target, before, after, metadata = {}, }) {
|
|
494
|
+
// ----------------------------------------------------------
|
|
495
|
+
// STEP 1:
|
|
496
|
+
// Generate unique operation id
|
|
497
|
+
// ----------------------------------------------------------
|
|
498
|
+
//
|
|
499
|
+
// Each operation should be independently identifiable.
|
|
500
|
+
//
|
|
501
|
+
// Useful later for:
|
|
502
|
+
// - rollback
|
|
503
|
+
// - debugging
|
|
504
|
+
// - tracing
|
|
505
|
+
// - operation references
|
|
506
|
+
//
|
|
507
|
+
// ----------------------------------------------------------
|
|
508
|
+
const operationId = `op_${Date.now()}_${crypto
|
|
509
|
+
.randomBytes(4)
|
|
510
|
+
.toString("hex")}`;
|
|
511
|
+
// ----------------------------------------------------------
|
|
512
|
+
// STEP 2:
|
|
513
|
+
// Create operation object
|
|
514
|
+
// ----------------------------------------------------------
|
|
515
|
+
//
|
|
516
|
+
// This object represents one mutation event.
|
|
517
|
+
//
|
|
518
|
+
// Example:
|
|
519
|
+
//
|
|
520
|
+
// {
|
|
521
|
+
// id,
|
|
522
|
+
// taskName,
|
|
523
|
+
// before,
|
|
524
|
+
// after
|
|
525
|
+
// }
|
|
526
|
+
//
|
|
527
|
+
// ----------------------------------------------------------
|
|
528
|
+
const operation = {
|
|
529
|
+
id: operationId,
|
|
530
|
+
type: "OPERATION",
|
|
531
|
+
timestamp: new Date().toISOString(),
|
|
532
|
+
taskName,
|
|
533
|
+
target,
|
|
534
|
+
before,
|
|
535
|
+
after,
|
|
536
|
+
metadata,
|
|
537
|
+
};
|
|
538
|
+
// ----------------------------------------------------------
|
|
539
|
+
// STEP 3:
|
|
540
|
+
// Convert operation into JSONL line
|
|
541
|
+
// ----------------------------------------------------------
|
|
542
|
+
//
|
|
543
|
+
// JSONL requires:
|
|
544
|
+
//
|
|
545
|
+
// one JSON object per line
|
|
546
|
+
//
|
|
547
|
+
// ----------------------------------------------------------
|
|
548
|
+
const operationLine = JSON.stringify(operation) + "\n";
|
|
549
|
+
// ----------------------------------------------------------
|
|
550
|
+
// STEP 4:
|
|
551
|
+
// Append operation into session file
|
|
552
|
+
// ----------------------------------------------------------
|
|
553
|
+
//
|
|
554
|
+
// appendFileSync adds operation
|
|
555
|
+
// without overwriting previous logs.
|
|
556
|
+
//
|
|
557
|
+
// ----------------------------------------------------------
|
|
558
|
+
console.log("Appending operation: ", taskName, operationId);
|
|
559
|
+
fs.appendFileSync(logFilePath, operationLine, "utf8");
|
|
560
|
+
// ----------------------------------------------------------
|
|
561
|
+
// STEP 5:
|
|
562
|
+
// Return appended operation
|
|
563
|
+
// ----------------------------------------------------------
|
|
564
|
+
return operation;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Rolls back a single Looma operation.
|
|
568
|
+
*
|
|
569
|
+
* ------------------------------------------------------------
|
|
570
|
+
* WHY THIS FUNCTION EXISTS
|
|
571
|
+
* ------------------------------------------------------------
|
|
572
|
+
*
|
|
573
|
+
* AI generated mutations are never guaranteed to be correct.
|
|
574
|
+
*
|
|
575
|
+
* Even with:
|
|
576
|
+
* - planners
|
|
577
|
+
* - validators
|
|
578
|
+
* - AST mutations
|
|
579
|
+
*
|
|
580
|
+
* mistakes can still happen.
|
|
581
|
+
*
|
|
582
|
+
* This function provides safety by restoring
|
|
583
|
+
* the system to its previous state.
|
|
584
|
+
*
|
|
585
|
+
* ------------------------------------------------------------
|
|
586
|
+
* HOW ROLLBACK WORKS
|
|
587
|
+
* ------------------------------------------------------------
|
|
588
|
+
*
|
|
589
|
+
* Every mutation operation stores:
|
|
590
|
+
*
|
|
591
|
+
* {
|
|
592
|
+
* before: ...
|
|
593
|
+
* after: ...
|
|
594
|
+
* }
|
|
595
|
+
*
|
|
596
|
+
* Rollback simply restores:
|
|
597
|
+
*
|
|
598
|
+
* BEFORE STATE
|
|
599
|
+
*
|
|
600
|
+
* ------------------------------------------------------------
|
|
601
|
+
* IMPORTANT ARCHITECTURE DECISION
|
|
602
|
+
* ------------------------------------------------------------
|
|
603
|
+
*
|
|
604
|
+
* Rollback logic is TASK SPECIFIC.
|
|
605
|
+
*
|
|
606
|
+
* Example:
|
|
607
|
+
*
|
|
608
|
+
* updateStyles rollback:
|
|
609
|
+
* restore old cssCode
|
|
610
|
+
*
|
|
611
|
+
* moveFile rollback:
|
|
612
|
+
* move file back
|
|
613
|
+
*
|
|
614
|
+
* renameComponent rollback:
|
|
615
|
+
* restore old component name
|
|
616
|
+
*
|
|
617
|
+
* Therefore:
|
|
618
|
+
*
|
|
619
|
+
* rollbackHandlers map task types
|
|
620
|
+
* to rollback implementations.
|
|
621
|
+
*
|
|
622
|
+
* ------------------------------------------------------------
|
|
623
|
+
* WHAT THIS FUNCTION DOES
|
|
624
|
+
* ------------------------------------------------------------
|
|
625
|
+
*
|
|
626
|
+
* - finds operation
|
|
627
|
+
* - validates rollback support
|
|
628
|
+
* - calls task-specific rollback handler
|
|
629
|
+
* - appends rollback entry into operation log
|
|
630
|
+
*
|
|
631
|
+
* ------------------------------------------------------------
|
|
632
|
+
* PARAMS
|
|
633
|
+
* ------------------------------------------------------------
|
|
634
|
+
*
|
|
635
|
+
* @param {Object} params
|
|
636
|
+
*
|
|
637
|
+
* @param {Object} params.operation
|
|
638
|
+
* Previously logged operation object.
|
|
639
|
+
*
|
|
640
|
+
* Example:
|
|
641
|
+
* {
|
|
642
|
+
* id,
|
|
643
|
+
* taskName,
|
|
644
|
+
* before,
|
|
645
|
+
* after
|
|
646
|
+
* }
|
|
647
|
+
*
|
|
648
|
+
* @param {Object} params.rollbackHandlers
|
|
649
|
+
* Map of rollback handlers by task name.
|
|
650
|
+
*
|
|
651
|
+
* Example:
|
|
652
|
+
* {
|
|
653
|
+
* updateStyles: fn,
|
|
654
|
+
* moveFile: fn,
|
|
655
|
+
* renameComponent: fn
|
|
656
|
+
* }
|
|
657
|
+
*
|
|
658
|
+
* Every task type has different rollback logic.
|
|
659
|
+
*
|
|
660
|
+
* @param {string} params.logFilePath
|
|
661
|
+
* Absolute path of current session JSONL log file.
|
|
662
|
+
*
|
|
663
|
+
* Used for appending rollback history entry.
|
|
664
|
+
*
|
|
665
|
+
* ------------------------------------------------------------
|
|
666
|
+
* RETURNS
|
|
667
|
+
* ------------------------------------------------------------
|
|
668
|
+
*
|
|
669
|
+
* {
|
|
670
|
+
* success: boolean,
|
|
671
|
+
* operationId,
|
|
672
|
+
* taskName
|
|
673
|
+
* }
|
|
674
|
+
*
|
|
675
|
+
*/
|
|
676
|
+
function rollbackOperation({ operation, rollbackHandlers, }) {
|
|
677
|
+
// ----------------------------------------------------------
|
|
678
|
+
// STEP 1:
|
|
679
|
+
// Validate operation existence
|
|
680
|
+
// ----------------------------------------------------------
|
|
681
|
+
//
|
|
682
|
+
// Rollback cannot happen without operation data.
|
|
683
|
+
//
|
|
684
|
+
// ----------------------------------------------------------
|
|
685
|
+
if (!operation) {
|
|
686
|
+
throw new LoomaError(ERROR_CODES.PAYLOAD_ERROR, "Operation is required for rollback.");
|
|
687
|
+
}
|
|
688
|
+
// ----------------------------------------------------------
|
|
689
|
+
// STEP 2:
|
|
690
|
+
// Extract task name
|
|
691
|
+
// ----------------------------------------------------------
|
|
692
|
+
//
|
|
693
|
+
// Example:
|
|
694
|
+
//
|
|
695
|
+
// updateStyles
|
|
696
|
+
// moveFile
|
|
697
|
+
// renameComponent
|
|
698
|
+
//
|
|
699
|
+
// ----------------------------------------------------------
|
|
700
|
+
const taskName = operation.task;
|
|
701
|
+
// ----------------------------------------------------------
|
|
702
|
+
// STEP 3:
|
|
703
|
+
// Resolve rollback handler
|
|
704
|
+
// ----------------------------------------------------------
|
|
705
|
+
//
|
|
706
|
+
// Every task has different rollback logic.
|
|
707
|
+
//
|
|
708
|
+
// Example:
|
|
709
|
+
//
|
|
710
|
+
// rollbackHandlers.updateStyles
|
|
711
|
+
//
|
|
712
|
+
// ----------------------------------------------------------
|
|
713
|
+
const rollbackHandler = rollbackHandlers[taskName];
|
|
714
|
+
// ----------------------------------------------------------
|
|
715
|
+
// STEP 4:
|
|
716
|
+
// Ensure rollback handler exists
|
|
717
|
+
// ----------------------------------------------------------
|
|
718
|
+
//
|
|
719
|
+
// Without rollback handler,
|
|
720
|
+
// operation cannot be reverted safely.
|
|
721
|
+
//
|
|
722
|
+
// ----------------------------------------------------------
|
|
723
|
+
if (!rollbackHandler) {
|
|
724
|
+
throw new LoomaError(ERROR_CODES.FUNCTION_NOT_FOUND, `Rollback handler missing for task: ${taskName}`);
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
// ----------------------------------------------------------
|
|
728
|
+
// STEP 5:
|
|
729
|
+
// Execute rollback handler
|
|
730
|
+
// ----------------------------------------------------------
|
|
731
|
+
//
|
|
732
|
+
// Handler restores BEFORE state.
|
|
733
|
+
//
|
|
734
|
+
// Example:
|
|
735
|
+
//
|
|
736
|
+
// restore previous CSS
|
|
737
|
+
// restore deleted file
|
|
738
|
+
// restore previous JSX
|
|
739
|
+
//
|
|
740
|
+
// ----------------------------------------------------------
|
|
741
|
+
rollbackHandler(operation);
|
|
742
|
+
// ----------------------------------------------------------
|
|
743
|
+
// STEP 6:
|
|
744
|
+
// Create rollback log entry
|
|
745
|
+
// ----------------------------------------------------------
|
|
746
|
+
//
|
|
747
|
+
// Rollback itself should also be logged.
|
|
748
|
+
//
|
|
749
|
+
// This is important for:
|
|
750
|
+
//
|
|
751
|
+
// - debugging
|
|
752
|
+
// - history tracking
|
|
753
|
+
// - redo support
|
|
754
|
+
// - future time-travel systems
|
|
755
|
+
//
|
|
756
|
+
// ----------------------------------------------------------
|
|
757
|
+
// const rollbackEntry = {
|
|
758
|
+
// type: "ROLLBACK",
|
|
759
|
+
// rolledBackOperationId: operation.id,
|
|
760
|
+
// taskName,
|
|
761
|
+
// timestamp: new Date().toISOString(),
|
|
762
|
+
// };
|
|
763
|
+
// ----------------------------------------------------------
|
|
764
|
+
// STEP 7:
|
|
765
|
+
// Append rollback entry into session log
|
|
766
|
+
// ----------------------------------------------------------
|
|
767
|
+
//
|
|
768
|
+
// JSONL format:
|
|
769
|
+
//
|
|
770
|
+
// one JSON object per line
|
|
771
|
+
//
|
|
772
|
+
// ----------------------------------------------------------
|
|
773
|
+
// fs.appendFileSync(
|
|
774
|
+
// logFilePath,
|
|
775
|
+
// JSON.stringify(rollbackEntry) + "\n",
|
|
776
|
+
// "utf8"
|
|
777
|
+
// );
|
|
778
|
+
// ----------------------------------------------------------
|
|
779
|
+
// STEP 8:
|
|
780
|
+
// Return rollback result
|
|
781
|
+
// ----------------------------------------------------------
|
|
782
|
+
return {
|
|
783
|
+
success: true,
|
|
784
|
+
taskName,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
catch (error) {
|
|
788
|
+
return {
|
|
789
|
+
success: false,
|
|
790
|
+
taskName,
|
|
791
|
+
error,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Rolls back all operations AFTER a target operation.
|
|
797
|
+
*
|
|
798
|
+
* ------------------------------------------------------------
|
|
799
|
+
* WHY THIS FUNCTION EXISTS
|
|
800
|
+
* ------------------------------------------------------------
|
|
801
|
+
*
|
|
802
|
+
* rollbackOperation() reverts only ONE operation.
|
|
803
|
+
*
|
|
804
|
+
* But sometimes we need to restore the project
|
|
805
|
+
* to an earlier stable checkpoint.
|
|
806
|
+
*
|
|
807
|
+
* Example:
|
|
808
|
+
*
|
|
809
|
+
* op1 -> createComponent
|
|
810
|
+
* op2 -> updateStyles
|
|
811
|
+
* op3 -> insertJSX
|
|
812
|
+
* op4 -> renameComponent
|
|
813
|
+
* op5 -> moveFile
|
|
814
|
+
*
|
|
815
|
+
* rollbackToOperation(op2)
|
|
816
|
+
*
|
|
817
|
+
* Result:
|
|
818
|
+
* - op5 reverted
|
|
819
|
+
* - op4 reverted
|
|
820
|
+
* - op3 reverted
|
|
821
|
+
*
|
|
822
|
+
* Final project state becomes exactly as it was
|
|
823
|
+
* after op2 execution.
|
|
824
|
+
*
|
|
825
|
+
* ------------------------------------------------------------
|
|
826
|
+
* IMPORTANT
|
|
827
|
+
* ------------------------------------------------------------
|
|
828
|
+
*
|
|
829
|
+
* Rollback must happen in REVERSE ORDER.
|
|
830
|
+
*
|
|
831
|
+
* Example:
|
|
832
|
+
*
|
|
833
|
+
* Original order:
|
|
834
|
+
* op1 -> op2 -> op3
|
|
835
|
+
*
|
|
836
|
+
* Rollback order:
|
|
837
|
+
* op3 -> op2 -> op1
|
|
838
|
+
*
|
|
839
|
+
* This is critical because later operations
|
|
840
|
+
* may depend on earlier mutations.
|
|
841
|
+
*
|
|
842
|
+
* ------------------------------------------------------------
|
|
843
|
+
* PARAMS
|
|
844
|
+
* ------------------------------------------------------------
|
|
845
|
+
*
|
|
846
|
+
* @param {Object} params
|
|
847
|
+
*
|
|
848
|
+
* @param {Array<Object>} params.operations
|
|
849
|
+
* Array of all session operations.
|
|
850
|
+
*
|
|
851
|
+
* Example:
|
|
852
|
+
* [
|
|
853
|
+
* operation1,
|
|
854
|
+
* operation2,
|
|
855
|
+
* operation3
|
|
856
|
+
* ]
|
|
857
|
+
*
|
|
858
|
+
* @param {string} params.targetOperationId
|
|
859
|
+
* Rollback checkpoint operation id.
|
|
860
|
+
*
|
|
861
|
+
* All operations AFTER this operation
|
|
862
|
+
* will be reverted.
|
|
863
|
+
*
|
|
864
|
+
* @param {Object} params.rollbackHandlers
|
|
865
|
+
* Map of rollback handlers by task name.
|
|
866
|
+
*
|
|
867
|
+
* Example:
|
|
868
|
+
* {
|
|
869
|
+
* updateStyles: fn,
|
|
870
|
+
* moveFile: fn
|
|
871
|
+
* }
|
|
872
|
+
*
|
|
873
|
+
* @param {string} params.logFilePath
|
|
874
|
+
* Absolute path of current session log file.
|
|
875
|
+
*
|
|
876
|
+
* Used for appending rollback history.
|
|
877
|
+
*
|
|
878
|
+
* ------------------------------------------------------------
|
|
879
|
+
* RETURNS
|
|
880
|
+
* ------------------------------------------------------------
|
|
881
|
+
*
|
|
882
|
+
* {
|
|
883
|
+
* success: boolean,
|
|
884
|
+
* rollbackCount: number,
|
|
885
|
+
* targetOperationId: string,
|
|
886
|
+
* revertedOperationIds: string[]
|
|
887
|
+
* }
|
|
888
|
+
*
|
|
889
|
+
*/
|
|
890
|
+
function rollbackToOperation({ operations, targetOperationId, rollbackHandlers, logFilePath, }) {
|
|
891
|
+
// ----------------------------------------------------------
|
|
892
|
+
// STEP 1:
|
|
893
|
+
// Validate operations list
|
|
894
|
+
// ----------------------------------------------------------
|
|
895
|
+
if (!Array.isArray(operations)) {
|
|
896
|
+
throw new LoomaError(ERROR_CODES.PAYLOAD_ERROR, "operations must be an array.");
|
|
897
|
+
}
|
|
898
|
+
// ----------------------------------------------------------
|
|
899
|
+
// STEP 2:
|
|
900
|
+
// Find checkpoint operation index
|
|
901
|
+
// ----------------------------------------------------------
|
|
902
|
+
//
|
|
903
|
+
// Example:
|
|
904
|
+
//
|
|
905
|
+
// [
|
|
906
|
+
// op1,
|
|
907
|
+
// op2,
|
|
908
|
+
// op3,
|
|
909
|
+
// op4
|
|
910
|
+
// ]
|
|
911
|
+
//
|
|
912
|
+
// target = op2
|
|
913
|
+
//
|
|
914
|
+
// index = 1
|
|
915
|
+
//
|
|
916
|
+
// ----------------------------------------------------------
|
|
917
|
+
const checkpointIndex = operations.findIndex((operation) => operation.id === targetOperationId);
|
|
918
|
+
// ----------------------------------------------------------
|
|
919
|
+
// STEP 3:
|
|
920
|
+
// Ensure checkpoint exists
|
|
921
|
+
// ----------------------------------------------------------
|
|
922
|
+
if (checkpointIndex === -1) {
|
|
923
|
+
throw new LoomaError(ERROR_CODES.ROLLBACK_ERROR, `Target operation not found: ${targetOperationId}`);
|
|
924
|
+
}
|
|
925
|
+
// ----------------------------------------------------------
|
|
926
|
+
// STEP 4:
|
|
927
|
+
// Extract operations that need rollback
|
|
928
|
+
// ----------------------------------------------------------
|
|
929
|
+
//
|
|
930
|
+
// We rollback ALL operations AFTER checkpoint.
|
|
931
|
+
//
|
|
932
|
+
// Example:
|
|
933
|
+
//
|
|
934
|
+
// checkpoint = op2
|
|
935
|
+
//
|
|
936
|
+
// rollback:
|
|
937
|
+
// op3
|
|
938
|
+
// op4
|
|
939
|
+
// op5
|
|
940
|
+
//
|
|
941
|
+
// ----------------------------------------------------------
|
|
942
|
+
const operationsToRollback = operations.slice(checkpointIndex + 1);
|
|
943
|
+
// ----------------------------------------------------------
|
|
944
|
+
// STEP 5:
|
|
945
|
+
// Reverse rollback order
|
|
946
|
+
// ----------------------------------------------------------
|
|
947
|
+
//
|
|
948
|
+
// Rollback must happen backwards.
|
|
949
|
+
//
|
|
950
|
+
// Latest mutation gets reverted first.
|
|
951
|
+
//
|
|
952
|
+
// ----------------------------------------------------------
|
|
953
|
+
operationsToRollback.reverse();
|
|
954
|
+
// ----------------------------------------------------------
|
|
955
|
+
// STEP 6:
|
|
956
|
+
// Prepare rollback tracking array
|
|
957
|
+
// ----------------------------------------------------------
|
|
958
|
+
const revertedOperationIds = [];
|
|
959
|
+
// ----------------------------------------------------------
|
|
960
|
+
// STEP 7:
|
|
961
|
+
// Rollback operations one by one
|
|
962
|
+
// ----------------------------------------------------------
|
|
963
|
+
for (const operation of operationsToRollback) {
|
|
964
|
+
// --------------------------------------------------------
|
|
965
|
+
// Resolve rollback handler
|
|
966
|
+
// --------------------------------------------------------
|
|
967
|
+
const rollbackHandler = rollbackHandlers[operation.taskName];
|
|
968
|
+
// --------------------------------------------------------
|
|
969
|
+
// Ensure rollback handler exists
|
|
970
|
+
// --------------------------------------------------------
|
|
971
|
+
if (!rollbackHandler) {
|
|
972
|
+
throw new LoomaError(ERROR_CODES.FUNCTION_NOT_FOUND, `Rollback handler missing for task: ${operation.taskName}`);
|
|
973
|
+
}
|
|
974
|
+
// --------------------------------------------------------
|
|
975
|
+
// Execute rollback
|
|
976
|
+
// --------------------------------------------------------
|
|
977
|
+
rollbackHandler(operation);
|
|
978
|
+
// --------------------------------------------------------
|
|
979
|
+
// Store reverted operation id
|
|
980
|
+
// --------------------------------------------------------
|
|
981
|
+
revertedOperationIds.push(operation.id);
|
|
982
|
+
// --------------------------------------------------------
|
|
983
|
+
// Append rollback entry into session log
|
|
984
|
+
// --------------------------------------------------------
|
|
985
|
+
const rollbackEntry = {
|
|
986
|
+
type: "ROLLBACK",
|
|
987
|
+
rolledBackOperationId: operation.id,
|
|
988
|
+
taskName: operation.taskName,
|
|
989
|
+
timestamp: new Date().toISOString(),
|
|
990
|
+
};
|
|
991
|
+
fs.appendFileSync(logFilePath, JSON.stringify(rollbackEntry) + "\n", "utf8");
|
|
992
|
+
}
|
|
993
|
+
// ----------------------------------------------------------
|
|
994
|
+
// STEP 8:
|
|
995
|
+
// Return rollback result
|
|
996
|
+
// ----------------------------------------------------------
|
|
997
|
+
return {
|
|
998
|
+
success: true,
|
|
999
|
+
rollbackCount: revertedOperationIds.length,
|
|
1000
|
+
targetOperationId,
|
|
1001
|
+
revertedOperationIds,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
function rollbackAction({ operations }) {
|
|
1005
|
+
console.log("rolling back actions");
|
|
1006
|
+
}
|
|
1007
|
+
function getOperations(logFilePath) {
|
|
1008
|
+
const content = fs.readFileSync(logFilePath, "utf8");
|
|
1009
|
+
return content
|
|
1010
|
+
.split("\n")
|
|
1011
|
+
.filter(Boolean)
|
|
1012
|
+
.map((line) => JSON.parse(line));
|
|
1013
|
+
}
|
|
1014
|
+
function getLatestOperation(logFilePath) {
|
|
1015
|
+
const operations = getOperations(logFilePath);
|
|
1016
|
+
const latestOperation = operations.at(-1);
|
|
1017
|
+
// console.log("Latest operation: ", latestOperation);
|
|
1018
|
+
return latestOperation;
|
|
1019
|
+
}
|
|
1020
|
+
export default {
|
|
1021
|
+
createOperationLog,
|
|
1022
|
+
appendOperation,
|
|
1023
|
+
rollbackOperation,
|
|
1024
|
+
rollbackToOperation,
|
|
1025
|
+
getLatestOperation,
|
|
1026
|
+
undo,
|
|
1027
|
+
redo,
|
|
1028
|
+
rollbackAction,
|
|
1029
|
+
};
|
|
1030
|
+
//# sourceMappingURL=history-manager.js.map
|