@coreyuan/vector-mind 1.0.53 → 1.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.
Files changed (140) hide show
  1. package/README.md +99 -72
  2. package/admin-panel/README.md +77 -0
  3. package/admin-panel/dist/client/assets/index-Bzyjdhh2.css +1 -0
  4. package/admin-panel/dist/client/assets/index-Ph73ewzt.js +9 -0
  5. package/admin-panel/dist/client/index.html +14 -0
  6. package/admin-panel/package.json +25 -0
  7. package/admin-panel/server/index.mjs +1424 -0
  8. package/admin-panel/server/security.mjs +117 -0
  9. package/admin-panel/server/security.test.mjs +113 -0
  10. package/admin-panel/server/storage.mjs +49 -0
  11. package/admin-panel/server/storage.test.mjs +36 -0
  12. package/dist/builtin-conventions.js +1 -1
  13. package/dist/builtin-conventions.js.map +1 -1
  14. package/dist/builtin-instructions.d.ts +1 -1
  15. package/dist/builtin-instructions.js +1 -1
  16. package/dist/builtin-instructions.js.map +1 -1
  17. package/dist/checkpoint-snapshot.d.ts +56 -0
  18. package/dist/checkpoint-snapshot.js +411 -0
  19. package/dist/checkpoint-snapshot.js.map +1 -0
  20. package/dist/config.d.ts +4 -1
  21. package/dist/config.js +25 -1
  22. package/dist/config.js.map +1 -1
  23. package/dist/context-governance.d.ts +56 -0
  24. package/dist/context-governance.js +176 -0
  25. package/dist/context-governance.js.map +1 -0
  26. package/dist/database-runtime.d.ts +6 -1
  27. package/dist/database-runtime.js +555 -127
  28. package/dist/database-runtime.js.map +1 -1
  29. package/dist/development-warnings.d.ts +2 -1
  30. package/dist/development-warnings.js +92 -22
  31. package/dist/development-warnings.js.map +1 -1
  32. package/dist/file-indexing.d.ts +2 -1
  33. package/dist/file-indexing.js +127 -47
  34. package/dist/file-indexing.js.map +1 -1
  35. package/dist/grep.js +4 -0
  36. package/dist/grep.js.map +1 -1
  37. package/dist/index.js +8 -2
  38. package/dist/index.js.map +1 -1
  39. package/dist/large-file-split.d.ts +53 -8
  40. package/dist/large-file-split.js +294 -75
  41. package/dist/large-file-split.js.map +1 -1
  42. package/dist/memory-maintenance/compaction.d.ts +7 -0
  43. package/dist/memory-maintenance/compaction.js +133 -0
  44. package/dist/memory-maintenance/compaction.js.map +1 -0
  45. package/dist/memory-maintenance/context.d.ts +37 -0
  46. package/dist/memory-maintenance/context.js +52 -0
  47. package/dist/memory-maintenance/context.js.map +1 -0
  48. package/dist/memory-maintenance/index-prune.d.ts +22 -0
  49. package/dist/memory-maintenance/index-prune.js +243 -0
  50. package/dist/memory-maintenance/index-prune.js.map +1 -0
  51. package/dist/memory-maintenance/metrics.d.ts +5 -0
  52. package/dist/memory-maintenance/metrics.js +30 -0
  53. package/dist/memory-maintenance/metrics.js.map +1 -0
  54. package/dist/memory-maintenance/pending.d.ts +4 -0
  55. package/dist/memory-maintenance/pending.js +74 -0
  56. package/dist/memory-maintenance/pending.js.map +1 -0
  57. package/dist/memory-maintenance/purge.d.ts +7 -0
  58. package/dist/memory-maintenance/purge.js +115 -0
  59. package/dist/memory-maintenance/purge.js.map +1 -0
  60. package/dist/memory-maintenance/runner.d.ts +5 -0
  61. package/dist/memory-maintenance/runner.js +182 -0
  62. package/dist/memory-maintenance/runner.js.map +1 -0
  63. package/dist/memory-maintenance/sqlite.d.ts +10 -0
  64. package/dist/memory-maintenance/sqlite.js +67 -0
  65. package/dist/memory-maintenance/sqlite.js.map +1 -0
  66. package/dist/memory-maintenance/types.d.ts +87 -0
  67. package/dist/memory-maintenance/types.js +2 -0
  68. package/dist/memory-maintenance/types.js.map +1 -0
  69. package/dist/memory-maintenance.d.ts +5 -80
  70. package/dist/memory-maintenance.js +3 -536
  71. package/dist/memory-maintenance.js.map +1 -1
  72. package/dist/memory-mutations.d.ts +3 -3
  73. package/dist/memory-mutations.js +109 -43
  74. package/dist/memory-mutations.js.map +1 -1
  75. package/dist/memory-recall.d.ts +3 -1
  76. package/dist/memory-recall.js +33 -1
  77. package/dist/memory-recall.js.map +1 -1
  78. package/dist/operation-scope.js +42 -12
  79. package/dist/operation-scope.js.map +1 -1
  80. package/dist/path-containment.d.ts +5 -0
  81. package/dist/path-containment.js +64 -0
  82. package/dist/path-containment.js.map +1 -0
  83. package/dist/path-filters.d.ts +1 -1
  84. package/dist/path-filters.js +7 -6
  85. package/dist/path-filters.js.map +1 -1
  86. package/dist/path-rules.d.ts +1 -0
  87. package/dist/path-rules.js +23 -1
  88. package/dist/path-rules.js.map +1 -1
  89. package/dist/pending-changes.d.ts +5 -0
  90. package/dist/pending-changes.js +30 -16
  91. package/dist/pending-changes.js.map +1 -1
  92. package/dist/project-files.d.ts +1 -0
  93. package/dist/project-files.js +19 -10
  94. package/dist/project-files.js.map +1 -1
  95. package/dist/root.d.ts +4 -1
  96. package/dist/root.js +34 -4
  97. package/dist/root.js.map +1 -1
  98. package/dist/rtk-integrity.d.ts +5 -0
  99. package/dist/rtk-integrity.js +35 -0
  100. package/dist/rtk-integrity.js.map +1 -0
  101. package/dist/rtk-shim.js +22 -9
  102. package/dist/rtk-shim.js.map +1 -1
  103. package/dist/rtk-tools.d.ts +8 -0
  104. package/dist/rtk-tools.js +59 -42
  105. package/dist/rtk-tools.js.map +1 -1
  106. package/dist/server-instructions.js +8 -88
  107. package/dist/server-instructions.js.map +1 -1
  108. package/dist/tool-catalog.d.ts +2 -0
  109. package/dist/tool-catalog.js +77 -24
  110. package/dist/tool-catalog.js.map +1 -1
  111. package/dist/tool-handlers/context-recovery.js +79 -54
  112. package/dist/tool-handlers/context-recovery.js.map +1 -1
  113. package/dist/tool-handlers/large-files.js +615 -41
  114. package/dist/tool-handlers/large-files.js.map +1 -1
  115. package/dist/tool-handlers/memory-diagnostics.js +240 -45
  116. package/dist/tool-handlers/memory-diagnostics.js.map +1 -1
  117. package/dist/tool-handlers/memory.js +167 -22
  118. package/dist/tool-handlers/memory.js.map +1 -1
  119. package/dist/tool-handlers/operations.js +28 -8
  120. package/dist/tool-handlers/operations.js.map +1 -1
  121. package/dist/tool-handlers/requirement-status.d.ts +5 -0
  122. package/dist/tool-handlers/requirement-status.js +267 -0
  123. package/dist/tool-handlers/requirement-status.js.map +1 -0
  124. package/dist/tool-handlers/requirements.js +851 -115
  125. package/dist/tool-handlers/requirements.js.map +1 -1
  126. package/dist/tool-handlers.d.ts +4 -0
  127. package/dist/tool-handlers.js +439 -6
  128. package/dist/tool-handlers.js.map +1 -1
  129. package/dist/tool-output.d.ts +71 -2
  130. package/dist/tool-output.js +56 -13
  131. package/dist/tool-output.js.map +1 -1
  132. package/dist/tool-schemas.d.ts +243 -0
  133. package/dist/tool-schemas.js +112 -26
  134. package/dist/tool-schemas.js.map +1 -1
  135. package/dist/types.d.ts +10 -1
  136. package/docs/capability-matrix.md +23 -17
  137. package/package.json +21 -5
  138. package/skills/vector-mind-autopilot/SKILL.md +82 -23
  139. package/skills/vector-mind-autopilot/references/claude-project-instructions.md +14 -11
  140. package/skills/vector-mind-autopilot/references/universal-system-prompt.md +24 -16
