devsmind-mcp 2.3.0 → 3.0.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.
Files changed (130) hide show
  1. package/README.md +326 -234
  2. package/dist/cli/activity.d.ts +9 -0
  3. package/dist/cli/activity.js +69 -0
  4. package/dist/cli/activity.js.map +1 -0
  5. package/dist/cli/describe.d.ts +31 -0
  6. package/dist/cli/describe.js +201 -0
  7. package/dist/cli/describe.js.map +1 -0
  8. package/dist/cli/diff.d.ts +14 -0
  9. package/dist/cli/diff.js +161 -0
  10. package/dist/cli/diff.js.map +1 -0
  11. package/dist/cli/embed.d.ts +6 -0
  12. package/dist/cli/embed.js +109 -0
  13. package/dist/cli/embed.js.map +1 -0
  14. package/dist/cli/extract-agent.d.ts +66 -0
  15. package/dist/cli/extract-agent.js +192 -0
  16. package/dist/cli/extract-agent.js.map +1 -0
  17. package/dist/cli/feedback.d.ts +11 -0
  18. package/dist/cli/feedback.js +90 -0
  19. package/dist/cli/feedback.js.map +1 -0
  20. package/dist/cli/index.js +128 -5
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/init.d.ts +30 -0
  23. package/dist/cli/init.js +98 -18
  24. package/dist/cli/init.js.map +1 -1
  25. package/dist/cli/integrations/memory-topics.d.ts +44 -0
  26. package/dist/cli/integrations/memory-topics.js +295 -0
  27. package/dist/cli/integrations/memory-topics.js.map +1 -0
  28. package/dist/cli/integrations/memory.d.ts +11 -5
  29. package/dist/cli/integrations/memory.js +132 -36
  30. package/dist/cli/integrations/memory.js.map +1 -1
  31. package/dist/cli/integrations/prompt.d.ts +8 -0
  32. package/dist/cli/integrations/prompt.js +24 -3
  33. package/dist/cli/integrations/prompt.js.map +1 -1
  34. package/dist/cli/integrations/registry.d.ts +19 -5
  35. package/dist/cli/integrations/registry.js +23 -13
  36. package/dist/cli/integrations/registry.js.map +1 -1
  37. package/dist/cli/llm-client.d.ts +116 -0
  38. package/dist/cli/llm-client.js +322 -0
  39. package/dist/cli/llm-client.js.map +1 -0
  40. package/dist/cli/prune.js +4 -3
  41. package/dist/cli/prune.js.map +1 -1
  42. package/dist/cli/rule.d.ts +22 -2
  43. package/dist/cli/rule.js +116 -56
  44. package/dist/cli/rule.js.map +1 -1
  45. package/dist/cli/runner.d.ts +21 -0
  46. package/dist/cli/runner.js +94 -276
  47. package/dist/cli/runner.js.map +1 -1
  48. package/dist/cli/sync-progress.d.ts +9 -0
  49. package/dist/cli/sync-progress.js +41 -0
  50. package/dist/cli/sync-progress.js.map +1 -0
  51. package/dist/cli/sync.js +5 -2
  52. package/dist/cli/sync.js.map +1 -1
  53. package/dist/cli/view.js +3 -2
  54. package/dist/cli/view.js.map +1 -1
  55. package/dist/cli/workflow.js +26 -20
  56. package/dist/cli/workflow.js.map +1 -1
  57. package/dist/db/activity.d.ts +200 -0
  58. package/dist/db/activity.js +347 -0
  59. package/dist/db/activity.js.map +1 -0
  60. package/dist/db/analyze.js +25 -9
  61. package/dist/db/analyze.js.map +1 -1
  62. package/dist/db/database.d.ts +607 -79
  63. package/dist/db/database.js +1936 -488
  64. package/dist/db/database.js.map +1 -1
  65. package/dist/db/edges.d.ts +24 -0
  66. package/dist/db/edges.js +84 -0
  67. package/dist/db/edges.js.map +1 -1
  68. package/dist/db/embedder.d.ts +39 -0
  69. package/dist/db/embedder.js +0 -0
  70. package/dist/db/embedder.js.map +1 -0
  71. package/dist/db/feedback.d.ts +128 -0
  72. package/dist/db/feedback.js +182 -0
  73. package/dist/db/feedback.js.map +1 -0
  74. package/dist/db/file-diff.d.ts +32 -0
  75. package/dist/db/file-diff.js +110 -0
  76. package/dist/db/file-diff.js.map +1 -0
  77. package/dist/db/grep.d.ts +78 -0
  78. package/dist/db/grep.js +475 -0
  79. package/dist/db/grep.js.map +1 -0
  80. package/dist/db/message-revert.d.ts +63 -0
  81. package/dist/db/message-revert.js +258 -0
  82. package/dist/db/message-revert.js.map +1 -0
  83. package/dist/db/revert.d.ts +31 -0
  84. package/dist/db/revert.js +108 -0
  85. package/dist/db/revert.js.map +1 -0
  86. package/dist/db/schema.d.ts +49 -4
  87. package/dist/db/schema.js +125 -73
  88. package/dist/db/schema.js.map +1 -1
  89. package/dist/db/search-index.d.ts +65 -0
  90. package/dist/db/search-index.js +74 -0
  91. package/dist/db/search-index.js.map +1 -0
  92. package/dist/db/staging.d.ts +90 -5
  93. package/dist/db/staging.js +148 -22
  94. package/dist/db/staging.js.map +1 -1
  95. package/dist/mcp/server.d.ts +29 -7
  96. package/dist/mcp/server.js +2755 -745
  97. package/dist/mcp/server.js.map +1 -1
  98. package/dist/mcp/vendor/3d-force-graph.min.js +5 -0
  99. package/dist/mcp/vendor/force-graph.min.js +5 -0
  100. package/dist/mcp/vendor/model/model_int8.onnx +0 -0
  101. package/dist/mcp/vendor/model/vocab.txt +30522 -0
  102. package/dist/mcp/vendor/three.min.js +7 -0
  103. package/dist/mcp/view.css +419 -0
  104. package/dist/mcp/view.html +161 -0
  105. package/dist/mcp/view.js +245 -0
  106. package/dist/mcp/view_chat.js +382 -0
  107. package/dist/mcp/view_graph.js +576 -0
  108. package/dist/mcp/visualizer.d.ts +25 -2
  109. package/dist/mcp/visualizer.js +31 -4
  110. package/dist/mcp/visualizer.js.map +1 -1
  111. package/dist/utils/ast.d.ts +187 -0
  112. package/dist/utils/ast.js +640 -20
  113. package/dist/utils/ast.js.map +1 -1
  114. package/dist/utils/diff.d.ts +44 -0
  115. package/dist/utils/diff.js +78 -0
  116. package/dist/utils/diff.js.map +1 -0
  117. package/dist/utils/edit.d.ts +47 -0
  118. package/dist/utils/edit.js +196 -0
  119. package/dist/utils/edit.js.map +1 -0
  120. package/dist/utils/scanner.js +12 -4
  121. package/dist/utils/scanner.js.map +1 -1
  122. package/dist/utils/tokenize.d.ts +45 -0
  123. package/dist/utils/tokenize.js +129 -0
  124. package/dist/utils/tokenize.js.map +1 -0
  125. package/dist/utils/version.d.ts +14 -0
  126. package/dist/utils/version.js +61 -0
  127. package/dist/utils/version.js.map +1 -0
  128. package/package.json +18 -5
  129. package/dist/mcp/visualizer_2d.html +0 -635
  130. package/dist/mcp/visualizer_3d.html +0 -613
