@coreyuan/vector-mind 1.0.42 → 1.0.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +375 -342
- package/dist/builtin-conventions.js +18 -3
- package/dist/builtin-conventions.js.map +1 -1
- package/dist/builtin-instructions.d.ts +10 -9
- package/dist/builtin-instructions.js +3 -2
- package/dist/builtin-instructions.js.map +1 -1
- package/dist/index.js +1724 -45
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/builtin-policy.d.ts +0 -7
- package/dist/builtin-policy.js +0 -24
- package/dist/builtin-policy.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -14,9 +14,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
14
14
|
import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
|
|
15
15
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
16
16
|
import { BUILTIN_CONVENTIONS } from "./builtin-conventions.js";
|
|
17
|
-
import { BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS, BUILTIN_DESTRUCTIVE_OPERATION_GUARD_INSTRUCTIONS, BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS, BUILTIN_GIT_COMMIT_SUMMARY_INSTRUCTIONS, BUILTIN_LOW_OVERHEAD_WORKFLOW_INSTRUCTIONS, BUILTIN_PAYLOAD_GUARD_INSTRUCTIONS, BUILTIN_PLAN_LITE_INSTRUCTIONS, BUILTIN_THREAD_HANDOFF_SWITCH_INSTRUCTIONS, BUILTIN_WRITE_POLICY_INSTRUCTIONS, } from "./builtin-instructions.js";
|
|
17
|
+
import { BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS, BUILTIN_DESTRUCTIVE_OPERATION_GUARD_INSTRUCTIONS, BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS, BUILTIN_GIT_COMMIT_SUMMARY_INSTRUCTIONS, BUILTIN_LOW_OVERHEAD_WORKFLOW_INSTRUCTIONS, BUILTIN_PAYLOAD_GUARD_INSTRUCTIONS, BUILTIN_PLAN_LITE_INSTRUCTIONS, BUILTIN_REQUIREMENT_BOUNDARY_AND_MODULARITY_INSTRUCTIONS, BUILTIN_THREAD_HANDOFF_SWITCH_INSTRUCTIONS, BUILTIN_WRITE_POLICY_INSTRUCTIONS, } from "./builtin-instructions.js";
|
|
18
18
|
const SERVER_NAME = "vector-mind";
|
|
19
|
-
const SERVER_VERSION = "1.0.
|
|
19
|
+
const SERVER_VERSION = "1.0.48";
|
|
20
20
|
const rootFromEnv = process.env.VECTORMIND_ROOT?.trim() ?? "";
|
|
21
21
|
const prettyJsonOutput = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_PRETTY_JSON ?? "").trim().toLowerCase());
|
|
22
22
|
const debugLogEnabled = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_DEBUG_LOG ?? "").trim().toLowerCase());
|
|
@@ -65,6 +65,42 @@ const PENDING_PRUNE_EVERY = (() => {
|
|
|
65
65
|
return 500;
|
|
66
66
|
return n;
|
|
67
67
|
})();
|
|
68
|
+
const DEVELOPMENT_WARN_FILE_LINES = (() => {
|
|
69
|
+
const raw = process.env.VECTORMIND_WARN_FILE_LINES?.trim();
|
|
70
|
+
if (!raw)
|
|
71
|
+
return 800;
|
|
72
|
+
const n = Number.parseInt(raw, 10);
|
|
73
|
+
if (!Number.isFinite(n) || n < 100)
|
|
74
|
+
return 800;
|
|
75
|
+
return Math.min(50_000, n);
|
|
76
|
+
})();
|
|
77
|
+
const DEVELOPMENT_BLOCK_FILE_LINES = (() => {
|
|
78
|
+
const raw = process.env.VECTORMIND_BLOCK_FILE_LINES?.trim();
|
|
79
|
+
if (!raw)
|
|
80
|
+
return 1200;
|
|
81
|
+
const n = Number.parseInt(raw, 10);
|
|
82
|
+
if (!Number.isFinite(n) || n < DEVELOPMENT_WARN_FILE_LINES)
|
|
83
|
+
return Math.max(1200, DEVELOPMENT_WARN_FILE_LINES);
|
|
84
|
+
return Math.min(100_000, n);
|
|
85
|
+
})();
|
|
86
|
+
const DEVELOPMENT_WARN_FILE_BYTES = (() => {
|
|
87
|
+
const raw = process.env.VECTORMIND_WARN_FILE_BYTES?.trim();
|
|
88
|
+
if (!raw)
|
|
89
|
+
return 120_000;
|
|
90
|
+
const n = Number.parseInt(raw, 10);
|
|
91
|
+
if (!Number.isFinite(n) || n < 10_000)
|
|
92
|
+
return 120_000;
|
|
93
|
+
return Math.min(20_000_000, n);
|
|
94
|
+
})();
|
|
95
|
+
const DEVELOPMENT_WARN_PENDING_FILES = (() => {
|
|
96
|
+
const raw = process.env.VECTORMIND_WARN_PENDING_FILES?.trim();
|
|
97
|
+
if (!raw)
|
|
98
|
+
return 12;
|
|
99
|
+
const n = Number.parseInt(raw, 10);
|
|
100
|
+
if (!Number.isFinite(n) || n < 1)
|
|
101
|
+
return 12;
|
|
102
|
+
return Math.min(500, n);
|
|
103
|
+
})();
|
|
68
104
|
const RIPGREP_RESOLVE_TIMEOUT_MS = 5_000;
|
|
69
105
|
const RIPGREP_SEARCH_TIMEOUT_MS = 30_000;
|
|
70
106
|
const RIPGREP_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
@@ -122,6 +158,48 @@ const INDEX_AUTO_PRUNE_IGNORED = (() => {
|
|
|
122
158
|
return true;
|
|
123
159
|
return ["1", "true", "on", "yes"].includes(raw);
|
|
124
160
|
})();
|
|
161
|
+
const MAINTENANCE_AUTO_ENABLED = (() => {
|
|
162
|
+
const raw = (process.env.VECTORMIND_MAINTENANCE_AUTO ?? "").trim().toLowerCase();
|
|
163
|
+
if (!raw)
|
|
164
|
+
return true;
|
|
165
|
+
return ["1", "true", "on", "yes"].includes(raw);
|
|
166
|
+
})();
|
|
167
|
+
const MAINTENANCE_INTERVAL_HOURS = (() => {
|
|
168
|
+
const raw = process.env.VECTORMIND_MAINTENANCE_INTERVAL_HOURS?.trim();
|
|
169
|
+
if (!raw)
|
|
170
|
+
return 24;
|
|
171
|
+
const n = Number.parseInt(raw, 10);
|
|
172
|
+
if (!Number.isFinite(n) || n < 1)
|
|
173
|
+
return 24;
|
|
174
|
+
return Math.min(24 * 30, n);
|
|
175
|
+
})();
|
|
176
|
+
const MAINTENANCE_COMPACT_AFTER_DAYS = (() => {
|
|
177
|
+
const raw = process.env.VECTORMIND_COMPACT_AFTER_DAYS?.trim();
|
|
178
|
+
if (!raw)
|
|
179
|
+
return 45;
|
|
180
|
+
const n = Number.parseInt(raw, 10);
|
|
181
|
+
if (!Number.isFinite(n) || n < 1)
|
|
182
|
+
return 45;
|
|
183
|
+
return Math.min(3650, n);
|
|
184
|
+
})();
|
|
185
|
+
const MAINTENANCE_MAX_MEMORY_ITEMS = (() => {
|
|
186
|
+
const raw = process.env.VECTORMIND_MAINTENANCE_MAX_MEMORY_ITEMS?.trim();
|
|
187
|
+
if (!raw)
|
|
188
|
+
return 250;
|
|
189
|
+
const n = Number.parseInt(raw, 10);
|
|
190
|
+
if (!Number.isFinite(n) || n < 1)
|
|
191
|
+
return 250;
|
|
192
|
+
return Math.min(5000, n);
|
|
193
|
+
})();
|
|
194
|
+
const MAINTENANCE_MAX_INDEX_FILES = (() => {
|
|
195
|
+
const raw = process.env.VECTORMIND_MAINTENANCE_MAX_INDEX_FILES?.trim();
|
|
196
|
+
if (!raw)
|
|
197
|
+
return 1500;
|
|
198
|
+
const n = Number.parseInt(raw, 10);
|
|
199
|
+
if (!Number.isFinite(n) || n < 1)
|
|
200
|
+
return 1500;
|
|
201
|
+
return Math.min(50_000, n);
|
|
202
|
+
})();
|
|
125
203
|
const ROOTS_LIST_TIMEOUT_MS = (() => {
|
|
126
204
|
const raw = process.env.VECTORMIND_ROOTS_TIMEOUT_MS?.trim();
|
|
127
205
|
if (!raw)
|
|
@@ -140,6 +218,15 @@ const BOOTSTRAP_SEMANTIC_TIMEOUT_MS = (() => {
|
|
|
140
218
|
return 2500;
|
|
141
219
|
return n;
|
|
142
220
|
})();
|
|
221
|
+
const SEMANTIC_EMBEDDINGS_TIMEOUT_MS = (() => {
|
|
222
|
+
const raw = process.env.VECTORMIND_EMBEDDINGS_TIMEOUT_MS?.trim();
|
|
223
|
+
if (!raw)
|
|
224
|
+
return 1500;
|
|
225
|
+
const n = Number.parseInt(raw, 10);
|
|
226
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
227
|
+
return 1500;
|
|
228
|
+
return n;
|
|
229
|
+
})();
|
|
143
230
|
let initialized = false;
|
|
144
231
|
let rootSource = "cwd";
|
|
145
232
|
let projectRoot = "";
|
|
@@ -150,6 +237,7 @@ let watcherReady = false;
|
|
|
150
237
|
let initializationPromise = null;
|
|
151
238
|
let insertRequirementStmt;
|
|
152
239
|
let getActiveRequirementStmt;
|
|
240
|
+
let listActiveRequirementsStmt;
|
|
153
241
|
let listRecentRequirementsStmt;
|
|
154
242
|
let completeAllActiveRequirementsStmt;
|
|
155
243
|
let completeRequirementByIdStmt;
|
|
@@ -170,6 +258,7 @@ let listCurrentDecisionsStmt;
|
|
|
170
258
|
let upsertProjectSummaryStmt;
|
|
171
259
|
let getProjectSummaryStmt;
|
|
172
260
|
let listRecentNotesStmt;
|
|
261
|
+
let listRecentContextItemsStmt;
|
|
173
262
|
let getLatestChangeIntentForFileStmt;
|
|
174
263
|
let deleteFileChunkItemsStmt;
|
|
175
264
|
let getEmbeddingMetaStmt;
|
|
@@ -189,6 +278,8 @@ let insertTokenSavingsStmt;
|
|
|
189
278
|
let summarizeTokenSavingsStmt;
|
|
190
279
|
let summarizeTokenSavingsByToolStmt;
|
|
191
280
|
let listRecentTokenSavingsStmt;
|
|
281
|
+
let getKvStmt;
|
|
282
|
+
let setKvStmt;
|
|
192
283
|
let indexFileSymbolsTx = null;
|
|
193
284
|
let activitySeq = 0;
|
|
194
285
|
const activityLog = [];
|
|
@@ -284,6 +375,8 @@ function summarizeActivityEvent(e) {
|
|
|
284
375
|
return `sync_change_intent #${String(d.req_id ?? "")} files=${String(d.files_total ?? "")}`;
|
|
285
376
|
case "complete_requirement":
|
|
286
377
|
return `complete_requirement ${String(d.all_active ? "all_active" : d.req_id ?? "")}`;
|
|
378
|
+
case "memory_maintenance":
|
|
379
|
+
return `memory_maintenance trigger=${String(d.trigger ?? "")} compacted=${String(d.compacted ?? "")} stale=${String(d.stale_files ?? "")} chunks_deleted=${String(d.chunks_deleted ?? "")}`;
|
|
287
380
|
default:
|
|
288
381
|
return e.type;
|
|
289
382
|
}
|
|
@@ -723,9 +816,9 @@ function pruneFilenameNoiseIndexes() {
|
|
|
723
816
|
return { chunks_deleted: 0, symbols_deleted: 0 };
|
|
724
817
|
try {
|
|
725
818
|
const suffixWhere = NOISE_FILE_SUFFIXES.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
|
|
726
|
-
const baseWhere = NOISE_FILE_BASENAMES.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
|
|
819
|
+
const baseWhere = NOISE_FILE_BASENAMES.map(() => "(LOWER(file_path) = ? OR LOWER(file_path) LIKE ?)").join(" OR ");
|
|
727
820
|
const suffixArgs = NOISE_FILE_SUFFIXES.map((s) => `%${s}`);
|
|
728
|
-
const baseArgs = NOISE_FILE_BASENAMES.
|
|
821
|
+
const baseArgs = NOISE_FILE_BASENAMES.flatMap((n) => [n, `%/${n}`]);
|
|
729
822
|
const whereParts = [];
|
|
730
823
|
const args = [];
|
|
731
824
|
if (suffixWhere) {
|
|
@@ -764,6 +857,446 @@ function pruneFilenameNoiseIndexes() {
|
|
|
764
857
|
return { chunks_deleted: 0, symbols_deleted: 0 };
|
|
765
858
|
}
|
|
766
859
|
}
|
|
860
|
+
function kvGet(key) {
|
|
861
|
+
try {
|
|
862
|
+
const row = getKvStmt?.get(key);
|
|
863
|
+
return row?.value ?? null;
|
|
864
|
+
}
|
|
865
|
+
catch {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function kvSet(key, value) {
|
|
870
|
+
try {
|
|
871
|
+
setKvStmt?.run(key, value);
|
|
872
|
+
}
|
|
873
|
+
catch (err) {
|
|
874
|
+
console.error("[vectormind] kv set failed:", err);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
function distinctChunkAndSymbolFilePaths(limit) {
|
|
878
|
+
const rows = db
|
|
879
|
+
.prepare(`SELECT file_path
|
|
880
|
+
FROM (
|
|
881
|
+
SELECT file_path, MAX(updated_at) AS updated_at
|
|
882
|
+
FROM memory_items
|
|
883
|
+
WHERE file_path IS NOT NULL
|
|
884
|
+
AND (kind = 'code_chunk' OR kind = 'doc_chunk')
|
|
885
|
+
GROUP BY file_path
|
|
886
|
+
UNION
|
|
887
|
+
SELECT file_path, CURRENT_TIMESTAMP AS updated_at
|
|
888
|
+
FROM symbols
|
|
889
|
+
WHERE file_path IS NOT NULL
|
|
890
|
+
GROUP BY file_path
|
|
891
|
+
)
|
|
892
|
+
WHERE file_path IS NOT NULL
|
|
893
|
+
ORDER BY updated_at ASC
|
|
894
|
+
LIMIT ?`)
|
|
895
|
+
.all(limit);
|
|
896
|
+
return Array.from(new Set(rows.map((r) => r.file_path).filter(Boolean)));
|
|
897
|
+
}
|
|
898
|
+
function classifyStaleIndexFile(filePath) {
|
|
899
|
+
if (!filePath)
|
|
900
|
+
return "empty_path";
|
|
901
|
+
if (shouldIgnoreDbFilePath(filePath))
|
|
902
|
+
return "ignored_path";
|
|
903
|
+
if (shouldIgnoreContentFile(filePath))
|
|
904
|
+
return "filename_noise";
|
|
905
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath);
|
|
906
|
+
const rel = path.relative(projectRoot, absPath);
|
|
907
|
+
if (rel.startsWith("..") || path.isAbsolute(rel))
|
|
908
|
+
return "outside_project";
|
|
909
|
+
let stat;
|
|
910
|
+
try {
|
|
911
|
+
stat = fs.statSync(absPath);
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
return "missing_file";
|
|
915
|
+
}
|
|
916
|
+
if (!stat.isFile())
|
|
917
|
+
return "not_file";
|
|
918
|
+
if (!isContentIndexableFile(absPath) && !isSymbolIndexableFile(absPath))
|
|
919
|
+
return "not_indexable";
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
function pruneStaleFileIndexes(opts) {
|
|
923
|
+
const filePaths = distinctChunkAndSymbolFilePaths(Math.min(50_000, opts.maxIndexFiles * 3));
|
|
924
|
+
const matched = [];
|
|
925
|
+
for (const fp of filePaths) {
|
|
926
|
+
if (matched.length >= opts.maxIndexFiles)
|
|
927
|
+
break;
|
|
928
|
+
const reason = classifyStaleIndexFile(fp);
|
|
929
|
+
if (reason)
|
|
930
|
+
matched.push({ file_path: fp, reason });
|
|
931
|
+
}
|
|
932
|
+
let chunksDeleted = 0;
|
|
933
|
+
let symbolsDeleted = 0;
|
|
934
|
+
const samples = matched.slice(0, 20).map((m) => `${m.file_path} (${m.reason})`);
|
|
935
|
+
if (!opts.dryRun && matched.length) {
|
|
936
|
+
const tx = db.transaction(() => {
|
|
937
|
+
for (const m of matched) {
|
|
938
|
+
chunksDeleted += deleteFileChunkItemsStmt.run(m.file_path).changes;
|
|
939
|
+
symbolsDeleted += deleteSymbolsForFileStmt.run(m.file_path).changes;
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
try {
|
|
943
|
+
tx();
|
|
944
|
+
}
|
|
945
|
+
catch (err) {
|
|
946
|
+
console.error("[vectormind] prune stale indexes failed:", err);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
else if (opts.dryRun && matched.length) {
|
|
950
|
+
const countChunksStmt = db.prepare(`SELECT COUNT(1) AS c
|
|
951
|
+
FROM memory_items
|
|
952
|
+
WHERE file_path = ?
|
|
953
|
+
AND (kind = 'code_chunk' OR kind = 'doc_chunk')`);
|
|
954
|
+
const countSymbolsStmt = db.prepare(`SELECT COUNT(1) AS c FROM symbols WHERE file_path = ?`);
|
|
955
|
+
for (const m of matched) {
|
|
956
|
+
chunksDeleted += Number(countChunksStmt.get(m.file_path)?.c ?? 0);
|
|
957
|
+
symbolsDeleted += Number(countSymbolsStmt.get(m.file_path)?.c ?? 0);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (!opts.dryRun && (chunksDeleted || symbolsDeleted)) {
|
|
961
|
+
logActivity("index_prune", {
|
|
962
|
+
reason: "stale_files",
|
|
963
|
+
files_matched: matched.length,
|
|
964
|
+
chunks_deleted: chunksDeleted,
|
|
965
|
+
symbols_deleted: symbolsDeleted,
|
|
966
|
+
samples,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
files_checked: filePaths.length,
|
|
971
|
+
files_matched: matched.length,
|
|
972
|
+
chunks_deleted: chunksDeleted,
|
|
973
|
+
symbols_deleted: symbolsDeleted,
|
|
974
|
+
samples,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function countIgnoredIndexDeletes() {
|
|
978
|
+
if (!IGNORED_LIKE_PATTERNS.length)
|
|
979
|
+
return { chunks_deleted: 0, symbols_deleted: 0 };
|
|
980
|
+
const where = IGNORED_LIKE_PATTERNS
|
|
981
|
+
.map(() => "LOWER(REPLACE(file_path, '\\\\', '/')) LIKE ?")
|
|
982
|
+
.join(" OR ");
|
|
983
|
+
const chunksDeleted = Number(db
|
|
984
|
+
.prepare(`SELECT COUNT(1) AS c
|
|
985
|
+
FROM memory_items
|
|
986
|
+
WHERE file_path IS NOT NULL
|
|
987
|
+
AND (kind = 'code_chunk' OR kind = 'doc_chunk')
|
|
988
|
+
AND (${where})`)
|
|
989
|
+
.get(...IGNORED_LIKE_PATTERNS)?.c ?? 0);
|
|
990
|
+
const symbolsDeleted = Number(db
|
|
991
|
+
.prepare(`SELECT COUNT(1) AS c
|
|
992
|
+
FROM symbols
|
|
993
|
+
WHERE file_path IS NOT NULL
|
|
994
|
+
AND (${where})`)
|
|
995
|
+
.get(...IGNORED_LIKE_PATTERNS)?.c ?? 0);
|
|
996
|
+
return { chunks_deleted: chunksDeleted, symbols_deleted: symbolsDeleted };
|
|
997
|
+
}
|
|
998
|
+
function countFilenameNoiseIndexDeletes() {
|
|
999
|
+
const suffixWhere = NOISE_FILE_SUFFIXES.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
|
|
1000
|
+
const baseWhere = NOISE_FILE_BASENAMES.map(() => "(LOWER(file_path) = ? OR LOWER(file_path) LIKE ?)").join(" OR ");
|
|
1001
|
+
const suffixArgs = NOISE_FILE_SUFFIXES.map((s) => `%${s}`);
|
|
1002
|
+
const baseArgs = NOISE_FILE_BASENAMES.flatMap((n) => [n, `%/${n}`]);
|
|
1003
|
+
const whereParts = [];
|
|
1004
|
+
const args = [];
|
|
1005
|
+
if (suffixWhere) {
|
|
1006
|
+
whereParts.push(`(${suffixWhere})`);
|
|
1007
|
+
args.push(...suffixArgs);
|
|
1008
|
+
}
|
|
1009
|
+
if (baseWhere) {
|
|
1010
|
+
whereParts.push(`(${baseWhere})`);
|
|
1011
|
+
args.push(...baseArgs);
|
|
1012
|
+
}
|
|
1013
|
+
if (!whereParts.length)
|
|
1014
|
+
return { chunks_deleted: 0, symbols_deleted: 0 };
|
|
1015
|
+
const where = whereParts.join(" OR ");
|
|
1016
|
+
const chunksDeleted = Number(db
|
|
1017
|
+
.prepare(`SELECT COUNT(1) AS c
|
|
1018
|
+
FROM memory_items
|
|
1019
|
+
WHERE file_path IS NOT NULL
|
|
1020
|
+
AND (kind = 'code_chunk' OR kind = 'doc_chunk')
|
|
1021
|
+
AND (${where})`)
|
|
1022
|
+
.get(...args)?.c ?? 0);
|
|
1023
|
+
const symbolsDeleted = Number(db
|
|
1024
|
+
.prepare(`SELECT COUNT(1) AS c
|
|
1025
|
+
FROM symbols
|
|
1026
|
+
WHERE file_path IS NOT NULL
|
|
1027
|
+
AND (${where})`)
|
|
1028
|
+
.get(...args)?.c ?? 0);
|
|
1029
|
+
return { chunks_deleted: chunksDeleted, symbols_deleted: symbolsDeleted };
|
|
1030
|
+
}
|
|
1031
|
+
function hiddenEmbeddingIds(limit = 10_000) {
|
|
1032
|
+
const rows = db
|
|
1033
|
+
.prepare(`SELECT e.memory_id AS memory_id, m.metadata_json AS metadata_json
|
|
1034
|
+
FROM embeddings e
|
|
1035
|
+
JOIN memory_items m ON m.id = e.memory_id
|
|
1036
|
+
WHERE m.metadata_json LIKE '%compacted%'
|
|
1037
|
+
OR m.metadata_json LIKE '%superseded%'
|
|
1038
|
+
LIMIT ?`)
|
|
1039
|
+
.all(limit);
|
|
1040
|
+
return rows
|
|
1041
|
+
.filter((r) => isHiddenFromDefaultRecall({ metadata_json: r.metadata_json }))
|
|
1042
|
+
.map((r) => r.memory_id);
|
|
1043
|
+
}
|
|
1044
|
+
function pruneHiddenEmbeddings(dryRun) {
|
|
1045
|
+
const ids = hiddenEmbeddingIds();
|
|
1046
|
+
if (!ids.length)
|
|
1047
|
+
return { embeddings_deleted: 0 };
|
|
1048
|
+
if (!dryRun) {
|
|
1049
|
+
const deleteStmt = db.prepare(`DELETE FROM embeddings WHERE memory_id = ?`);
|
|
1050
|
+
const tx = db.transaction(() => {
|
|
1051
|
+
for (const id of ids)
|
|
1052
|
+
deleteStmt.run(id);
|
|
1053
|
+
});
|
|
1054
|
+
try {
|
|
1055
|
+
tx();
|
|
1056
|
+
}
|
|
1057
|
+
catch (err) {
|
|
1058
|
+
console.error("[vectormind] prune hidden embeddings failed:", err);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return { embeddings_deleted: ids.length };
|
|
1062
|
+
}
|
|
1063
|
+
function selectCompactionCandidates(opts) {
|
|
1064
|
+
const kinds = opts.compactNotes
|
|
1065
|
+
? ["requirement", "change_intent", "note"]
|
|
1066
|
+
: ["requirement", "change_intent"];
|
|
1067
|
+
const placeholders = kinds.map(() => "?").join(", ");
|
|
1068
|
+
const rows = db
|
|
1069
|
+
.prepare(`SELECT
|
|
1070
|
+
m.id, m.kind, m.title, m.content, m.file_path, m.start_line, m.end_line,
|
|
1071
|
+
m.req_id, m.metadata_json, m.content_hash, m.created_at, m.updated_at,
|
|
1072
|
+
r.status AS req_status
|
|
1073
|
+
FROM memory_items m
|
|
1074
|
+
LEFT JOIN requirements r ON r.id = m.req_id
|
|
1075
|
+
WHERE m.kind IN (${placeholders})
|
|
1076
|
+
AND m.updated_at < datetime('now', ?)
|
|
1077
|
+
ORDER BY m.updated_at ASC, m.id ASC
|
|
1078
|
+
LIMIT ?`)
|
|
1079
|
+
.all(...kinds, `-${opts.compactAfterDays} days`, Math.min(20_000, opts.maxMemoryItems * 5));
|
|
1080
|
+
return rows
|
|
1081
|
+
.filter((row) => !isHiddenFromDefaultRecall(row))
|
|
1082
|
+
.filter((row) => metadataStatus(row) !== "current" && metadataStatus(row) !== "active")
|
|
1083
|
+
.filter((row) => row.req_status !== "active")
|
|
1084
|
+
.filter((row) => row.kind !== "note" || opts.compactNotes)
|
|
1085
|
+
.slice(0, opts.maxMemoryItems);
|
|
1086
|
+
}
|
|
1087
|
+
function compactionLine(row) {
|
|
1088
|
+
const date = oneLine(row.updated_at || row.created_at, 19);
|
|
1089
|
+
const title = row.title ? ` ${oneLine(row.title, 80)}` : "";
|
|
1090
|
+
const file = row.file_path ? ` file=${row.file_path}${row.start_line != null ? `:${row.start_line}` : ""}` : "";
|
|
1091
|
+
const req = row.req_id != null ? ` req#${row.req_id}` : "";
|
|
1092
|
+
return `- ${date} #${row.id} ${row.kind}${req}${file}${title}: ${oneLine(row.content, 220)}`;
|
|
1093
|
+
}
|
|
1094
|
+
function compactOldMemoryItems(opts) {
|
|
1095
|
+
const candidates = selectCompactionCandidates(opts);
|
|
1096
|
+
const cutoff = new Date(Date.now() - opts.compactAfterDays * 86_400_000).toISOString();
|
|
1097
|
+
const samples = candidates.slice(0, 20).map((row) => ({
|
|
1098
|
+
id: row.id,
|
|
1099
|
+
kind: row.kind,
|
|
1100
|
+
title: row.title,
|
|
1101
|
+
file_path: row.file_path,
|
|
1102
|
+
updated_at: row.updated_at,
|
|
1103
|
+
}));
|
|
1104
|
+
if (opts.dryRun || !candidates.length) {
|
|
1105
|
+
return {
|
|
1106
|
+
cutoff,
|
|
1107
|
+
candidates: candidates.length,
|
|
1108
|
+
compacted: 0,
|
|
1109
|
+
summary_memory_id: null,
|
|
1110
|
+
archived: 0,
|
|
1111
|
+
samples,
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
const now = new Date().toISOString();
|
|
1115
|
+
const lines = [
|
|
1116
|
+
`Auto-compacted ${candidates.length} old VectorMind memory items.`,
|
|
1117
|
+
`Cutoff: items updated before ${cutoff} (${opts.compactAfterDays} days).`,
|
|
1118
|
+
"",
|
|
1119
|
+
"This compact summary keeps old history searchable while detailed stale items are hidden from default recall.",
|
|
1120
|
+
"Durable decisions, conventions, and project summaries are never compacted by this automatic pass.",
|
|
1121
|
+
"",
|
|
1122
|
+
...candidates.map(compactionLine),
|
|
1123
|
+
];
|
|
1124
|
+
const content = lines.join("\n");
|
|
1125
|
+
const title = `Memory compaction ${now.slice(0, 10)}`;
|
|
1126
|
+
const metadata = {
|
|
1127
|
+
source: "maintenance",
|
|
1128
|
+
status: "current",
|
|
1129
|
+
compacted_item_ids: candidates.map((c) => c.id),
|
|
1130
|
+
compact_after_days: opts.compactAfterDays,
|
|
1131
|
+
compact_notes: opts.compactNotes,
|
|
1132
|
+
generated_at: now,
|
|
1133
|
+
};
|
|
1134
|
+
let summaryMemoryId = 0;
|
|
1135
|
+
let archived = 0;
|
|
1136
|
+
const archiveStmt = db.prepare(`INSERT OR IGNORE INTO memory_item_archive
|
|
1137
|
+
(memory_id, original_kind, original_title, original_content, original_file_path,
|
|
1138
|
+
original_start_line, original_end_line, original_req_id, original_metadata_json,
|
|
1139
|
+
original_content_hash, original_created_at, original_updated_at, archive_reason, compacted_into_id)
|
|
1140
|
+
VALUES
|
|
1141
|
+
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
1142
|
+
const updateMemoryStmt = db.prepare(`UPDATE memory_items
|
|
1143
|
+
SET content = ?, metadata_json = ?, content_hash = ?, updated_at = CURRENT_TIMESTAMP
|
|
1144
|
+
WHERE id = ?`);
|
|
1145
|
+
const deleteEmbeddingStmt = db.prepare(`DELETE FROM embeddings WHERE memory_id = ?`);
|
|
1146
|
+
const tx = db.transaction(() => {
|
|
1147
|
+
const info = insertMemoryItemStmt.run("memory_compaction", title, content, null, null, null, null, safeJson(metadata), sha256Hex(content));
|
|
1148
|
+
summaryMemoryId = Number(info.lastInsertRowid);
|
|
1149
|
+
for (const row of candidates) {
|
|
1150
|
+
const archiveInfo = archiveStmt.run(row.id, row.kind, row.title, row.content, row.file_path, row.start_line, row.end_line, row.req_id, row.metadata_json, row.content_hash, row.created_at, row.updated_at, "auto_compaction", summaryMemoryId);
|
|
1151
|
+
if (archiveInfo.changes > 0)
|
|
1152
|
+
archived += 1;
|
|
1153
|
+
const patchedMeta = {
|
|
1154
|
+
...parseMetadataJson(row.metadata_json),
|
|
1155
|
+
status: "compacted",
|
|
1156
|
+
compacted: true,
|
|
1157
|
+
compacted_at: now,
|
|
1158
|
+
compacted_into_memory_id: summaryMemoryId,
|
|
1159
|
+
};
|
|
1160
|
+
const stub = [
|
|
1161
|
+
`[compacted into memory item #${summaryMemoryId}]`,
|
|
1162
|
+
`Original ${row.kind} #${row.id} was older than ${opts.compactAfterDays} days and is excluded from default recall.`,
|
|
1163
|
+
`Summary: ${oneLine(row.title || row.content, 260)}`,
|
|
1164
|
+
].join("\n");
|
|
1165
|
+
updateMemoryStmt.run(stub, safeJson(patchedMeta), sha256Hex(stub), row.id);
|
|
1166
|
+
deleteEmbeddingStmt.run(row.id);
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
try {
|
|
1170
|
+
tx();
|
|
1171
|
+
if (summaryMemoryId)
|
|
1172
|
+
enqueueEmbedding(summaryMemoryId);
|
|
1173
|
+
}
|
|
1174
|
+
catch (err) {
|
|
1175
|
+
console.error("[vectormind] compact old memory failed:", err);
|
|
1176
|
+
summaryMemoryId = 0;
|
|
1177
|
+
}
|
|
1178
|
+
if (summaryMemoryId) {
|
|
1179
|
+
logActivity("memory_maintenance", {
|
|
1180
|
+
reason: "compact_old_memories",
|
|
1181
|
+
compacted: candidates.length,
|
|
1182
|
+
summary_memory_id: summaryMemoryId,
|
|
1183
|
+
archived,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
cutoff,
|
|
1188
|
+
candidates: candidates.length,
|
|
1189
|
+
compacted: summaryMemoryId ? candidates.length : 0,
|
|
1190
|
+
summary_memory_id: summaryMemoryId || null,
|
|
1191
|
+
archived,
|
|
1192
|
+
samples,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function runMemoryMaintenance(args, trigger = "manual") {
|
|
1196
|
+
const compactedMemory = args.compact_old_memories
|
|
1197
|
+
? compactOldMemoryItems({
|
|
1198
|
+
dryRun: args.dry_run,
|
|
1199
|
+
compactAfterDays: args.compact_after_days,
|
|
1200
|
+
maxMemoryItems: args.max_memory_items,
|
|
1201
|
+
compactNotes: args.compact_notes,
|
|
1202
|
+
})
|
|
1203
|
+
: {
|
|
1204
|
+
cutoff: new Date(Date.now() - args.compact_after_days * 86_400_000).toISOString(),
|
|
1205
|
+
candidates: 0,
|
|
1206
|
+
compacted: 0,
|
|
1207
|
+
summary_memory_id: null,
|
|
1208
|
+
archived: 0,
|
|
1209
|
+
samples: [],
|
|
1210
|
+
};
|
|
1211
|
+
const ignoredPaths = args.prune_ignored_paths
|
|
1212
|
+
? args.dry_run
|
|
1213
|
+
? countIgnoredIndexDeletes()
|
|
1214
|
+
: pruneIgnoredIndexesByPathPatterns()
|
|
1215
|
+
: { chunks_deleted: 0, symbols_deleted: 0 };
|
|
1216
|
+
const filenameNoise = args.prune_filename_noise
|
|
1217
|
+
? args.dry_run
|
|
1218
|
+
? countFilenameNoiseIndexDeletes()
|
|
1219
|
+
: pruneFilenameNoiseIndexes()
|
|
1220
|
+
: { chunks_deleted: 0, symbols_deleted: 0 };
|
|
1221
|
+
const staleFiles = args.prune_stale_indexes
|
|
1222
|
+
? pruneStaleFileIndexes({ dryRun: args.dry_run, maxIndexFiles: args.max_index_files })
|
|
1223
|
+
: { files_checked: 0, files_matched: 0, chunks_deleted: 0, symbols_deleted: 0, samples: [] };
|
|
1224
|
+
const hiddenEmbeddings = args.prune_hidden_embeddings
|
|
1225
|
+
? pruneHiddenEmbeddings(args.dry_run)
|
|
1226
|
+
: { embeddings_deleted: 0 };
|
|
1227
|
+
let vacuumed = false;
|
|
1228
|
+
if (!args.dry_run && args.vacuum) {
|
|
1229
|
+
try {
|
|
1230
|
+
db.exec("VACUUM");
|
|
1231
|
+
vacuumed = true;
|
|
1232
|
+
}
|
|
1233
|
+
catch (err) {
|
|
1234
|
+
console.error("[vectormind] maintenance vacuum failed:", err);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const result = {
|
|
1238
|
+
ok: true,
|
|
1239
|
+
dry_run: args.dry_run,
|
|
1240
|
+
trigger,
|
|
1241
|
+
generated_at: new Date().toISOString(),
|
|
1242
|
+
project_root: projectRoot,
|
|
1243
|
+
db_path: dbPath,
|
|
1244
|
+
config: {
|
|
1245
|
+
compact_after_days: args.compact_after_days,
|
|
1246
|
+
max_memory_items: args.max_memory_items,
|
|
1247
|
+
max_index_files: args.max_index_files,
|
|
1248
|
+
compact_notes: args.compact_notes,
|
|
1249
|
+
},
|
|
1250
|
+
compacted_memory: compactedMemory,
|
|
1251
|
+
pruned: {
|
|
1252
|
+
ignored_paths: ignoredPaths,
|
|
1253
|
+
filename_noise: filenameNoise,
|
|
1254
|
+
stale_files: staleFiles,
|
|
1255
|
+
hidden_embeddings: hiddenEmbeddings,
|
|
1256
|
+
},
|
|
1257
|
+
vacuumed,
|
|
1258
|
+
};
|
|
1259
|
+
logActivity("memory_maintenance", {
|
|
1260
|
+
trigger,
|
|
1261
|
+
dry_run: args.dry_run,
|
|
1262
|
+
compacted: result.compacted_memory.compacted,
|
|
1263
|
+
stale_files: result.pruned.stale_files.files_matched,
|
|
1264
|
+
chunks_deleted: result.pruned.ignored_paths.chunks_deleted +
|
|
1265
|
+
result.pruned.filename_noise.chunks_deleted +
|
|
1266
|
+
result.pruned.stale_files.chunks_deleted,
|
|
1267
|
+
});
|
|
1268
|
+
return result;
|
|
1269
|
+
}
|
|
1270
|
+
function runAutoMaintenanceIfDue() {
|
|
1271
|
+
if (!MAINTENANCE_AUTO_ENABLED || !db)
|
|
1272
|
+
return;
|
|
1273
|
+
const lastRaw = kvGet("maintenance.last_auto_at");
|
|
1274
|
+
const last = lastRaw ? Date.parse(lastRaw) : 0;
|
|
1275
|
+
const dueMs = MAINTENANCE_INTERVAL_HOURS * 3_600_000;
|
|
1276
|
+
if (Number.isFinite(last) && last > 0 && Date.now() - last < dueMs)
|
|
1277
|
+
return;
|
|
1278
|
+
try {
|
|
1279
|
+
runMemoryMaintenance({
|
|
1280
|
+
project_root: projectRoot,
|
|
1281
|
+
dry_run: false,
|
|
1282
|
+
format: "compact",
|
|
1283
|
+
compact_old_memories: true,
|
|
1284
|
+
compact_notes: false,
|
|
1285
|
+
prune_stale_indexes: true,
|
|
1286
|
+
prune_ignored_paths: true,
|
|
1287
|
+
prune_filename_noise: true,
|
|
1288
|
+
prune_hidden_embeddings: true,
|
|
1289
|
+
compact_after_days: MAINTENANCE_COMPACT_AFTER_DAYS,
|
|
1290
|
+
max_memory_items: MAINTENANCE_MAX_MEMORY_ITEMS,
|
|
1291
|
+
max_index_files: MAINTENANCE_MAX_INDEX_FILES,
|
|
1292
|
+
vacuum: false,
|
|
1293
|
+
}, "auto");
|
|
1294
|
+
kvSet("maintenance.last_auto_at", new Date().toISOString());
|
|
1295
|
+
}
|
|
1296
|
+
catch (err) {
|
|
1297
|
+
console.error("[vectormind] auto maintenance failed:", err);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
767
1300
|
function shouldIgnorePath(inputPath) {
|
|
768
1301
|
const normalizedAbs = path.resolve(inputPath);
|
|
769
1302
|
const rel = path.relative(projectRoot, normalizedAbs);
|
|
@@ -887,6 +1420,416 @@ function isContentIndexableFile(filePath) {
|
|
|
887
1420
|
return false;
|
|
888
1421
|
return getContentChunkKind(filePath) !== null;
|
|
889
1422
|
}
|
|
1423
|
+
function isLikelySourceImplementationFile(filePath) {
|
|
1424
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1425
|
+
return new Set([
|
|
1426
|
+
".ts",
|
|
1427
|
+
".tsx",
|
|
1428
|
+
".js",
|
|
1429
|
+
".jsx",
|
|
1430
|
+
".mjs",
|
|
1431
|
+
".cjs",
|
|
1432
|
+
".py",
|
|
1433
|
+
".go",
|
|
1434
|
+
".rs",
|
|
1435
|
+
".java",
|
|
1436
|
+
".kt",
|
|
1437
|
+
".cs",
|
|
1438
|
+
".c",
|
|
1439
|
+
".cc",
|
|
1440
|
+
".cpp",
|
|
1441
|
+
".h",
|
|
1442
|
+
".hpp",
|
|
1443
|
+
".vue",
|
|
1444
|
+
".svelte",
|
|
1445
|
+
]).has(ext);
|
|
1446
|
+
}
|
|
1447
|
+
function countFileLinesBounded(absPath, maxBytes) {
|
|
1448
|
+
let stat;
|
|
1449
|
+
try {
|
|
1450
|
+
stat = fs.statSync(absPath);
|
|
1451
|
+
}
|
|
1452
|
+
catch {
|
|
1453
|
+
return null;
|
|
1454
|
+
}
|
|
1455
|
+
if (!stat.isFile())
|
|
1456
|
+
return null;
|
|
1457
|
+
const bytesToRead = Math.min(stat.size, maxBytes);
|
|
1458
|
+
const fd = fs.openSync(absPath, "r");
|
|
1459
|
+
try {
|
|
1460
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
1461
|
+
const read = fs.readSync(fd, buffer, 0, bytesToRead, 0);
|
|
1462
|
+
let lines = read > 0 ? 1 : 0;
|
|
1463
|
+
for (let i = 0; i < read; i++) {
|
|
1464
|
+
if (buffer[i] === 10)
|
|
1465
|
+
lines += 1;
|
|
1466
|
+
}
|
|
1467
|
+
return { lines, truncated: stat.size > bytesToRead };
|
|
1468
|
+
}
|
|
1469
|
+
finally {
|
|
1470
|
+
fs.closeSync(fd);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
function isPathInsideProjectRoot(absPath) {
|
|
1474
|
+
const root = path.resolve(projectRoot);
|
|
1475
|
+
const rel = path.relative(root, path.resolve(absPath));
|
|
1476
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
1477
|
+
}
|
|
1478
|
+
function checkPathScope(inputPath) {
|
|
1479
|
+
const normalizedInput = inputPath.trim() || ".";
|
|
1480
|
+
const absPath = path.resolve(path.isAbsolute(normalizedInput) ? normalizedInput : path.join(projectRoot, normalizedInput));
|
|
1481
|
+
return {
|
|
1482
|
+
input_path: inputPath,
|
|
1483
|
+
abs_path: absPath,
|
|
1484
|
+
in_project: isPathInsideProjectRoot(absPath),
|
|
1485
|
+
project_root: path.resolve(projectRoot),
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function buildCrossProjectPathWarnings(paths) {
|
|
1489
|
+
const checks = (paths ?? []).map((p) => checkPathScope(p)).filter((c) => !c.in_project);
|
|
1490
|
+
if (!checks.length)
|
|
1491
|
+
return [];
|
|
1492
|
+
return [
|
|
1493
|
+
{
|
|
1494
|
+
code: "cross_project_path",
|
|
1495
|
+
severity: "warning",
|
|
1496
|
+
message: "A path points outside the current project_root. Switch project_root intentionally before reading/searching another repo; do not mix unrelated project context into the current requirement.",
|
|
1497
|
+
files: checks.slice(0, 10).map((c) => c.input_path),
|
|
1498
|
+
details: {
|
|
1499
|
+
project_root: path.resolve(projectRoot),
|
|
1500
|
+
paths: checks.slice(0, 10),
|
|
1501
|
+
total_paths: checks.length,
|
|
1502
|
+
},
|
|
1503
|
+
},
|
|
1504
|
+
];
|
|
1505
|
+
}
|
|
1506
|
+
function buildFileReadDevelopmentWarnings(filePath, absPath, stat) {
|
|
1507
|
+
const warnings = [];
|
|
1508
|
+
if (!isPathInsideProjectRoot(absPath)) {
|
|
1509
|
+
warnings.push(...buildCrossProjectPathWarnings([filePath]));
|
|
1510
|
+
return warnings;
|
|
1511
|
+
}
|
|
1512
|
+
if (!isLikelySourceImplementationFile(filePath))
|
|
1513
|
+
return warnings;
|
|
1514
|
+
let st = stat;
|
|
1515
|
+
try {
|
|
1516
|
+
st ??= fs.statSync(absPath);
|
|
1517
|
+
}
|
|
1518
|
+
catch {
|
|
1519
|
+
return warnings;
|
|
1520
|
+
}
|
|
1521
|
+
if (!st.isFile())
|
|
1522
|
+
return warnings;
|
|
1523
|
+
const lineInfo = countFileLinesBounded(absPath, 2_000_000);
|
|
1524
|
+
const lineCount = lineInfo?.lines ?? 0;
|
|
1525
|
+
const tooManyLines = lineCount >= DEVELOPMENT_BLOCK_FILE_LINES;
|
|
1526
|
+
const warnLines = lineCount >= DEVELOPMENT_WARN_FILE_LINES;
|
|
1527
|
+
const warnBytes = st.size >= DEVELOPMENT_WARN_FILE_BYTES;
|
|
1528
|
+
if (!tooManyLines && !warnLines && !warnBytes)
|
|
1529
|
+
return warnings;
|
|
1530
|
+
warnings.push({
|
|
1531
|
+
code: "large_file_read",
|
|
1532
|
+
severity: tooManyLines ? "blocker" : "warning",
|
|
1533
|
+
message: tooManyLines
|
|
1534
|
+
? "You are reading a very large implementation file. Do not keep patching new feature code into it; identify a narrow function and split new behavior into focused modules unless this task is explicitly a planned extraction."
|
|
1535
|
+
: "You are reading a large implementation file. Keep the target narrow and prefer extracting focused modules before adding responsibilities.",
|
|
1536
|
+
files: [filePath],
|
|
1537
|
+
details: {
|
|
1538
|
+
lines: lineInfo?.truncated ? `${lineCount}+` : lineCount,
|
|
1539
|
+
bytes: st.size,
|
|
1540
|
+
warn_lines: DEVELOPMENT_WARN_FILE_LINES,
|
|
1541
|
+
block_lines: DEVELOPMENT_BLOCK_FILE_LINES,
|
|
1542
|
+
warn_bytes: DEVELOPMENT_WARN_FILE_BYTES,
|
|
1543
|
+
},
|
|
1544
|
+
});
|
|
1545
|
+
return warnings;
|
|
1546
|
+
}
|
|
1547
|
+
function buildMatchedFileDevelopmentWarnings(filePaths) {
|
|
1548
|
+
const seen = new Set();
|
|
1549
|
+
const warnings = [];
|
|
1550
|
+
for (const fp of filePaths) {
|
|
1551
|
+
if (!fp || seen.has(fp))
|
|
1552
|
+
continue;
|
|
1553
|
+
seen.add(fp);
|
|
1554
|
+
const abs = path.isAbsolute(fp) ? path.resolve(fp) : path.join(projectRoot, fp);
|
|
1555
|
+
warnings.push(...buildFileReadDevelopmentWarnings(normalizeToDbPath(fp), abs));
|
|
1556
|
+
if (warnings.length >= 8)
|
|
1557
|
+
break;
|
|
1558
|
+
}
|
|
1559
|
+
return warnings;
|
|
1560
|
+
}
|
|
1561
|
+
function buildRequirementStartWarnings(args) {
|
|
1562
|
+
const warnings = [];
|
|
1563
|
+
const activeReqs = listActiveRequirementsStmt.all(10);
|
|
1564
|
+
if (!args.close_previous && activeReqs.length > 0) {
|
|
1565
|
+
warnings.push({
|
|
1566
|
+
code: "multiple_active_requirements",
|
|
1567
|
+
severity: "warning",
|
|
1568
|
+
message: "Starting a requirement without closing previous active requirements can mix unrelated context. Only keep multiple active requirements when the user explicitly asked for parallel work.",
|
|
1569
|
+
details: {
|
|
1570
|
+
active_requirements: activeReqs.slice(0, 5).map((r) => ({ id: r.id, title: r.title, status: r.status })),
|
|
1571
|
+
},
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
const text = `${args.title}\n${args.background}`.toLowerCase();
|
|
1575
|
+
const broadTerms = [
|
|
1576
|
+
"顺便",
|
|
1577
|
+
"一起",
|
|
1578
|
+
"所有",
|
|
1579
|
+
"全部",
|
|
1580
|
+
"整体",
|
|
1581
|
+
"重构",
|
|
1582
|
+
"统一",
|
|
1583
|
+
"优化一下",
|
|
1584
|
+
"顺手",
|
|
1585
|
+
"相关的",
|
|
1586
|
+
"all ",
|
|
1587
|
+
"everything",
|
|
1588
|
+
"refactor",
|
|
1589
|
+
"cleanup",
|
|
1590
|
+
"clean up",
|
|
1591
|
+
];
|
|
1592
|
+
const matched = broadTerms.filter((term) => text.includes(term));
|
|
1593
|
+
if (matched.length >= 2 || text.length > 1800) {
|
|
1594
|
+
warnings.push({
|
|
1595
|
+
code: "broad_requirement_scope",
|
|
1596
|
+
severity: "warning",
|
|
1597
|
+
message: "The requirement wording looks broad. Treat the current user request as the only boundary; do not add extra workflows, fields, pages, interfaces, or touch completed related features unless explicitly required.",
|
|
1598
|
+
details: { matched_terms: matched.slice(0, 10), text_length: text.length },
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
return warnings;
|
|
1602
|
+
}
|
|
1603
|
+
function normalizeScopeTerms(values) {
|
|
1604
|
+
return Array.from(new Set((values ?? []).map((v) => v.trim()).filter(Boolean)));
|
|
1605
|
+
}
|
|
1606
|
+
function buildRequirementScopeContract(args) {
|
|
1607
|
+
const allowTerms = normalizeScopeTerms(args.scope_allow);
|
|
1608
|
+
const denyTerms = normalizeScopeTerms(args.scope_deny);
|
|
1609
|
+
const allowedPaths = normalizeScopeTerms(args.allowed_paths).map(normalizeToDbPath);
|
|
1610
|
+
const deniedPaths = normalizeScopeTerms(args.denied_paths).map((p) => p.replace(/\\/g, "/"));
|
|
1611
|
+
return {
|
|
1612
|
+
allow_terms: allowTerms,
|
|
1613
|
+
deny_terms: Array.from(new Set(denyTerms)),
|
|
1614
|
+
allowed_paths: Array.from(new Set(allowedPaths)),
|
|
1615
|
+
denied_paths: Array.from(new Set(deniedPaths)),
|
|
1616
|
+
inferred_from: [],
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
function getRequirementScopeContract(reqId) {
|
|
1620
|
+
const memId = getRequirementMemoryItemIdStmt.get(reqId)?.id;
|
|
1621
|
+
if (memId == null)
|
|
1622
|
+
return null;
|
|
1623
|
+
const row = getMemoryItemByIdStmt.get(memId);
|
|
1624
|
+
const meta = parseMetadataJson(row?.metadata_json);
|
|
1625
|
+
const raw = meta.scope_contract;
|
|
1626
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
1627
|
+
return null;
|
|
1628
|
+
const obj = raw;
|
|
1629
|
+
return {
|
|
1630
|
+
allow_terms: Array.isArray(obj.allow_terms) ? obj.allow_terms.filter((v) => typeof v === "string") : [],
|
|
1631
|
+
deny_terms: Array.isArray(obj.deny_terms) ? obj.deny_terms.filter((v) => typeof v === "string") : [],
|
|
1632
|
+
allowed_paths: Array.isArray(obj.allowed_paths) ? obj.allowed_paths.filter((v) => typeof v === "string") : [],
|
|
1633
|
+
denied_paths: Array.isArray(obj.denied_paths) ? obj.denied_paths.filter((v) => typeof v === "string") : [],
|
|
1634
|
+
inferred_from: Array.isArray(obj.inferred_from) ? obj.inferred_from.filter((v) => typeof v === "string") : [],
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
function wildcardToRegex(pattern) {
|
|
1638
|
+
const source = escapeRegExp(pattern).replace(/\\\*/g, ".*");
|
|
1639
|
+
return new RegExp(source, "i");
|
|
1640
|
+
}
|
|
1641
|
+
function pathMatchesAnyPattern(filePath, patterns) {
|
|
1642
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
1643
|
+
return patterns.filter((p) => wildcardToRegex(p.replace(/\\/g, "/")).test(normalized));
|
|
1644
|
+
}
|
|
1645
|
+
function fileContentHasDeniedTerms(filePath, terms) {
|
|
1646
|
+
if (!terms.length || filePath === "(unspecified)")
|
|
1647
|
+
return [];
|
|
1648
|
+
const abs = path.isAbsolute(filePath) ? path.resolve(filePath) : path.join(projectRoot, filePath);
|
|
1649
|
+
let st;
|
|
1650
|
+
try {
|
|
1651
|
+
st = fs.statSync(abs);
|
|
1652
|
+
}
|
|
1653
|
+
catch {
|
|
1654
|
+
return [];
|
|
1655
|
+
}
|
|
1656
|
+
if (!st.isFile() || st.size > 2_000_000)
|
|
1657
|
+
return [];
|
|
1658
|
+
let content = "";
|
|
1659
|
+
try {
|
|
1660
|
+
content = fs.readFileSync(abs, "utf8");
|
|
1661
|
+
}
|
|
1662
|
+
catch {
|
|
1663
|
+
return [];
|
|
1664
|
+
}
|
|
1665
|
+
const lower = `${filePath}\n${content.slice(0, 250_000)}`.toLowerCase();
|
|
1666
|
+
return terms.filter((term) => lower.includes(term.toLowerCase()));
|
|
1667
|
+
}
|
|
1668
|
+
function mergeScopeContracts(base, extra) {
|
|
1669
|
+
if (!base && !extra)
|
|
1670
|
+
return null;
|
|
1671
|
+
return {
|
|
1672
|
+
allow_terms: Array.from(new Set([...(base?.allow_terms ?? []), ...(extra?.allow_terms ?? [])])),
|
|
1673
|
+
deny_terms: Array.from(new Set([...(base?.deny_terms ?? []), ...(extra?.deny_terms ?? [])])),
|
|
1674
|
+
allowed_paths: Array.from(new Set([...(base?.allowed_paths ?? []), ...(extra?.allowed_paths ?? [])])),
|
|
1675
|
+
denied_paths: Array.from(new Set([...(base?.denied_paths ?? []), ...(extra?.denied_paths ?? [])])),
|
|
1676
|
+
inferred_from: Array.from(new Set([...(base?.inferred_from ?? []), ...(extra?.inferred_from ?? [])])),
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
function buildScopeDriftWarnings(args) {
|
|
1680
|
+
const requirementContract = args.requirement ? getRequirementScopeContract(args.requirement.id) : null;
|
|
1681
|
+
const contract = mergeScopeContracts(requirementContract, args.contract);
|
|
1682
|
+
const hasScopeRules = !!contract && (contract.allow_terms.length > 0 ||
|
|
1683
|
+
contract.deny_terms.length > 0 ||
|
|
1684
|
+
contract.allowed_paths.length > 0 ||
|
|
1685
|
+
contract.denied_paths.length > 0);
|
|
1686
|
+
const warnings = [];
|
|
1687
|
+
if (!contract || !hasScopeRules) {
|
|
1688
|
+
if (args.includeMissingContractHint && args.files.length > 0) {
|
|
1689
|
+
warnings.push({
|
|
1690
|
+
code: "scope_contract_missing",
|
|
1691
|
+
severity: "warning",
|
|
1692
|
+
message: "No explicit scope allow/deny contract is set for this requirement, so the planned files cannot be proven in-scope before editing. Define scope_allow/scope_deny or allowed_paths/denied_paths before editing.",
|
|
1693
|
+
files: args.files.map((f) => normalizeToDbPath(f.file_path)).slice(0, 12),
|
|
1694
|
+
details: {
|
|
1695
|
+
requirement_id: args.requirement?.id ?? null,
|
|
1696
|
+
requirement_title: args.requirement?.title ?? null,
|
|
1697
|
+
},
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
return warnings;
|
|
1701
|
+
}
|
|
1702
|
+
const allowTerms = contract.allow_terms;
|
|
1703
|
+
const denyTerms = contract.deny_terms;
|
|
1704
|
+
const allowedPaths = contract.allowed_paths;
|
|
1705
|
+
const deniedPaths = contract.denied_paths;
|
|
1706
|
+
const intentText = args.intent ?? "";
|
|
1707
|
+
const intentDenied = denyTerms.filter((term) => intentText.toLowerCase().includes(term.toLowerCase()));
|
|
1708
|
+
const suspicious = [];
|
|
1709
|
+
for (const f of args.files) {
|
|
1710
|
+
const fp = normalizeToDbPath(f.file_path);
|
|
1711
|
+
if (fp === "(unspecified)")
|
|
1712
|
+
continue;
|
|
1713
|
+
const matchedDeniedPaths = pathMatchesAnyPattern(fp, deniedPaths);
|
|
1714
|
+
const matchedDeniedTerms = [
|
|
1715
|
+
...denyTerms.filter((term) => fp.toLowerCase().includes(term.toLowerCase())),
|
|
1716
|
+
...fileContentHasDeniedTerms(fp, denyTerms),
|
|
1717
|
+
];
|
|
1718
|
+
const isExplicitlyAllowed = pathMatchesAnyPattern(fp, allowedPaths).length > 0 ||
|
|
1719
|
+
allowTerms.some((term) => fp.toLowerCase().includes(term.toLowerCase()));
|
|
1720
|
+
const violatesAllowedPaths = allowedPaths.length > 0 && pathMatchesAnyPattern(fp, allowedPaths).length === 0;
|
|
1721
|
+
if ((matchedDeniedPaths.length || matchedDeniedTerms.length || intentDenied.length || violatesAllowedPaths) &&
|
|
1722
|
+
!isExplicitlyAllowed) {
|
|
1723
|
+
suspicious.push({
|
|
1724
|
+
file_path: fp,
|
|
1725
|
+
matched_terms: Array.from(new Set([...matchedDeniedTerms, ...intentDenied])).slice(0, 12),
|
|
1726
|
+
matched_paths: [
|
|
1727
|
+
...matchedDeniedPaths.slice(0, 12),
|
|
1728
|
+
...(violatesAllowedPaths ? [`outside allowed_paths: ${allowedPaths.slice(0, 5).join(", ")}`] : []),
|
|
1729
|
+
],
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
if (suspicious.length) {
|
|
1734
|
+
warnings.push({
|
|
1735
|
+
code: "scope_drift",
|
|
1736
|
+
severity: "blocker",
|
|
1737
|
+
message: "The current requirement appears to be touching a denied or out-of-scope domain. Stop and narrow the change unless the user explicitly expanded this requirement.",
|
|
1738
|
+
files: suspicious.slice(0, 12).map((s) => s.file_path),
|
|
1739
|
+
details: {
|
|
1740
|
+
requirement_id: args.requirement?.id ?? null,
|
|
1741
|
+
requirement_title: args.requirement?.title ?? null,
|
|
1742
|
+
inferred_from: contract.inferred_from,
|
|
1743
|
+
deny_terms: denyTerms.slice(0, 30),
|
|
1744
|
+
denied_paths: deniedPaths.slice(0, 30),
|
|
1745
|
+
suspicious: suspicious.slice(0, 12),
|
|
1746
|
+
},
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
return warnings;
|
|
1750
|
+
}
|
|
1751
|
+
function buildDevelopmentWarnings(files, opts = {}) {
|
|
1752
|
+
const warnings = [];
|
|
1753
|
+
const uniqueFiles = Array.from(new Set(files
|
|
1754
|
+
.map((f) => f.file_path)
|
|
1755
|
+
.filter((f) => !!f && f !== "(unspecified)")
|
|
1756
|
+
.map((f) => normalizeToDbPath(f))));
|
|
1757
|
+
if (opts.includeUnspecified || files.some((f) => f.file_path === "(unspecified)")) {
|
|
1758
|
+
warnings.push({
|
|
1759
|
+
code: "unspecified_change_target",
|
|
1760
|
+
severity: "warning",
|
|
1761
|
+
message: "No changed file target was captured. For development work, sync concrete files so the current requirement owns only its real changes.",
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
if (uniqueFiles.length >= DEVELOPMENT_WARN_PENDING_FILES) {
|
|
1765
|
+
warnings.push({
|
|
1766
|
+
code: "many_pending_files",
|
|
1767
|
+
severity: "warning",
|
|
1768
|
+
message: "This requirement touches many files. Re-check the user request and keep only files required by the current requirement.",
|
|
1769
|
+
files: uniqueFiles.slice(0, 20),
|
|
1770
|
+
details: { total_files: uniqueFiles.length, threshold: DEVELOPMENT_WARN_PENDING_FILES },
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
const topDirs = new Set(uniqueFiles
|
|
1774
|
+
.map((f) => f.replace(/\\/g, "/").split("/").filter(Boolean)[0] ?? "")
|
|
1775
|
+
.filter(Boolean));
|
|
1776
|
+
if (uniqueFiles.length >= 6 && topDirs.size >= 4) {
|
|
1777
|
+
warnings.push({
|
|
1778
|
+
code: "broad_change_surface",
|
|
1779
|
+
severity: "warning",
|
|
1780
|
+
message: "Changed files span several top-level areas. Avoid modifying completed or merely related features unless the current requirement explicitly needs it.",
|
|
1781
|
+
files: uniqueFiles.slice(0, 20),
|
|
1782
|
+
details: { top_level_dirs: Array.from(topDirs).slice(0, 12), total_dirs: topDirs.size },
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
for (const relPath of uniqueFiles) {
|
|
1786
|
+
if (!isLikelySourceImplementationFile(relPath))
|
|
1787
|
+
continue;
|
|
1788
|
+
const absPath = path.isAbsolute(relPath) ? relPath : path.join(projectRoot, relPath);
|
|
1789
|
+
let stat;
|
|
1790
|
+
try {
|
|
1791
|
+
stat = fs.statSync(absPath);
|
|
1792
|
+
}
|
|
1793
|
+
catch {
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
if (!stat.isFile())
|
|
1797
|
+
continue;
|
|
1798
|
+
const lineInfo = countFileLinesBounded(absPath, 2_000_000);
|
|
1799
|
+
const lineCount = lineInfo?.lines ?? 0;
|
|
1800
|
+
const tooManyLines = lineCount >= DEVELOPMENT_BLOCK_FILE_LINES;
|
|
1801
|
+
const warnLines = lineCount >= DEVELOPMENT_WARN_FILE_LINES;
|
|
1802
|
+
const warnBytes = stat.size >= DEVELOPMENT_WARN_FILE_BYTES;
|
|
1803
|
+
if (!tooManyLines && !warnLines && !warnBytes)
|
|
1804
|
+
continue;
|
|
1805
|
+
warnings.push({
|
|
1806
|
+
code: tooManyLines ? "very_large_file" : "large_file",
|
|
1807
|
+
severity: tooManyLines ? "blocker" : "warning",
|
|
1808
|
+
message: tooManyLines
|
|
1809
|
+
? "This implementation file is already very large. Do not add new feature code here by default; split into a focused module/service/component and keep this file as a thin entry."
|
|
1810
|
+
: "This implementation file is getting large. Prefer extracting focused modules instead of continuing to pile unrelated responsibilities into it.",
|
|
1811
|
+
files: [relPath],
|
|
1812
|
+
details: {
|
|
1813
|
+
lines: lineInfo?.truncated ? `${lineCount}+` : lineCount,
|
|
1814
|
+
bytes: stat.size,
|
|
1815
|
+
warn_lines: DEVELOPMENT_WARN_FILE_LINES,
|
|
1816
|
+
block_lines: DEVELOPMENT_BLOCK_FILE_LINES,
|
|
1817
|
+
warn_bytes: DEVELOPMENT_WARN_FILE_BYTES,
|
|
1818
|
+
},
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
return warnings;
|
|
1822
|
+
}
|
|
1823
|
+
function compactDevelopmentWarningsText(warnings) {
|
|
1824
|
+
if (!warnings.length)
|
|
1825
|
+
return [];
|
|
1826
|
+
const lines = ["development warnings:"];
|
|
1827
|
+
for (const w of warnings.slice(0, 8)) {
|
|
1828
|
+
const files = w.files?.length ? ` files=${w.files.slice(0, 5).join(",")}` : "";
|
|
1829
|
+
lines.push(`- ${w.severity} ${w.code}: ${oneLine(w.message, 180)}${files}`);
|
|
1830
|
+
}
|
|
1831
|
+
return lines;
|
|
1832
|
+
}
|
|
890
1833
|
function extractSymbols(filePath, content) {
|
|
891
1834
|
const ext = path.extname(filePath).toLowerCase();
|
|
892
1835
|
if (ext === ".py")
|
|
@@ -1247,12 +2190,25 @@ const StartRequirementArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
|
1247
2190
|
title: z.string().min(1),
|
|
1248
2191
|
background: z.string().optional().default(""),
|
|
1249
2192
|
close_previous: z.boolean().optional().default(true),
|
|
2193
|
+
scope_allow: z.array(z.string().min(1)).optional(),
|
|
2194
|
+
scope_deny: z.array(z.string().min(1)).optional(),
|
|
2195
|
+
allowed_paths: z.array(z.string().min(1)).optional(),
|
|
2196
|
+
denied_paths: z.array(z.string().min(1)).optional(),
|
|
1250
2197
|
}));
|
|
1251
2198
|
const SyncChangeIntentArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
1252
2199
|
intent: z.string().min(1),
|
|
1253
2200
|
files: z.array(z.string().min(1)).optional(),
|
|
1254
2201
|
affected_files: z.array(z.string().min(1)).optional(),
|
|
1255
2202
|
}));
|
|
2203
|
+
const PreflightChangeScopeArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
|
|
2204
|
+
intent: z.string().optional().default(""),
|
|
2205
|
+
files: z.array(z.string().min(1)).optional(),
|
|
2206
|
+
planned_files: z.array(z.string().min(1)).optional(),
|
|
2207
|
+
scope_allow: z.array(z.string().min(1)).optional(),
|
|
2208
|
+
scope_deny: z.array(z.string().min(1)).optional(),
|
|
2209
|
+
allowed_paths: z.array(z.string().min(1)).optional(),
|
|
2210
|
+
denied_paths: z.array(z.string().min(1)).optional(),
|
|
2211
|
+
}));
|
|
1256
2212
|
const QueryCodebaseArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
|
|
1257
2213
|
query: z.string().min(1),
|
|
1258
2214
|
}));
|
|
@@ -1352,6 +2308,19 @@ const SupersedeMemoryArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
|
1352
2308
|
replacement_memory_id: z.number().int().positive().optional(),
|
|
1353
2309
|
reason: z.string().min(1),
|
|
1354
2310
|
}));
|
|
2311
|
+
const MaintainMemoryArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
|
|
2312
|
+
dry_run: z.boolean().optional().default(true),
|
|
2313
|
+
compact_old_memories: z.boolean().optional().default(true),
|
|
2314
|
+
compact_notes: z.boolean().optional().default(false),
|
|
2315
|
+
prune_stale_indexes: z.boolean().optional().default(true),
|
|
2316
|
+
prune_ignored_paths: z.boolean().optional().default(true),
|
|
2317
|
+
prune_filename_noise: z.boolean().optional().default(true),
|
|
2318
|
+
prune_hidden_embeddings: z.boolean().optional().default(true),
|
|
2319
|
+
compact_after_days: z.number().int().min(1).max(3650).optional().default(MAINTENANCE_COMPACT_AFTER_DAYS),
|
|
2320
|
+
max_memory_items: z.number().int().min(1).max(5000).optional().default(MAINTENANCE_MAX_MEMORY_ITEMS),
|
|
2321
|
+
max_index_files: z.number().int().min(1).max(50_000).optional().default(MAINTENANCE_MAX_INDEX_FILES),
|
|
2322
|
+
vacuum: z.boolean().optional().default(false),
|
|
2323
|
+
}));
|
|
1355
2324
|
const DEFAULT_PENDING_LIMIT = 10;
|
|
1356
2325
|
const MAX_PENDING_LIMIT = 2000;
|
|
1357
2326
|
const PendingPagingSchema = z.object({
|
|
@@ -1371,13 +2340,22 @@ const DEFAULT_RECENT_CHANGES_PER_REQ = 3;
|
|
|
1371
2340
|
const DEFAULT_RECENT_NOTES = 3;
|
|
1372
2341
|
const DEFAULT_CONVENTIONS_LIMIT = 0;
|
|
1373
2342
|
const DEFAULT_DECISIONS_LIMIT = 5;
|
|
2343
|
+
const DEFAULT_CURRENT_CONTEXT_LIMIT = 8;
|
|
1374
2344
|
const MAX_DECISIONS_LIMIT = 50;
|
|
2345
|
+
const MAX_CURRENT_CONTEXT_LIMIT = 50;
|
|
1375
2346
|
const BrainDumpLimitsSchema = z.object({
|
|
1376
2347
|
requirements_limit: z.number().int().min(1).max(20).optional().default(DEFAULT_RECENT_REQUIREMENTS),
|
|
1377
2348
|
changes_limit: z.number().int().min(1).max(100).optional().default(DEFAULT_RECENT_CHANGES_PER_REQ),
|
|
1378
2349
|
notes_limit: z.number().int().min(0).max(50).optional().default(DEFAULT_RECENT_NOTES),
|
|
1379
2350
|
conventions_limit: z.number().int().min(0).max(200).optional().default(DEFAULT_CONVENTIONS_LIMIT),
|
|
1380
2351
|
decisions_limit: z.number().int().min(0).max(MAX_DECISIONS_LIMIT).optional().default(DEFAULT_DECISIONS_LIMIT),
|
|
2352
|
+
current_context_limit: z
|
|
2353
|
+
.number()
|
|
2354
|
+
.int()
|
|
2355
|
+
.min(0)
|
|
2356
|
+
.max(MAX_CURRENT_CONTEXT_LIMIT)
|
|
2357
|
+
.optional()
|
|
2358
|
+
.default(DEFAULT_CURRENT_CONTEXT_LIMIT),
|
|
1381
2359
|
});
|
|
1382
2360
|
const GetPendingChangesArgsSchema = ProjectRootArgSchema.merge(z.object({
|
|
1383
2361
|
offset: z.number().int().min(0).optional().default(0),
|
|
@@ -1560,6 +2538,7 @@ function compactGrepText(data) {
|
|
|
1560
2538
|
const lines = [
|
|
1561
2539
|
`grep ${data.backend}${fallback} mode=${data.mode} matches=${data.matches.length}/${total} truncated=${data.truncated}${candidateText} q="${oneLine(data.query, 100)}"`,
|
|
1562
2540
|
];
|
|
2541
|
+
lines.push(...compactDevelopmentWarningsText(data.development_warnings ?? []));
|
|
1563
2542
|
if (data.ripgrep_error)
|
|
1564
2543
|
lines.push(`ripgrep_error ${oneLine(data.ripgrep_error, 180)}`);
|
|
1565
2544
|
for (const m of data.matches.slice(0, 80)) {
|
|
@@ -1589,15 +2568,18 @@ function compactReadTextFileText(data) {
|
|
|
1589
2568
|
const offset = data.offset != null ? ` offset=${data.offset}` : "";
|
|
1590
2569
|
const header = `file ${data.file_path}${offset} chars=${data.returned_chars}/${data.total_chars} truncated=${data.truncated}`;
|
|
1591
2570
|
const hint = data.truncated ? "\nhint: continue with offset or read_file_lines; use format=json for metadata fields" : "";
|
|
1592
|
-
|
|
2571
|
+
const warnings = compactDevelopmentWarningsText(data.development_warnings ?? []).join("\n");
|
|
2572
|
+
return `${header}${warnings ? `\n${warnings}` : ""}\n${data.text}${hint}`;
|
|
1593
2573
|
}
|
|
1594
2574
|
function compactReadFileLinesText(data) {
|
|
1595
2575
|
const header = `lines ${data.file_path}:${data.from_line}-${data.to_line} returned=${data.returned} truncated=${data.truncated}`;
|
|
1596
2576
|
const hint = data.truncated ? "\nhint: narrow range or raise max_lines/max_chars; use format=json for metadata fields" : "";
|
|
1597
|
-
|
|
2577
|
+
const warnings = compactDevelopmentWarningsText(data.development_warnings ?? []).join("\n");
|
|
2578
|
+
return `${header}${warnings ? `\n${warnings}` : ""}\n${data.text}${hint}`;
|
|
1598
2579
|
}
|
|
1599
2580
|
function compactQueryCodebaseText(data) {
|
|
1600
2581
|
const lines = [`query_codebase matches=${data.matches.length} q="${oneLine(data.query, 100)}"`];
|
|
2582
|
+
lines.push(...compactDevelopmentWarningsText(data.development_warnings ?? []));
|
|
1601
2583
|
for (const m of data.matches.slice(0, 50)) {
|
|
1602
2584
|
lines.push(`${m.file_path}: ${m.type} ${m.name}${m.signature ? ` — ${oneLine(m.signature, 160)}` : ""}`);
|
|
1603
2585
|
}
|
|
@@ -1605,6 +2587,47 @@ function compactQueryCodebaseText(data) {
|
|
|
1605
2587
|
lines.push("- no matches");
|
|
1606
2588
|
return lines.join("\n");
|
|
1607
2589
|
}
|
|
2590
|
+
function compactMaintenanceText(data) {
|
|
2591
|
+
const prunedChunks = data.pruned.ignored_paths.chunks_deleted +
|
|
2592
|
+
data.pruned.filename_noise.chunks_deleted +
|
|
2593
|
+
data.pruned.stale_files.chunks_deleted;
|
|
2594
|
+
const prunedSymbols = data.pruned.ignored_paths.symbols_deleted +
|
|
2595
|
+
data.pruned.filename_noise.symbols_deleted +
|
|
2596
|
+
data.pruned.stale_files.symbols_deleted;
|
|
2597
|
+
const lines = [
|
|
2598
|
+
`maintain_memory ok dry_run=${data.dry_run} trigger=${data.trigger} compacted=${data.compacted_memory.compacted}/${data.compacted_memory.candidates} archived=${data.compacted_memory.archived} pruned_chunks=${prunedChunks} pruned_symbols=${prunedSymbols} hidden_embeddings=${data.pruned.hidden_embeddings.embeddings_deleted}`,
|
|
2599
|
+
];
|
|
2600
|
+
if (data.compacted_memory.summary_memory_id) {
|
|
2601
|
+
lines.push(`summary memory_compaction #${data.compacted_memory.summary_memory_id}`);
|
|
2602
|
+
}
|
|
2603
|
+
if (data.compacted_memory.samples.length) {
|
|
2604
|
+
lines.push("memory candidates:");
|
|
2605
|
+
for (const s of data.compacted_memory.samples.slice(0, 8)) {
|
|
2606
|
+
lines.push(`- #${s.id} ${s.kind} ${s.file_path ?? ""} ${oneLine(s.title ?? "", 80)} ${s.updated_at}`);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
if (data.pruned.stale_files.samples.length) {
|
|
2610
|
+
lines.push("stale index samples:");
|
|
2611
|
+
for (const s of data.pruned.stale_files.samples.slice(0, 8))
|
|
2612
|
+
lines.push(`- ${s}`);
|
|
2613
|
+
}
|
|
2614
|
+
lines.push("hint: dry_run=false applies changes; vacuum=true reclaims sqlite file space after pruning");
|
|
2615
|
+
return lines.join("\n");
|
|
2616
|
+
}
|
|
2617
|
+
function compactPreflightChangeScopeText(data) {
|
|
2618
|
+
const req = data.active_requirement ? `#${data.active_requirement.id} ${data.active_requirement.title}` : "none";
|
|
2619
|
+
const lines = [
|
|
2620
|
+
`preflight_change_scope ok=${data.ok} safe_to_edit=${data.safe_to_edit} requirement=${req} files=${data.files.length} intent="${oneLine(data.intent, 120)}"`,
|
|
2621
|
+
`action: ${oneLine(data.recommended_action, 180)}`,
|
|
2622
|
+
];
|
|
2623
|
+
if (data.scope_contract) {
|
|
2624
|
+
lines.push(`scope allow_terms=${data.scope_contract.allow_terms.length} deny_terms=${data.scope_contract.deny_terms.length} allowed_paths=${data.scope_contract.allowed_paths.length} denied_paths=${data.scope_contract.denied_paths.length}`);
|
|
2625
|
+
}
|
|
2626
|
+
lines.push(...compactDevelopmentWarningsText(data.development_warnings));
|
|
2627
|
+
if (!data.development_warnings.length)
|
|
2628
|
+
lines.push("- no development warnings");
|
|
2629
|
+
return lines.join("\n");
|
|
2630
|
+
}
|
|
1608
2631
|
function compactBootstrapText(data) {
|
|
1609
2632
|
const lines = [];
|
|
1610
2633
|
lines.push(`ok ctx ${data.root_source} watcher=${data.watcher_enabled ? (data.watcher_ready ? "ready" : "starting") : "off"} root=${data.project_root}`);
|
|
@@ -1615,6 +2638,11 @@ function compactBootstrapText(data) {
|
|
|
1615
2638
|
for (const d of data.decisions.slice(0, 5))
|
|
1616
2639
|
lines.push(`- ${compactMemoryLabel(d, 160)}`);
|
|
1617
2640
|
}
|
|
2641
|
+
if (data.current_context.length) {
|
|
2642
|
+
lines.push("current context:");
|
|
2643
|
+
for (const c of data.current_context.slice(0, 8))
|
|
2644
|
+
lines.push(`- ${compactMemoryLabel(c, 160)}`);
|
|
2645
|
+
}
|
|
1618
2646
|
if (data.pending_total) {
|
|
1619
2647
|
lines.push(`pending ${data.pending_changes.length}/${data.pending_total}${data.pending_truncated ? " truncated" : ""}: ${data.pending_changes
|
|
1620
2648
|
.slice(0, 8)
|
|
@@ -1624,6 +2652,7 @@ function compactBootstrapText(data) {
|
|
|
1624
2652
|
else {
|
|
1625
2653
|
lines.push("pending 0");
|
|
1626
2654
|
}
|
|
2655
|
+
lines.push(...compactDevelopmentWarningsText(data.development_warnings ?? []));
|
|
1627
2656
|
if (data.items.length) {
|
|
1628
2657
|
lines.push("requirements:");
|
|
1629
2658
|
for (const item of data.items) {
|
|
@@ -1719,8 +2748,8 @@ function runRtkProbe(spec) {
|
|
|
1719
2748
|
? `Prefer prefixing shell commands with ${spec.displayCommand} for compact outputs. This is VectorMind's bundled RTK shim; first run auto-installs/caches rtk-ai/rtk if needed.`
|
|
1720
2749
|
: "Prefer prefixing shell commands with rtk for compact outputs, e.g. rtk git status / rtk npm run build / rtk rg pattern ."
|
|
1721
2750
|
: spec.source === "package_shim"
|
|
1722
|
-
? "VectorMind's bundled RTK shim exists, but `gain` failed. Check
|
|
1723
|
-
: "An rtk binary exists, but `rtk gain` failed. This may be the wrong rtk project. Use install_rtk with uninstall_wrong_cargo_rtk=true only
|
|
2751
|
+
? "VectorMind's bundled RTK shim exists, but `gain` failed. Check npm/cache or set VECTORMIND_RTK_REAL to an existing rtk-ai/rtk binary."
|
|
2752
|
+
: "An rtk binary exists, but `rtk gain` failed. This may be the wrong rtk project. Use install_rtk with uninstall_wrong_cargo_rtk=true only when you intentionally want to replace it.",
|
|
1724
2753
|
};
|
|
1725
2754
|
}
|
|
1726
2755
|
return null;
|
|
@@ -1755,7 +2784,7 @@ function detectRtk() {
|
|
|
1755
2784
|
path: shimPath ?? undefined,
|
|
1756
2785
|
source: shimPath ? "package_shim" : undefined,
|
|
1757
2786
|
note: shimPath
|
|
1758
|
-
? "rtk was not found on PATH, and VectorMind's bundled RTK shim could not verify rtk gain. VectorMind compact MCP output still works; check
|
|
2787
|
+
? "rtk was not found on PATH, and VectorMind's bundled RTK shim could not verify rtk gain. VectorMind compact MCP output still works; check npm/cache or set VECTORMIND_RTK_REAL."
|
|
1759
2788
|
: "rtk was not found on PATH and the package RTK shim is unavailable. VectorMind compact MCP output still works; install rtk to compact shell command output too.",
|
|
1760
2789
|
};
|
|
1761
2790
|
}
|
|
@@ -2129,6 +3158,80 @@ function dotProduct(a, b) {
|
|
|
2129
3158
|
s += a[i] * b[i];
|
|
2130
3159
|
return s;
|
|
2131
3160
|
}
|
|
3161
|
+
const SEMANTIC_TOKEN_STOPWORDS = new Set([
|
|
3162
|
+
"a",
|
|
3163
|
+
"an",
|
|
3164
|
+
"and",
|
|
3165
|
+
"are",
|
|
3166
|
+
"as",
|
|
3167
|
+
"at",
|
|
3168
|
+
"be",
|
|
3169
|
+
"by",
|
|
3170
|
+
"for",
|
|
3171
|
+
"from",
|
|
3172
|
+
"has",
|
|
3173
|
+
"have",
|
|
3174
|
+
"if",
|
|
3175
|
+
"in",
|
|
3176
|
+
"into",
|
|
3177
|
+
"is",
|
|
3178
|
+
"it",
|
|
3179
|
+
"its",
|
|
3180
|
+
"of",
|
|
3181
|
+
"on",
|
|
3182
|
+
"or",
|
|
3183
|
+
"that",
|
|
3184
|
+
"the",
|
|
3185
|
+
"this",
|
|
3186
|
+
"to",
|
|
3187
|
+
"with",
|
|
3188
|
+
]);
|
|
3189
|
+
const BOOTSTRAP_DEFAULT_CONTEXT_KINDS = [
|
|
3190
|
+
"decision",
|
|
3191
|
+
"convention",
|
|
3192
|
+
"project_summary",
|
|
3193
|
+
"memory_compaction",
|
|
3194
|
+
"note",
|
|
3195
|
+
"requirement",
|
|
3196
|
+
"change_intent",
|
|
3197
|
+
];
|
|
3198
|
+
const TOKEN_SEARCH_DEFAULT_KINDS = [
|
|
3199
|
+
"decision",
|
|
3200
|
+
"convention",
|
|
3201
|
+
"project_summary",
|
|
3202
|
+
"memory_compaction",
|
|
3203
|
+
"note",
|
|
3204
|
+
"requirement",
|
|
3205
|
+
"change_intent",
|
|
3206
|
+
"code_chunk",
|
|
3207
|
+
"doc_chunk",
|
|
3208
|
+
];
|
|
3209
|
+
const DECISION_CANDIDATE_KEYWORDS = [
|
|
3210
|
+
"用户确认",
|
|
3211
|
+
"用户要求",
|
|
3212
|
+
"明确",
|
|
3213
|
+
"架构决策",
|
|
3214
|
+
"最终",
|
|
3215
|
+
"默认",
|
|
3216
|
+
"只保留",
|
|
3217
|
+
"统一",
|
|
3218
|
+
"不需要",
|
|
3219
|
+
"无需",
|
|
3220
|
+
"不再",
|
|
3221
|
+
"改成",
|
|
3222
|
+
"改为",
|
|
3223
|
+
"直接通过",
|
|
3224
|
+
"不用审核",
|
|
3225
|
+
"decision",
|
|
3226
|
+
"decided",
|
|
3227
|
+
"confirmed",
|
|
3228
|
+
"must",
|
|
3229
|
+
"default",
|
|
3230
|
+
"only",
|
|
3231
|
+
"single",
|
|
3232
|
+
"no longer",
|
|
3233
|
+
"instead",
|
|
3234
|
+
];
|
|
2132
3235
|
function parseMetadataJson(metadata) {
|
|
2133
3236
|
if (!metadata)
|
|
2134
3237
|
return {};
|
|
@@ -2150,6 +3253,13 @@ function isSupersededMemory(row) {
|
|
|
2150
3253
|
const meta = parseMetadataJson(row.metadata_json);
|
|
2151
3254
|
return meta.superseded === true || meta.status === "superseded";
|
|
2152
3255
|
}
|
|
3256
|
+
function isCompactedMemory(row) {
|
|
3257
|
+
const meta = parseMetadataJson(row.metadata_json);
|
|
3258
|
+
return meta.compacted === true || meta.status === "compacted";
|
|
3259
|
+
}
|
|
3260
|
+
function isHiddenFromDefaultRecall(row) {
|
|
3261
|
+
return isSupersededMemory(row) || isCompactedMemory(row);
|
|
3262
|
+
}
|
|
2153
3263
|
function semanticRecencyWeight(updatedAt) {
|
|
2154
3264
|
if (!updatedAt)
|
|
2155
3265
|
return 0;
|
|
@@ -2168,7 +3278,7 @@ function semanticRecencyWeight(updatedAt) {
|
|
|
2168
3278
|
function semanticKindWeight(kind) {
|
|
2169
3279
|
switch (kind) {
|
|
2170
3280
|
case "decision":
|
|
2171
|
-
return
|
|
3281
|
+
return 16;
|
|
2172
3282
|
case "convention":
|
|
2173
3283
|
return 2.6;
|
|
2174
3284
|
case "project_summary":
|
|
@@ -2179,16 +3289,20 @@ function semanticKindWeight(kind) {
|
|
|
2179
3289
|
return 0.4;
|
|
2180
3290
|
case "change_intent":
|
|
2181
3291
|
return 0.2;
|
|
3292
|
+
case "memory_compaction":
|
|
3293
|
+
return 0.7;
|
|
2182
3294
|
default:
|
|
2183
3295
|
return 0;
|
|
2184
3296
|
}
|
|
2185
3297
|
}
|
|
2186
3298
|
function adjustSemanticScore(row, rawScore) {
|
|
2187
|
-
if (
|
|
3299
|
+
if (isHiddenFromDefaultRecall(row))
|
|
2188
3300
|
return rawScore - 1000;
|
|
2189
3301
|
let score = rawScore + semanticKindWeight(row.kind) + semanticRecencyWeight(row.updated_at);
|
|
2190
3302
|
const status = metadataStatus(row);
|
|
2191
|
-
if (status === "
|
|
3303
|
+
if (status === "current")
|
|
3304
|
+
score += row.kind === "decision" ? 24 : 1.2;
|
|
3305
|
+
if (status === "active")
|
|
2192
3306
|
score += 1.2;
|
|
2193
3307
|
if (row.kind === "change_intent" && row.file_path && shouldIgnoreDbFilePath(row.file_path)) {
|
|
2194
3308
|
// Human-synced intent for generated/build/runtime files is often the only durable
|
|
@@ -2197,11 +3311,110 @@ function adjustSemanticScore(row, rawScore) {
|
|
|
2197
3311
|
}
|
|
2198
3312
|
return score;
|
|
2199
3313
|
}
|
|
3314
|
+
function normalizeSearchText(input) {
|
|
3315
|
+
return (input ?? "").normalize("NFKC").toLowerCase();
|
|
3316
|
+
}
|
|
3317
|
+
function extractSearchTokens(raw) {
|
|
3318
|
+
const text = normalizeSearchText(raw);
|
|
3319
|
+
const tokens = new Set();
|
|
3320
|
+
for (const token of text.match(/[a-z0-9_./:@#-]{2,}/g) ?? []) {
|
|
3321
|
+
if (!SEMANTIC_TOKEN_STOPWORDS.has(token))
|
|
3322
|
+
tokens.add(token);
|
|
3323
|
+
for (const part of token.split(/[^a-z0-9]+/).filter((p) => p.length >= 2)) {
|
|
3324
|
+
if (!SEMANTIC_TOKEN_STOPWORDS.has(part))
|
|
3325
|
+
tokens.add(part);
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
for (const seq of text.match(/\p{Script=Han}+/gu) ?? []) {
|
|
3329
|
+
if (seq.length >= 2 && seq.length <= 18)
|
|
3330
|
+
tokens.add(seq);
|
|
3331
|
+
for (const n of [2, 3, 4]) {
|
|
3332
|
+
if (seq.length < n)
|
|
3333
|
+
continue;
|
|
3334
|
+
for (let i = 0; i <= seq.length - n; i++) {
|
|
3335
|
+
tokens.add(seq.slice(i, i + n));
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
return Array.from(tokens)
|
|
3340
|
+
.filter((token) => token.length >= 2 && !SEMANTIC_TOKEN_STOPWORDS.has(token))
|
|
3341
|
+
.sort((a, b) => b.length - a.length)
|
|
3342
|
+
.slice(0, 48);
|
|
3343
|
+
}
|
|
3344
|
+
function countNeedleOccurrences(haystack, needle) {
|
|
3345
|
+
if (!haystack || !needle)
|
|
3346
|
+
return 0;
|
|
3347
|
+
let count = 0;
|
|
3348
|
+
let idx = 0;
|
|
3349
|
+
while ((idx = haystack.indexOf(needle, idx)) >= 0) {
|
|
3350
|
+
count++;
|
|
3351
|
+
idx += Math.max(1, needle.length);
|
|
3352
|
+
if (count >= 8)
|
|
3353
|
+
break;
|
|
3354
|
+
}
|
|
3355
|
+
return count;
|
|
3356
|
+
}
|
|
3357
|
+
function tokenLexicalScore(row, query, tokens) {
|
|
3358
|
+
if (!tokens.length)
|
|
3359
|
+
return 0;
|
|
3360
|
+
const title = normalizeSearchText(row.title);
|
|
3361
|
+
const content = normalizeSearchText(row.content);
|
|
3362
|
+
const filePath = normalizeSearchText(row.file_path);
|
|
3363
|
+
const metadata = normalizeSearchText(row.metadata_json);
|
|
3364
|
+
const exact = normalizeSearchText(query).trim();
|
|
3365
|
+
let score = 0;
|
|
3366
|
+
if (exact.length >= 4) {
|
|
3367
|
+
if (title.includes(exact))
|
|
3368
|
+
score += 8;
|
|
3369
|
+
if (content.includes(exact))
|
|
3370
|
+
score += 6;
|
|
3371
|
+
if (filePath.includes(exact))
|
|
3372
|
+
score += 4;
|
|
3373
|
+
}
|
|
3374
|
+
let matched = 0;
|
|
3375
|
+
for (const token of tokens) {
|
|
3376
|
+
let tokenScore = 0;
|
|
3377
|
+
if (title.includes(token))
|
|
3378
|
+
tokenScore += 3.2;
|
|
3379
|
+
if (filePath.includes(token))
|
|
3380
|
+
tokenScore += 2.4;
|
|
3381
|
+
const contentHits = countNeedleOccurrences(content, token);
|
|
3382
|
+
if (contentHits)
|
|
3383
|
+
tokenScore += Math.min(3.2, 0.75 + contentHits * 0.45);
|
|
3384
|
+
if (metadata.includes(token))
|
|
3385
|
+
tokenScore += 0.8;
|
|
3386
|
+
if (tokenScore > 0) {
|
|
3387
|
+
matched++;
|
|
3388
|
+
score += tokenScore * Math.min(2.4, Math.max(1, token.length / 4));
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
if (matched >= Math.min(3, tokens.length))
|
|
3392
|
+
score += 2;
|
|
3393
|
+
score += matched / Math.max(1, tokens.length);
|
|
3394
|
+
return score;
|
|
3395
|
+
}
|
|
3396
|
+
function looksLikeDecisionContent(content) {
|
|
3397
|
+
const text = normalizeSearchText(content);
|
|
3398
|
+
return DECISION_CANDIDATE_KEYWORDS.some((kw) => text.includes(normalizeSearchText(kw)));
|
|
3399
|
+
}
|
|
3400
|
+
function mergeSemanticMatches(sets, opts) {
|
|
3401
|
+
const best = new Map();
|
|
3402
|
+
for (const matches of sets) {
|
|
3403
|
+
for (const match of matches) {
|
|
3404
|
+
const prev = best.get(match.item.id);
|
|
3405
|
+
if (!prev || match.score > prev.score)
|
|
3406
|
+
best.set(match.item.id, match);
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
return Array.from(best.values())
|
|
3410
|
+
.sort((a, b) => b.score - a.score || b.item.id - a.item.id)
|
|
3411
|
+
.slice(0, opts.topK);
|
|
3412
|
+
}
|
|
2200
3413
|
function filterAndRankSemanticRows(rows, scoreOf, opts) {
|
|
2201
3414
|
return rows
|
|
2202
3415
|
.map((r) => ({ row: r, score: adjustSemanticScore(r, scoreOf(r)) }))
|
|
2203
3416
|
.filter(({ row }) => {
|
|
2204
|
-
if (
|
|
3417
|
+
if (isHiddenFromDefaultRecall(row))
|
|
2205
3418
|
return false;
|
|
2206
3419
|
if (shouldIgnoreDbFilePath(row.file_path) && row.kind !== "change_intent")
|
|
2207
3420
|
return false;
|
|
@@ -2290,10 +3503,43 @@ function getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars) {
|
|
|
2290
3503
|
return [];
|
|
2291
3504
|
const rows = listCurrentDecisionsStmt.all(Math.min(MAX_DECISIONS_LIMIT * 4, Math.max(decisionsLimit, decisionsLimit * 4)));
|
|
2292
3505
|
return rows
|
|
2293
|
-
.filter((d) => !
|
|
3506
|
+
.filter((d) => !isHiddenFromDefaultRecall(d))
|
|
2294
3507
|
.slice(0, decisionsLimit)
|
|
2295
3508
|
.map((d) => toMemoryItemPreview(d, false, previewChars, contentMaxChars));
|
|
2296
3509
|
}
|
|
3510
|
+
function getCurrentContextPreviews(currentContextLimit, previewChars, contentMaxChars) {
|
|
3511
|
+
if (currentContextLimit <= 0)
|
|
3512
|
+
return [];
|
|
3513
|
+
const picked = new Map();
|
|
3514
|
+
const addRow = (row) => {
|
|
3515
|
+
if (!row)
|
|
3516
|
+
return;
|
|
3517
|
+
if (isHiddenFromDefaultRecall(row))
|
|
3518
|
+
return;
|
|
3519
|
+
if (shouldIgnoreDbFilePath(row.file_path) && row.kind !== "change_intent")
|
|
3520
|
+
return;
|
|
3521
|
+
if (!picked.has(row.id)) {
|
|
3522
|
+
picked.set(row.id, toMemoryItemPreview(row, false, previewChars, contentMaxChars));
|
|
3523
|
+
}
|
|
3524
|
+
};
|
|
3525
|
+
const activeReqs = listActiveRequirementsStmt.all(Math.max(currentContextLimit, 10));
|
|
3526
|
+
for (const req of activeReqs) {
|
|
3527
|
+
const memId = getRequirementMemoryItemIdStmt.get(req.id)?.id;
|
|
3528
|
+
if (memId != null)
|
|
3529
|
+
addRow(getMemoryItemByIdStmt.get(memId));
|
|
3530
|
+
}
|
|
3531
|
+
const recentRows = listRecentContextItemsStmt.all(Math.max(currentContextLimit * 8, 40));
|
|
3532
|
+
for (const row of recentRows) {
|
|
3533
|
+
if (picked.size >= currentContextLimit)
|
|
3534
|
+
break;
|
|
3535
|
+
if (row.kind === "requirement" || row.kind === "change_intent") {
|
|
3536
|
+
if (!looksLikeDecisionContent(`${row.title ?? ""}\n${row.content}`))
|
|
3537
|
+
continue;
|
|
3538
|
+
}
|
|
3539
|
+
addRow(row);
|
|
3540
|
+
}
|
|
3541
|
+
return Array.from(picked.values()).slice(0, currentContextLimit);
|
|
3542
|
+
}
|
|
2297
3543
|
function toRequirementPreview(req, includeContent, previewChars, contentMaxChars) {
|
|
2298
3544
|
const context = req.context_data ?? null;
|
|
2299
3545
|
const contextPreview = context ? makePreviewText(context, previewChars) : null;
|
|
@@ -2431,7 +3677,7 @@ async function semanticSearchInternal(opts) {
|
|
|
2431
3677
|
.filter(Boolean);
|
|
2432
3678
|
const filtered = matches
|
|
2433
3679
|
.filter((m) => {
|
|
2434
|
-
if (
|
|
3680
|
+
if (isHiddenFromDefaultRecall({ metadata_json: m.item.metadata_json }))
|
|
2435
3681
|
return false;
|
|
2436
3682
|
if (shouldIgnoreDbFilePath(m.item.file_path) && m.item.kind !== "change_intent")
|
|
2437
3683
|
return false;
|
|
@@ -2591,24 +3837,212 @@ function likeSearchInternal(opts) {
|
|
|
2591
3837
|
const matches = filterAndRankSemanticRows(rows, (r) => Number(r.score), opts);
|
|
2592
3838
|
return { query: q, top_k: opts.topK, mode: "like", matches };
|
|
2593
3839
|
}
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
3840
|
+
function tokenSearchInternal(opts) {
|
|
3841
|
+
const q = opts.query.trim();
|
|
3842
|
+
if (!q)
|
|
3843
|
+
return { query: "", top_k: opts.topK, mode: "token", matches: [] };
|
|
3844
|
+
const tokens = extractSearchTokens(q);
|
|
3845
|
+
if (!tokens.length)
|
|
3846
|
+
return { query: q, top_k: opts.topK, mode: "token", matches: [] };
|
|
3847
|
+
const rawLimit = Math.min(160, Math.max(opts.topK * 12, 80));
|
|
3848
|
+
const searchTokens = tokens.slice(0, 8);
|
|
3849
|
+
const effectiveKinds = opts.kinds?.length ? opts.kinds : TOKEN_SEARCH_DEFAULT_KINDS;
|
|
3850
|
+
const kindClause = effectiveKinds.length
|
|
3851
|
+
? `AND kind IN (${effectiveKinds.map(() => "?").join(", ")})`
|
|
3852
|
+
: "";
|
|
3853
|
+
const recencyBoost = `
|
|
3854
|
+
+ CASE
|
|
3855
|
+
WHEN updated_at >= datetime('now', '-2 days') THEN 4
|
|
3856
|
+
WHEN updated_at >= datetime('now', '-14 days') THEN 2
|
|
3857
|
+
WHEN updated_at >= datetime('now', '-60 days') THEN 1
|
|
3858
|
+
ELSE 0
|
|
3859
|
+
END`;
|
|
3860
|
+
const candidateScore = `
|
|
3861
|
+
(
|
|
3862
|
+
CASE kind
|
|
3863
|
+
WHEN 'decision' THEN 9
|
|
3864
|
+
WHEN 'convention' THEN 7
|
|
3865
|
+
WHEN 'project_summary' THEN 6
|
|
3866
|
+
WHEN 'note' THEN 5
|
|
3867
|
+
WHEN 'requirement' THEN 4
|
|
3868
|
+
WHEN 'change_intent' THEN 3
|
|
3869
|
+
ELSE 0
|
|
3870
|
+
END
|
|
3871
|
+
${recencyBoost}
|
|
3872
|
+
)`;
|
|
3873
|
+
const includesIndexedChunks = effectiveKinds.some((k) => k === "code_chunk" || k === "doc_chunk");
|
|
3874
|
+
if (!includesIndexedChunks) {
|
|
3875
|
+
const candidateLimit = Math.min(1600, Math.max(rawLimit * 5, 800));
|
|
3876
|
+
const stmt = db.prepare(`
|
|
3877
|
+
SELECT
|
|
3878
|
+
id,
|
|
3879
|
+
kind,
|
|
3880
|
+
title,
|
|
3881
|
+
content,
|
|
3882
|
+
file_path,
|
|
3883
|
+
start_line,
|
|
3884
|
+
end_line,
|
|
3885
|
+
req_id,
|
|
3886
|
+
metadata_json,
|
|
3887
|
+
updated_at
|
|
3888
|
+
FROM memory_items
|
|
3889
|
+
WHERE 1=1
|
|
3890
|
+
${kindClause}
|
|
3891
|
+
ORDER BY
|
|
3892
|
+
${candidateScore} DESC,
|
|
3893
|
+
updated_at DESC,
|
|
3894
|
+
id DESC
|
|
3895
|
+
LIMIT ?
|
|
3896
|
+
`);
|
|
3897
|
+
const candidates = stmt.all(...effectiveKinds, candidateLimit);
|
|
3898
|
+
const scoreMap = new Map();
|
|
3899
|
+
const rows = candidates.filter((row) => {
|
|
3900
|
+
const score = tokenLexicalScore(row, q, tokens);
|
|
3901
|
+
if (score <= 0)
|
|
3902
|
+
return false;
|
|
3903
|
+
scoreMap.set(row.id, score);
|
|
3904
|
+
return true;
|
|
3905
|
+
});
|
|
3906
|
+
const matches = filterAndRankSemanticRows(rows, (r) => scoreMap.get(r.id) ?? 0, opts);
|
|
3907
|
+
return { query: q, top_k: opts.topK, mode: "token", matches };
|
|
3908
|
+
}
|
|
3909
|
+
const memoryFirstKinds = effectiveKinds.filter((k) => k !== "code_chunk" && k !== "doc_chunk");
|
|
3910
|
+
const memoryFirstLimit = Math.min(1200, Math.max(rawLimit * 4, 300));
|
|
3911
|
+
let memoryFirstRows = [];
|
|
3912
|
+
const memoryFirstScores = new Map();
|
|
3913
|
+
if (memoryFirstKinds.length) {
|
|
3914
|
+
const memoryKindClause = `AND kind IN (${memoryFirstKinds.map(() => "?").join(", ")})`;
|
|
3915
|
+
const memoryStmt = db.prepare(`
|
|
3916
|
+
SELECT
|
|
3917
|
+
id,
|
|
3918
|
+
kind,
|
|
3919
|
+
title,
|
|
3920
|
+
content,
|
|
3921
|
+
file_path,
|
|
3922
|
+
start_line,
|
|
3923
|
+
end_line,
|
|
3924
|
+
req_id,
|
|
3925
|
+
metadata_json,
|
|
3926
|
+
updated_at
|
|
3927
|
+
FROM memory_items
|
|
3928
|
+
WHERE 1=1
|
|
3929
|
+
${memoryKindClause}
|
|
3930
|
+
ORDER BY
|
|
3931
|
+
${candidateScore} DESC,
|
|
3932
|
+
updated_at DESC,
|
|
3933
|
+
id DESC
|
|
3934
|
+
LIMIT ?
|
|
3935
|
+
`);
|
|
3936
|
+
const candidates = memoryStmt.all(...memoryFirstKinds, memoryFirstLimit);
|
|
3937
|
+
memoryFirstRows = candidates.filter((row) => {
|
|
3938
|
+
const score = tokenLexicalScore(row, q, tokens);
|
|
3939
|
+
if (score <= 0)
|
|
3940
|
+
return false;
|
|
3941
|
+
memoryFirstScores.set(row.id, score);
|
|
3942
|
+
return true;
|
|
3943
|
+
});
|
|
2602
3944
|
}
|
|
3945
|
+
const conditions = [];
|
|
3946
|
+
const values = [];
|
|
3947
|
+
for (const token of searchTokens) {
|
|
3948
|
+
const like = `%${escapeLike(token)}%`;
|
|
3949
|
+
conditions.push(`content LIKE ? ESCAPE '\\'`);
|
|
3950
|
+
values.push(like);
|
|
3951
|
+
conditions.push(`title LIKE ? ESCAPE '\\'`);
|
|
3952
|
+
values.push(like);
|
|
3953
|
+
conditions.push(`file_path LIKE ? ESCAPE '\\'`);
|
|
3954
|
+
values.push(like);
|
|
3955
|
+
}
|
|
3956
|
+
if (!conditions.length)
|
|
3957
|
+
return { query: q, top_k: opts.topK, mode: "token", matches: [] };
|
|
3958
|
+
const stmt = db.prepare(`
|
|
3959
|
+
SELECT
|
|
3960
|
+
id,
|
|
3961
|
+
kind,
|
|
3962
|
+
title,
|
|
3963
|
+
content,
|
|
3964
|
+
file_path,
|
|
3965
|
+
start_line,
|
|
3966
|
+
end_line,
|
|
3967
|
+
req_id,
|
|
3968
|
+
metadata_json,
|
|
3969
|
+
updated_at
|
|
3970
|
+
FROM memory_items
|
|
3971
|
+
WHERE (${conditions.join(" OR ")})
|
|
3972
|
+
${kindClause}
|
|
3973
|
+
ORDER BY
|
|
3974
|
+
${candidateScore} DESC,
|
|
3975
|
+
updated_at DESC,
|
|
3976
|
+
id DESC
|
|
3977
|
+
LIMIT ?
|
|
3978
|
+
`);
|
|
3979
|
+
const rows = stmt.all(...values, ...effectiveKinds, rawLimit);
|
|
3980
|
+
const scoreMap = new Map(memoryFirstScores);
|
|
3981
|
+
for (const row of rows) {
|
|
3982
|
+
if (!scoreMap.has(row.id))
|
|
3983
|
+
scoreMap.set(row.id, tokenLexicalScore(row, q, tokens));
|
|
3984
|
+
}
|
|
3985
|
+
const rowMap = new Map();
|
|
3986
|
+
for (const row of memoryFirstRows)
|
|
3987
|
+
rowMap.set(row.id, row);
|
|
3988
|
+
for (const row of rows)
|
|
3989
|
+
rowMap.set(row.id, row);
|
|
3990
|
+
const matches = filterAndRankSemanticRows(Array.from(rowMap.values()), (r) => scoreMap.get(r.id) ?? 0, opts);
|
|
3991
|
+
return { query: q, top_k: opts.topK, mode: "token", matches };
|
|
3992
|
+
}
|
|
3993
|
+
function chooseLexicalResult(opts) {
|
|
3994
|
+
const tokenResult = tokenSearchInternal(opts);
|
|
3995
|
+
const tokenTopScore = tokenResult.matches[0]?.score ?? 0;
|
|
3996
|
+
const tokenEnough = tokenResult.matches.length >= Math.min(opts.topK, 3) && tokenTopScore >= 8;
|
|
3997
|
+
if (tokenEnough || tokenResult.matches.length >= opts.topK) {
|
|
3998
|
+
return { result: tokenResult, mode: "token" };
|
|
3999
|
+
}
|
|
4000
|
+
let textResult = null;
|
|
2603
4001
|
if (ftsAvailable) {
|
|
2604
4002
|
try {
|
|
2605
|
-
|
|
4003
|
+
textResult = ftsSearchInternal(opts);
|
|
2606
4004
|
}
|
|
2607
4005
|
catch (err) {
|
|
2608
4006
|
console.error("[vectormind] fts semantic_search failed; falling back:", err);
|
|
2609
4007
|
}
|
|
2610
4008
|
}
|
|
2611
|
-
|
|
4009
|
+
if (!textResult) {
|
|
4010
|
+
textResult = likeSearchInternal(opts);
|
|
4011
|
+
}
|
|
4012
|
+
const merged = mergeSemanticMatches([textResult.matches, tokenResult.matches], opts);
|
|
4013
|
+
const tokenIds = new Set(tokenResult.matches.map((m) => m.item.id));
|
|
4014
|
+
const ftsKept = textResult.matches.some((m) => !tokenIds.has(m.item.id));
|
|
4015
|
+
if (tokenResult.matches.length && ftsKept) {
|
|
4016
|
+
return {
|
|
4017
|
+
result: { query: opts.query.trim(), top_k: opts.topK, mode: "hybrid", matches: merged },
|
|
4018
|
+
mode: "hybrid",
|
|
4019
|
+
};
|
|
4020
|
+
}
|
|
4021
|
+
if (tokenResult.matches.length) {
|
|
4022
|
+
return { result: { query: opts.query.trim(), top_k: opts.topK, mode: "token", matches: merged }, mode: "token" };
|
|
4023
|
+
}
|
|
4024
|
+
return { result: textResult, mode: textResult.mode === "fts" ? "fts" : "like" };
|
|
4025
|
+
}
|
|
4026
|
+
async function semanticSearchHybridInternal(opts) {
|
|
4027
|
+
const lexical = chooseLexicalResult(opts).result;
|
|
4028
|
+
if (!embeddingsEnabled)
|
|
4029
|
+
return lexical;
|
|
4030
|
+
const embeddingsResult = await Promise.race([
|
|
4031
|
+
semanticSearchInternal(opts),
|
|
4032
|
+
new Promise((resolve) => setTimeout(resolve, SEMANTIC_EMBEDDINGS_TIMEOUT_MS, null)),
|
|
4033
|
+
]).catch((err) => {
|
|
4034
|
+
console.error("[vectormind] embeddings semantic_search failed; falling back:", err);
|
|
4035
|
+
return null;
|
|
4036
|
+
});
|
|
4037
|
+
if (!embeddingsResult)
|
|
4038
|
+
return lexical;
|
|
4039
|
+
const merged = mergeSemanticMatches([lexical.matches, embeddingsResult.matches], opts);
|
|
4040
|
+
return {
|
|
4041
|
+
query: opts.query.trim(),
|
|
4042
|
+
top_k: opts.topK,
|
|
4043
|
+
mode: merged.length ? "hybrid" : embeddingsResult.mode,
|
|
4044
|
+
matches: merged,
|
|
4045
|
+
};
|
|
2612
4046
|
}
|
|
2613
4047
|
function escapeRegExp(literal) {
|
|
2614
4048
|
return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -3313,7 +4747,7 @@ function listProjectFilesInternal(opts) {
|
|
|
3313
4747
|
function buildServerInstructions() {
|
|
3314
4748
|
return [
|
|
3315
4749
|
"VectorMind MCP is available in this session. Use it to avoid guessing project context.",
|
|
3316
|
-
"Development guideline scope: VectorMind instructions define development conventions, project-memory conventions, code-organization conventions, and delivery-quality expectations
|
|
4750
|
+
"Development guideline scope: VectorMind instructions define development conventions, project-memory conventions, code-organization conventions, and delivery-quality expectations.",
|
|
3317
4751
|
"Project root resolution order: tool argument project_root (recommended for clients without roots/list), then VECTORMIND_ROOT (avoid hardcoding in global config), then MCP roots/list (best-effort; falls back quickly if unsupported), then process.cwd() (so start your MCP client in the project directory for per-project isolation).",
|
|
3318
4752
|
"If root_source is fallback, file watching/indexing is disabled (pass project_root to enable per-project tracking).",
|
|
3319
4753
|
"",
|
|
@@ -3329,6 +4763,9 @@ function buildServerInstructions() {
|
|
|
3329
4763
|
"Built-in architecture and code-organization quality policy:",
|
|
3330
4764
|
BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS,
|
|
3331
4765
|
"",
|
|
4766
|
+
"Built-in requirement boundary and modularity quality policy:",
|
|
4767
|
+
BUILTIN_REQUIREMENT_BOUNDARY_AND_MODULARITY_INSTRUCTIONS,
|
|
4768
|
+
"",
|
|
3332
4769
|
"Built-in frontend output-purity quality policy:",
|
|
3333
4770
|
BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS,
|
|
3334
4771
|
"",
|
|
@@ -3348,24 +4785,30 @@ function buildServerInstructions() {
|
|
|
3348
4785
|
"- Tool outputs are compact by default. Pass format=json only when you need full structured data.",
|
|
3349
4786
|
"- On every new conversation/session for analysis/design/development work: call bootstrap_context({ query: <current goal> }) first (or at least get_brain_dump()) to restore compact context and retrieve relevant matches from the local memory store (vector if enabled; otherwise FTS/LIKE).",
|
|
3350
4787
|
" - Output is compact by default. Use include_content=true only when you truly need full text (it increases tokens).",
|
|
3351
|
-
" -
|
|
4788
|
+
" - bootstrap_context/get_brain_dump always include a small recency anchor named current_context (latest active requirements, recent notes, and recent change intents) in addition to query matches; tune output size with: requirements_limit/changes_limit/notes_limit/decisions_limit/current_context_limit, preview_chars, pending_limit/pending_offset.",
|
|
3352
4789
|
" - Prefer read_memory_item(id, offset, limit) to fetch full text on demand instead of returning large content in other tool outputs.",
|
|
3353
4790
|
"- For pure execution-first tasks with explicit targets (for example compile/build/run/launch/package/publish/test rerun), you may skip retrieval and go straight to the minimum necessary shell or host tools unless code/context lookup is actually needed to unblock execution.",
|
|
3354
4791
|
"- If rtk is installed or VectorMind's bundled RTK shim is verified (detect_rtk with gain_ok=true), prefix shell commands with the command returned by detect_rtk. Usually this is rtk (rtk git status, rtk npm run build, rtk rg ...); in npx/MCP-only installs it may be a package shim command such as node <...>/rtk-shim.js.",
|
|
3355
|
-
"- If rtk is missing and the user asks to install it, use install_rtk first with dry_run=true to show the exact commands; execute with dry_run=false only
|
|
3356
|
-
"- To read local Codex skill/prompt/rule files (for example SKILL.md under CODEX_HOME or AGENTS_HOME), prefer read_codex_text_file({ path }) instead of assuming
|
|
4792
|
+
"- If rtk is missing and the user asks to install it, use install_rtk first with dry_run=true to show the exact commands; execute with dry_run=false only when the user explicitly asks to install/init.",
|
|
4793
|
+
"- To read local Codex skill/prompt/rule files (for example SKILL.md under CODEX_HOME or AGENTS_HOME), prefer read_codex_text_file({ path }) instead of assuming another local-file MCP resource server exists.",
|
|
3357
4794
|
"- For project file/directory browsing, prefer list_project_files({ path, recursive?, max_depth? }) over shelling out to Get-ChildItem/ls. It respects ignore rules and keeps output bounded.",
|
|
3358
4795
|
"- For small/medium raw file reads, prefer read_file_text({ path, offset?, max_chars? }) over Get-Content -Raw. Use read_file_lines(...) when you need deterministic line ranges or the file may be large.",
|
|
3359
4796
|
"- For raw repo text search with exact file+line+col matches, prefer grep({ query: <pattern> }). It uses ripgrep against real project files when available, applies built-in noise filters, and only falls back to indexed search if ripgrep is unavailable.",
|
|
3360
4797
|
"- To read a bounded segment of a file, prefer read_file_lines({ path: <file>, from_line/to_line or total_count }) over unbounded file reads.",
|
|
3361
4798
|
"- BEFORE editing code: call start_requirement(title, background) to set the active requirement.",
|
|
4799
|
+
" - When the user names a narrow feature/domain, pass scope_allow/allowed_paths and scope_deny/denied_paths when useful. These are generic project-specific scope boundaries, not business-specific built-ins.",
|
|
4800
|
+
"- BEFORE editing once target files/modules are known: call preflight_change_scope(intent, files/planned_files, optional scope_allow/scope_deny/allowed_paths/denied_paths). If ok=false/safe_to_edit=false or it returns development_warnings, stop before editing and narrow the plan unless the user explicitly expands the requirement.",
|
|
4801
|
+
"- Treat the active requirement as the only change boundary. Do not add extra business behavior, new flows, new fields, new interfaces, or touch completed/related features unless the user explicitly asked or the change is strictly necessary.",
|
|
4802
|
+
"- Do not keep piling new feature code into a large single file. Prefer small modules/services/components; if an implementation file is already large, split it before adding more responsibilities.",
|
|
3362
4803
|
"- AFTER editing + saving: call get_pending_changes() to see unsynced files, then call sync_change_intent(intent, files). (You can omit files to auto-link all pending changes.)",
|
|
4804
|
+
"- If preflight_change_scope, read_file_lines, grep, query_codebase, get_pending_changes, or sync_change_intent returns development_warnings, address those warnings before continuing or explain why the current requirement truly needs that scope.",
|
|
3363
4805
|
"- After major milestones/decisions: call upsert_project_summary(summary) and/or add_note(...) to persist durable context locally.",
|
|
3364
4806
|
"- When a requirement or user decision changes/reverses an older behavior, call upsert_decision(key, title, content, supersedes_req_ids?/supersedes_memory_ids?) and/or supersede_memory(...). Current decisions are shown in bootstrap_context/get_brain_dump and superseded memories are hidden from default semantic recall so stale requirements do not override newer facts.",
|
|
3365
4807
|
"- If the user states a durable project convention (build commands, frameworks, naming rules, output paths): call upsert_convention(key, content, tags) so it is applied in future sessions.",
|
|
3366
4808
|
"- When you need full text for a specific note/summary/match: call read_memory_item(id, offset, limit) and page through it.",
|
|
3367
4809
|
"- When asked to locate code (class/function/type): call query_codebase(query) instead of guessing.",
|
|
3368
|
-
"- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing.",
|
|
4810
|
+
"- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing. It blends lexical/FTS recall with embeddings when enabled, so recent explicit wording and durable decisions are not hidden by older semantically similar matches.",
|
|
4811
|
+
"- VectorMind automatically runs small, throttled memory maintenance to compact old completed history and prune stale indexes in long-lived projects. For large repos that feel slow, call maintain_memory({ dry_run: true }) first, then maintain_memory({ dry_run: false }) if the plan looks correct.",
|
|
3369
4812
|
"- Use get_token_savings({ format: 'compact' }) when you need to verify how many tokens VectorMind compact outputs saved.",
|
|
3370
4813
|
"",
|
|
3371
4814
|
"If tool output conflicts with assumptions, trust the tool output.",
|
|
@@ -3483,6 +4926,15 @@ function initMemoryItemsFts() {
|
|
|
3483
4926
|
ftsAvailable = false;
|
|
3484
4927
|
}
|
|
3485
4928
|
}
|
|
4929
|
+
function columnExists(table, column) {
|
|
4930
|
+
try {
|
|
4931
|
+
const rows = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
4932
|
+
return rows.some((row) => row.name === column);
|
|
4933
|
+
}
|
|
4934
|
+
catch {
|
|
4935
|
+
return false;
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
3486
4938
|
function initDatabase() {
|
|
3487
4939
|
const vmDir = path.join(projectRoot, ".vectormind");
|
|
3488
4940
|
try {
|
|
@@ -3535,7 +4987,8 @@ function initDatabase() {
|
|
|
3535
4987
|
title TEXT NOT NULL,
|
|
3536
4988
|
status TEXT DEFAULT 'active',
|
|
3537
4989
|
context_data TEXT,
|
|
3538
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
4990
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
4991
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
3539
4992
|
);
|
|
3540
4993
|
|
|
3541
4994
|
CREATE TABLE IF NOT EXISTS change_logs (
|
|
@@ -3634,6 +5087,49 @@ function initDatabase() {
|
|
|
3634
5087
|
|
|
3635
5088
|
CREATE INDEX IF NOT EXISTS idx_token_savings_tool
|
|
3636
5089
|
ON token_savings(tool);
|
|
5090
|
+
|
|
5091
|
+
CREATE TABLE IF NOT EXISTS memory_item_archive (
|
|
5092
|
+
memory_id INTEGER PRIMARY KEY,
|
|
5093
|
+
original_kind TEXT NOT NULL,
|
|
5094
|
+
original_title TEXT,
|
|
5095
|
+
original_content TEXT NOT NULL,
|
|
5096
|
+
original_file_path TEXT,
|
|
5097
|
+
original_start_line INTEGER,
|
|
5098
|
+
original_end_line INTEGER,
|
|
5099
|
+
original_req_id INTEGER,
|
|
5100
|
+
original_metadata_json TEXT,
|
|
5101
|
+
original_content_hash TEXT,
|
|
5102
|
+
original_created_at DATETIME,
|
|
5103
|
+
original_updated_at DATETIME,
|
|
5104
|
+
archive_reason TEXT NOT NULL,
|
|
5105
|
+
compacted_into_id INTEGER,
|
|
5106
|
+
archived_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
5107
|
+
);
|
|
5108
|
+
|
|
5109
|
+
CREATE INDEX IF NOT EXISTS idx_memory_item_archive_compacted_into
|
|
5110
|
+
ON memory_item_archive(compacted_into_id);
|
|
5111
|
+
|
|
5112
|
+
CREATE TABLE IF NOT EXISTS meta_kv (
|
|
5113
|
+
key TEXT PRIMARY KEY,
|
|
5114
|
+
value TEXT NOT NULL,
|
|
5115
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
5116
|
+
);
|
|
5117
|
+
`);
|
|
5118
|
+
if (!columnExists("requirements", "updated_at")) {
|
|
5119
|
+
db.exec(`ALTER TABLE requirements ADD COLUMN updated_at DATETIME`);
|
|
5120
|
+
db.exec(`UPDATE requirements SET updated_at = COALESCE(created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL`);
|
|
5121
|
+
}
|
|
5122
|
+
db.exec(`
|
|
5123
|
+
UPDATE requirements SET updated_at = COALESCE(updated_at, created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL;
|
|
5124
|
+
CREATE INDEX IF NOT EXISTS idx_requirements_status_updated_at
|
|
5125
|
+
ON requirements(status, updated_at DESC, id DESC);
|
|
5126
|
+
CREATE TRIGGER IF NOT EXISTS vectormind_requirements_touch_updated_at
|
|
5127
|
+
AFTER UPDATE ON requirements
|
|
5128
|
+
FOR EACH ROW
|
|
5129
|
+
WHEN NEW.updated_at = OLD.updated_at
|
|
5130
|
+
BEGIN
|
|
5131
|
+
UPDATE requirements SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
|
5132
|
+
END;
|
|
3637
5133
|
`);
|
|
3638
5134
|
initMemoryItemsFts();
|
|
3639
5135
|
insertRequirementStmt = db.prepare(`INSERT INTO requirements (title, context_data, status) VALUES (?, ?, 'active')`);
|
|
@@ -3642,11 +5138,16 @@ function initDatabase() {
|
|
|
3642
5138
|
getActiveRequirementStmt = db.prepare(`SELECT id, title, status, context_data, created_at
|
|
3643
5139
|
FROM requirements
|
|
3644
5140
|
WHERE status = 'active'
|
|
3645
|
-
ORDER BY created_at DESC, id DESC
|
|
5141
|
+
ORDER BY updated_at DESC, created_at DESC, id DESC
|
|
3646
5142
|
LIMIT 1`);
|
|
5143
|
+
listActiveRequirementsStmt = db.prepare(`SELECT id, title, status, context_data, created_at
|
|
5144
|
+
FROM requirements
|
|
5145
|
+
WHERE status = 'active'
|
|
5146
|
+
ORDER BY updated_at DESC, created_at DESC, id DESC
|
|
5147
|
+
LIMIT ?`);
|
|
3647
5148
|
listRecentRequirementsStmt = db.prepare(`SELECT id, title, status, context_data, created_at
|
|
3648
5149
|
FROM requirements
|
|
3649
|
-
ORDER BY created_at DESC, id DESC
|
|
5150
|
+
ORDER BY updated_at DESC, created_at DESC, id DESC
|
|
3650
5151
|
LIMIT ?`);
|
|
3651
5152
|
completeAllActiveRequirementMemoryItemsStmt = db.prepare(`UPDATE memory_items
|
|
3652
5153
|
SET metadata_json = ?, updated_at = CURRENT_TIMESTAMP
|
|
@@ -3723,6 +5224,11 @@ function initDatabase() {
|
|
|
3723
5224
|
WHERE kind = 'note'
|
|
3724
5225
|
ORDER BY updated_at DESC, id DESC
|
|
3725
5226
|
LIMIT ?`);
|
|
5227
|
+
listRecentContextItemsStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
|
|
5228
|
+
FROM memory_items
|
|
5229
|
+
WHERE kind IN ('note', 'requirement', 'change_intent')
|
|
5230
|
+
ORDER BY updated_at DESC, id DESC
|
|
5231
|
+
LIMIT ?`);
|
|
3726
5232
|
getLatestChangeIntentForFileStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
|
|
3727
5233
|
FROM memory_items
|
|
3728
5234
|
WHERE kind = 'change_intent' AND file_path = ?
|
|
@@ -3801,6 +5307,10 @@ function initDatabase() {
|
|
|
3801
5307
|
FROM token_savings
|
|
3802
5308
|
ORDER BY created_at DESC, id DESC
|
|
3803
5309
|
LIMIT ?`);
|
|
5310
|
+
getKvStmt = db.prepare(`SELECT value FROM meta_kv WHERE key = ?`);
|
|
5311
|
+
setKvStmt = db.prepare(`INSERT INTO meta_kv (key, value)
|
|
5312
|
+
VALUES (?, ?)
|
|
5313
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`);
|
|
3804
5314
|
indexFileSymbolsTx = db.transaction((filePath, symbols) => {
|
|
3805
5315
|
deleteSymbolsForFileStmt.run(filePath);
|
|
3806
5316
|
for (const s of symbols) {
|
|
@@ -3816,6 +5326,9 @@ function initDatabase() {
|
|
|
3816
5326
|
// Clean up common "file name noise" recorded by older versions.
|
|
3817
5327
|
// (These files are ignored by current index rules; keep the DB consistent automatically.)
|
|
3818
5328
|
pruneFilenameNoiseIndexes();
|
|
5329
|
+
// Bounded, throttled maintenance keeps long-lived project memory fast without
|
|
5330
|
+
// deleting durable decisions/conventions/project summaries.
|
|
5331
|
+
runAutoMaintenanceIfDue();
|
|
3819
5332
|
}
|
|
3820
5333
|
function initWatcher() {
|
|
3821
5334
|
watcherReady = false;
|
|
@@ -3947,14 +5460,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
3947
5460
|
tools: [
|
|
3948
5461
|
{
|
|
3949
5462
|
name: "start_requirement",
|
|
3950
|
-
description: "MUST call BEFORE editing code. Starts/activates
|
|
5463
|
+
description: "MUST call BEFORE editing code. Starts/activates the concrete user requirement so subsequent changes stay inside that requirement boundary and do not accumulate unrelated work. Supports scope_allow/scope_deny and allowed_paths/denied_paths to prevent unrelated domain drift.",
|
|
3951
5464
|
inputSchema: toJsonSchemaCompat(StartRequirementArgsSchema),
|
|
3952
5465
|
},
|
|
3953
5466
|
{
|
|
3954
5467
|
name: "sync_change_intent",
|
|
3955
|
-
description: "MUST call AFTER you edit code and save files. Archives the intent summary
|
|
5468
|
+
description: "MUST call AFTER you edit code and save files. Archives the intent summary, links affected files to the current active requirement, and returns development_warnings for oversized files, broad change scope, or missing file targets.",
|
|
3956
5469
|
inputSchema: toJsonSchemaCompat(SyncChangeIntentArgsSchema),
|
|
3957
5470
|
},
|
|
5471
|
+
{
|
|
5472
|
+
name: "preflight_change_scope",
|
|
5473
|
+
description: "MUST call BEFORE editing once you know the intended files/modules. Checks planned files against the active requirement and optional generic scope_allow/scope_deny/allowed_paths/denied_paths. If ok=false/safe_to_edit=false, stop before editing and narrow the plan or scope contract.",
|
|
5474
|
+
inputSchema: toJsonSchemaCompat(PreflightChangeScopeArgsSchema),
|
|
5475
|
+
},
|
|
3958
5476
|
{
|
|
3959
5477
|
name: "get_brain_dump",
|
|
3960
5478
|
description: "Restore recent requirements/changes/notes/summary/pending changes. Prefer bootstrap_context() at session start when you also want recall from the local memory store.",
|
|
@@ -3962,12 +5480,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
3962
5480
|
},
|
|
3963
5481
|
{
|
|
3964
5482
|
name: "bootstrap_context",
|
|
3965
|
-
description: "MUST call at the start of every new chat/session. Returns brain dump + pending changes, and (if you pass query) matches from the local memory store to avoid guessing.",
|
|
5483
|
+
description: "MUST call at the start of every new chat/session. Returns brain dump + pending changes + development_warnings, and (if you pass query) matches from the local memory store to avoid guessing.",
|
|
3966
5484
|
inputSchema: toJsonSchemaCompat(BootstrapContextArgsSchema),
|
|
3967
5485
|
},
|
|
3968
5486
|
{
|
|
3969
5487
|
name: "get_pending_changes",
|
|
3970
|
-
description: "List files that changed locally but have not been acknowledged by sync_change_intent yet.
|
|
5488
|
+
description: "List files that changed locally but have not been acknowledged by sync_change_intent yet. Also returns development_warnings to catch god-file growth, broad change scope, and requirement-boundary drift.",
|
|
3971
5489
|
inputSchema: toJsonSchemaCompat(GetPendingChangesArgsSchema),
|
|
3972
5490
|
},
|
|
3973
5491
|
{
|
|
@@ -4012,7 +5530,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
4012
5530
|
},
|
|
4013
5531
|
{
|
|
4014
5532
|
name: "grep",
|
|
4015
|
-
description: "Repo text search with precise file/line/col matches, powered by ripgrep against real project files plus built-in noise filters. Falls back to indexed search only when ripgrep is unavailable.",
|
|
5533
|
+
description: "Repo text search with precise file/line/col matches, powered by ripgrep against real project files plus built-in noise filters. Falls back to indexed search only when ripgrep is unavailable. Returns development_warnings for cross-project paths or huge implementation-file matches.",
|
|
4016
5534
|
inputSchema: toJsonSchemaCompat(GrepArgsSchema),
|
|
4017
5535
|
},
|
|
4018
5536
|
{
|
|
@@ -4022,12 +5540,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
4022
5540
|
},
|
|
4023
5541
|
{
|
|
4024
5542
|
name: "read_codex_text_file",
|
|
4025
|
-
description: "Read bounded text from local Codex/agents files such as SKILL.md, prompt files, and rules under CODEX_HOME/AGENTS_HOME. Prefer this over assuming
|
|
5543
|
+
description: "Read bounded text from local Codex/agents files such as SKILL.md, prompt files, and rules under CODEX_HOME/AGENTS_HOME. Prefer this over assuming another local-file MCP resource server exists.",
|
|
4026
5544
|
inputSchema: toJsonSchemaCompat(ReadCodexTextFileArgsSchema),
|
|
4027
5545
|
},
|
|
4028
5546
|
{
|
|
4029
5547
|
name: "read_file_lines",
|
|
4030
|
-
description: "Read a specific line range from a file under project_root (with strict size limits). Prefer this over Get-Content for deterministic reads.",
|
|
5548
|
+
description: "Read a specific line range from a file under project_root (with strict size limits). Prefer this over Get-Content for deterministic reads. Returns development_warnings when the target is a huge implementation file.",
|
|
4031
5549
|
inputSchema: toJsonSchemaCompat(ReadFileLinesArgsSchema),
|
|
4032
5550
|
},
|
|
4033
5551
|
{
|
|
@@ -4037,7 +5555,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
4037
5555
|
},
|
|
4038
5556
|
{
|
|
4039
5557
|
name: "query_codebase",
|
|
4040
|
-
description: "Search the symbol index for class/function/type names (or substrings) to locate definitions by file path and signature. Use this when you need to find code
|
|
5558
|
+
description: "Search the symbol index for class/function/type names (or substrings) to locate definitions by file path and signature. Use this when you need to find code; do not guess locations. Returns development_warnings when matches point at huge implementation files.",
|
|
4041
5559
|
inputSchema: toJsonSchemaCompat(QueryCodebaseArgsSchema),
|
|
4042
5560
|
},
|
|
4043
5561
|
{
|
|
@@ -4070,6 +5588,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
4070
5588
|
description: "Semantic search across the local memory store (requirements, change intents, notes, project summary, and indexed code/doc chunks). Use this to retrieve relevant context instead of guessing.",
|
|
4071
5589
|
inputSchema: toJsonSchemaCompat(SemanticSearchArgsSchema),
|
|
4072
5590
|
},
|
|
5591
|
+
{
|
|
5592
|
+
name: "maintain_memory",
|
|
5593
|
+
description: "Compact old completed memory and prune stale/noisy indexes to keep long-lived large projects fast. Defaults to dry_run=true; automatic safe maintenance also runs periodically.",
|
|
5594
|
+
inputSchema: toJsonSchemaCompat(MaintainMemoryArgsSchema),
|
|
5595
|
+
},
|
|
4073
5596
|
{
|
|
4074
5597
|
name: "prune_index",
|
|
4075
5598
|
description: "Prune noisy auto-indexed items (code_chunk/doc_chunk + symbols). Useful after tightening ignore rules to shrink the index and improve search relevance.",
|
|
@@ -4086,6 +5609,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4086
5609
|
if (toolName === "start_requirement") {
|
|
4087
5610
|
const args = StartRequirementArgsSchema.parse(rawArgs);
|
|
4088
5611
|
flushPendingChangeBuffer();
|
|
5612
|
+
const scope_contract = buildRequirementScopeContract({
|
|
5613
|
+
title: args.title,
|
|
5614
|
+
background: args.background,
|
|
5615
|
+
scope_allow: args.scope_allow,
|
|
5616
|
+
scope_deny: args.scope_deny,
|
|
5617
|
+
allowed_paths: args.allowed_paths,
|
|
5618
|
+
denied_paths: args.denied_paths,
|
|
5619
|
+
});
|
|
5620
|
+
const development_warnings = buildRequirementStartWarnings({
|
|
5621
|
+
title: args.title,
|
|
5622
|
+
background: args.background,
|
|
5623
|
+
close_previous: args.close_previous,
|
|
5624
|
+
});
|
|
4089
5625
|
if (args.close_previous) {
|
|
4090
5626
|
try {
|
|
4091
5627
|
completeAllActiveRequirementsStmt.run();
|
|
@@ -4099,13 +5635,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4099
5635
|
const id = Number(info.lastInsertRowid);
|
|
4100
5636
|
const background = args.background?.trim() ?? "";
|
|
4101
5637
|
const content = background ? `${args.title}\n\n${background}` : args.title;
|
|
4102
|
-
const memoryInfo = insertMemoryItemStmt.run("requirement", args.title, content, null, null, null, id, safeJson({ status: "active" }), sha256Hex(content));
|
|
5638
|
+
const memoryInfo = insertMemoryItemStmt.run("requirement", args.title, content, null, null, null, id, safeJson({ status: "active", scope_contract }), sha256Hex(content));
|
|
4103
5639
|
const memory_id = Number(memoryInfo.lastInsertRowid);
|
|
4104
5640
|
enqueueEmbedding(memory_id);
|
|
4105
5641
|
logActivity("start_requirement", {
|
|
4106
5642
|
req_id: id,
|
|
4107
5643
|
title: args.title,
|
|
4108
5644
|
closed_previous: args.close_previous,
|
|
5645
|
+
scope_contract,
|
|
5646
|
+
development_warnings: development_warnings.length,
|
|
4109
5647
|
});
|
|
4110
5648
|
return {
|
|
4111
5649
|
content: [
|
|
@@ -4116,6 +5654,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4116
5654
|
requirement: { id, title: args.title },
|
|
4117
5655
|
memory_item: { id: memory_id },
|
|
4118
5656
|
closed_previous: args.close_previous,
|
|
5657
|
+
scope_contract,
|
|
5658
|
+
development_warnings,
|
|
4119
5659
|
}),
|
|
4120
5660
|
},
|
|
4121
5661
|
],
|
|
@@ -4257,6 +5797,78 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4257
5797
|
],
|
|
4258
5798
|
};
|
|
4259
5799
|
}
|
|
5800
|
+
if (toolName === "maintain_memory") {
|
|
5801
|
+
const args = MaintainMemoryArgsSchema.parse(rawArgs);
|
|
5802
|
+
flushPendingChangeBuffer();
|
|
5803
|
+
const result = runMemoryMaintenance(args, "manual");
|
|
5804
|
+
return {
|
|
5805
|
+
content: [
|
|
5806
|
+
{
|
|
5807
|
+
type: "text",
|
|
5808
|
+
text: toolCompactOrJson("maintain_memory", result, compactMaintenanceText(result), args.format),
|
|
5809
|
+
},
|
|
5810
|
+
],
|
|
5811
|
+
};
|
|
5812
|
+
}
|
|
5813
|
+
if (toolName === "preflight_change_scope") {
|
|
5814
|
+
const args = PreflightChangeScopeArgsSchema.parse(rawArgs);
|
|
5815
|
+
flushPendingChangeBuffer();
|
|
5816
|
+
const files = (args.files ?? args.planned_files ?? []).filter((f) => typeof f === "string" && f.length > 0);
|
|
5817
|
+
const active = getActiveRequirementStmt.get();
|
|
5818
|
+
const explicitContract = buildRequirementScopeContract({
|
|
5819
|
+
title: active?.title ?? "",
|
|
5820
|
+
background: active?.context_data ?? "",
|
|
5821
|
+
scope_allow: args.scope_allow,
|
|
5822
|
+
scope_deny: args.scope_deny,
|
|
5823
|
+
allowed_paths: args.allowed_paths,
|
|
5824
|
+
denied_paths: args.denied_paths,
|
|
5825
|
+
});
|
|
5826
|
+
const fileInputs = files.map((file_path) => ({ file_path }));
|
|
5827
|
+
const development_warnings = [
|
|
5828
|
+
...buildDevelopmentWarnings(fileInputs, { includeUnspecified: fileInputs.length === 0 }),
|
|
5829
|
+
...buildScopeDriftWarnings({
|
|
5830
|
+
requirement: active,
|
|
5831
|
+
contract: explicitContract,
|
|
5832
|
+
intent: args.intent,
|
|
5833
|
+
files: fileInputs,
|
|
5834
|
+
includeMissingContractHint: true,
|
|
5835
|
+
}),
|
|
5836
|
+
];
|
|
5837
|
+
const scope_contract = mergeScopeContracts(active ? getRequirementScopeContract(active.id) : null, explicitContract);
|
|
5838
|
+
logActivity("preflight_change_scope", {
|
|
5839
|
+
req_id: active?.id ?? null,
|
|
5840
|
+
intent_preview: makePreviewText(args.intent, 200),
|
|
5841
|
+
files: files.slice(0, 25),
|
|
5842
|
+
files_total: files.length,
|
|
5843
|
+
development_warnings: development_warnings.length,
|
|
5844
|
+
});
|
|
5845
|
+
const hasTargetFiles = fileInputs.length > 0;
|
|
5846
|
+
const hasBlockingWarnings = development_warnings.some((w) => w.severity === "blocker" || w.severity === "warning");
|
|
5847
|
+
const safeToEdit = hasTargetFiles && !hasBlockingWarnings;
|
|
5848
|
+
const recommendedAction = !hasTargetFiles
|
|
5849
|
+
? "Identify the intended target files/modules and rerun preflight_change_scope before editing."
|
|
5850
|
+
: hasBlockingWarnings
|
|
5851
|
+
? "Stop before editing. Narrow the planned files or explicitly expand the current requirement/scope contract."
|
|
5852
|
+
: "Planned files are within the current generic scope checks.";
|
|
5853
|
+
const outputValue = {
|
|
5854
|
+
ok: safeToEdit,
|
|
5855
|
+
safe_to_edit: safeToEdit,
|
|
5856
|
+
recommended_action: recommendedAction,
|
|
5857
|
+
active_requirement: active ? { id: active.id, title: active.title } : null,
|
|
5858
|
+
intent: args.intent,
|
|
5859
|
+
files: files.map(normalizeToDbPath),
|
|
5860
|
+
scope_contract,
|
|
5861
|
+
development_warnings,
|
|
5862
|
+
};
|
|
5863
|
+
return {
|
|
5864
|
+
content: [
|
|
5865
|
+
{
|
|
5866
|
+
type: "text",
|
|
5867
|
+
text: toolCompactOrJson("preflight_change_scope", outputValue, compactPreflightChangeScopeText(outputValue), args.format),
|
|
5868
|
+
},
|
|
5869
|
+
],
|
|
5870
|
+
};
|
|
5871
|
+
}
|
|
4260
5872
|
if (toolName === "sync_change_intent") {
|
|
4261
5873
|
const args = SyncChangeIntentArgsSchema.parse(rawArgs);
|
|
4262
5874
|
flushPendingChangeBuffer();
|
|
@@ -4341,12 +5953,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4341
5953
|
}
|
|
4342
5954
|
});
|
|
4343
5955
|
insertTx();
|
|
5956
|
+
const development_warnings = [
|
|
5957
|
+
...buildDevelopmentWarnings(synced_files, {
|
|
5958
|
+
includeUnspecified: synced_files.some((f) => f.file_path === "(unspecified)"),
|
|
5959
|
+
}),
|
|
5960
|
+
...buildScopeDriftWarnings({
|
|
5961
|
+
requirement: active,
|
|
5962
|
+
intent: args.intent,
|
|
5963
|
+
files: synced_files,
|
|
5964
|
+
}),
|
|
5965
|
+
];
|
|
4344
5966
|
logActivity("sync_change_intent", {
|
|
4345
5967
|
req_id: active.id,
|
|
4346
5968
|
title: active.title,
|
|
4347
5969
|
intent_preview: makePreviewText(args.intent, 200),
|
|
4348
5970
|
files: synced_files.slice(0, 25),
|
|
4349
5971
|
files_total: synced_files.length,
|
|
5972
|
+
development_warnings: development_warnings.length,
|
|
4350
5973
|
});
|
|
4351
5974
|
return {
|
|
4352
5975
|
content: [
|
|
@@ -4357,6 +5980,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4357
5980
|
linked_to_requirement: { id: active.id, title: active.title },
|
|
4358
5981
|
synced_files,
|
|
4359
5982
|
created,
|
|
5983
|
+
development_warnings,
|
|
4360
5984
|
}),
|
|
4361
5985
|
},
|
|
4362
5986
|
],
|
|
@@ -4373,6 +5997,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4373
5997
|
const notesLimit = args.notes_limit;
|
|
4374
5998
|
const conventionsLimit = args.conventions_limit;
|
|
4375
5999
|
const decisionsLimit = args.decisions_limit;
|
|
6000
|
+
const currentContextLimit = args.current_context_limit;
|
|
4376
6001
|
const recent = listRecentRequirementsStmt.all(requirementsLimit);
|
|
4377
6002
|
const items = recent.map((req) => {
|
|
4378
6003
|
const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
|
|
@@ -4388,6 +6013,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4388
6013
|
const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
|
|
4389
6014
|
const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
|
|
4390
6015
|
const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
|
|
6016
|
+
const current_context = getCurrentContextPreviews(currentContextLimit, previewChars, contentMaxChars);
|
|
4391
6017
|
const pending_offset = args.pending_offset;
|
|
4392
6018
|
const pending_limit = args.pending_limit;
|
|
4393
6019
|
const pendingDbRows = listPendingChangesStmt.all();
|
|
@@ -4395,13 +6021,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4395
6021
|
const pending_total = mergedPending.total;
|
|
4396
6022
|
const pending_truncated = mergedPending.truncated;
|
|
4397
6023
|
const pending_changes = mergedPending.page;
|
|
6024
|
+
const activeForScope = getActiveRequirementStmt.get();
|
|
6025
|
+
const development_warnings = [
|
|
6026
|
+
...buildDevelopmentWarnings(pending_changes),
|
|
6027
|
+
...(activeForScope
|
|
6028
|
+
? buildScopeDriftWarnings({ requirement: activeForScope, files: pending_changes })
|
|
6029
|
+
: []),
|
|
6030
|
+
];
|
|
4398
6031
|
const q = args.query?.trim() ?? "";
|
|
6032
|
+
const semanticKinds = args.kinds?.length ? args.kinds : BOOTSTRAP_DEFAULT_CONTEXT_KINDS;
|
|
4399
6033
|
const semantic = q
|
|
4400
6034
|
? await Promise.race([
|
|
4401
6035
|
semanticSearchHybridInternal({
|
|
4402
6036
|
query: q,
|
|
4403
6037
|
topK: args.top_k,
|
|
4404
|
-
kinds:
|
|
6038
|
+
kinds: semanticKinds,
|
|
4405
6039
|
includeContent,
|
|
4406
6040
|
previewChars,
|
|
4407
6041
|
contentMaxChars,
|
|
@@ -4418,6 +6052,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4418
6052
|
pending_returned: pending_changes.length,
|
|
4419
6053
|
requirements_returned: items.length,
|
|
4420
6054
|
decisions_returned: decisions.length,
|
|
6055
|
+
current_context_returned: current_context.length,
|
|
4421
6056
|
conventions_returned: conventions.length,
|
|
4422
6057
|
semantic_mode: semantic?.mode ?? null,
|
|
4423
6058
|
semantic_matches: semantic?.matches?.length ?? 0,
|
|
@@ -4444,17 +6079,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4444
6079
|
changes_limit: changesLimit,
|
|
4445
6080
|
notes_limit: notesLimit,
|
|
4446
6081
|
decisions_limit: decisionsLimit,
|
|
6082
|
+
current_context_limit: currentContextLimit,
|
|
4447
6083
|
conventions_limit: conventionsLimit,
|
|
4448
6084
|
},
|
|
4449
6085
|
project_summary,
|
|
4450
6086
|
decisions,
|
|
4451
6087
|
conventions,
|
|
6088
|
+
current_context,
|
|
4452
6089
|
recent_notes,
|
|
4453
6090
|
pending_total,
|
|
4454
6091
|
pending_offset,
|
|
4455
6092
|
pending_limit,
|
|
4456
6093
|
pending_truncated,
|
|
4457
6094
|
pending_changes,
|
|
6095
|
+
development_warnings,
|
|
4458
6096
|
items,
|
|
4459
6097
|
semantic,
|
|
4460
6098
|
};
|
|
@@ -4478,6 +6116,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4478
6116
|
const notesLimit = args.notes_limit;
|
|
4479
6117
|
const conventionsLimit = args.conventions_limit;
|
|
4480
6118
|
const decisionsLimit = args.decisions_limit;
|
|
6119
|
+
const currentContextLimit = args.current_context_limit;
|
|
4481
6120
|
const recent = listRecentRequirementsStmt.all(requirementsLimit);
|
|
4482
6121
|
const items = recent.map((req) => {
|
|
4483
6122
|
const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
|
|
@@ -4493,6 +6132,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4493
6132
|
const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
|
|
4494
6133
|
const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
|
|
4495
6134
|
const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
|
|
6135
|
+
const current_context = getCurrentContextPreviews(currentContextLimit, previewChars, contentMaxChars);
|
|
4496
6136
|
const pending_offset = args.pending_offset;
|
|
4497
6137
|
const pending_limit = args.pending_limit;
|
|
4498
6138
|
const pendingDbRows = listPendingChangesStmt.all();
|
|
@@ -4500,12 +6140,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4500
6140
|
const pending_total = mergedPending.total;
|
|
4501
6141
|
const pending_truncated = mergedPending.truncated;
|
|
4502
6142
|
const pending_changes = mergedPending.page;
|
|
6143
|
+
const activeForScope = getActiveRequirementStmt.get();
|
|
6144
|
+
const development_warnings = [
|
|
6145
|
+
...buildDevelopmentWarnings(pending_changes),
|
|
6146
|
+
...(activeForScope
|
|
6147
|
+
? buildScopeDriftWarnings({ requirement: activeForScope, files: pending_changes })
|
|
6148
|
+
: []),
|
|
6149
|
+
];
|
|
4503
6150
|
logActivity("get_brain_dump", {
|
|
4504
6151
|
pending_total,
|
|
4505
6152
|
pending_returned: pending_changes.length,
|
|
4506
6153
|
requirements_returned: items.length,
|
|
4507
6154
|
notes_returned: recent_notes.length,
|
|
4508
6155
|
decisions_returned: decisions.length,
|
|
6156
|
+
current_context_returned: current_context.length,
|
|
4509
6157
|
conventions_returned: conventions.length,
|
|
4510
6158
|
});
|
|
4511
6159
|
const outputValue = {
|
|
@@ -4530,17 +6178,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4530
6178
|
changes_limit: changesLimit,
|
|
4531
6179
|
notes_limit: notesLimit,
|
|
4532
6180
|
decisions_limit: decisionsLimit,
|
|
6181
|
+
current_context_limit: currentContextLimit,
|
|
4533
6182
|
conventions_limit: conventionsLimit,
|
|
4534
6183
|
},
|
|
4535
6184
|
project_summary,
|
|
4536
6185
|
decisions,
|
|
4537
6186
|
conventions,
|
|
6187
|
+
current_context,
|
|
4538
6188
|
recent_notes,
|
|
4539
6189
|
pending_total,
|
|
4540
6190
|
pending_offset,
|
|
4541
6191
|
pending_limit,
|
|
4542
6192
|
pending_truncated,
|
|
4543
6193
|
pending_changes,
|
|
6194
|
+
development_warnings,
|
|
4544
6195
|
items,
|
|
4545
6196
|
semantic: null,
|
|
4546
6197
|
};
|
|
@@ -4563,18 +6214,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4563
6214
|
const total = mergedPending.total;
|
|
4564
6215
|
const truncated = mergedPending.truncated;
|
|
4565
6216
|
const pending = mergedPending.page;
|
|
6217
|
+
const activeForScope = getActiveRequirementStmt.get();
|
|
6218
|
+
const development_warnings = [
|
|
6219
|
+
...buildDevelopmentWarnings(pending),
|
|
6220
|
+
...(activeForScope ? buildScopeDriftWarnings({ requirement: activeForScope, files: pending }) : []),
|
|
6221
|
+
];
|
|
4566
6222
|
logActivity("get_pending_changes", {
|
|
4567
6223
|
total,
|
|
4568
6224
|
offset,
|
|
4569
6225
|
limit,
|
|
4570
6226
|
returned: pending.length,
|
|
4571
6227
|
truncated,
|
|
6228
|
+
development_warnings: development_warnings.length,
|
|
4572
6229
|
});
|
|
4573
6230
|
return {
|
|
4574
6231
|
content: [
|
|
4575
6232
|
{
|
|
4576
6233
|
type: "text",
|
|
4577
|
-
text: toolJson({ ok: true, total, offset, limit, truncated, pending }),
|
|
6234
|
+
text: toolJson({ ok: true, total, offset, limit, truncated, pending, development_warnings }),
|
|
4578
6235
|
},
|
|
4579
6236
|
],
|
|
4580
6237
|
};
|
|
@@ -4765,6 +6422,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4765
6422
|
const includePaths = args.include_paths?.length ? args.include_paths : null;
|
|
4766
6423
|
const excludePaths = args.exclude_paths?.length ? args.exclude_paths : null;
|
|
4767
6424
|
const maxResults = args.max_results;
|
|
6425
|
+
const development_warnings = [
|
|
6426
|
+
...buildCrossProjectPathWarnings(includePaths),
|
|
6427
|
+
...buildCrossProjectPathWarnings(excludePaths),
|
|
6428
|
+
];
|
|
4768
6429
|
const caseSensitive = args.case_sensitive ?? (smartCase ? hasUppercaseAscii(q) : true);
|
|
4769
6430
|
const ripgrepResult = runRipgrepSearch({
|
|
4770
6431
|
query: q,
|
|
@@ -4776,6 +6437,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4776
6437
|
maxResults,
|
|
4777
6438
|
});
|
|
4778
6439
|
if (ripgrepResult.ok) {
|
|
6440
|
+
const grepDevelopmentWarnings = [
|
|
6441
|
+
...development_warnings,
|
|
6442
|
+
...buildMatchedFileDevelopmentWarnings(ripgrepResult.matches.map((m) => m.file_path)),
|
|
6443
|
+
];
|
|
4779
6444
|
logActivity("grep", {
|
|
4780
6445
|
backend: ripgrepResult.backend,
|
|
4781
6446
|
rg_command: ripgrepResult.rg_command,
|
|
@@ -4788,6 +6453,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4788
6453
|
matches: ripgrepResult.matches.length,
|
|
4789
6454
|
total_matches: ripgrepResult.total_matches,
|
|
4790
6455
|
truncated: ripgrepResult.truncated,
|
|
6456
|
+
development_warnings: grepDevelopmentWarnings.length,
|
|
4791
6457
|
});
|
|
4792
6458
|
const outputValue = {
|
|
4793
6459
|
ok: true,
|
|
@@ -4802,6 +6468,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4802
6468
|
matches: ripgrepResult.matches,
|
|
4803
6469
|
total_matches: ripgrepResult.total_matches,
|
|
4804
6470
|
truncated: ripgrepResult.truncated,
|
|
6471
|
+
development_warnings: grepDevelopmentWarnings,
|
|
4805
6472
|
};
|
|
4806
6473
|
return {
|
|
4807
6474
|
content: [
|
|
@@ -4867,6 +6534,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4867
6534
|
],
|
|
4868
6535
|
};
|
|
4869
6536
|
}
|
|
6537
|
+
const grepDevelopmentWarnings = [
|
|
6538
|
+
...development_warnings,
|
|
6539
|
+
...buildMatchedFileDevelopmentWarnings(indexedResult.matches.map((m) => m.file_path)),
|
|
6540
|
+
];
|
|
4870
6541
|
logActivity("grep", {
|
|
4871
6542
|
backend: indexedResult.backend,
|
|
4872
6543
|
fallback_reason: "ripgrep_unavailable",
|
|
@@ -4883,6 +6554,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4883
6554
|
candidates_scanned: indexedResult.candidates.scanned,
|
|
4884
6555
|
matches: indexedResult.matches.length,
|
|
4885
6556
|
truncated: indexedResult.truncated,
|
|
6557
|
+
development_warnings: grepDevelopmentWarnings.length,
|
|
4886
6558
|
});
|
|
4887
6559
|
const outputValue = {
|
|
4888
6560
|
ok: true,
|
|
@@ -4901,6 +6573,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4901
6573
|
candidates: indexedResult.candidates,
|
|
4902
6574
|
matches: indexedResult.matches,
|
|
4903
6575
|
truncated: indexedResult.truncated,
|
|
6576
|
+
development_warnings: grepDevelopmentWarnings,
|
|
4904
6577
|
};
|
|
4905
6578
|
return {
|
|
4906
6579
|
content: [
|
|
@@ -5020,6 +6693,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5020
6693
|
total_chars: result.totalChars,
|
|
5021
6694
|
truncated: result.truncated,
|
|
5022
6695
|
});
|
|
6696
|
+
const development_warnings = buildFileReadDevelopmentWarnings(resolved.dbFilePath, resolved.absPath, st);
|
|
5023
6697
|
const outputValue = {
|
|
5024
6698
|
ok: true,
|
|
5025
6699
|
file_path: resolved.dbFilePath,
|
|
@@ -5027,6 +6701,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5027
6701
|
returned_chars: result.returnedChars,
|
|
5028
6702
|
total_chars: result.totalChars,
|
|
5029
6703
|
truncated: result.truncated,
|
|
6704
|
+
development_warnings,
|
|
5030
6705
|
text: result.text,
|
|
5031
6706
|
};
|
|
5032
6707
|
return {
|
|
@@ -5156,6 +6831,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5156
6831
|
returned: result.returned,
|
|
5157
6832
|
truncated: result.truncated,
|
|
5158
6833
|
});
|
|
6834
|
+
const development_warnings = buildFileReadDevelopmentWarnings(resolved.dbFilePath, resolved.absPath, st);
|
|
5159
6835
|
const outputValue = {
|
|
5160
6836
|
ok: true,
|
|
5161
6837
|
file_path: resolved.dbFilePath,
|
|
@@ -5163,6 +6839,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5163
6839
|
to_line: toLine,
|
|
5164
6840
|
returned: result.returned,
|
|
5165
6841
|
truncated: result.truncated,
|
|
6842
|
+
development_warnings,
|
|
5166
6843
|
text: result.text,
|
|
5167
6844
|
};
|
|
5168
6845
|
return {
|
|
@@ -5181,12 +6858,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5181
6858
|
const like = `%${escaped}%`;
|
|
5182
6859
|
const rows = searchSymbolsStmt.all(like, like, q, like, 250);
|
|
5183
6860
|
const filtered = rows.filter((r) => !shouldIgnoreDbFilePath(r.file_path)).slice(0, 50);
|
|
6861
|
+
const development_warnings = buildMatchedFileDevelopmentWarnings(filtered.map((m) => m.file_path));
|
|
5184
6862
|
logActivity("query_codebase", {
|
|
5185
6863
|
query: q,
|
|
5186
6864
|
matches: filtered.length,
|
|
6865
|
+
development_warnings: development_warnings.length,
|
|
5187
6866
|
sample: filtered.slice(0, 10).map((m) => ({ name: m.name, type: m.type, file_path: m.file_path })),
|
|
5188
6867
|
});
|
|
5189
|
-
const outputValue = { ok: true, query: q, matches: filtered };
|
|
6868
|
+
const outputValue = { ok: true, query: q, matches: filtered, development_warnings };
|
|
5190
6869
|
return {
|
|
5191
6870
|
content: [
|
|
5192
6871
|
{
|