@@ -2,20 +2,26 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import Database from "better-sqlite3";
4
4
  import { MEMORY_ITEMS_FTS_TABLE } from "./memory-recall.js";
5
+ import { shouldIgnoreDbFilePath } from "./path-rules.js";
5
6
  export const FTS_TABLE_NAME = MEMORY_ITEMS_FTS_TABLE;
6
7
  let db;
7
8
  let dbPath = "";
8
9
  let ftsAvailable = false;
9
10
  let insertRequirementStmt;
10
11
  let getActiveRequirementStmt;
12
+ let getActiveRequirementByIdStmt;
13
+ let getActiveRequirementByGoalKeyStmt;
11
14
  let listActiveRequirementsStmt;
12
15
  let listRecentRequirementsStmt;
13
16
  let completeAllActiveRequirementsStmt;
14
17
  let completeRequirementByIdStmt;
15
- let completeAllActiveRequirementMemoryItemsStmt;
16
18
  let completeRequirementMemoryItemByReqIdStmt;
17
19
  let listChangeLogsForRequirementStmt;
18
20
  let insertChangeLogStmt;
21
+ let upsertSyncedFileStateStmt;
22
+ let getSyncedFileStateStmt;
23
+ let insertMcpGuardEventStmt;
24
+ let insertMcpToolMetricStmt;
19
25
  let insertMemoryItemStmt;
20
26
  let getMemoryItemByIdStmt;
21
27
  let getRequirementMemoryItemIdStmt;