@@ -0,0 +1,258 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.revertMessage = revertMessage;
4
+ exports.unrevertMessage = unrevertMessage;
5
+ exports.revertMessageFile = revertMessageFile;
6
+ exports.unrevertMessageFile = unrevertMessageFile;
7
+ exports.revertMessageEdit = revertMessageEdit;
8
+ exports.unrevertMessageEdit = unrevertMessageEdit;
9
+ const activity_1 = require("./activity");
10
+ const edit_1 = require("../utils/edit");
11
+ const ast_1 = require("../utils/ast");
12
+ /** All messages, oldest-created first — the timeline order the stack model operates over. */
13
+ function timeline(devmindPath) {
14
+ return (0, activity_1.listMessages)(devmindPath).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
15
+ }
16
+ /**
17
+ * Applies one edit's `from` -> `to` text via the same exact-match, atomic-write primitive
18
+ * `edit_node` uses. A failure here means the file has drifted since this edit was recorded — a
19
+ * hand-edit, a pull, a later message's edit to the same text, anything — and is reported rather
20
+ * than forced. This exact-match requirement is also what makes file-level and single-edit revert
21
+ * safe without any separate "is this the newest" bookkeeping: an edit whose recorded text isn't
22
+ * uniquely present anymore simply can't be applied, whatever the reason.
23
+ */
24
+ function applyEdit(edit, from, to) {
25
+ if (from === to)
26
+ return { ok: true }; // a no-op edit (e.g. before === after) has nothing to write
27
+ const r = (0, edit_1.replaceTextInFile)(edit.file_path, from, to, false);
28
+ if (!r.ok)
29
+ return { ok: false, error: r.error };
30
+ (0, ast_1.invalidateParsedFile)(edit.file_path);
31
+ return { ok: true };
32
+ }
33
+ /**
34
+ * Reverts one message's edits as a single atomic unit: newest edit first (later edits to the same
35
+ * node were built on earlier ones, so they must come off in reverse), and if any edit can't be
36
+ * applied, everything from this same message that DID succeed in this attempt is rolled back
37
+ * immediately — a message is never left half-reverted by THIS call. (It can still end up
38
+ * `partial` from an earlier, separate file/edit-level revert — this function skips edits already
39
+ * in the target state rather than re-applying them, so it composes safely with that.)
40
+ */
41
+ function revertOneMessageAtomically(message) {
42
+ const newestFirst = message.edits.slice().reverse();
43
+ const applied = [];
44
+ for (const edit of newestFirst) {
45
+ if (edit.reverted)
46
+ continue; // already reverted via a granular action — nothing to do
47
+ const r = applyEdit(edit, edit.after, edit.before);
48
+ if (!r.ok) {
49
+ // Undo in LIFO order — the most recently reverted edit must come off first. `applied` was
50
+ // built newest-edit-first, so `applied[0]` was reverted first and must be un-reverted LAST;
51
+ // reversing the array puts the most recent action first, exactly as a proper undo requires.
52
+ for (const undo of applied.slice().reverse()) {
53
+ applyEdit(undo, undo.before, undo.after); // best-effort; started from a known-good state
54
+ undo.reverted = false;
55
+ }
56
+ return { ok: false, error: `${edit.node_id}: ${r.error}` };
57
+ }
58
+ edit.reverted = true;
59
+ applied.push(edit);
60
+ }
61
+ return { ok: true };
62
+ }
63
+ /** Symmetric to revertOneMessageAtomically: oldest edit first, same rollback-on-failure guarantee. */
64
+ function unrevertOneMessageAtomically(message) {
65
+ const oldestFirst = message.edits;
66
+ const applied = [];
67
+ for (const edit of oldestFirst) {
68
+ if (!edit.reverted)
69
+ continue; // already applied — nothing to restore
70
+ const r = applyEdit(edit, edit.before, edit.after);
71
+ if (!r.ok) {
72
+ for (const undo of applied.slice().reverse()) {
73
+ applyEdit(undo, undo.after, undo.before);
74
+ undo.reverted = true;
75
+ }
76
+ return { ok: false, error: `${edit.node_id}: ${r.error}` };
77
+ }
78
+ edit.reverted = false;
79
+ applied.push(edit);
80
+ }
81
+ return { ok: true };
82
+ }
83
+ /**
84
+ * Reverts `messageId` and every message after it that isn't already fully reverted — the stack
85
+ * model: later messages were built on top of earlier ones, so undoing one partway through the
86
+ * stack undoes everything above it too, newest first. Stops at the first message that can't be
87
+ * fully reverted (a drifted edit) and reports which; everything reverted before that point in
88
+ * this call stays reverted. A `partial` message (touched by an earlier file/edit-level revert)
89
+ * counts as "not yet fully reverted" and is swept up here too, finishing the job on it.
90
+ */
91
+ function revertMessage(devmindPath, messageId) {
92
+ const target = (0, activity_1.readMessage)(devmindPath, messageId);
93
+ if (!target)
94
+ return { ok: false, reverted: [], blocked_at: { message_id: messageId, reason: 'message not found' } };
95
+ if (target.status === 'reverted') {
96
+ return { ok: false, reverted: [], blocked_at: { message_id: messageId, reason: 'already reverted' } };
97
+ }
98
+ const all = timeline(devmindPath);
99
+ const targetIndex = all.findIndex(m => m.id === messageId);
100
+ // Newest-first: everything from the end of the timeline down to (and including) the target.
101
+ const candidates = all.slice(targetIndex).filter(m => m.status !== 'reverted').reverse();
102
+ const reverted = [];
103
+ for (const message of candidates) {
104
+ const result = revertOneMessageAtomically(message);
105
+ if (!result.ok) {
106
+ message.status = (0, activity_1.deriveStatus)(message.edits);
107
+ (0, activity_1.saveMessage)(devmindPath, message);
108
+ return { ok: reverted.length > 0, reverted, blocked_at: { message_id: message.id, reason: result.error } };
109
+ }
110
+ message.status = (0, activity_1.deriveStatus)(message.edits);
111
+ message.updated_at = new Date().toISOString();
112
+ (0, activity_1.saveMessage)(devmindPath, message);
113
+ reverted.push(message.id);
114
+ }
115
+ return { ok: true, reverted };
116
+ }
117
+ /**
118
+ * Re-applies exactly one message: the OLDEST currently-**fully**-reverted one. Un-revert restores
119
+ * up the stack in the order things came off it — out-of-order restoration would recreate the same
120
+ * "later edit built on code that isn't there yet" hazard the revert direction guards against, so
121
+ * it's refused rather than attempted. A `partial` message wasn't produced by that cascade — it was
122
+ * touched by a surgical file/edit-level revert — so it can be un-reverted independently of the
123
+ * fully-reverted stack's ordering.
124
+ */
125
+ function unrevertMessage(devmindPath, messageId) {
126
+ const target = (0, activity_1.readMessage)(devmindPath, messageId);
127
+ if (!target)
128
+ return { ok: false, error: 'message not found' };
129
+ if (target.status === 'applied')
130
+ return { ok: false, error: 'message is not reverted — nothing to restore' };
131
+ if (target.status === 'reverted') {
132
+ const reverted = timeline(devmindPath).filter(m => m.status === 'reverted');
133
+ const oldest = reverted[0];
134
+ /* istanbul ignore next -- unreachable: `target.status === 'reverted'` (just checked above) means
135
+ `target` itself is in `timeline(devmindPath)` with status 'reverted', so `reverted` can never
136
+ be empty here. Kept as a real guard (not just a comment) in case that invariant ever breaks. */
137
+ if (!oldest)
138
+ return { ok: false, error: 'no reverted messages found' };
139
+ if (oldest.id !== messageId) {
140
+ return {
141
+ ok: false,
142
+ error: `un-revert must restore the oldest reverted message first: "${oldest.summary}" (${oldest.id}). Restore that one before this one.`
143
+ };
144
+ }
145
+ }
146
+ const result = unrevertOneMessageAtomically(target);
147
+ if (!result.ok) {
148
+ target.status = (0, activity_1.deriveStatus)(target.edits);
149
+ (0, activity_1.saveMessage)(devmindPath, target);
150
+ return { ok: false, error: result.error };
151
+ }
152
+ target.status = (0, activity_1.deriveStatus)(target.edits);
153
+ target.updated_at = new Date().toISOString();
154
+ (0, activity_1.saveMessage)(devmindPath, target);
155
+ return { ok: true, message_id: target.id };
156
+ }
157
+ /**
158
+ * Reverts just the edits ONE file picked up within ONE message, leaving the message's other
159
+ * files untouched. Newest-edit-first within that file, atomic (rolls back this operation's own
160
+ * partial progress on failure, same as the whole-message path) — never leaves this one file
161
+ * half-reverted, though the message as a whole is expected to end up `partial`.
162
+ */
163
+ function revertMessageFile(devmindPath, messageId, filePath) {
164
+ const message = (0, activity_1.readMessage)(devmindPath, messageId);
165
+ if (!message)
166
+ return { ok: false, error: 'message not found' };
167
+ const fileEdits = message.edits.filter(e => e.file_path === filePath && !e.reverted);
168
+ if (!fileEdits.length)
169
+ return { ok: false, error: 'no applied edits for this file in this message' };
170
+ const newestFirst = fileEdits.slice().reverse();
171
+ const applied = [];
172
+ for (const edit of newestFirst) {
173
+ const r = applyEdit(edit, edit.after, edit.before);
174
+ if (!r.ok) {
175
+ for (const undo of applied.slice().reverse()) {
176
+ applyEdit(undo, undo.before, undo.after);
177
+ undo.reverted = false;
178
+ }
179
+ return { ok: false, error: `${edit.node_id}: ${r.error}` };
180
+ }
181
+ edit.reverted = true;
182
+ applied.push(edit);
183
+ }
184
+ message.status = (0, activity_1.deriveStatus)(message.edits);
185
+ message.updated_at = new Date().toISOString();
186
+ (0, activity_1.saveMessage)(devmindPath, message);
187
+ return { ok: true, edit_ids: applied.map(e => e.id) };
188
+ }
189
+ /** Symmetric to revertMessageFile: oldest-edit-first, same atomic rollback. */
190
+ function unrevertMessageFile(devmindPath, messageId, filePath) {
191
+ const message = (0, activity_1.readMessage)(devmindPath, messageId);
192
+ if (!message)
193
+ return { ok: false, error: 'message not found' };
194
+ const fileEdits = message.edits.filter(e => e.file_path === filePath && e.reverted);
195
+ if (!fileEdits.length)
196
+ return { ok: false, error: 'no reverted edits for this file in this message' };
197
+ const applied = [];
198
+ for (const edit of fileEdits) {
199
+ const r = applyEdit(edit, edit.before, edit.after);
200
+ if (!r.ok) {
201
+ for (const undo of applied.slice().reverse()) {
202
+ applyEdit(undo, undo.after, undo.before);
203
+ undo.reverted = true;
204
+ }
205
+ return { ok: false, error: `${edit.node_id}: ${r.error}` };
206
+ }
207
+ edit.reverted = false;
208
+ applied.push(edit);
209
+ }
210
+ message.status = (0, activity_1.deriveStatus)(message.edits);
211
+ message.updated_at = new Date().toISOString();
212
+ (0, activity_1.saveMessage)(devmindPath, message);
213
+ return { ok: true, edit_ids: applied.map(e => e.id) };
214
+ }
215
+ /**
216
+ * Reverts exactly ONE edit, identified by its stable `id`, regardless of what else in the message
217
+ * or file is applied. Safe by construction: if a later edit (to the same node, in this message or
218
+ * a later one) already changed this text further, this edit's `after` string won't be found on
219
+ * disk anymore and the exact-match guard in applyEdit refuses rather than guessing.
220
+ */
221
+ function revertMessageEdit(devmindPath, messageId, editId) {
222
+ const message = (0, activity_1.readMessage)(devmindPath, messageId);
223
+ if (!message)
224
+ return { ok: false, error: 'message not found' };
225
+ const edit = message.edits.find(e => e.id === editId);
226
+ if (!edit)
227
+ return { ok: false, error: 'edit not found in this message' };
228
+ if (edit.reverted)
229
+ return { ok: false, error: 'already reverted' };
230
+ const r = applyEdit(edit, edit.after, edit.before);
231
+ if (!r.ok)
232
+ return { ok: false, error: r.error };
233
+ edit.reverted = true;
234
+ message.status = (0, activity_1.deriveStatus)(message.edits);
235
+ message.updated_at = new Date().toISOString();
236
+ (0, activity_1.saveMessage)(devmindPath, message);
237
+ return { ok: true };
238
+ }
239
+ /** Symmetric to revertMessageEdit. */
240
+ function unrevertMessageEdit(devmindPath, messageId, editId) {
241
+ const message = (0, activity_1.readMessage)(devmindPath, messageId);
242
+ if (!message)
243
+ return { ok: false, error: 'message not found' };
244
+ const edit = message.edits.find(e => e.id === editId);
245
+ if (!edit)
246
+ return { ok: false, error: 'edit not found in this message' };
247
+ if (!edit.reverted)
248
+ return { ok: false, error: 'not reverted' };
249
+ const r = applyEdit(edit, edit.before, edit.after);
250
+ if (!r.ok)
251
+ return { ok: false, error: r.error };
252
+ edit.reverted = false;
253
+ message.status = (0, activity_1.deriveStatus)(message.edits);
254
+ message.updated_at = new Date().toISOString();
255
+ (0, activity_1.saveMessage)(devmindPath, message);
256
+ return { ok: true };
257
+ }
258
+ //# sourceMappingURL=message-revert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-revert.js","sourceRoot":"","sources":["../../src/db/message-revert.ts"],"names":[],"mappings":";;AAiHA,sCA2BC;AAUD,0CA+BC;AAQD,8CAsBC;AAGD,kDAqBC;AAQD,8CAeC;AAGD,kDAeC;AApRD,yCAAgH;AAChH,wCAAkD;AAClD,sCAAoD;AA8BpD,6FAA6F;AAC7F,SAAS,QAAQ,CAAC,WAAmB;IACnC,OAAO,IAAA,uBAAY,EAAC,WAAW,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;AACpG,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,SAAS,CAAC,IAAiB,EAAE,IAAY,EAAE,EAAU;IAC5D,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,4DAA4D;IAClG,MAAM,CAAC,GAAG,IAAA,wBAAiB,EAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;IAC7D,IAAI,CAAC,CAAC,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IAChD,IAAA,0BAAoB,EAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACrC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,0BAA0B,CAAC,OAAwB;IAC1D,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,CAAC;IACpD,MAAM,OAAO,GAAkB,EAAE,CAAC;IAElC,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,IAAI,IAAI,CAAC,QAAQ;YAAE,SAAS,CAAC,yDAAyD;QACtF,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACnD,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YACV,0FAA0F;YAC1F,4FAA4F;YAC5F,4FAA4F;YAC5F,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC;gBAC7C,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,+CAA+C;gBACzF,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;YACxB,CAAC;YACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,OAAO,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QAC7D,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC;AAED,sGAAsG;AACtG,SAAS,4BAA4B,CAAC,OAAwB;IAC5D,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC;IAClC,MAAM,OAAO,GAAkB,EAAE,CAAC;IAElC,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE,SAAS,CAAC,uCAAuC;QACrE,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QACnD,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YACV,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC;gBAC7C,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;gBACzC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACvB,CAAC;YACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,OAAO,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QAC7D,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,aAAa,CAAC,WAAmB,EAAE,SAAiB;IAClE,MAAM,MAAM,GAAG,IAAA,sBAAW,EAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACnD,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,mBAAmB,EAAE,EAAE,CAAC;IACpH,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACjC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,CAAC;IACxG,CAAC;IAED,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;IAClC,MAAM,WAAW,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS,CAAC,CAAC;IAC3D,4FAA4F;IAC5F,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,OAAO,EAAE,CAAC;IAEzF,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,OAAO,IAAI,UAAU,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,0BAA0B,CAAC,OAAO,CAAC,CAAC;QACnD,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,OAAO,CAAC,MAAM,GAAG,IAAA,uBAAY,EAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC7C,IAAA,sBAAW,EAAC,WAAW,EAAE,OAAO,CAAC,CAAC;YAClC,OAAO,EAAE,EAAE,EAAE,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,KAAM,EAAE,EAAE,CAAC;QAC9G,CAAC;QACD,OAAO,CAAC,MAAM,GAAG,IAAA,uBAAY,EAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC7C,OAAO,CAAC,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC9C,IAAA,sBAAW,EAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAClC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC5B,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAChC,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,eAAe,CAAC,WAAmB,EAAE,SAAiB;IACpE,MAAM,MAAM,GAAG,IAAA,sBAAW,EAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACnD,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC9D,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,8CAA8C,EAAE,CAAC;IAE7G,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACjC,MAAM,QAAQ,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC;QAC5E,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC3B;;0GAEkG;QAClG,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAC;QACvE,IAAI,MAAM,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,8DAA8D,MAAM,CAAC,OAAO,MAAM,MAAM,CAAC,EAAE,sCAAsC;aACzI,CAAC;QACJ,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,4BAA4B,CAAC,MAAM,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,MAAM,CAAC,MAAM,GAAG,IAAA,uBAAY,EAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAA,sBAAW,EAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QACjC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;IAC5C,CAAC;IAED,MAAM,CAAC,MAAM,GAAG,IAAA,uBAAY,EAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3C,MAAM,CAAC,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC7C,IAAA,sBAAW,EAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;AAC7C,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,WAAmB,EAAE,SAAiB,EAAE,QAAgB;IACxF,MAAM,OAAO,GAAG,IAAA,sBAAW,EAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACpD,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC/D,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IACrF,IAAI,CAAC,SAAS,CAAC,MAAM;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,gDAAgD,EAAE,CAAC;IAErG,MAAM,WAAW,GAAG,SAAS,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,CAAC;IAChD,MAAM,OAAO,GAAkB,EAAE,CAAC;IAClC,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACnD,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YACV,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC;gBAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;gBAAC,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;YAAC,CAAC;YAClH,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,OAAO,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QAC7D,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,CAAC,MAAM,GAAG,IAAA,uBAAY,EAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7C,OAAO,CAAC,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,IAAA,sBAAW,EAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAClC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACxD,CAAC;AAED,+EAA+E;AAC/E,SAAgB,mBAAmB,CAAC,WAAmB,EAAE,SAAiB,EAAE,QAAgB;IAC1F,MAAM,OAAO,GAAG,IAAA,sBAAW,EAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACpD,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC/D,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;IACpF,IAAI,CAAC,SAAS,CAAC,MAAM;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,iDAAiD,EAAE,CAAC;IAEtG,MAAM,OAAO,GAAkB,EAAE,CAAC;IAClC,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QACnD,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YACV,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC;gBAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;gBAAC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YAAC,CAAC;YACjH,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,OAAO,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QAC7D,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,CAAC,MAAM,GAAG,IAAA,uBAAY,EAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7C,OAAO,CAAC,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,IAAA,sBAAW,EAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAClC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACxD,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,WAAmB,EAAE,SAAiB,EAAE,MAAc;IACtF,MAAM,OAAO,GAAG,IAAA,sBAAW,EAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACpD,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC/D,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC;IACtD,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAC;IACzE,IAAI,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAEnE,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,CAAC,CAAC,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IAEhD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;IACrB,OAAO,CAAC,MAAM,GAAG,IAAA,uBAAY,EAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7C,OAAO,CAAC,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,IAAA,sBAAW,EAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAClC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC;AAED,sCAAsC;AACtC,SAAgB,mBAAmB,CAAC,WAAmB,EAAE,SAAiB,EAAE,MAAc;IACxF,MAAM,OAAO,GAAG,IAAA,sBAAW,EAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACpD,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC/D,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC;IACtD,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAC;IACzE,IAAI,CAAC,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC;IAEhE,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IACnD,IAAI,CAAC,CAAC,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IAEhD,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;IACtB,OAAO,CAAC,MAAM,GAAG,IAAA,uBAAY,EAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7C,OAAO,CAAC,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,IAAA,sBAAW,EAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAClC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { DevMindDatabase } from './database';
2
+ export interface RevertResult {
3
+ ok: boolean;
4
+ error?: string;
5
+ node_id?: string;
6
+ file_path?: string;
7
+ /** True when the change was still staged, so there was no recorded history to unwind. */
8
+ was_staged?: boolean;
9
+ entry_deleted?: boolean;
10
+ /** Set when the code was restored but the history entry could not be fully removed. */
11
+ note?: string;
12
+ }
13
+ /**
14
+ * Undoes the most recent recorded edit to one entity: restores the file, then erases what was
15
+ * written about it.
16
+ *
17
+ * Only the newest edit is reversible. An older one cannot be undone in isolation — every edit
18
+ * after it was made against the code it produced, so putting its "before" back would overwrite
19
+ * work that has nothing to do with it. Git is the tool for that, and it already does it properly.
20
+ *
21
+ * `expectedHistoryId`, when given, is the specific entry the caller intended to revert (e.g. the
22
+ * one a "Revert this change" button was clicked on). It must match the node's actual latest entry
23
+ * — if it doesn't (a newer entry was recorded since, perhaps from another session), the revert is
24
+ * refused rather than silently reverting that newer entry instead, which would undo the wrong
25
+ * change from the caller's point of view.
26
+ *
27
+ * The file is restored by swapping the recorded `after` back to the recorded `before`, which
28
+ * routes through the same exact-match write `edit_node` uses: if that text is no longer uniquely
29
+ * present, the write is refused rather than guessed at.
30
+ */
31
+ export declare function revertLastEdit(db: DevMindDatabase, devmindPath: string, nodeId: string, expectedHistoryId?: string): RevertResult;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.revertLastEdit = revertLastEdit;
4
+ const staging_1 = require("./staging");
5
+ const edit_1 = require("../utils/edit");
6
+ const ast_1 = require("../utils/ast");
7
+ /**
8
+ * Undoes the most recent recorded edit to one entity: restores the file, then erases what was
9
+ * written about it.
10
+ *
11
+ * Only the newest edit is reversible. An older one cannot be undone in isolation — every edit
12
+ * after it was made against the code it produced, so putting its "before" back would overwrite
13
+ * work that has nothing to do with it. Git is the tool for that, and it already does it properly.
14
+ *
15
+ * `expectedHistoryId`, when given, is the specific entry the caller intended to revert (e.g. the
16
+ * one a "Revert this change" button was clicked on). It must match the node's actual latest entry
17
+ * — if it doesn't (a newer entry was recorded since, perhaps from another session), the revert is
18
+ * refused rather than silently reverting that newer entry instead, which would undo the wrong
19
+ * change from the caller's point of view.
20
+ *
21
+ * The file is restored by swapping the recorded `after` back to the recorded `before`, which
22
+ * routes through the same exact-match write `edit_node` uses: if that text is no longer uniquely
23
+ * present, the write is refused rather than guessed at.
24
+ */
25
+ function revertLastEdit(db, devmindPath, nodeId, expectedHistoryId) {
26
+ const node = db.getNode(nodeId);
27
+ const resolvedId = node ? node.id : nodeId;
28
+ // Staged edits are the newest thing that happened, and were never recorded — undo those first,
29
+ // or a revert would restore the file underneath a staged entry still waiting to be committed.
30
+ const staged = (0, staging_1.readStaged)(devmindPath).filter(e => e.node_id === resolvedId);
31
+ if (staged.length) {
32
+ const last = staged[staged.length - 1];
33
+ if (last.code_before === undefined) {
34
+ return { ok: false, error: `The staged change to ${resolvedId} has no recorded before-state, so it cannot be undone here. Use git to restore the file.` };
35
+ }
36
+ const restored = restoreFile(last.file_path, last.code_snapshot, last.code_before ?? '');
37
+ if (!restored.ok)
38
+ return { ok: false, error: restored.error, node_id: resolvedId, file_path: last.file_path };
39
+ (0, staging_1.removeLastStagedEntry)(devmindPath, resolvedId);
40
+ return { ok: true, node_id: resolvedId, file_path: last.file_path, was_staged: true };
41
+ }
42
+ const entry = db.getLatestHistory(resolvedId);
43
+ if (!entry)
44
+ return { ok: false, error: `No history recorded for ${resolvedId} — nothing to revert.` };
45
+ if (expectedHistoryId && entry.id !== expectedHistoryId) {
46
+ return {
47
+ ok: false,
48
+ error: `A newer change to ${resolvedId} was recorded since this one — reverting it would undo that newer change instead. Refresh and revert the newest entry, or use git.`,
49
+ node_id: resolvedId
50
+ };
51
+ }
52
+ if (!entry.edits.length) {
53
+ return {
54
+ ok: false,
55
+ error: `The last change to ${resolvedId} was recorded without a before-state, so there is nothing to restore it to. Entries written before edit_node tracked diffs, and entries from stage_change (non-TS/JS files), are both like this. Use git to restore the file.`
56
+ };
57
+ }
58
+ const last = entry.edits[entry.edits.length - 1];
59
+ if (!node)
60
+ return { ok: false, error: `${resolvedId} has history but no node in the graph — cannot locate its file.` };
61
+ // The recorded `after` must still be what is on disk. If it isn't, the code moved on after this
62
+ // edit and restoring `before` would silently discard whatever came since.
63
+ const live = db.getLiveCode(resolvedId);
64
+ if (live.source !== 'live' || live.code === undefined) {
65
+ return {
66
+ ok: false,
67
+ error: `Could not read ${resolvedId} from disk to confirm what is there now (the symbol may have been renamed, moved, or deleted). Revert refused — use git.`,
68
+ node_id: resolvedId
69
+ };
70
+ }
71
+ if (live.code !== last.after) {
72
+ return {
73
+ ok: false,
74
+ error: `${resolvedId} has changed since that edit was recorded — reverting would discard the newer change. Revert refused; use git to restore the file.`,
75
+ node_id: resolvedId,
76
+ file_path: node.file_path
77
+ };
78
+ }
79
+ const restored = restoreFile(node.file_path, last.after, last.before);
80
+ if (!restored.ok)
81
+ return { ok: false, error: restored.error, node_id: resolvedId, file_path: node.file_path };
82
+ const erased = db.eraseLastEdit(entry.id);
83
+ return {
84
+ ok: true,
85
+ node_id: resolvedId,
86
+ file_path: node.file_path,
87
+ entry_deleted: erased.entry_deleted,
88
+ note: erased.entry_deleted ? undefined : erased.reason
89
+ };
90
+ }
91
+ /**
92
+ * Swaps `after` back to `before` in the file.
93
+ *
94
+ * `before` is empty when the edit created the entity, so the revert deletes it — which is the
95
+ * correct inverse, and the one case where the two strings could not both be present.
96
+ */
97
+ function restoreFile(filePath, after, before) {
98
+ const paths = String(filePath).split(',').map(s => s.trim()).filter(Boolean);
99
+ const target = paths[0];
100
+ if (!target)
101
+ return { ok: false, error: 'no file path recorded for this entity' };
102
+ const r = (0, edit_1.replaceTextInFile)(target, after, before, false);
103
+ if (!r.ok)
104
+ return { ok: false, error: `could not restore ${target}: ${r.error}` };
105
+ (0, ast_1.invalidateParsedFile)(target);
106
+ return { ok: true };
107
+ }
108
+ //# sourceMappingURL=revert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revert.js","sourceRoot":"","sources":["../../src/db/revert.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,22 @@ export interface DbNode {
4
4
  name: string;