@@ -50,66 +56,69 @@ let indexFileSymbolsTx = null;
50
56
  function initMemoryItemsFts() {
51
57
  ftsAvailable = false;
52
58
  try {
53
- const existed = db
54
- .prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name=?`)
55
- .get(FTS_TABLE_NAME);
56
- const alreadyExists = !!existed;
57
- if (!alreadyExists) {
58
- try {
59
- db.exec(`
60
- CREATE VIRTUAL TABLE ${FTS_TABLE_NAME} USING fts5(
61
- kind,
62
- title,
63
- content,
64
- file_path,
65
- metadata_json,
66
- content='memory_items',
67
- content_rowid='id',
68
- tokenize='trigram'
69
- );
70
- `);
59
+ const initializeFts = db.transaction(() => {
60
+ const existed = db
61
+ .prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name=?`)
62
+ .get(FTS_TABLE_NAME);
63
+ const alreadyExists = !!existed;
64
+ if (!alreadyExists) {
65
+ try {
66
+ db.exec(`
67
+ CREATE VIRTUAL TABLE ${FTS_TABLE_NAME} USING fts5(
68
+ kind,
69
+ title,
70
+ content,
71
+ file_path,
72
+ metadata_json,
73
+ content='memory_items',
74
+ content_rowid='id',
75
+ tokenize='trigram'
76
+ );
77
+ `);
78
+ }
79
+ catch {
80
+ db.exec(`
81
+ CREATE VIRTUAL TABLE ${FTS_TABLE_NAME} USING fts5(
82
+ kind,
83
+ title,
84
+ content,
85
+ file_path,
86
+ metadata_json,
87
+ content='memory_items',
88
+ content_rowid='id'
89
+ );
90
+ `);
91
+ }
92
+ try {
93
+ db.exec(`INSERT INTO ${FTS_TABLE_NAME}(${FTS_TABLE_NAME}) VALUES('rebuild');`);
94
+ }
95
+ catch (err) {
96
+ console.error("[vectormind] fts rebuild failed:", err);
97
+ }
71
98
  }
72
- catch {
73
- db.exec(`
74
- CREATE VIRTUAL TABLE ${FTS_TABLE_NAME} USING fts5(
75
- kind,
76
- title,
77
- content,
78
- file_path,
79
- metadata_json,
80
- content='memory_items',
81
- content_rowid='id'
82
- );
83
- `);
84
- }
85
- try {
86
- db.exec(`INSERT INTO ${FTS_TABLE_NAME}(${FTS_TABLE_NAME}) VALUES('rebuild');`);
87
- }
88
- catch (err) {
89
- console.error("[vectormind] fts rebuild failed:", err);
90
- }
91
- }
92
- db.exec(`
93
- CREATE TRIGGER IF NOT EXISTS vectormind_memory_items_fts_ai
94
- AFTER INSERT ON memory_items BEGIN
95
- INSERT INTO ${FTS_TABLE_NAME}(rowid, kind, title, content, file_path, metadata_json)
96
- VALUES (new.id, new.kind, new.title, new.content, new.file_path, new.metadata_json);
97
- END;
98
-
99
- CREATE TRIGGER IF NOT EXISTS vectormind_memory_items_fts_ad
100
- AFTER DELETE ON memory_items BEGIN
101
- INSERT INTO ${FTS_TABLE_NAME}(${FTS_TABLE_NAME}, rowid, kind, title, content, file_path, metadata_json)
102
- VALUES ('delete', old.id, old.kind, old.title, old.content, old.file_path, old.metadata_json);
103
- END;
104
-
105
- CREATE TRIGGER IF NOT EXISTS vectormind_memory_items_fts_au
106
- AFTER UPDATE ON memory_items BEGIN
107
- INSERT INTO ${FTS_TABLE_NAME}(${FTS_TABLE_NAME}, rowid, kind, title, content, file_path, metadata_json)
108
- VALUES ('delete', old.id, old.kind, old.title, old.content, old.file_path, old.metadata_json);
109
- INSERT INTO ${FTS_TABLE_NAME}(rowid, kind, title, content, file_path, metadata_json)
110
- VALUES (new.id, new.kind, new.title, new.content, new.file_path, new.metadata_json);
111
- END;
112
- `);
99
+ db.exec(`
100
+ CREATE TRIGGER IF NOT EXISTS vectormind_memory_items_fts_ai
101
+ AFTER INSERT ON memory_items BEGIN
102
+ INSERT INTO ${FTS_TABLE_NAME}(rowid, kind, title, content, file_path, metadata_json)
103
+ VALUES (new.id, new.kind, new.title, new.content, new.file_path, new.metadata_json);
104
+ END;
105
+
106
+ CREATE TRIGGER IF NOT EXISTS vectormind_memory_items_fts_ad
107
+ AFTER DELETE ON memory_items BEGIN
108
+ INSERT INTO ${FTS_TABLE_NAME}(${FTS_TABLE_NAME}, rowid, kind, title, content, file_path, metadata_json)
109
+ VALUES ('delete', old.id, old.kind, old.title, old.content, old.file_path, old.metadata_json);
110
+ END;
111
+
112
+ CREATE TRIGGER IF NOT EXISTS vectormind_memory_items_fts_au
113
+ AFTER UPDATE ON memory_items BEGIN
114
+ INSERT INTO ${FTS_TABLE_NAME}(${FTS_TABLE_NAME}, rowid, kind, title, content, file_path, metadata_json)
115
+ VALUES ('delete', old.id, old.kind, old.title, old.content, old.file_path, old.metadata_json);
116
+ INSERT INTO ${FTS_TABLE_NAME}(rowid, kind, title, content, file_path, metadata_json)
117
+ VALUES (new.id, new.kind, new.title, new.content, new.file_path, new.metadata_json);
118
+ END;
119
+ `);
120
+ });
121
+ initializeFts.immediate();
113
122
  db.prepare(`SELECT rowid FROM ${FTS_TABLE_NAME} LIMIT 1`).get();
114
123
  ftsAvailable = true;
115
124
  }
@@ -126,6 +135,295 @@ function columnExists(table, column) {
126
135
  return false;
127
136
  }
128
137
  }
138
+ function safeParseObject(value) {
139
+ if (!value)
140
+ return {};
141
+ try {
142
+ const parsed = JSON.parse(value);
143
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
144
+ }
145
+ catch {
146
+ return {};
147
+ }
148
+ }
149
+ function stringValue(value) {
150
+ return typeof value === "string" && value.length > 0 ? value : undefined;
151
+ }
152
+ function nullableStringValue(value) {
153
+ if (value === null)
154
+ return null;
155
+ return stringValue(value);
156
+ }
157
+ function mergeStringArrays(values) {
158
+ const merged = [];
159
+ for (const value of values) {
160
+ if (!Array.isArray(value))
161
+ continue;
162
+ for (const item of value) {
163
+ if (typeof item === "string" && item.length > 0 && !merged.includes(item))
164
+ merged.push(item);
165
+ }
166
+ }
167
+ return merged;
168
+ }
169
+ function migrationFileKey(filePath) {
170
+ const normalized = filePath.replace(/\\/g, "/");
171
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
172
+ }
173
+ function addMigrationFileEntry(filesByPath, filePath, metadata = {}) {
174
+ const normalizedFilePath = stringValue(filePath)?.replace(/\\/g, "/");
175
+ if (!normalizedFilePath || normalizedFilePath === "(multiple)" || normalizedFilePath === "(unspecified)")
176
+ return;
177
+ if (shouldIgnoreDbFilePath(normalizedFilePath))
178
+ return;
179
+ const key = migrationFileKey(normalizedFilePath);
180
+ const existing = filesByPath.get(key) ?? { file_path: normalizedFilePath };
181
+ const event = stringValue(metadata.event);
182
+ const source = stringValue(metadata.source);
183
+ const fileStateHash = nullableStringValue(metadata.file_state_hash);
184
+ if (event && !existing.event)
185
+ existing.event = event;
186
+ if (source && !existing.source)
187
+ existing.source = source;
188
+ if (fileStateHash !== undefined && existing.file_state_hash === undefined)
189
+ existing.file_state_hash = fileStateHash;
190
+ filesByPath.set(key, existing);
191
+ }
192
+ function addMigrationFilesFromMetadata(filesByPath, metadata) {
193
+ const files = metadata.files;
194
+ if (!Array.isArray(files))
195
+ return;
196
+ for (const item of files) {
197
+ if (!item || typeof item !== "object" || Array.isArray(item))
198
+ continue;
199
+ const entry = item;
200
+ addMigrationFileEntry(filesByPath, entry.file_path, entry);
201
+ }
202
+ }
203
+ function migrationFilesJson(filesByPath) {
204
+ return JSON.stringify([...filesByPath.values()].map((file) => ({
205
+ file_path: file.file_path,
206
+ event: file.event,
207
+ source: file.source,
208
+ file_state_hash: file.file_state_hash ?? null,
209
+ })));
210
+ }
211
+ function repairDuplicateChangeLogs() {
212
+ const groups = db
213
+ .prepare(`SELECT req_id, timestamp, intent_summary, MIN(id) AS keep_id, COUNT(*) AS row_count
214
+ FROM change_logs
215
+ GROUP BY req_id, timestamp, intent_summary
216
+ HAVING COUNT(*) > 1`)
217
+ .all();
218
+ if (!groups.length)
219
+ return 0;
220
+ const selectGroupRows = db.prepare(`SELECT id, req_id, file_path, intent_summary, files_json, file_count, timestamp
221
+ FROM change_logs
222
+ WHERE req_id IS ? AND timestamp IS ? AND intent_summary IS ?
223
+ ORDER BY id ASC`);
224
+ const updateKeepRow = db.prepare(`UPDATE change_logs
225
+ SET file_path = ?, files_json = ?, file_count = ?
226
+ WHERE id = ?`);
227
+ const deleteRow = db.prepare(`DELETE FROM change_logs WHERE id = ?`);
228
+ let removedRows = 0;
229
+ const tx = db.transaction(() => {
230
+ for (const group of groups) {
231
+ const rows = selectGroupRows.all(group.req_id, group.timestamp, group.intent_summary);
232
+ if (rows.length <= 1)
233
+ continue;
234
+ const distinctLegacyFiles = new Set(rows.map((row) => row.file_path).filter((value) => !!value));
235
+ const hasAggregateRows = rows.some((row) => {
236
+ try {
237
+ const parsed = row.files_json ? JSON.parse(row.files_json) : null;
238
+ return Array.isArray(parsed) && parsed.length > 0;
239
+ }
240
+ catch {
241
+ return false;
242
+ }
243
+ });
244
+ if (hasAggregateRows || distinctLegacyFiles.size < 2)
245
+ continue;
246
+ const filesByPath = new Map();
247
+ for (const row of rows) {
248
+ addMigrationFilesFromMetadata(filesByPath, safeParseObject(row.files_json));
249
+ addMigrationFileEntry(filesByPath, row.file_path);
250
+ }
251
+ const files = [...filesByPath.values()];
252
+ const primaryFilePath = files.length === 1 ? files[0].file_path : files.length > 1 ? "(multiple)" : null;
253
+ const keepId = Number(group.keep_id);
254
+ updateKeepRow.run(primaryFilePath, migrationFilesJson(filesByPath), files.length, keepId);
255
+ for (const row of rows) {
256
+ if (row.id === keepId)
257
+ continue;
258
+ deleteRow.run(row.id);
259
+ removedRows += 1;
260
+ }
261
+ }
262
+ });
263
+ tx();
264
+ return removedRows;
265
+ }
266
+ function repairDuplicateChangeIntentMemoryItems() {
267
+ const groups = db
268
+ .prepare(`SELECT req_id, content, MIN(id) AS keep_id, COUNT(*) AS row_count
269
+ FROM memory_items
270
+ WHERE kind = 'change_intent'
271
+ GROUP BY req_id, content
272
+ HAVING COUNT(*) > 1`)
273
+ .all();
274
+ if (!groups.length)
275
+ return 0;
276
+ const selectGroupRows = db.prepare(`SELECT id, title, content, file_path, req_id, metadata_json, updated_at
277
+ FROM memory_items
278
+ WHERE kind = 'change_intent'
279
+ AND req_id IS ?
280
+ AND content IS ?
281
+ ORDER BY id ASC`);
282
+ const updateKeepRow = db.prepare(`UPDATE memory_items
283
+ SET file_path = ?, metadata_json = ?
284
+ WHERE id = ?`);
285
+ const deleteRow = db.prepare(`DELETE FROM memory_items WHERE id = ?`);
286
+ let removedRows = 0;
287
+ const tx = db.transaction(() => {
288
+ for (const group of groups) {
289
+ const rows = selectGroupRows.all(group.req_id, group.content);
290
+ if (rows.length <= 1)
291
+ continue;
292
+ const filesByPath = new Map();
293
+ const metadataList = rows.map((row) => safeParseObject(row.metadata_json));
294
+ const distinctLegacyFiles = new Set(rows.map((row) => row.file_path).filter((value) => !!value));
295
+ if (metadataList.some((metadata) => Array.isArray(metadata.files)) || distinctLegacyFiles.size < 2)
296
+ continue;
297
+ for (let i = 0; i < rows.length; i += 1) {
298
+ const row = rows[i];
299
+ const metadata = metadataList[i] ?? {};
300
+ addMigrationFilesFromMetadata(filesByPath, metadata);
301
+ addMigrationFileEntry(filesByPath, row.file_path, metadata);
302
+ }
303
+ const files = [...filesByPath.values()];
304
+ const keepId = Number(group.keep_id);
305
+ const keepRow = rows.find((row) => row.id === keepId) ?? rows[0];
306
+ const keepMetadata = safeParseObject(keepRow.metadata_json);
307
+ const sourceChangeIds = new Set();
308
+ for (const metadata of metadataList) {
309
+ if (typeof metadata.change_log_id === "number")
310
+ sourceChangeIds.add(metadata.change_log_id);
311
+ if (Array.isArray(metadata.source_change_ids)) {
312
+ for (const id of metadata.source_change_ids) {
313
+ if (typeof id === "number")
314
+ sourceChangeIds.add(id);
315
+ }
316
+ }
317
+ }
318
+ const nextMetadata = {
319
+ ...keepMetadata,
320
+ files: files.map((file) => ({
321
+ file_path: file.file_path,
322
+ event: file.event,
323
+ source: file.source,
324
+ file_state_hash: file.file_state_hash ?? null,
325
+ })),
326
+ file_count: files.length,
327
+ };
328
+ const verification = mergeStringArrays(metadataList.map((metadata) => metadata.verification));
329
+ const verificationGaps = mergeStringArrays(metadataList.map((metadata) => metadata.verification_gaps));
330
+ if (verification.length)
331
+ nextMetadata.verification = verification;
332
+ if (verificationGaps.length)
333
+ nextMetadata.verification_gaps = verificationGaps;
334
+ if (sourceChangeIds.size)
335
+ nextMetadata.source_change_ids = [...sourceChangeIds];
336
+ if (files.length === 1) {
337
+ const onlyFile = files[0];
338
+ nextMetadata.event = onlyFile.event;
339
+ nextMetadata.source = onlyFile.source;
340
+ nextMetadata.file_state_hash = onlyFile.file_state_hash ?? null;
341
+ }
342
+ else {
343
+ delete nextMetadata.event;
344
+ delete nextMetadata.source;
345
+ delete nextMetadata.file_state_hash;
346
+ }
347
+ updateKeepRow.run(files.length === 1 ? files[0].file_path : null, JSON.stringify(nextMetadata), keepId);
348
+ for (const row of rows) {
349
+ if (row.id === keepId)
350
+ continue;
351
+ deleteRow.run(row.id);
352
+ removedRows += 1;
353
+ }
354
+ }
355
+ });
356
+ tx();
357
+ return removedRows;
358
+ }
359
+ function backfillSyncedFileStatesFromChangeIntentMemory() {
360
+ const rows = db
361
+ .prepare(`SELECT id, file_path, metadata_json, updated_at
362
+ FROM memory_items
363
+ WHERE kind = 'change_intent'
364
+ ORDER BY updated_at ASC, id ASC`)
365
+ .all();
366
+ if (!rows.length)
367
+ return 0;
368
+ const upsert = db.prepare(`INSERT INTO synced_file_states (file_path, file_state_hash, source_change_id, updated_at)
369
+ VALUES (?, ?, ?, COALESCE(?, CURRENT_TIMESTAMP))
370
+ ON CONFLICT(file_path) DO UPDATE SET
371
+ file_state_hash = excluded.file_state_hash,
372
+ source_change_id = excluded.source_change_id,
373
+ updated_at = CASE
374
+ WHEN synced_file_states.updated_at IS NULL OR synced_file_states.updated_at <= excluded.updated_at
375
+ THEN excluded.updated_at
376
+ ELSE synced_file_states.updated_at
377
+ END`);
378
+ let written = 0;
379
+ const tx = db.transaction(() => {
380
+ for (const row of rows) {
381
+ const metadata = safeParseObject(row.metadata_json);
382
+ const sourceChangeId = typeof metadata.change_log_id === "number"
383
+ ? metadata.change_log_id
384
+ : Array.isArray(metadata.source_change_ids) && typeof metadata.source_change_ids[0] === "number"
385
+ ? metadata.source_change_ids[0]
386
+ : null;
387
+ const filesByPath = new Map();
388
+ addMigrationFilesFromMetadata(filesByPath, metadata);
389
+ addMigrationFileEntry(filesByPath, row.file_path, metadata);
390
+ for (const file of filesByPath.values()) {
391
+ if (typeof file.file_state_hash !== "string")
392
+ continue;
393
+ upsert.run(file.file_path, file.file_state_hash, sourceChangeId, row.updated_at);
394
+ written += 1;
395
+ }
396
+ }
397
+ });
398
+ tx();
399
+ return written;
400
+ }
401
+ function repairHistoricalDuplicateChangeRecords() {
402
+ const migrationKey = "migration:aggregate_change_records:v1";
403
+ try {
404
+ if (db.prepare(`SELECT 1 FROM meta_kv WHERE key = ? LIMIT 1`).get(migrationKey))
405
+ return;
406
+ const removedChangeLogs = repairDuplicateChangeLogs();
407
+ const removedChangeIntentMemory = repairDuplicateChangeIntentMemoryItems();
408
+ const syncedFileStates = backfillSyncedFileStatesFromChangeIntentMemory();
409
+ const result = JSON.stringify({
410
+ removed_change_logs: removedChangeLogs,
411
+ removed_change_intent_memory: removedChangeIntentMemory,
412
+ synced_file_states: syncedFileStates,
413
+ ran_at: new Date().toISOString(),
414
+ });
415
+ db.transaction(() => {
416
+ db.prepare(`INSERT INTO meta_kv (key, value, updated_at)
417
+ VALUES (?, ?, CURRENT_TIMESTAMP)`).run(migrationKey, result);
418
+ db.prepare(`INSERT INTO meta_kv (key, value, updated_at)
419
+ VALUES ('maintenance:dedupe_change_records:last_run', ?, CURRENT_TIMESTAMP)
420
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`).run(result);
421
+ })();
422
+ }
423
+ catch (err) {
424
+ console.error("[vectormind] historical duplicate change-record repair failed:", err);
425
+ }
426
+ }
129
427
  export function openDatabaseRuntime(projectRoot) {
130
428
  const vmDir = path.join(projectRoot, ".vectormind");
131
429
  try {
@@ -139,23 +437,26 @@ export function openDatabaseRuntime(projectRoot) {
139
437
  db.pragma("journal_mode = WAL");
140
438
  db.pragma("foreign_keys = ON");
141
439
  db.exec(`
142
- CREATE TABLE IF NOT EXISTS requirements (
143
- id INTEGER PRIMARY KEY AUTOINCREMENT,
144
- title TEXT NOT NULL,
145
- status TEXT DEFAULT 'active',
146
- context_data TEXT,
147
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
148
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
440
+ CREATE TABLE IF NOT EXISTS requirements (
441
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
442
+ title TEXT NOT NULL,
443
+ status TEXT DEFAULT 'active',
444
+ context_data TEXT,
445
+ goal_key TEXT,
446
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
447
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
149
448
  );
150
449
 
151
- CREATE TABLE IF NOT EXISTS change_logs (
152
- id INTEGER PRIMARY KEY AUTOINCREMENT,
153
- req_id INTEGER,
154
- file_path TEXT,
155
- intent_summary TEXT,
156
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
157
- FOREIGN KEY(req_id) REFERENCES requirements(id)
158
- );
450
+ CREATE TABLE IF NOT EXISTS change_logs (
451
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
452
+ req_id INTEGER,
453
+ file_path TEXT,
454
+ intent_summary TEXT,
455
+ files_json TEXT,
456
+ file_count INTEGER DEFAULT 1,
457
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
458
+ FOREIGN KEY(req_id) REFERENCES requirements(id)
459
+ );
159
460
 
160
461
  CREATE TABLE IF NOT EXISTS symbols (
161
462
  name TEXT,
@@ -165,11 +466,55 @@ export function openDatabaseRuntime(projectRoot) {
165
466
  PRIMARY KEY(name, file_path)
166
467
  );
167
468
 
168
- CREATE INDEX IF NOT EXISTS idx_change_logs_req_id_timestamp
169
- ON change_logs(req_id, timestamp DESC);
170
-
171
- CREATE INDEX IF NOT EXISTS idx_symbols_name
172
- ON symbols(name);
469
+ CREATE INDEX IF NOT EXISTS idx_change_logs_req_id_timestamp
470
+ ON change_logs(req_id, timestamp DESC);
471
+
472
+ CREATE TABLE IF NOT EXISTS synced_file_states (
473
+ file_path TEXT PRIMARY KEY,
474
+ file_state_hash TEXT,
475
+ source_change_id INTEGER,
476
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
477
+ );
478
+
479
+ CREATE INDEX IF NOT EXISTS idx_synced_file_states_updated_at
480
+ ON synced_file_states(updated_at DESC);
481
+
482
+ CREATE TABLE IF NOT EXISTS mcp_guard_events (
483
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
484
+ tool_name TEXT NOT NULL,
485
+ event_type TEXT NOT NULL,
486
+ severity TEXT DEFAULT 'info',
487
+ title TEXT NOT NULL,
488
+ detail TEXT,
489
+ metadata_json TEXT,
490
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
491
+ );
492
+
493
+ CREATE INDEX IF NOT EXISTS idx_mcp_guard_events_created_at
494
+ ON mcp_guard_events(created_at DESC, id DESC);
495
+
496
+ CREATE INDEX IF NOT EXISTS idx_mcp_guard_events_type_created_at
497
+ ON mcp_guard_events(event_type, created_at DESC);
498
+
499
+ CREATE TABLE IF NOT EXISTS mcp_tool_metrics (
500
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
501
+ tool_name TEXT NOT NULL,
502
+ duration_ms REAL NOT NULL,
503
+ raw_output_chars INTEGER NOT NULL,
504
+ output_chars INTEGER NOT NULL,
505
+ is_error INTEGER NOT NULL DEFAULT 0,
506
+ root_source TEXT,
507
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
508
+ );
509
+
510
+ CREATE INDEX IF NOT EXISTS idx_mcp_tool_metrics_created_at
511
+ ON mcp_tool_metrics(created_at DESC, id DESC);
512
+
513
+ CREATE INDEX IF NOT EXISTS idx_mcp_tool_metrics_tool_created_at
514
+ ON mcp_tool_metrics(tool_name, created_at DESC);
515
+
516
+ CREATE INDEX IF NOT EXISTS idx_symbols_name
517
+ ON symbols(name);
173
518
 
174
519
  CREATE TABLE IF NOT EXISTS memory_items (
175
520
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -259,58 +604,118 @@ export function openDatabaseRuntime(projectRoot) {
259
604
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
260
605
  );
261
606
  `);
262
- if (!columnExists("requirements", "updated_at")) {
263
- db.exec(`ALTER TABLE requirements ADD COLUMN updated_at DATETIME`);
264
- db.exec(`UPDATE requirements SET updated_at = COALESCE(created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL`);
265
- }
266
- db.exec(`
267
- UPDATE requirements SET updated_at = COALESCE(updated_at, created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL;
268
- CREATE INDEX IF NOT EXISTS idx_requirements_status_updated_at
269
- ON requirements(status, updated_at DESC, id DESC);
270
- CREATE TRIGGER IF NOT EXISTS vectormind_requirements_touch_updated_at
271
- AFTER UPDATE ON requirements
272
- FOR EACH ROW
273
- WHEN NEW.updated_at = OLD.updated_at
274
- BEGIN
275
- UPDATE requirements SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
276
- END;
277
- `);
607
+ const migrateLegacyColumns = db.transaction(() => {
608
+ if (!columnExists("requirements", "updated_at")) {
609
+ db.exec(`ALTER TABLE requirements ADD COLUMN updated_at DATETIME`);
610
+ db.exec(`UPDATE requirements SET updated_at = COALESCE(created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL`);
611
+ }
612
+ if (!columnExists("requirements", "goal_key")) {
613
+ db.exec(`ALTER TABLE requirements ADD COLUMN goal_key TEXT`);
614
+ }
615
+ if (!columnExists("change_logs", "files_json")) {
616
+ db.exec(`ALTER TABLE change_logs ADD COLUMN files_json TEXT`);
617
+ }
618
+ if (!columnExists("change_logs", "file_count")) {
619
+ db.exec(`ALTER TABLE change_logs ADD COLUMN file_count INTEGER DEFAULT 1`);
620
+ db.exec(`UPDATE change_logs SET file_count = 1 WHERE file_count IS NULL`);
621
+ }
622
+ });
623
+ migrateLegacyColumns.immediate();
624
+ db.transaction(() => {
625
+ db.exec(`
626
+ UPDATE requirements SET updated_at = COALESCE(updated_at, created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL;
627
+ UPDATE requirements
628
+ SET updated_at = (
629
+ SELECT MAX(mi.updated_at)
630
+ FROM memory_items mi
631
+ WHERE mi.kind = 'requirement' AND mi.req_id = requirements.id
632
+ )
633
+ WHERE (
634
+ SELECT MAX(mi.updated_at)
635
+ FROM memory_items mi
636
+ WHERE mi.kind = 'requirement' AND mi.req_id = requirements.id
637
+ ) IS NOT NULL
638
+ AND datetime((
639
+ SELECT MAX(mi.updated_at)
640
+ FROM memory_items mi
641
+ WHERE mi.kind = 'requirement' AND mi.req_id = requirements.id
642
+ )) > datetime(COALESCE(requirements.updated_at, requirements.created_at, '1970-01-01 00:00:00'));
643
+ UPDATE requirements
644
+ SET status = 'superseded', updated_at = CURRENT_TIMESTAMP
645
+ WHERE status = 'active'
646
+ AND goal_key IS NOT NULL
647
+ AND EXISTS (
648
+ SELECT 1 FROM requirements newer
649
+ WHERE newer.status = 'active'
650
+ AND newer.goal_key = requirements.goal_key
651
+ AND newer.id > requirements.id
652
+ );
653
+ UPDATE memory_items
654
+ SET metadata_json = json_set(
655
+ CASE
656
+ WHEN json_valid(COALESCE(metadata_json, '{}')) THEN COALESCE(metadata_json, '{}')
657
+ ELSE '{}'
658
+ END,
659
+ '$.status',
660
+ 'superseded',
661
+ '$.superseded',
662
+ json('true')
663
+ ),
664
+ updated_at = CURRENT_TIMESTAMP
665
+ WHERE kind = 'requirement'
666
+ AND req_id IN (
667
+ SELECT id FROM requirements WHERE status = 'superseded'
668
+ )
669
+ AND COALESCE(json_extract(
670
+ CASE
671
+ WHEN json_valid(COALESCE(metadata_json, '{}')) THEN COALESCE(metadata_json, '{}')
672
+ ELSE '{}'
673
+ END,
674
+ '$.status'
675
+ ), 'active') <> 'superseded';
676
+ CREATE INDEX IF NOT EXISTS idx_requirements_status_updated_at
677
+ ON requirements(status, updated_at DESC, id DESC);
678
+ CREATE INDEX IF NOT EXISTS idx_requirements_goal_key_status
679
+ ON requirements(goal_key, status, updated_at DESC, id DESC);
680
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_requirements_active_goal_key_unique
681
+ ON requirements(goal_key) WHERE status = 'active' AND goal_key IS NOT NULL;
682
+ CREATE TRIGGER IF NOT EXISTS vectormind_requirements_touch_updated_at
683
+ AFTER UPDATE ON requirements
684
+ FOR EACH ROW
685
+ WHEN NEW.updated_at = OLD.updated_at
686
+ BEGIN
687
+ UPDATE requirements SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
688
+ END;
689
+ `);
690
+ })();
278
691
  initMemoryItemsFts();
279
- insertRequirementStmt = db.prepare(`INSERT INTO requirements (title, context_data, status) VALUES (?, ?, 'active')`);
280
- completeAllActiveRequirementsStmt = db.prepare(`UPDATE requirements SET status = 'completed' WHERE status = 'active'`);
281
- completeRequirementByIdStmt = db.prepare(`UPDATE requirements SET status = 'completed' WHERE id = ?`);
282
- getActiveRequirementStmt = db.prepare(`SELECT id, title, status, context_data, created_at
283
- FROM requirements
692
+ repairHistoricalDuplicateChangeRecords();
693
+ insertRequirementStmt = db.prepare(`INSERT INTO requirements (title, context_data, goal_key, status) VALUES (?, ?, ?, 'active')`);
694
+ completeAllActiveRequirementsStmt = db.prepare(`UPDATE requirements SET status = 'completed', updated_at = CURRENT_TIMESTAMP WHERE status = 'active'`);
695
+ completeRequirementByIdStmt = db.prepare(`UPDATE requirements SET status = 'completed', updated_at = CURRENT_TIMESTAMP WHERE id = ?`);
696
+ getActiveRequirementStmt = db.prepare(`SELECT id, title, status, context_data, goal_key, created_at
697
+ FROM requirements
284
698
  WHERE status = 'active'
285
699
  ORDER BY updated_at DESC, created_at DESC, id DESC
286
700
  LIMIT 1`);
287
- listActiveRequirementsStmt = db.prepare(`SELECT id, title, status, context_data, created_at
701
+ getActiveRequirementByIdStmt = db.prepare(`SELECT id, title, status, context_data, goal_key, created_at
702
+ FROM requirements
703
+ WHERE status = 'active' AND id = ?
704
+ LIMIT 1`);
705
+ getActiveRequirementByGoalKeyStmt = db.prepare(`SELECT id, title, status, context_data, goal_key, created_at
706
+ FROM requirements
707
+ WHERE status = 'active' AND goal_key = ?
708
+ ORDER BY updated_at DESC, created_at DESC, id DESC
709
+ LIMIT 1`);
710
+ listActiveRequirementsStmt = db.prepare(`SELECT id, title, status, context_data, goal_key, created_at
288
711
  FROM requirements
289
712
  WHERE status = 'active'
290
713
  ORDER BY updated_at DESC, created_at DESC, id DESC
291
714
  LIMIT ?`);
292
- listRecentRequirementsStmt = db.prepare(`SELECT id, title, status, context_data, created_at
715
+ listRecentRequirementsStmt = db.prepare(`SELECT id, title, status, context_data, goal_key, created_at
293
716
  FROM requirements
294
717
  ORDER BY updated_at DESC, created_at DESC, id DESC
295
718
  LIMIT ?`);
296
- completeAllActiveRequirementMemoryItemsStmt = db.prepare(`UPDATE memory_items
297
- SET metadata_json = json_set(
298
- CASE
299
- WHEN json_valid(COALESCE(metadata_json, '{}')) THEN COALESCE(metadata_json, '{}')
300
- ELSE '{}'
301
- END,
302
- '$.status',
303
- ?
304
- ),
305
- updated_at = CURRENT_TIMESTAMP
306
- WHERE kind = 'requirement'
307
- AND json_extract(
308
- CASE
309
- WHEN json_valid(COALESCE(metadata_json, '{}')) THEN COALESCE(metadata_json, '{}')
310
- ELSE '{}'
311
- END,
312
- '$.status'
313
- ) = ?`);
314
719
  completeRequirementMemoryItemByReqIdStmt = db.prepare(`UPDATE memory_items
315
720
  SET metadata_json = json_set(
316
721
  CASE
@@ -323,12 +728,30 @@ export function openDatabaseRuntime(projectRoot) {
323
728
  updated_at = CURRENT_TIMESTAMP
324
729
  WHERE kind = 'requirement'
325
730
  AND req_id = ?`);
326
- listChangeLogsForRequirementStmt = db.prepare(`SELECT id, req_id, file_path, intent_summary, timestamp
327
- FROM change_logs
328
- WHERE req_id = ?
329
- ORDER BY timestamp DESC, id DESC
731
+ listChangeLogsForRequirementStmt = db.prepare(`SELECT id, req_id, file_path, intent_summary, files_json, file_count, timestamp
732
+ FROM change_logs
733
+ WHERE req_id = ?
734
+ ORDER BY timestamp DESC, id DESC
330
735
  LIMIT ?`);
331
- insertChangeLogStmt = db.prepare(`INSERT INTO change_logs (req_id, file_path, intent_summary) VALUES (?, ?, ?)`);
736
+ insertChangeLogStmt = db.prepare(`INSERT INTO change_logs (req_id, file_path, intent_summary, files_json, file_count)
737
+ VALUES (?, ?, ?, ?, ?)`);
738
+ upsertSyncedFileStateStmt = db.prepare(`INSERT INTO synced_file_states (file_path, file_state_hash, source_change_id, updated_at)
739
+ VALUES (?, ?, ?, CURRENT_TIMESTAMP)
740
+ ON CONFLICT(file_path) DO UPDATE SET
741
+ file_state_hash = excluded.file_state_hash,
742
+ source_change_id = excluded.source_change_id,
743
+ updated_at = CURRENT_TIMESTAMP`);
744
+ getSyncedFileStateStmt = db.prepare(`SELECT file_path, file_state_hash, source_change_id, updated_at
745
+ FROM synced_file_states
746
+ WHERE file_path = ?
747
+ LIMIT 1`);
748
+ insertMcpGuardEventStmt = db.prepare(`INSERT INTO mcp_guard_events
749
+ (tool_name, event_type, severity, title, detail, metadata_json)
750
+ VALUES
751
+ (?, ?, ?, ?, ?, ?)`);
752
+ insertMcpToolMetricStmt = db.prepare(`INSERT INTO mcp_tool_metrics
753
+ (tool_name, duration_ms, raw_output_chars, output_chars, is_error, root_source)
754
+ VALUES (?, ?, ?, ?, ?, ?)`);
332
755
  insertMemoryItemStmt = db.prepare(`INSERT INTO memory_items
333
756
  (kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash)
334
757
  VALUES
@@ -468,14 +891,19 @@ export function openDatabaseRuntime(projectRoot) {
468
891
  statements: {
469
892
  insertRequirementStmt,
470
893
  getActiveRequirementStmt,
894
+ getActiveRequirementByIdStmt,
895
+ getActiveRequirementByGoalKeyStmt,
471
896
  listActiveRequirementsStmt,
472
897
  listRecentRequirementsStmt,
473
898
  completeAllActiveRequirementsStmt,
474
899
  completeRequirementByIdStmt,
475
- completeAllActiveRequirementMemoryItemsStmt,
476
900
  completeRequirementMemoryItemByReqIdStmt,
477
901
  listChangeLogsForRequirementStmt,
478
902
  insertChangeLogStmt,
903
+ upsertSyncedFileStateStmt,
904
+ getSyncedFileStateStmt,
905
+ insertMcpGuardEventStmt,
906
+ insertMcpToolMetricStmt,
479
907
  insertMemoryItemStmt,
480
908
  getMemoryItemByIdStmt,
481
909
  getRequirementMemoryItemIdStmt,