5
5
  file_path: string;
6
6
  signature: string | null;
7
+ /**
8
+ * A 1-3 sentence natural-language description of what this entity does — the thing a plain
9
+ * identifier can't carry. This is what makes `search_nodes` findable by natural language
10
+ * instead of only by exact identifier/path substring. Null until an agent (or `devsmind
11
+ * describe`) writes one; `commit_changes` refuses to create a NEW node without it.
12
+ */
13
+ description: string | null;
14
+ /**
15
+ * Other names this SAME entity is referenced by — a generated hook (`useGetAdminOrdersQuery`
16
+ * for an RTK `getAdminOrders` endpoint), a renamed default-export import, or an `export { x as
17
+ * y }` re-export. One implementation, several exported handles: this is what lets the edge
18
+ * resolver match a caller against ANY of them instead of only the entity's own declared name,
19
+ * which is the fix for edges that otherwise can't exist because the reference in the code never
20
+ * spells out the original name at all. Empty array, not null, when there are none.
21
+ */
22
+ aliases: string[];
7
23
  deprecated: number;
8
24
  created_at: string;
9
25
  }
@@ -11,6 +27,13 @@ export interface DbConnection {
11
27
  source_node_id: string;
12
28
  target_node_id: string;
13
29
  }
30
+ /** One recorded edit within a history entry — what the code looked like either side of it. */
31
+ export interface HistoryEdit {
32
+ at: string;
33
+ before: string;
34
+ after: string;
35
+ reasoning: string;
36
+ }
14
37
  export interface DbHistory {
15
38
  id: string;
16
39
  node_id: string;
@@ -19,12 +42,21 @@ export interface DbHistory {
19
42
  updated_at: string;
20
43
  code_snapshot: string;
21
44
  reasoning: string;
45
+ /**
46
+ * Per-edit before/after trail, newest last. Lives only in the history JSON, never in SQLite —
47
+ * same as code_snapshot. Empty for entries written before this existed, and for `stage_change`
48
+ * entries, which have no before-state to record: both mean "no diff, no revert".
49
+ */
50
+ edits: HistoryEdit[];
22
51
  }
23
52
  export interface DbWorkflow {
24
53
  id: string;
25
54
  name: string;
26
55
  description: string;
27
- status: 'active' | 'paused' | 'completed';
56
+ /** 1 = hidden from the default listing. Replaces the old `status` field: nothing ever genuinely
57
+ * "completes" — a feature keeps getting changed — so a completed/paused/active lifecycle was a
58
+ * value nobody maintained. "Archived" claims only what it can deliver: hide this from the list. */
59
+ archived: number;
28
60
  created_at: string;
29
61
  updated_at: string;
30
62
  }
@@ -33,8 +65,21 @@ export interface DbWorkflowStep {
33
65
  workflow_id: string;
34
66
  step_index: number;
35
67
  summary: string;
36
- pending_tasks: string | null;
37
- history_ids: string | null;
68
+ /** The commit's own `why`/`goal`/`decision`, copied at write time rather than joined from
69
+ * `history`. Deliberate duplication: history reasoning MUTATES after the fact (the 1-hour merge
70
+ * appends under a `── Update @ … ──` separator, and eraseLastEdit can drop a block), so a step
71
+ * that joined to it would describe a moving target — disqualifying for a record whose whole
72
+ * purpose is "what did we think at the time". */
73
+ reasoning: string | null;
74
+ /** JSON array of the node ids this step touched. Replaces `history_ids`, which could not
75
+ * identify a commit: within an hour, two commits on the same node merge into ONE history row,
76
+ * so a step's ids could point at rows an earlier commit created, and one row could be cited by
77
+ * several steps. Node ids have no such ambiguity and are directly usable with get_node_code. */
78
+ node_ids: string | null;
79
+ /** JSON array of repo-relative paths to research/spec docs behind this step. A path, never a
80
+ * copy: the file is already versioned and already shared, so it cannot go stale the way the old
81
+ * `workflow_artifacts` content duplicates did. */
82
+ doc_paths: string | null;
38
83
  session_id: string | null;
39
84
  created_at: string;
40
85
  }
@@ -47,4 +92,4 @@ export interface DbWorkflowArtifact {
47
92
  file_path: string;
48
93
  created_at: string;
49
94
  }
50
- export declare const INIT_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n signature TEXT,\n deprecated INTEGER DEFAULT 0,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE IF NOT EXISTS node_connections (\n source_node_id TEXT,\n target_node_id TEXT,\n PRIMARY KEY (source_node_id, target_node_id),\n FOREIGN KEY (source_node_id) REFERENCES nodes (id) ON DELETE CASCADE,\n FOREIGN KEY (target_node_id) REFERENCES nodes (id) ON DELETE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS history (\n id TEXT PRIMARY KEY,\n node_id TEXT NOT NULL,\n session_id TEXT NOT NULL,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n code_snapshot TEXT NOT NULL,\n reasoning TEXT NOT NULL,\n FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS system_meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE IF NOT EXISTS workflows (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n description TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'active',\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE IF NOT EXISTS workflow_steps (\n id TEXT PRIMARY KEY,\n workflow_id TEXT NOT NULL,\n step_index INTEGER NOT NULL,\n summary TEXT NOT NULL,\n pending_tasks TEXT,\n history_ids TEXT,\n session_id TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (workflow_id) REFERENCES workflows (id) ON DELETE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS workflow_artifacts (\n id TEXT PRIMARY KEY,\n workflow_id TEXT NOT NULL,\n step_id TEXT,\n type TEXT NOT NULL,\n source_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (workflow_id) REFERENCES workflows (id) ON DELETE CASCADE\n);\n\n-- Index for searching nodes by name and type\nCREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes (name);\nCREATE INDEX IF NOT EXISTS idx_history_node_id ON history (node_id);\nCREATE INDEX IF NOT EXISTS idx_workflow_steps_workflow_id ON workflow_steps (workflow_id);\nCREATE INDEX IF NOT EXISTS idx_workflow_artifacts_workflow_id ON workflow_artifacts (workflow_id);\n";
95
+ export declare const INIT_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n signature TEXT,\n description TEXT,\n aliases TEXT DEFAULT '[]',\n deprecated INTEGER DEFAULT 0,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE IF NOT EXISTS node_connections (\n source_node_id TEXT,\n target_node_id TEXT,\n PRIMARY KEY (source_node_id, target_node_id),\n FOREIGN KEY (source_node_id) REFERENCES nodes (id) ON DELETE CASCADE,\n FOREIGN KEY (target_node_id) REFERENCES nodes (id) ON DELETE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS history (\n id TEXT PRIMARY KEY,\n node_id TEXT NOT NULL,\n session_id TEXT NOT NULL,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n code_snapshot TEXT NOT NULL,\n reasoning TEXT NOT NULL,\n FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS system_meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Local, derived search index \u2014 never synced or committed, purely a cache rebuilt from\n-- nodes+history whenever search-index.ts's fingerprint check finds it stale. Same category as\n-- brain.db itself: safe to wipe and rebuild from the source of truth at any time.\nCREATE TABLE IF NOT EXISTS node_tokens (\n node_id TEXT NOT NULL,\n token TEXT NOT NULL,\n field TEXT NOT NULL,\n tf INTEGER NOT NULL DEFAULT 1,\n PRIMARY KEY (node_id, token, field)\n);\nCREATE INDEX IF NOT EXISTS idx_node_tokens_token ON node_tokens (token);\n\n-- Semantic (dense) vectors, one per node, from the vendored ONNX embedder (src/db/embedder.ts).\n-- Unlike node_tokens, these ARE synced via git (vectors/**/*.json, mirroring graph/**/*.json) \u2014\n-- deliberately, per an explicit product decision to trade repo size for teammates skipping local\n-- inference. No foreign key to nodes(id): syncFromDisk() runs its destructive graph pass under\n-- foreign_keys=OFF, so a cascade here would silently never fire; orphans are swept explicitly\n-- instead (see syncFromDisk's vectors pass). model_id guards the git-sync hazard \u2014 vectors from\n-- a different embedding model are structurally valid but numerically meaningless, so any reader\n-- MUST check model_id before trusting a row, not just check for its presence.\nCREATE TABLE IF NOT EXISTS node_vectors (\n node_id TEXT PRIMARY KEY,\n model_id TEXT NOT NULL,\n dim INTEGER NOT NULL,\n description_hash TEXT NOT NULL,\n vector BLOB NOT NULL\n);\n\n-- status is vestigial: still declared (NOT NULL DEFAULT makes an INSERT that omits it succeed)\n-- but no longer read or written. archived replaces it -- see DbWorkflow.\nCREATE TABLE IF NOT EXISTS workflows (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n description TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'active',\n archived INTEGER NOT NULL DEFAULT 0,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\n-- pending_tasks and history_ids are vestigial: never written or read by current code, and kept\n-- only so an older CLI opening the same brain.db does not hit \"no such column\". They are dropped\n-- from DbWorkflowStep, so nothing in TypeScript can reach them by accident.\nCREATE TABLE IF NOT EXISTS workflow_steps (\n id TEXT PRIMARY KEY,\n workflow_id TEXT NOT NULL,\n step_index INTEGER NOT NULL,\n summary TEXT NOT NULL,\n reasoning TEXT,\n node_ids TEXT,\n doc_paths TEXT,\n pending_tasks TEXT,\n history_ids TEXT,\n session_id TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (workflow_id) REFERENCES workflows (id) ON DELETE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS workflow_artifacts (\n id TEXT PRIMARY KEY,\n workflow_id TEXT NOT NULL,\n step_id TEXT,\n type TEXT NOT NULL,\n source_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (workflow_id) REFERENCES workflows (id) ON DELETE CASCADE\n);\n\n-- Index for searching nodes by name and type\nCREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes (name);\n-- Matches getNodesByFilePath's own normalization (REPLACE(LOWER(file_path), '', '/')) exactly \u2014\n-- without this, that call is a full table scan, and get_node_code's file_outline now puts it on\n-- the hot path of the most-called read tool (once per call, to cross-reference outline entries\n-- against indexed nodes). Idempotent: existing brains pick this up on next open, no migration.\nCREATE INDEX IF NOT EXISTS idx_nodes_file_path ON nodes (REPLACE(LOWER(file_path), '', '/'));\n-- node_connections' PRIMARY KEY is (source_node_id, target_node_id), so its autoindex serves\n-- \"what does X use\" but NOT the reverse. Every \"who uses X\" lookup \u2014 the inbound half of getGraph's\n-- BFS, getConnections' used_by, getConnectionCounts \u2014 was therefore a full table scan, and the BFS\n-- runs one PER NODE VISITED (up to 500 in a single call). The trailing source_node_id makes this\n-- covering, so the inbound query is answered from the index alone and comes back already ordered\n-- (which is what makes getGraph's ORDER BY free rather than a sort on top of a scan).\n-- Idempotent: existing brains pick this up on next open, no migration.\nCREATE INDEX IF NOT EXISTS idx_node_connections_target ON node_connections (target_node_id, source_node_id);\nCREATE INDEX IF NOT EXISTS idx_history_node_id ON history (node_id);\nCREATE INDEX IF NOT EXISTS idx_workflow_steps_workflow_id ON workflow_steps (workflow_id);\nCREATE INDEX IF NOT EXISTS idx_workflow_artifacts_workflow_id ON workflow_artifacts (workflow_id);\n";