@shahmilsaari/memory-core 1.0.22 → 1.0.25
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 +249 -458
- package/dist/{chunk-UZDALJVQ.js → chunk-35ZWQFTO.js} +2594 -976
- package/dist/cli.js +471 -1582
- package/dist/dashboard/assets/index-B7gd4JQc.js +2 -0
- package/dist/dashboard/assets/{index-DXXHB1Ik.css → index-CHgjllWU.css} +1 -1
- package/dist/dashboard/index.html +2 -2
- package/dist/{dashboard-server-VOT2ZRVN.js → dashboard-server-SSYZLQKB.js} +68 -5
- package/package.json +1 -1
- package/dist/dashboard/assets/index-BRqvIBnm.js +0 -2
|
@@ -157,7 +157,12 @@ async function callOllama(cfg, messages, options = {}) {
|
|
|
157
157
|
throw new Error(body);
|
|
158
158
|
}
|
|
159
159
|
const data = await res.json();
|
|
160
|
-
|
|
160
|
+
const inputTokens = data.prompt_eval_count ?? 0;
|
|
161
|
+
const outputTokens = data.eval_count ?? 0;
|
|
162
|
+
return {
|
|
163
|
+
content: data.message.content.trim(),
|
|
164
|
+
usage: inputTokens || outputTokens ? { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens } : void 0
|
|
165
|
+
};
|
|
161
166
|
}
|
|
162
167
|
async function callOpenAICompat(cfg, messages, options = {}) {
|
|
163
168
|
const base = (cfg.baseUrl ?? "").replace(/\/$/, "") || "https://api.openai.com/v1";
|
|
@@ -176,7 +181,11 @@ async function callOpenAICompat(cfg, messages, options = {}) {
|
|
|
176
181
|
});
|
|
177
182
|
if (!res.ok) throw new Error(`OpenAI API error ${res.status}: ${await res.text()}`);
|
|
178
183
|
const data = await res.json();
|
|
179
|
-
|
|
184
|
+
const u = data.usage;
|
|
185
|
+
return {
|
|
186
|
+
content: data.choices[0].message.content.trim(),
|
|
187
|
+
usage: u ? { inputTokens: u.prompt_tokens, outputTokens: u.completion_tokens, totalTokens: u.total_tokens } : void 0
|
|
188
|
+
};
|
|
180
189
|
}
|
|
181
190
|
async function callAnthropic(cfg, messages, options = {}) {
|
|
182
191
|
const system = messages.find((m) => m.role === "system")?.content ?? "";
|
|
@@ -198,7 +207,11 @@ async function callAnthropic(cfg, messages, options = {}) {
|
|
|
198
207
|
});
|
|
199
208
|
if (!res.ok) throw new Error(`Anthropic API error ${res.status}: ${await res.text()}`);
|
|
200
209
|
const data = await res.json();
|
|
201
|
-
|
|
210
|
+
const u = data.usage;
|
|
211
|
+
return {
|
|
212
|
+
content: data.content[0].text.trim(),
|
|
213
|
+
usage: u ? { inputTokens: u.input_tokens, outputTokens: u.output_tokens, totalTokens: u.input_tokens + u.output_tokens } : void 0
|
|
214
|
+
};
|
|
202
215
|
}
|
|
203
216
|
async function callMiniMax(cfg, messages, options = {}) {
|
|
204
217
|
const res = await fetch("https://api.minimax.io/v1/chat/completions", {
|
|
@@ -216,7 +229,11 @@ async function callMiniMax(cfg, messages, options = {}) {
|
|
|
216
229
|
});
|
|
217
230
|
if (!res.ok) throw new Error(`MiniMax API error ${res.status}: ${await res.text()}`);
|
|
218
231
|
const data = await res.json();
|
|
219
|
-
|
|
232
|
+
const u = data.usage;
|
|
233
|
+
return {
|
|
234
|
+
content: data.choices[0].message.content.trim(),
|
|
235
|
+
usage: u ? { inputTokens: u.prompt_tokens, outputTokens: u.completion_tokens, totalTokens: u.total_tokens } : void 0
|
|
236
|
+
};
|
|
220
237
|
}
|
|
221
238
|
async function callChatModel(messages, options = {}) {
|
|
222
239
|
const cfg = getChatConfig();
|
|
@@ -1008,16 +1025,180 @@ var seeds = [
|
|
|
1008
1025
|
{ type: "rule", scope: "global", architecture: "svelte", title: "Avoid options API style \u2014 runes only", content: "Do not use the Svelte 4 options-style patterns (export let, $: reactive statements, $store subscriptions) in new Svelte 5 components. Use runes throughout.", reason: "Mixing the two reactivity systems in the same codebase creates two mental models, confuses new developers, and makes future migrations harder. Svelte 5 runes supersede every Svelte 4 pattern.", tags: ["svelte", "runes", "anti-pattern"] }
|
|
1009
1026
|
];
|
|
1010
1027
|
|
|
1011
|
-
// src/
|
|
1012
|
-
import {
|
|
1013
|
-
import {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1028
|
+
// src/memory-file.ts
|
|
1029
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
1030
|
+
import { join as join3 } from "path";
|
|
1031
|
+
var MEMORY_FILE = "memories.json";
|
|
1032
|
+
function toPortableMemory(memory) {
|
|
1033
|
+
return {
|
|
1034
|
+
type: memory.type,
|
|
1035
|
+
scope: memory.scope,
|
|
1036
|
+
architecture: memory.architecture,
|
|
1037
|
+
projectName: memory.project_name,
|
|
1038
|
+
title: memory.title,
|
|
1039
|
+
content: memory.content,
|
|
1040
|
+
reason: memory.reason,
|
|
1041
|
+
context: memory.context,
|
|
1042
|
+
tags: memory.tags ?? []
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function normalizeStringArray(value) {
|
|
1046
|
+
if (!Array.isArray(value)) return void 0;
|
|
1047
|
+
const entries = value.filter((entry) => typeof entry === "string" && entry.trim() !== "");
|
|
1048
|
+
return entries.length ? entries : void 0;
|
|
1049
|
+
}
|
|
1050
|
+
function parseContext(value) {
|
|
1051
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1052
|
+
const record = value;
|
|
1053
|
+
const context = {};
|
|
1054
|
+
const appliesTo = normalizeStringArray(record.appliesTo);
|
|
1055
|
+
const avoidWhen = normalizeStringArray(record.avoidWhen);
|
|
1056
|
+
const examples = normalizeStringArray(record.examples);
|
|
1057
|
+
if (appliesTo) context.appliesTo = appliesTo;
|
|
1058
|
+
if (avoidWhen) context.avoidWhen = avoidWhen;
|
|
1059
|
+
if (examples) context.examples = examples;
|
|
1060
|
+
if (typeof record.source === "string" && record.source.trim() !== "") context.source = record.source;
|
|
1061
|
+
return Object.keys(context).length ? context : void 0;
|
|
1062
|
+
}
|
|
1063
|
+
function writeMemoryFile(memories, cwd = process.cwd()) {
|
|
1064
|
+
const path = join3(cwd, MEMORY_FILE);
|
|
1065
|
+
writeFileSync(path, JSON.stringify(memories, null, 2) + "\n", "utf-8");
|
|
1066
|
+
return path;
|
|
1067
|
+
}
|
|
1068
|
+
function readMemoryFile(cwd = process.cwd()) {
|
|
1069
|
+
const path = join3(cwd, MEMORY_FILE);
|
|
1070
|
+
if (!existsSync3(path)) {
|
|
1071
|
+
throw new Error(`${MEMORY_FILE} not found. Run: memory-core export`);
|
|
1072
|
+
}
|
|
1073
|
+
return parseMemoryFile(readFileSync2(path, "utf-8"));
|
|
1074
|
+
}
|
|
1075
|
+
async function readMemoryFileFromUrl(url) {
|
|
1076
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(15e3) });
|
|
1077
|
+
if (!res.ok) throw new Error(`Failed to download ${url}: HTTP ${res.status}`);
|
|
1078
|
+
return parseMemoryFile(await res.text());
|
|
1079
|
+
}
|
|
1080
|
+
function parseMemoryFile(raw) {
|
|
1081
|
+
const parsed = JSON.parse(raw);
|
|
1082
|
+
if (!Array.isArray(parsed)) {
|
|
1083
|
+
throw new Error(`${MEMORY_FILE} must be a JSON array`);
|
|
1084
|
+
}
|
|
1085
|
+
return parsed.map((item, index) => {
|
|
1086
|
+
if (!item || typeof item !== "object") {
|
|
1087
|
+
throw new Error(`Memory at index ${index} must be an object`);
|
|
1088
|
+
}
|
|
1089
|
+
const record = item;
|
|
1090
|
+
if (typeof record.content !== "string" || record.content.trim() === "") {
|
|
1091
|
+
throw new Error(`Memory at index ${index} is missing content`);
|
|
1092
|
+
}
|
|
1093
|
+
return {
|
|
1094
|
+
type: typeof record.type === "string" ? record.type : "rule",
|
|
1095
|
+
scope: typeof record.scope === "string" ? record.scope : "project",
|
|
1096
|
+
architecture: typeof record.architecture === "string" ? record.architecture : void 0,
|
|
1097
|
+
projectName: typeof record.projectName === "string" ? record.projectName : void 0,
|
|
1098
|
+
title: typeof record.title === "string" ? record.title : void 0,
|
|
1099
|
+
content: record.content,
|
|
1100
|
+
reason: typeof record.reason === "string" ? record.reason : void 0,
|
|
1101
|
+
context: parseContext(record.context),
|
|
1102
|
+
tags: Array.isArray(record.tags) ? record.tags.filter((tag) => typeof tag === "string") : []
|
|
1103
|
+
};
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/modules/rule-engine/infrastructure/schema-violations.ts
|
|
1108
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
1109
|
+
import { join as join4 } from "path";
|
|
1110
|
+
function parseSchemaRule(content) {
|
|
1111
|
+
try {
|
|
1112
|
+
const parsed = JSON.parse(content);
|
|
1113
|
+
if (parsed !== null && typeof parsed === "object" && "tsFile" in parsed && "goFile" in parsed && typeof parsed.tsFile === "string" && typeof parsed.goFile === "string") {
|
|
1114
|
+
return parsed;
|
|
1115
|
+
}
|
|
1116
|
+
} catch {
|
|
1117
|
+
}
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
function extractTsFields(source) {
|
|
1121
|
+
const fields = [];
|
|
1122
|
+
const bodyMatch = source.match(/(?:interface|type)\s+\w+(?:<[^>]*>)?\s*(?:=\s*)?\{([^}]+)\}/s);
|
|
1123
|
+
if (!bodyMatch) return fields;
|
|
1124
|
+
for (const line of bodyMatch[1].split("\n")) {
|
|
1125
|
+
const match = line.match(/^\s*(\w+)\??:/);
|
|
1126
|
+
if (match) fields.push(match[1]);
|
|
1127
|
+
}
|
|
1128
|
+
return fields;
|
|
1129
|
+
}
|
|
1130
|
+
function extractGoFields(source) {
|
|
1131
|
+
const fields = [];
|
|
1132
|
+
const bodyMatch = source.match(/type\s+\w+\s+struct\s*\{([^}]+)\}/s);
|
|
1133
|
+
if (!bodyMatch) return fields;
|
|
1134
|
+
for (const line of bodyMatch[1].split("\n")) {
|
|
1135
|
+
const jsonTag = line.match(/json:"([^",\s]+)/);
|
|
1136
|
+
if (jsonTag) {
|
|
1137
|
+
if (jsonTag[1] !== "-") fields.push(jsonTag[1]);
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
const fieldMatch = line.match(/^\s+([A-Z]\w*)/);
|
|
1141
|
+
if (fieldMatch) {
|
|
1142
|
+
const name = fieldMatch[1];
|
|
1143
|
+
fields.push(name.charAt(0).toLowerCase() + name.slice(1));
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return fields;
|
|
1147
|
+
}
|
|
1148
|
+
async function findSchemaViolations(opts) {
|
|
1149
|
+
if (!opts.memoryEngine) return [];
|
|
1150
|
+
let schemaMemories;
|
|
1151
|
+
try {
|
|
1152
|
+
schemaMemories = await opts.memoryEngine.list({ type: "schema", limit: 100 });
|
|
1153
|
+
} catch {
|
|
1154
|
+
return [];
|
|
1155
|
+
}
|
|
1156
|
+
const violations = [];
|
|
1157
|
+
for (const memory of schemaMemories) {
|
|
1158
|
+
const rule = parseSchemaRule(memory.content);
|
|
1159
|
+
if (!rule) continue;
|
|
1160
|
+
const tsPath = join4(opts.cwd, rule.tsFile);
|
|
1161
|
+
const goPath = join4(opts.cwd, rule.goFile);
|
|
1162
|
+
if (!existsSync4(tsPath) || !existsSync4(goPath)) continue;
|
|
1163
|
+
const tsSource = readFileSync3(tsPath, "utf-8");
|
|
1164
|
+
const goSource = readFileSync3(goPath, "utf-8");
|
|
1165
|
+
const tsFields = new Set(extractTsFields(tsSource));
|
|
1166
|
+
const goFields = new Set(extractGoFields(goSource));
|
|
1167
|
+
for (const field of goFields) {
|
|
1168
|
+
if (!tsFields.has(field)) {
|
|
1169
|
+
violations.push({
|
|
1170
|
+
rule: `Schema alignment: ${rule.tsFile} must match ${rule.goFile}`,
|
|
1171
|
+
file: rule.tsFile,
|
|
1172
|
+
issue: `Go field "${field}" missing in TypeScript file`,
|
|
1173
|
+
suggestion: `Add "${field}" to the TypeScript interface/type in ${rule.tsFile}`,
|
|
1174
|
+
reason: "Schema drift between TypeScript and Go causes runtime mismatches"
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
for (const field of tsFields) {
|
|
1179
|
+
if (!goFields.has(field)) {
|
|
1180
|
+
violations.push({
|
|
1181
|
+
rule: `Schema alignment: ${rule.tsFile} must match ${rule.goFile}`,
|
|
1182
|
+
file: rule.goFile,
|
|
1183
|
+
issue: `TypeScript field "${field}" missing in Go struct`,
|
|
1184
|
+
suggestion: `Add "${field}" to the Go struct in ${rule.goFile}`,
|
|
1185
|
+
reason: "Schema drift between TypeScript and Go causes runtime mismatches"
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return violations;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// src/hook.ts
|
|
1194
|
+
import { execSync, spawnSync as spawnSync2 } from "child_process";
|
|
1195
|
+
import { writeFileSync as writeFileSync5, existsSync as existsSync9, unlinkSync, readFileSync as readFileSync8, chmodSync, statSync as statSync3 } from "fs";
|
|
1196
|
+
import { join as join10 } from "path";
|
|
1197
|
+
import chalk2 from "chalk";
|
|
1017
1198
|
|
|
1018
1199
|
// src/generator.ts
|
|
1019
|
-
import { readFileSync as
|
|
1020
|
-
import { join as
|
|
1200
|
+
import { readFileSync as readFileSync7, readdirSync as readdirSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync8 } from "fs";
|
|
1201
|
+
import { join as join9, dirname as dirname4, basename } from "path";
|
|
1021
1202
|
import { fileURLToPath } from "url";
|
|
1022
1203
|
import Handlebars from "handlebars";
|
|
1023
1204
|
import yaml from "js-yaml";
|
|
@@ -1038,7 +1219,8 @@ var ChatLlmProvider = class {
|
|
|
1038
1219
|
return getChatProviderLabel();
|
|
1039
1220
|
}
|
|
1040
1221
|
async generateText(messages) {
|
|
1041
|
-
|
|
1222
|
+
const result = await callChatModel(messages);
|
|
1223
|
+
return result.content;
|
|
1042
1224
|
}
|
|
1043
1225
|
};
|
|
1044
1226
|
|
|
@@ -1125,9 +1307,9 @@ var PostgresMemoryRepository = class {
|
|
|
1125
1307
|
};
|
|
1126
1308
|
|
|
1127
1309
|
// src/infrastructure/persistence/filesystem/file-graph-repository.ts
|
|
1128
|
-
import { existsSync as
|
|
1129
|
-
import { dirname, join as
|
|
1130
|
-
var DEFAULT_FILE =
|
|
1310
|
+
import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1311
|
+
import { dirname, join as join5 } from "path";
|
|
1312
|
+
var DEFAULT_FILE = join5(".memory-core", "graph-snapshots.json");
|
|
1131
1313
|
function asPosix2(value) {
|
|
1132
1314
|
return value.replace(/\\/g, "/");
|
|
1133
1315
|
}
|
|
@@ -1166,11 +1348,11 @@ var FileGraphRepository = class {
|
|
|
1166
1348
|
}
|
|
1167
1349
|
readData() {
|
|
1168
1350
|
const filePath = this.absolutePath();
|
|
1169
|
-
if (!
|
|
1351
|
+
if (!existsSync5(filePath)) {
|
|
1170
1352
|
return { version: 1, snapshots: [] };
|
|
1171
1353
|
}
|
|
1172
1354
|
try {
|
|
1173
|
-
const parsed = JSON.parse(
|
|
1355
|
+
const parsed = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
1174
1356
|
if (!Array.isArray(parsed.snapshots)) {
|
|
1175
1357
|
return { version: 1, snapshots: [] };
|
|
1176
1358
|
}
|
|
@@ -1187,11 +1369,11 @@ var FileGraphRepository = class {
|
|
|
1187
1369
|
writeData(data) {
|
|
1188
1370
|
const filePath = this.absolutePath();
|
|
1189
1371
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
1190
|
-
|
|
1372
|
+
writeFileSync2(filePath, `${JSON.stringify(data, null, 2)}
|
|
1191
1373
|
`, "utf-8");
|
|
1192
1374
|
}
|
|
1193
1375
|
absolutePath() {
|
|
1194
|
-
return
|
|
1376
|
+
return join5(this.rootPath, this.relativeFilePath);
|
|
1195
1377
|
}
|
|
1196
1378
|
};
|
|
1197
1379
|
|
|
@@ -1236,172 +1418,20 @@ var ResilientGraphRepository = class {
|
|
|
1236
1418
|
}
|
|
1237
1419
|
};
|
|
1238
1420
|
|
|
1239
|
-
// src/
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
debug: options.debug,
|
|
1246
|
-
scanOnStart: options.scanOnStart
|
|
1247
|
-
});
|
|
1248
|
-
}
|
|
1249
|
-
async scan(options = {}) {
|
|
1250
|
-
return scanFiles({
|
|
1251
|
-
path: options.path,
|
|
1252
|
-
verbose: options.verbose,
|
|
1253
|
-
debug: options.debug
|
|
1254
|
-
});
|
|
1255
|
-
}
|
|
1256
|
-
};
|
|
1257
|
-
|
|
1258
|
-
// src/infrastructure/events/in-memory-event-bus.ts
|
|
1259
|
-
var InMemoryEventBus = class {
|
|
1260
|
-
handlers = /* @__PURE__ */ new Map();
|
|
1261
|
-
async publish(event) {
|
|
1262
|
-
const handlers = this.handlers.get(event.type);
|
|
1263
|
-
if (!handlers || handlers.size === 0) return;
|
|
1264
|
-
for (const handler of handlers) {
|
|
1265
|
-
await handler(event);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
subscribe(eventType, handler) {
|
|
1269
|
-
const existing = this.handlers.get(eventType) ?? /* @__PURE__ */ new Set();
|
|
1270
|
-
existing.add(handler);
|
|
1271
|
-
this.handlers.set(eventType, existing);
|
|
1272
|
-
return () => {
|
|
1273
|
-
const current = this.handlers.get(eventType);
|
|
1274
|
-
if (!current) return;
|
|
1275
|
-
current.delete(handler);
|
|
1276
|
-
if (current.size === 0) {
|
|
1277
|
-
this.handlers.delete(eventType);
|
|
1278
|
-
}
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
};
|
|
1282
|
-
|
|
1283
|
-
// src/modules/memory-engine/application/memory-engine-service.ts
|
|
1284
|
-
var MemoryEngineService = class {
|
|
1285
|
-
constructor(memoryRepository, embeddingProvider) {
|
|
1286
|
-
this.memoryRepository = memoryRepository;
|
|
1287
|
-
this.embeddingProvider = embeddingProvider;
|
|
1288
|
-
}
|
|
1289
|
-
memoryRepository;
|
|
1290
|
-
embeddingProvider;
|
|
1291
|
-
withReason(input) {
|
|
1292
|
-
const reason = input.reason?.trim();
|
|
1293
|
-
return {
|
|
1294
|
-
...input,
|
|
1295
|
-
reason: reason || `Captured as a ${input.type} memory because it should be remembered: ${input.content}`
|
|
1296
|
-
};
|
|
1297
|
-
}
|
|
1298
|
-
async remember(input) {
|
|
1299
|
-
const normalized = this.withReason(input);
|
|
1300
|
-
const embedding = await this.embeddingProvider.embed(normalized.content);
|
|
1301
|
-
return this.memoryRepository.upsert({ ...normalized, embedding });
|
|
1302
|
-
}
|
|
1303
|
-
async rememberForce(input) {
|
|
1304
|
-
const normalized = this.withReason(input);
|
|
1305
|
-
const embedding = await this.embeddingProvider.embed(normalized.content);
|
|
1306
|
-
await this.memoryRepository.save({ ...normalized, embedding });
|
|
1307
|
-
}
|
|
1308
|
-
async list(filters = {}) {
|
|
1309
|
-
return this.memoryRepository.list(filters);
|
|
1310
|
-
}
|
|
1311
|
-
async getById(id) {
|
|
1312
|
-
return this.memoryRepository.getById(id);
|
|
1313
|
-
}
|
|
1314
|
-
async removeById(id) {
|
|
1315
|
-
return this.memoryRepository.removeById(id);
|
|
1316
|
-
}
|
|
1317
|
-
async removeMany(filters) {
|
|
1318
|
-
return this.memoryRepository.removeMany(filters);
|
|
1319
|
-
}
|
|
1320
|
-
async update(id, patch) {
|
|
1321
|
-
const current = await this.memoryRepository.getById(id);
|
|
1322
|
-
if (!current) return null;
|
|
1323
|
-
const content = patch.content ?? current.content;
|
|
1324
|
-
const embedding = content !== current.content ? await this.embeddingProvider.embed(content) : void 0;
|
|
1325
|
-
return this.memoryRepository.update(id, { ...patch, embedding });
|
|
1326
|
-
}
|
|
1327
|
-
async search(query, architectures, limit = 10) {
|
|
1328
|
-
const embedding = await this.embeddingProvider.embed(query);
|
|
1329
|
-
return this.memoryRepository.searchByEmbedding({ embedding, architectures, limit });
|
|
1330
|
-
}
|
|
1331
|
-
};
|
|
1332
|
-
|
|
1333
|
-
// src/modules/retrieval-engine/application/retrieval-engine-service.ts
|
|
1334
|
-
var RetrievalEngineService = class {
|
|
1335
|
-
constructor(embeddingProvider, memoryRepository) {
|
|
1336
|
-
this.embeddingProvider = embeddingProvider;
|
|
1337
|
-
this.memoryRepository = memoryRepository;
|
|
1338
|
-
}
|
|
1339
|
-
embeddingProvider;
|
|
1340
|
-
memoryRepository;
|
|
1341
|
-
async retrieve(query) {
|
|
1342
|
-
const limit = query.limit ?? 10;
|
|
1343
|
-
const embedding = await this.embeddingProvider.embed(query.text);
|
|
1344
|
-
const vectorMatches = await this.memoryRepository.searchByEmbedding({
|
|
1345
|
-
embedding,
|
|
1346
|
-
architectures: query.architectures,
|
|
1347
|
-
limit
|
|
1348
|
-
});
|
|
1349
|
-
const lexicalMatches = (await this.memoryRepository.list({
|
|
1350
|
-
architecture: query.architectures,
|
|
1351
|
-
includeGlobal: true,
|
|
1352
|
-
limit: Math.max(limit * 2, 20)
|
|
1353
|
-
})).filter((memory) => {
|
|
1354
|
-
const haystack = `${memory.content} ${memory.title ?? ""} ${(memory.tags ?? []).join(" ")}`.toLowerCase();
|
|
1355
|
-
return haystack.includes(query.text.toLowerCase());
|
|
1356
|
-
});
|
|
1357
|
-
const merged = [...vectorMatches];
|
|
1358
|
-
for (const memory of lexicalMatches) {
|
|
1359
|
-
if (!merged.some((candidate) => candidate.id === memory.id)) {
|
|
1360
|
-
merged.push(memory);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
return { items: merged.slice(0, limit) };
|
|
1364
|
-
}
|
|
1365
|
-
};
|
|
1366
|
-
|
|
1367
|
-
// src/modules/rule-engine/application/rule-engine-service.ts
|
|
1368
|
-
var RuleEngineService = class {
|
|
1369
|
-
constructor(rules, llmProvider) {
|
|
1370
|
-
this.rules = rules;
|
|
1371
|
-
this.llmProvider = llmProvider;
|
|
1372
|
-
}
|
|
1373
|
-
rules;
|
|
1374
|
-
llmProvider;
|
|
1375
|
-
async evaluate(input, includeExplanation = false) {
|
|
1376
|
-
const allViolations = [];
|
|
1377
|
-
for (const rule of this.rules) {
|
|
1378
|
-
allViolations.push(...await rule.evaluate(input));
|
|
1379
|
-
}
|
|
1380
|
-
if (!includeExplanation || allViolations.length === 0 || !this.llmProvider) {
|
|
1381
|
-
return { violations: allViolations };
|
|
1382
|
-
}
|
|
1383
|
-
const explanation = await this.llmProvider.generateText([
|
|
1384
|
-
{
|
|
1385
|
-
role: "system",
|
|
1386
|
-
content: "Summarize architecture violations and provide practical fixes in concise bullet points."
|
|
1387
|
-
},
|
|
1388
|
-
{
|
|
1389
|
-
role: "user",
|
|
1390
|
-
content: JSON.stringify(allViolations, null, 2)
|
|
1391
|
-
}
|
|
1392
|
-
]);
|
|
1393
|
-
return { violations: allViolations, explanation };
|
|
1394
|
-
}
|
|
1395
|
-
};
|
|
1421
|
+
// src/watcher.ts
|
|
1422
|
+
import { watch } from "chokidar";
|
|
1423
|
+
import { spawnSync } from "child_process";
|
|
1424
|
+
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync6, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1425
|
+
import { dirname as dirname3, join as join7, relative, resolve as resolve3, sep } from "path";
|
|
1426
|
+
import chalk from "chalk";
|
|
1396
1427
|
|
|
1397
|
-
// src/modules/
|
|
1398
|
-
import {
|
|
1399
|
-
import { join as join5, relative, resolve as resolve2 } from "path";
|
|
1428
|
+
// src/modules/rule-engine/infrastructure/ast-deterministic-violations.ts
|
|
1429
|
+
import { resolve as resolve2 } from "path";
|
|
1400
1430
|
|
|
1401
1431
|
// src/infrastructure/ast/import-analysis.ts
|
|
1402
1432
|
import { builtinModules } from "module";
|
|
1403
|
-
import { existsSync as
|
|
1404
|
-
import { dirname as dirname2, extname, isAbsolute, join as
|
|
1433
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
1434
|
+
import { dirname as dirname2, extname, isAbsolute, join as join6, normalize, resolve } from "path";
|
|
1405
1435
|
var SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
1406
1436
|
var NODE_BUILTINS = /* @__PURE__ */ new Set([...builtinModules, ...builtinModules.map((entry) => `node:${entry}`)]);
|
|
1407
1437
|
function countNewlines(value) {
|
|
@@ -1432,15 +1462,15 @@ function parseImports(source) {
|
|
|
1432
1462
|
return imports;
|
|
1433
1463
|
}
|
|
1434
1464
|
function tryResolveFilePath(candidate) {
|
|
1435
|
-
if (
|
|
1465
|
+
if (existsSync6(candidate)) return normalize(candidate);
|
|
1436
1466
|
if (!extname(candidate)) {
|
|
1437
1467
|
for (const ext of SOURCE_EXTENSIONS) {
|
|
1438
1468
|
const withExt = `${candidate}${ext}`;
|
|
1439
|
-
if (
|
|
1469
|
+
if (existsSync6(withExt)) return normalize(withExt);
|
|
1440
1470
|
}
|
|
1441
1471
|
for (const ext of SOURCE_EXTENSIONS) {
|
|
1442
|
-
const indexFile =
|
|
1443
|
-
if (
|
|
1472
|
+
const indexFile = join6(candidate, `index${ext}`);
|
|
1473
|
+
if (existsSync6(indexFile)) return normalize(indexFile);
|
|
1444
1474
|
}
|
|
1445
1475
|
}
|
|
1446
1476
|
return void 0;
|
|
@@ -1464,8 +1494,8 @@ function resolveImportPath(fromFile, specifier, cwd = process.cwd()) {
|
|
|
1464
1494
|
return void 0;
|
|
1465
1495
|
}
|
|
1466
1496
|
function collectResolvedImports(filePath, cwd = process.cwd()) {
|
|
1467
|
-
if (!
|
|
1468
|
-
const source =
|
|
1497
|
+
if (!existsSync6(filePath)) return [];
|
|
1498
|
+
const source = readFileSync5(filePath, "utf-8");
|
|
1469
1499
|
const imports = parseImports(source);
|
|
1470
1500
|
return imports.map((entry) => {
|
|
1471
1501
|
if (looksLikeExternal(entry.specifier) || NODE_BUILTINS.has(entry.specifier)) {
|
|
@@ -1571,34 +1601,1132 @@ function isExternalFrameworkSpecifier(specifier) {
|
|
|
1571
1601
|
return frameworkPrefixes.some((prefix) => specifier === prefix || specifier.startsWith(`${prefix}/`));
|
|
1572
1602
|
}
|
|
1573
1603
|
|
|
1574
|
-
// src/modules/
|
|
1575
|
-
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
1576
|
-
var IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", ".memory-core"]);
|
|
1577
|
-
function isSourceFile(pathValue) {
|
|
1578
|
-
const extension = pathValue.slice(pathValue.lastIndexOf("."));
|
|
1579
|
-
return SOURCE_EXTENSIONS2.has(extension);
|
|
1580
|
-
}
|
|
1604
|
+
// src/modules/rule-engine/infrastructure/ast-deterministic-violations.ts
|
|
1581
1605
|
function asPosix4(value) {
|
|
1582
1606
|
return value.replace(/\\/g, "/");
|
|
1583
1607
|
}
|
|
1584
|
-
function
|
|
1585
|
-
|
|
1608
|
+
function hasPath(value, pathSegment) {
|
|
1609
|
+
const normalized = asPosix4(value);
|
|
1610
|
+
const trimmed = pathSegment.startsWith("/") ? pathSegment.slice(1) : pathSegment;
|
|
1611
|
+
return normalized.includes(pathSegment) || normalized.includes(trimmed);
|
|
1586
1612
|
}
|
|
1587
|
-
function
|
|
1588
|
-
|
|
1613
|
+
function isLegacyOrCompatibilitySpecifier(specifier) {
|
|
1614
|
+
const normalized = asPosix4(specifier);
|
|
1615
|
+
return normalized.includes("/compatibility/") || normalized.includes("compatibility/") || normalized.includes("legacy-");
|
|
1589
1616
|
}
|
|
1590
|
-
function
|
|
1591
|
-
|
|
1592
|
-
|
|
1617
|
+
function isLegacyOrCompatibilityPath(pathValue) {
|
|
1618
|
+
if (!pathValue) return false;
|
|
1619
|
+
const normalized = asPosix4(pathValue);
|
|
1620
|
+
return normalized.includes("/src/compatibility/") || normalized.includes("/legacy-");
|
|
1593
1621
|
}
|
|
1594
|
-
function
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1622
|
+
function moduleNameFromPath2(pathValue) {
|
|
1623
|
+
const match = asPosix4(pathValue).match(/src\/modules\/([^/]+)\//);
|
|
1624
|
+
return match?.[1];
|
|
1625
|
+
}
|
|
1626
|
+
function isModulePublicPath(pathValue) {
|
|
1627
|
+
const normalized = asPosix4(pathValue);
|
|
1628
|
+
return /src\/modules\/[^/]+\/(public|api)\//.test(normalized) || /src\/modules\/[^/]+\/index\.(ts|tsx|js|jsx|mjs|cjs)$/.test(normalized);
|
|
1629
|
+
}
|
|
1630
|
+
function detectCleanLayer(pathValue) {
|
|
1631
|
+
const normalized = asPosix4(pathValue);
|
|
1632
|
+
if (hasPath(normalized, "/src/domain/") || hasPath(normalized, "/src/core/domain/")) return "domain";
|
|
1633
|
+
if (hasPath(normalized, "/src/application/") || hasPath(normalized, "/src/core/application/")) return "application";
|
|
1634
|
+
if (hasPath(normalized, "/src/infrastructure/")) return "infrastructure";
|
|
1635
|
+
if (hasPath(normalized, "/src/interfaces/")) return "interface";
|
|
1636
|
+
return "unknown";
|
|
1637
|
+
}
|
|
1638
|
+
function detectHexLayer(pathValue) {
|
|
1639
|
+
const normalized = asPosix4(pathValue);
|
|
1640
|
+
if (hasPath(normalized, "/src/core/")) return "core";
|
|
1641
|
+
if (hasPath(normalized, "/src/adapters/inbound/")) return "adapter-inbound";
|
|
1642
|
+
if (hasPath(normalized, "/src/adapters/outbound/")) return "adapter-outbound";
|
|
1643
|
+
if (hasPath(normalized, "/src/adapters/")) return "adapter-other";
|
|
1644
|
+
return "unknown";
|
|
1645
|
+
}
|
|
1646
|
+
function activeArchitectures(config2, rules = []) {
|
|
1647
|
+
const names = /* @__PURE__ */ new Set();
|
|
1648
|
+
if (config2?.backendArchitecture) names.add(config2.backendArchitecture);
|
|
1649
|
+
if (config2?.frontendFramework) names.add(config2.frontendFramework);
|
|
1650
|
+
const text = rules.join("\n").toLowerCase();
|
|
1651
|
+
if (text.includes("modular monolith")) names.add("modular-monolith");
|
|
1652
|
+
if (text.includes("clean architecture")) names.add("clean-architecture");
|
|
1653
|
+
if (text.includes("hexagonal")) names.add("hexagonal");
|
|
1654
|
+
return names;
|
|
1655
|
+
}
|
|
1656
|
+
function pushUnique(target, incoming) {
|
|
1657
|
+
if (target.some(
|
|
1658
|
+
(entry) => entry.rule === incoming.rule && entry.file === incoming.file && entry.line === incoming.line && entry.issue === incoming.issue
|
|
1659
|
+
)) {
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
target.push(incoming);
|
|
1663
|
+
}
|
|
1664
|
+
function evaluateFile(file, options) {
|
|
1665
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1666
|
+
const rules = options.rules ?? [];
|
|
1667
|
+
const architectures = activeArchitectures(options.config, rules);
|
|
1668
|
+
const reasonLookup = options.reasonLookup ?? /* @__PURE__ */ new Map();
|
|
1669
|
+
const violations = [];
|
|
1670
|
+
const absFile = resolve2(cwd, file);
|
|
1671
|
+
const normalizedFile = asPosix4(file);
|
|
1672
|
+
const imports = collectResolvedImports(absFile, cwd);
|
|
1673
|
+
const fromCleanLayer = detectCleanLayer(normalizedFile);
|
|
1674
|
+
const fromHexLayer = detectHexLayer(normalizedFile);
|
|
1675
|
+
for (const imp of imports) {
|
|
1676
|
+
const target = imp.resolvedPath ? asPosix4(imp.resolvedPath) : void 0;
|
|
1677
|
+
if (isLegacyOrCompatibilitySpecifier(imp.specifier) || isLegacyOrCompatibilityPath(target)) {
|
|
1678
|
+
const rule = "Application code must not import compatibility or legacy adapter paths";
|
|
1679
|
+
pushUnique(violations, {
|
|
1680
|
+
rule,
|
|
1681
|
+
file,
|
|
1682
|
+
line: imp.line,
|
|
1683
|
+
issue: `Import references a removed migration path: ${imp.specifier}`,
|
|
1684
|
+
suggestion: "Import module services/ports from src/app, src/modules, src/shared/ports, or current infrastructure adapters.",
|
|
1685
|
+
reason: reasonLookup.get(rule)
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
if (architectures.has("modular-monolith")) {
|
|
1689
|
+
const fromModule = moduleNameFromPath2(normalizedFile);
|
|
1690
|
+
const toModule = target ? moduleNameFromPath2(target) : void 0;
|
|
1691
|
+
if (fromModule && toModule && target && fromModule !== toModule && !isModulePublicPath(target)) {
|
|
1692
|
+
const rule = "Modules communicate only through public interfaces or events \u2014 never by importing internals";
|
|
1693
|
+
pushUnique(violations, {
|
|
1694
|
+
rule,
|
|
1695
|
+
file,
|
|
1696
|
+
line: imp.line,
|
|
1697
|
+
issue: `Cross-module import from "${fromModule}" to private path in module "${toModule}"`,
|
|
1698
|
+
suggestion: `Expose a public API from src/modules/${toModule}/index.ts (or public/) and import through it.`,
|
|
1699
|
+
reason: reasonLookup.get(rule)
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (architectures.has("clean-architecture")) {
|
|
1704
|
+
const toCleanLayer = target ? detectCleanLayer(target) : "unknown";
|
|
1705
|
+
if (fromCleanLayer === "domain" && ["application", "infrastructure", "interface"].includes(toCleanLayer)) {
|
|
1706
|
+
const rule = "Entities encapsulate core business logic and have no external dependencies";
|
|
1707
|
+
pushUnique(violations, {
|
|
1708
|
+
rule,
|
|
1709
|
+
file,
|
|
1710
|
+
line: imp.line,
|
|
1711
|
+
issue: `Domain layer imports ${toCleanLayer} layer: ${imp.specifier}`,
|
|
1712
|
+
suggestion: "Keep domain isolated and move orchestration concerns into application layer.",
|
|
1713
|
+
reason: reasonLookup.get(rule)
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
if (fromCleanLayer === "application" && (toCleanLayer === "infrastructure" || toCleanLayer === "interface")) {
|
|
1717
|
+
const rule = "Infrastructure layer (DB, HTTP, queues) depends on application \u2014 never the reverse";
|
|
1718
|
+
pushUnique(violations, {
|
|
1719
|
+
rule,
|
|
1720
|
+
file,
|
|
1721
|
+
line: imp.line,
|
|
1722
|
+
issue: `Application layer imports ${toCleanLayer} layer: ${imp.specifier}`,
|
|
1723
|
+
suggestion: "Invert dependency via repository/port interface in application layer.",
|
|
1724
|
+
reason: reasonLookup.get(rule)
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
if (fromCleanLayer === "interface" && toCleanLayer === "infrastructure") {
|
|
1728
|
+
const rule = "Controllers must only validate input and delegate to use cases";
|
|
1729
|
+
pushUnique(violations, {
|
|
1730
|
+
rule,
|
|
1731
|
+
file,
|
|
1732
|
+
line: imp.line,
|
|
1733
|
+
issue: `Interface/controller layer imports infrastructure directly: ${imp.specifier}`,
|
|
1734
|
+
suggestion: "Delegate to application use cases instead of calling infrastructure directly.",
|
|
1735
|
+
reason: reasonLookup.get(rule)
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
if (fromCleanLayer === "domain" && imp.isExternal && isExternalFrameworkSpecifier(imp.specifier)) {
|
|
1739
|
+
const rule = "Domain layer must not import any framework or library code";
|
|
1740
|
+
pushUnique(violations, {
|
|
1741
|
+
rule,
|
|
1742
|
+
file,
|
|
1743
|
+
line: imp.line,
|
|
1744
|
+
issue: `Domain file imports framework package: ${imp.specifier}`,
|
|
1745
|
+
suggestion: "Keep domain pure. Move framework-specific logic to infrastructure/adapters.",
|
|
1746
|
+
reason: reasonLookup.get(rule)
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
if (architectures.has("hexagonal")) {
|
|
1751
|
+
const toHexLayer = target ? detectHexLayer(target) : "unknown";
|
|
1752
|
+
if (fromHexLayer === "core" && (toHexLayer === "adapter-inbound" || toHexLayer === "adapter-outbound" || toHexLayer === "adapter-other")) {
|
|
1753
|
+
const rule = "Direct imports of adapter code inside the core";
|
|
1754
|
+
pushUnique(violations, {
|
|
1755
|
+
rule,
|
|
1756
|
+
file,
|
|
1757
|
+
line: imp.line,
|
|
1758
|
+
issue: `Core imports adapter path directly: ${imp.specifier}`,
|
|
1759
|
+
suggestion: "Define a core port and resolve adapter at composition root.",
|
|
1760
|
+
reason: reasonLookup.get(rule)
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
const crossAdapterBoundary = fromHexLayer === "adapter-inbound" && (toHexLayer === "adapter-outbound" || toHexLayer === "adapter-other") || fromHexLayer === "adapter-outbound" && (toHexLayer === "adapter-inbound" || toHexLayer === "adapter-other");
|
|
1764
|
+
if (crossAdapterBoundary) {
|
|
1765
|
+
const rule = "Adapters implement ports \u2014 one adapter per external system (DB, HTTP, queue, etc.)";
|
|
1766
|
+
pushUnique(violations, {
|
|
1767
|
+
rule,
|
|
1768
|
+
file,
|
|
1769
|
+
line: imp.line,
|
|
1770
|
+
issue: `Adapter imports another adapter layer directly: ${imp.specifier}`,
|
|
1771
|
+
suggestion: "Route adapter collaboration through core ports/use-cases, not direct adapter imports.",
|
|
1772
|
+
reason: reasonLookup.get(rule)
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return violations;
|
|
1778
|
+
}
|
|
1779
|
+
function findAstDeterministicViolationsForFile(file, options = {}) {
|
|
1780
|
+
return evaluateFile(file, options);
|
|
1781
|
+
}
|
|
1782
|
+
function findAstDeterministicViolationsForDiff(diff, options = {}) {
|
|
1783
|
+
const files = parseChangedFilesFromDiff(diff).filter((file) => /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file));
|
|
1784
|
+
const violations = [];
|
|
1785
|
+
for (const file of files) {
|
|
1786
|
+
for (const violation of evaluateFile(file, options)) {
|
|
1787
|
+
pushUnique(violations, violation);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
const architectures = activeArchitectures(options.config, options.rules ?? []);
|
|
1791
|
+
if (architectures.has("modular-monolith")) {
|
|
1792
|
+
const edges = buildModuleDependencyEdges(files, options.cwd ?? process.cwd());
|
|
1793
|
+
const cycles = detectModuleCycles(edges);
|
|
1794
|
+
for (const cycle of cycles) {
|
|
1795
|
+
const representative = edges.find((edge) => edge.fromModule === cycle[0] && edge.toModule === cycle[1]);
|
|
1796
|
+
if (!representative) continue;
|
|
1797
|
+
const rule = "No circular dependencies between modules";
|
|
1798
|
+
pushUnique(violations, {
|
|
1799
|
+
rule,
|
|
1800
|
+
file: representative.file,
|
|
1801
|
+
line: representative.line,
|
|
1802
|
+
issue: `Module dependency cycle detected: ${cycle.join(" -> ")}`,
|
|
1803
|
+
suggestion: "Break the cycle by introducing a public port/event or moving shared logic into src/shared.",
|
|
1804
|
+
reason: options.reasonLookup?.get(rule)
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return violations;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// src/watcher.ts
|
|
1812
|
+
function getFileLines(filePath) {
|
|
1813
|
+
try {
|
|
1814
|
+
return readFileSync6(filePath, "utf-8").split("\n");
|
|
1815
|
+
} catch {
|
|
1816
|
+
return [];
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
function printCodeContext(filePath, line, contextLines = 2) {
|
|
1820
|
+
const lines = getFileLines(filePath);
|
|
1821
|
+
if (lines.length === 0) return;
|
|
1822
|
+
const start = Math.max(0, line - 1 - contextLines);
|
|
1823
|
+
const end = Math.min(lines.length - 1, line - 1 + contextLines);
|
|
1824
|
+
console.log(chalk.dim(" \u250C\u2500 code \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1825
|
+
for (let i = start; i <= end; i++) {
|
|
1826
|
+
const lineNum = String(i + 1).padStart(4, " ");
|
|
1827
|
+
const isViolation = i === line - 1;
|
|
1828
|
+
if (isViolation) {
|
|
1829
|
+
console.log(chalk.red(` \u2502 ${lineNum} \u25B6 ${lines[i]}`));
|
|
1830
|
+
} else {
|
|
1831
|
+
console.log(chalk.dim(` \u2502 ${lineNum} ${lines[i]}`));
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
console.log(chalk.dim(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1835
|
+
}
|
|
1836
|
+
function formatCodeContext(filePath, line, contextLines = 2) {
|
|
1837
|
+
const lines = getFileLines(filePath);
|
|
1838
|
+
if (lines.length === 0) return void 0;
|
|
1839
|
+
const start = Math.max(0, line - 1 - contextLines);
|
|
1840
|
+
const end = Math.min(lines.length - 1, line - 1 + contextLines);
|
|
1841
|
+
return Array.from({ length: end - start + 1 }, (_, index) => {
|
|
1842
|
+
const current = start + index;
|
|
1843
|
+
const lineNum = String(current + 1).padStart(4, " ");
|
|
1844
|
+
const marker = current === line - 1 ? ">" : " ";
|
|
1845
|
+
return `${lineNum} ${marker} ${lines[current]}`;
|
|
1846
|
+
}).join("\n");
|
|
1847
|
+
}
|
|
1848
|
+
var SOURCE_EXTENSIONS2 = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
|
|
1849
|
+
var reasonMap = new Map(
|
|
1850
|
+
seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
|
|
1851
|
+
);
|
|
1852
|
+
function findProjectRoot(startPath) {
|
|
1853
|
+
let current = resolve3(startPath);
|
|
1854
|
+
while (true) {
|
|
1855
|
+
if (existsSync7(join7(current, ".memory-core.json"))) return current;
|
|
1856
|
+
const parent = dirname3(current);
|
|
1857
|
+
if (parent === current) return null;
|
|
1858
|
+
current = parent;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
function resolveWatchPaths(pathOption, projectRootOption) {
|
|
1862
|
+
if (projectRootOption) {
|
|
1863
|
+
const projectRoot2 = resolve3(projectRootOption);
|
|
1864
|
+
return {
|
|
1865
|
+
projectRoot: projectRoot2,
|
|
1866
|
+
watchPath: resolve3(projectRoot2, pathOption ?? ".")
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
const cwdRoot = resolve3(process.cwd());
|
|
1870
|
+
const watchPath = resolve3(cwdRoot, pathOption ?? ".");
|
|
1871
|
+
const projectRoot = findProjectRoot(watchPath) ?? findProjectRoot(cwdRoot) ?? cwdRoot;
|
|
1872
|
+
return { projectRoot, watchPath };
|
|
1873
|
+
}
|
|
1874
|
+
function readStatsFile(statsPath) {
|
|
1875
|
+
if (!existsSync7(statsPath)) return { rules: {}, files: {} };
|
|
1876
|
+
try {
|
|
1877
|
+
return JSON.parse(readFileSync6(statsPath, "utf-8"));
|
|
1878
|
+
} catch {
|
|
1879
|
+
return { rules: {}, files: {} };
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function rebuildLiveCounters(byFile) {
|
|
1883
|
+
const rules = {};
|
|
1884
|
+
const files = {};
|
|
1885
|
+
for (const [file, violations] of Object.entries(byFile)) {
|
|
1886
|
+
if (!Array.isArray(violations) || violations.length === 0) continue;
|
|
1887
|
+
files[file] = violations.length;
|
|
1888
|
+
for (const violation of violations) {
|
|
1889
|
+
rules[violation.rule] = (rules[violation.rule] ?? 0) + 1;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return { rules, files };
|
|
1893
|
+
}
|
|
1894
|
+
function resetLiveStats(cwd) {
|
|
1895
|
+
const statsPath = join7(cwd, ".memory-core-stats.json");
|
|
1896
|
+
const stats = readStatsFile(statsPath);
|
|
1897
|
+
stats.rules ??= {};
|
|
1898
|
+
stats.files ??= {};
|
|
1899
|
+
stats.live = {
|
|
1900
|
+
rules: {},
|
|
1901
|
+
files: {},
|
|
1902
|
+
byFile: {}
|
|
1903
|
+
};
|
|
1904
|
+
writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
1905
|
+
}
|
|
1906
|
+
function recordWatchResult(cwd, file, violations) {
|
|
1907
|
+
const statsPath = join7(cwd, ".memory-core-stats.json");
|
|
1908
|
+
const stats = readStatsFile(statsPath);
|
|
1909
|
+
stats.rules ??= {};
|
|
1910
|
+
stats.files ??= {};
|
|
1911
|
+
stats.live ??= { rules: {}, files: {}, byFile: {} };
|
|
1912
|
+
stats.live.byFile ??= {};
|
|
1913
|
+
if (violations.length === 0) {
|
|
1914
|
+
delete stats.live.byFile[file];
|
|
1915
|
+
} else {
|
|
1916
|
+
stats.live.byFile[file] = violations;
|
|
1917
|
+
}
|
|
1918
|
+
const live = rebuildLiveCounters(stats.live.byFile);
|
|
1919
|
+
stats.live.rules = live.rules;
|
|
1920
|
+
stats.live.files = live.files;
|
|
1921
|
+
for (const violation of violations) {
|
|
1922
|
+
stats.rules[violation.rule] = (stats.rules[violation.rule] ?? 0) + 1;
|
|
1923
|
+
if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
|
|
1924
|
+
}
|
|
1925
|
+
if (violations.length > 0) {
|
|
1926
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1927
|
+
const recent = violations.map((violation) => ({ ...violation, timestamp, source: "watch" }));
|
|
1928
|
+
stats.recentViolations = [...recent, ...stats.recentViolations ?? []].slice(0, 50);
|
|
1929
|
+
}
|
|
1930
|
+
writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
1931
|
+
}
|
|
1932
|
+
function loadConfig(cwd) {
|
|
1933
|
+
const configPath = join7(cwd, ".memory-core.json");
|
|
1934
|
+
if (!existsSync7(configPath)) return null;
|
|
1935
|
+
try {
|
|
1936
|
+
return JSON.parse(readFileSync6(configPath, "utf-8"));
|
|
1937
|
+
} catch {
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
function getProfileRules(config2) {
|
|
1942
|
+
const rules = [];
|
|
1943
|
+
const avoids = [];
|
|
1944
|
+
if (config2.backendArchitecture) {
|
|
1945
|
+
const profile = listProfiles("backend").find((p) => p.name === config2.backendArchitecture);
|
|
1946
|
+
if (profile) {
|
|
1947
|
+
rules.push(...profile.rules);
|
|
1948
|
+
avoids.push(...profile.avoid);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
if (config2.frontendFramework) {
|
|
1952
|
+
const profile = listProfiles("frontend").find((p) => p.name === config2.frontendFramework);
|
|
1953
|
+
if (profile) {
|
|
1954
|
+
rules.push(...profile.rules);
|
|
1955
|
+
avoids.push(...profile.avoid);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
return { rules, avoids };
|
|
1959
|
+
}
|
|
1960
|
+
async function loadRelevantRules(cwd, config2, rel, diff, fallbackRules) {
|
|
1961
|
+
try {
|
|
1962
|
+
const query = buildContextQuery([
|
|
1963
|
+
rel,
|
|
1964
|
+
diff.slice(0, 1200),
|
|
1965
|
+
config2.backendArchitecture,
|
|
1966
|
+
config2.frontendFramework,
|
|
1967
|
+
config2.language
|
|
1968
|
+
]);
|
|
1969
|
+
const memories = await retrieveContextualMemories({
|
|
1970
|
+
query,
|
|
1971
|
+
cwd,
|
|
1972
|
+
config: config2,
|
|
1973
|
+
limit: 15
|
|
1974
|
+
});
|
|
1975
|
+
const selected = memories.filter((memory) => ["rule", "pattern", "decision"].includes(memory.type)).map((memory) => memory.content);
|
|
1976
|
+
return selected.length > 0 ? selected : fallbackRules;
|
|
1977
|
+
} catch {
|
|
1978
|
+
return fallbackRules;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
function applyAllowPatterns(violations, allowPatterns) {
|
|
1982
|
+
if (allowPatterns.length === 0) return violations;
|
|
1983
|
+
return violations.filter((violation) => {
|
|
1984
|
+
const haystack = `${violation.rule}
|
|
1985
|
+
${violation.issue}
|
|
1986
|
+
${violation.file}`.toLowerCase();
|
|
1987
|
+
return !allowPatterns.some((pattern) => haystack.includes(pattern.toLowerCase()));
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
async function verifyViolations(inputText, violations, allowPatterns, debug, mode = "diff") {
|
|
1991
|
+
if (violations.length === 0) return violations;
|
|
1992
|
+
const sourceLabel = mode === "snapshot" ? "file content" : "diff";
|
|
1993
|
+
const systemPrompt = `You are verifying candidate architecture violations.
|
|
1994
|
+
Only keep violations that are directly supported by the ${sourceLabel}.
|
|
1995
|
+
Reject speculative or weak matches.
|
|
1996
|
+
Treat these allowlisted patterns as intentional and valid:
|
|
1997
|
+
${allowPatterns.length ? allowPatterns.map((pattern, index) => `${index + 1}. ${pattern}`).join("\n") : "(none)"}
|
|
1998
|
+
|
|
1999
|
+
Return strict JSON:
|
|
2000
|
+
{"violations":[{"rule":"...","file":"...","line":1,"issue":"...","suggestion":"...","reason":"..."}]}
|
|
2001
|
+
Do not include any text outside the JSON.`;
|
|
2002
|
+
const userPrompt = `${mode === "snapshot" ? "File content" : "Diff"}:
|
|
2003
|
+
${inputText.slice(0, 6e3)}
|
|
2004
|
+
|
|
2005
|
+
Candidate violations:
|
|
2006
|
+
${JSON.stringify(violations, null, 2)}`;
|
|
2007
|
+
if (debug) {
|
|
2008
|
+
console.log(chalk.gray("\n [debug] verifier prompt:"));
|
|
2009
|
+
console.log(chalk.dim(systemPrompt));
|
|
2010
|
+
console.log(chalk.dim(userPrompt));
|
|
2011
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2012
|
+
}
|
|
2013
|
+
try {
|
|
2014
|
+
const { content: raw, usage: verifyUsage } = await callChatModel([
|
|
2015
|
+
{ role: "system", content: systemPrompt },
|
|
2016
|
+
{ role: "user", content: userPrompt }
|
|
2017
|
+
]);
|
|
2018
|
+
accumulateTokenUsage(verifyUsage);
|
|
2019
|
+
const parsed = JSON.parse(raw);
|
|
2020
|
+
if (Array.isArray(parsed?.violations)) return parsed.violations;
|
|
2021
|
+
if (Array.isArray(parsed)) return parsed;
|
|
2022
|
+
return violations;
|
|
2023
|
+
} catch {
|
|
2024
|
+
return violations;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
async function loadIgnorePatterns() {
|
|
2028
|
+
try {
|
|
2029
|
+
const app = getDefaultApplicationContainer();
|
|
2030
|
+
const ignores = await app.services.memoryEngine.list({ type: "ignore", limit: 1e3 });
|
|
2031
|
+
return ignores.map((ignore) => ignore.content);
|
|
2032
|
+
} catch {
|
|
2033
|
+
return [];
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
function normalizeForGit(pathLike) {
|
|
2037
|
+
return pathLike.split(sep).join("/");
|
|
2038
|
+
}
|
|
2039
|
+
function listSourceFilesFromFilesystem(dir) {
|
|
2040
|
+
if (!existsSync7(dir)) return [];
|
|
2041
|
+
const files = [];
|
|
2042
|
+
const stack = [dir];
|
|
2043
|
+
while (stack.length > 0) {
|
|
2044
|
+
const current = stack.pop();
|
|
2045
|
+
let entries = [];
|
|
2046
|
+
try {
|
|
2047
|
+
entries = readdirSync(current);
|
|
2048
|
+
} catch {
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
2051
|
+
for (const entry of entries) {
|
|
2052
|
+
const absolute = join7(current, entry);
|
|
2053
|
+
let isDirectory = false;
|
|
2054
|
+
let isFile = false;
|
|
2055
|
+
try {
|
|
2056
|
+
const stats = statSync(absolute);
|
|
2057
|
+
isDirectory = stats.isDirectory();
|
|
2058
|
+
isFile = stats.isFile();
|
|
2059
|
+
} catch {
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
if (isDirectory) {
|
|
2063
|
+
if (entry === "node_modules" || entry === ".git" || entry === "dist" || entry === "build" || entry === "coverage") {
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
stack.push(absolute);
|
|
2067
|
+
continue;
|
|
2068
|
+
}
|
|
2069
|
+
if (isFile && SOURCE_EXTENSIONS2.test(absolute)) files.push(absolute);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
return files;
|
|
2073
|
+
}
|
|
2074
|
+
function listTrackedSourceFiles(projectRoot, watchPath) {
|
|
2075
|
+
const relPrefix = normalizeForGit(relative(projectRoot, watchPath));
|
|
2076
|
+
const inRoot = relPrefix === "" || relPrefix === ".";
|
|
2077
|
+
const prefixWithSlash = inRoot ? "" : `${relPrefix}/`;
|
|
2078
|
+
const listed = spawnSync("git", ["ls-files"], { encoding: "utf-8", cwd: projectRoot });
|
|
2079
|
+
if (listed.status !== 0) {
|
|
2080
|
+
return listSourceFilesFromFilesystem(watchPath).sort();
|
|
2081
|
+
}
|
|
2082
|
+
const files = (listed.stdout ?? "").split("\n").filter((file) => file.length > 0).filter((file) => SOURCE_EXTENSIONS2.test(file)).filter((file) => inRoot || file.startsWith(prefixWithSlash)).map((file) => join7(projectRoot, file)).filter((file) => existsSync7(file));
|
|
2083
|
+
return [...new Set(files)].sort();
|
|
2084
|
+
}
|
|
2085
|
+
async function runSnapshotScan(projectRoot, watchPath, config2, verbose, debug, onEvent) {
|
|
2086
|
+
const files = listTrackedSourceFiles(projectRoot, watchPath);
|
|
2087
|
+
if (files.length === 0) {
|
|
2088
|
+
console.log(chalk.yellow("\n No tracked source files found for scan.\n"));
|
|
2089
|
+
return {
|
|
2090
|
+
filesChecked: 0,
|
|
2091
|
+
filesWithViolations: 0,
|
|
2092
|
+
violations: 0
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
console.log(chalk.dim(`
|
|
2096
|
+
scanning ${files.length} tracked source files...
|
|
2097
|
+
`));
|
|
2098
|
+
const summary = {
|
|
2099
|
+
filesChecked: 0,
|
|
2100
|
+
filesWithViolations: 0,
|
|
2101
|
+
violations: 0
|
|
2102
|
+
};
|
|
2103
|
+
for (const filePath of files) {
|
|
2104
|
+
const rel = normalizeForGit(relative(projectRoot, filePath));
|
|
2105
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2106
|
+
onEvent?.({ type: "saved", timestamp, file: rel });
|
|
2107
|
+
const result = await checkFile(filePath, projectRoot, config2, verbose, debug, "snapshot", onEvent);
|
|
2108
|
+
if (result.type !== "checked") {
|
|
2109
|
+
if (result.type === "skipped") {
|
|
2110
|
+
onEvent?.({ type: "skipped", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, reason: result.reason });
|
|
2111
|
+
} else {
|
|
2112
|
+
onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message: result.message });
|
|
2113
|
+
}
|
|
2114
|
+
continue;
|
|
2115
|
+
}
|
|
2116
|
+
summary.filesChecked += 1;
|
|
2117
|
+
if (result.violations.length === 0) {
|
|
2118
|
+
onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
|
|
2119
|
+
continue;
|
|
2120
|
+
}
|
|
2121
|
+
summary.filesWithViolations += 1;
|
|
2122
|
+
summary.violations += result.violations.length;
|
|
2123
|
+
onEvent?.({ type: "violations", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, violations: result.violations });
|
|
2124
|
+
}
|
|
2125
|
+
return summary;
|
|
2126
|
+
}
|
|
2127
|
+
async function autoFixFile(filePath, projectRoot, violations, rules, avoids, debug) {
|
|
2128
|
+
if (!existsSync7(filePath)) return false;
|
|
2129
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
2130
|
+
const rel = relative(projectRoot, filePath).split(sep).join("/");
|
|
2131
|
+
const violationSummary = violations.map(
|
|
2132
|
+
(v, i) => `${i + 1}. Rule: "${v.rule}"
|
|
2133
|
+
Issue: ${v.issue}${v.suggestion ? `
|
|
2134
|
+
Fix: ${v.suggestion}` : ""}${v.line ? `
|
|
2135
|
+
Line: ${v.line}` : ""}`
|
|
2136
|
+
).join("\n\n");
|
|
2137
|
+
const systemPrompt = `You are an expert code fixer. You will be given a file with architecture violations.
|
|
2138
|
+
Fix ONLY the violations listed below. Do not change anything else.
|
|
2139
|
+
Return ONLY the complete fixed file content \u2014 no markdown, no explanation, no code blocks.`;
|
|
2140
|
+
const userPrompt = `File: ${rel}
|
|
2141
|
+
|
|
2142
|
+
Violations to fix:
|
|
2143
|
+
${violationSummary}
|
|
2144
|
+
|
|
2145
|
+
Rules being enforced:
|
|
2146
|
+
${rules.slice(0, 10).join("\n")}
|
|
2147
|
+
${avoids.length > 0 ? `
|
|
2148
|
+
Things that must never appear:
|
|
2149
|
+
${avoids.slice(0, 5).join("\n")}` : ""}
|
|
2150
|
+
|
|
2151
|
+
Current file content:
|
|
2152
|
+
${content}`;
|
|
2153
|
+
if (debug) {
|
|
2154
|
+
console.log(chalk.gray(" [debug] auto-fix prompt:"));
|
|
2155
|
+
console.log(chalk.dim(userPrompt.slice(0, 500) + "..."));
|
|
2156
|
+
}
|
|
2157
|
+
try {
|
|
2158
|
+
console.log(chalk.cyan(` \u26A1 Auto-fixing ${violations.length} violation${violations.length > 1 ? "s" : ""} in ${rel}\u2026`));
|
|
2159
|
+
const { content: fixed, usage: fixUsage } = await callChatModel([
|
|
2160
|
+
{ role: "system", content: systemPrompt },
|
|
2161
|
+
{ role: "user", content: userPrompt }
|
|
2162
|
+
]);
|
|
2163
|
+
accumulateTokenUsage(fixUsage);
|
|
2164
|
+
if (!fixed.trim()) return false;
|
|
2165
|
+
writeFileSync3(filePath, fixed, "utf-8");
|
|
2166
|
+
try {
|
|
2167
|
+
const app = getDefaultApplicationContainer();
|
|
2168
|
+
for (const v of violations) {
|
|
2169
|
+
await app.services.memoryEngine.remember({
|
|
2170
|
+
type: "rule",
|
|
2171
|
+
scope: "project",
|
|
2172
|
+
content: v.rule,
|
|
2173
|
+
reason: `Auto-fixed by AI: ${v.issue}`,
|
|
2174
|
+
tags: ["auto-fix"]
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
} catch {
|
|
2178
|
+
}
|
|
2179
|
+
return true;
|
|
2180
|
+
} catch (err) {
|
|
2181
|
+
if (debug) console.log(chalk.yellow(` [debug] auto-fix failed: ${err.message}`));
|
|
2182
|
+
return false;
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
async function checkFile(filePath, projectRoot, config2, verbose, debug, mode = "diff", onEvent) {
|
|
2186
|
+
const rel = relative(projectRoot, filePath).split(sep).join("/");
|
|
2187
|
+
if (rel.startsWith("..")) return { type: "skipped", reason: "File is outside project root" };
|
|
2188
|
+
let inputText;
|
|
2189
|
+
if (mode === "snapshot") {
|
|
2190
|
+
if (!existsSync7(filePath)) return { type: "skipped", reason: "File no longer exists" };
|
|
2191
|
+
inputText = readFileSync6(filePath, "utf-8");
|
|
2192
|
+
if (!inputText.trim()) return { type: "skipped", reason: "File is empty" };
|
|
2193
|
+
} else {
|
|
2194
|
+
const headResult = spawnSync("git", ["diff", "HEAD", "--", rel], { encoding: "utf-8", cwd: projectRoot });
|
|
2195
|
+
if (headResult.stdout?.trim()) {
|
|
2196
|
+
inputText = headResult.stdout;
|
|
2197
|
+
} else {
|
|
2198
|
+
const noIndexResult = spawnSync("git", ["diff", "--no-index", "/dev/null", rel], {
|
|
2199
|
+
encoding: "utf-8",
|
|
2200
|
+
cwd: projectRoot
|
|
2201
|
+
});
|
|
2202
|
+
inputText = noIndexResult.stdout ?? "";
|
|
2203
|
+
}
|
|
2204
|
+
if (!inputText.trim()) return { type: "skipped", reason: "No changes compared with HEAD" };
|
|
2205
|
+
}
|
|
2206
|
+
const { rules: fallbackRules, avoids } = getProfileRules(config2);
|
|
2207
|
+
const rules = await loadRelevantRules(projectRoot, config2, rel, inputText, fallbackRules);
|
|
2208
|
+
if (rules.length === 0) return { type: "skipped", reason: "No applicable architecture rules" };
|
|
2209
|
+
const MAX_INPUT = 6e3;
|
|
2210
|
+
const truncated = inputText.length > MAX_INPUT;
|
|
2211
|
+
const inputToSend = truncated ? inputText.slice(0, MAX_INPUT) + "\n\n[input truncated]" : inputText;
|
|
2212
|
+
if (verbose || debug) {
|
|
2213
|
+
const label = mode === "snapshot" ? "snapshot" : `${inputText.length} chars`;
|
|
2214
|
+
console.log(chalk.dim(`
|
|
2215
|
+
[watch] checking ${rel} (${label})\u2026`));
|
|
2216
|
+
}
|
|
2217
|
+
const rulesWithReasons = rules.map((r, i) => {
|
|
2218
|
+
const why = reasonMap.get(r);
|
|
2219
|
+
return why ? `${i + 1}. ${r}
|
|
2220
|
+
WHY: ${why}` : `${i + 1}. ${r}`;
|
|
2221
|
+
}).join("\n");
|
|
2222
|
+
const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config2), ...await loadIgnorePatterns()])];
|
|
2223
|
+
const astViolations = findAstDeterministicViolationsForFile(rel, {
|
|
2224
|
+
cwd: projectRoot,
|
|
2225
|
+
config: config2,
|
|
2226
|
+
rules,
|
|
2227
|
+
reasonLookup: reasonMap
|
|
2228
|
+
}).map((violation) => ({
|
|
2229
|
+
...violation,
|
|
2230
|
+
severity: "error"
|
|
2231
|
+
}));
|
|
2232
|
+
const analysisTarget = mode === "snapshot" ? "file content" : "file diff";
|
|
2233
|
+
const systemPrompt = `You are a strict code reviewer enforcing architecture rules.
|
|
2234
|
+
Analyze the ${analysisTarget} and identify ONLY clear, definite rule violations.
|
|
2235
|
+
Use the WHY for each rule to understand intent and judge edge cases.
|
|
2236
|
+
|
|
2237
|
+
Rules to enforce:
|
|
2238
|
+
${rulesWithReasons}
|
|
2239
|
+
|
|
2240
|
+
Things that must never appear:
|
|
2241
|
+
${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
|
|
2242
|
+
|
|
2243
|
+
Never flag these accepted project patterns:
|
|
2244
|
+
${allowPatterns.length ? allowPatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
|
|
2245
|
+
|
|
2246
|
+
IMPORTANT: Respond with JSON: {"violations":[...]} or {"violations":[]}.
|
|
2247
|
+
Each violation: {"rule":"...","file":"...","line":N,"issue":"...","suggestion":"...","reason":"..."}.
|
|
2248
|
+
No text outside the JSON.`;
|
|
2249
|
+
if (debug) {
|
|
2250
|
+
console.log(chalk.gray("\n [debug] prompt:"));
|
|
2251
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2252
|
+
console.log(systemPrompt);
|
|
2253
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2254
|
+
console.log(chalk.gray(` [debug] input length: ${inputText.length} chars`));
|
|
2255
|
+
console.log(chalk.dim(inputToSend));
|
|
2256
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2257
|
+
}
|
|
2258
|
+
try {
|
|
2259
|
+
const reviewPrompt = mode === "snapshot" ? `Review this file ${rel}:
|
|
2260
|
+
|
|
2261
|
+
${inputToSend}` : `Review this diff for ${rel}:
|
|
2262
|
+
|
|
2263
|
+
${inputToSend}`;
|
|
2264
|
+
onEvent?.({ type: "log", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, message: `checking with ${getChatProviderLabel()} | ${rules.length} rule${rules.length === 1 ? "" : "s"}` });
|
|
2265
|
+
const { content: raw, usage: reviewUsage } = await callChatModel([
|
|
2266
|
+
{ role: "system", content: systemPrompt },
|
|
2267
|
+
{ role: "user", content: reviewPrompt }
|
|
2268
|
+
]);
|
|
2269
|
+
accumulateTokenUsage(reviewUsage);
|
|
2270
|
+
if (debug) {
|
|
2271
|
+
console.log(chalk.gray(" [debug] raw response:"));
|
|
2272
|
+
console.log(chalk.dim(raw));
|
|
2273
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2274
|
+
}
|
|
2275
|
+
let violations = [];
|
|
2276
|
+
try {
|
|
2277
|
+
const parsed = JSON.parse(raw);
|
|
2278
|
+
if (Array.isArray(parsed)) {
|
|
2279
|
+
violations = parsed;
|
|
2280
|
+
} else if (Array.isArray(parsed?.violations)) {
|
|
2281
|
+
violations = parsed.violations;
|
|
2282
|
+
} else if (parsed?.rule) {
|
|
2283
|
+
violations = [parsed];
|
|
2284
|
+
}
|
|
2285
|
+
} catch {
|
|
2286
|
+
violations = [];
|
|
2287
|
+
}
|
|
2288
|
+
onEvent?.({ type: "log", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, message: `model returned ${violations.length} candidate violation${violations.length === 1 ? "" : "s"}` });
|
|
2289
|
+
violations = await verifyViolations(inputText, violations, allowPatterns, debug, mode);
|
|
2290
|
+
violations = [...astViolations, ...violations];
|
|
2291
|
+
violations = applyAllowPatterns(violations, allowPatterns);
|
|
2292
|
+
violations = violations.map((violation) => ({
|
|
2293
|
+
...violation,
|
|
2294
|
+
code: violation.code ?? (violation.line ? formatCodeContext(filePath, violation.line, 1) : void 0)
|
|
2295
|
+
}));
|
|
2296
|
+
if (violations.length === 0) {
|
|
2297
|
+
recordWatchResult(projectRoot, rel, []);
|
|
2298
|
+
console.log(chalk.green(` \u2713 ${rel}`) + chalk.dim(" \u2014 no violations"));
|
|
2299
|
+
return { type: "checked", violations: [] };
|
|
2300
|
+
}
|
|
2301
|
+
console.log(
|
|
2302
|
+
chalk.red.bold(`
|
|
2303
|
+
\u2717 ${violations.length} violation${violations.length > 1 ? "s" : ""} in ${rel}
|
|
2304
|
+
`)
|
|
2305
|
+
);
|
|
2306
|
+
violations.forEach((v, i) => {
|
|
2307
|
+
const loc = v.line ? `${v.file ?? rel}:${v.line}` : v.file ?? rel;
|
|
2308
|
+
console.log(chalk.bold(` [${i + 1}] ${loc}`));
|
|
2309
|
+
console.log(chalk.yellow(" Rule: ") + v.rule);
|
|
2310
|
+
const why = v.reason ?? reasonMap.get(v.rule);
|
|
2311
|
+
if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
|
|
2312
|
+
if (v.line && existsSync7(filePath)) {
|
|
2313
|
+
printCodeContext(filePath, v.line, 1);
|
|
2314
|
+
}
|
|
2315
|
+
if (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
|
|
2316
|
+
if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
|
|
2317
|
+
console.log();
|
|
2318
|
+
});
|
|
2319
|
+
recordWatchResult(projectRoot, rel, violations);
|
|
2320
|
+
console.log(chalk.dim(' Fix violations or run: memory-core remember "<lesson>"'));
|
|
2321
|
+
console.log();
|
|
2322
|
+
return { type: "checked", violations };
|
|
2323
|
+
} catch (err) {
|
|
2324
|
+
const aiUnavailable = err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED");
|
|
2325
|
+
const message = aiUnavailable ? `Model unreachable for ${rel}; using deterministic checks only.` : `AI check failed for ${rel}; using deterministic checks only.`;
|
|
2326
|
+
console.log(chalk.yellow(` \u26A0 ${message}`));
|
|
2327
|
+
let violations = applyAllowPatterns(astViolations, allowPatterns);
|
|
2328
|
+
violations = violations.map((violation) => ({
|
|
2329
|
+
...violation,
|
|
2330
|
+
code: violation.code ?? (violation.line ? formatCodeContext(filePath, violation.line, 1) : void 0)
|
|
2331
|
+
}));
|
|
2332
|
+
if (violations.length === 0) {
|
|
2333
|
+
recordWatchResult(projectRoot, rel, []);
|
|
2334
|
+
console.log(chalk.green(` \u2713 ${rel}`) + chalk.dim(" \u2014 no deterministic violations"));
|
|
2335
|
+
return { type: "checked", violations: [] };
|
|
2336
|
+
}
|
|
2337
|
+
console.log(
|
|
2338
|
+
chalk.red.bold(`
|
|
2339
|
+
\u2717 ${violations.length} deterministic violation${violations.length > 1 ? "s" : ""} in ${rel}
|
|
2340
|
+
`)
|
|
2341
|
+
);
|
|
2342
|
+
violations.forEach((v, i) => {
|
|
2343
|
+
const loc = v.line ? `${v.file ?? rel}:${v.line}` : v.file ?? rel;
|
|
2344
|
+
console.log(chalk.bold(` [${i + 1}] ${loc}`));
|
|
2345
|
+
console.log(chalk.yellow(" Rule: ") + v.rule);
|
|
2346
|
+
const why = v.reason ?? reasonMap.get(v.rule);
|
|
2347
|
+
if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
|
|
2348
|
+
if (v.line && existsSync7(filePath)) {
|
|
2349
|
+
printCodeContext(filePath, v.line, 1);
|
|
2350
|
+
}
|
|
2351
|
+
if (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
|
|
2352
|
+
if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
|
|
2353
|
+
console.log();
|
|
2354
|
+
});
|
|
2355
|
+
recordWatchResult(projectRoot, rel, violations);
|
|
2356
|
+
console.log(chalk.dim(' Fix violations or run: memory-core remember "<lesson>"'));
|
|
2357
|
+
console.log();
|
|
2358
|
+
return { type: "checked", violations };
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
async function scanFiles(options = {}) {
|
|
2362
|
+
const { projectRoot, watchPath } = resolveWatchPaths(options.path, options.projectRoot);
|
|
2363
|
+
const config2 = loadConfig(projectRoot);
|
|
2364
|
+
if (!config2) {
|
|
2365
|
+
throw new Error("No .memory-core.json found. Run: memory-core init");
|
|
2366
|
+
}
|
|
2367
|
+
const { rules } = getProfileRules(config2);
|
|
2368
|
+
if (rules.length === 0) {
|
|
2369
|
+
console.log(chalk.yellow("\n No architecture rules configured in .memory-core.json \u2014 nothing to scan.\n"));
|
|
2370
|
+
return {
|
|
2371
|
+
filesChecked: 0,
|
|
2372
|
+
filesWithViolations: 0,
|
|
2373
|
+
violations: 0
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
resetLiveStats(projectRoot);
|
|
2377
|
+
console.log(chalk.cyan("\n archmind scan \u2014 checking tracked source files\n"));
|
|
2378
|
+
console.log(chalk.dim(` project: ${projectRoot}`));
|
|
2379
|
+
console.log(chalk.dim(` path: ${watchPath}`));
|
|
2380
|
+
console.log(chalk.dim(` model: ${getChatProviderLabel()}`));
|
|
2381
|
+
console.log(chalk.dim(` rules: ${rules.length}
|
|
2382
|
+
`));
|
|
2383
|
+
const summary = await runSnapshotScan(
|
|
2384
|
+
projectRoot,
|
|
2385
|
+
watchPath,
|
|
2386
|
+
config2,
|
|
2387
|
+
options.verbose ?? false,
|
|
2388
|
+
options.debug ?? false,
|
|
2389
|
+
options.onEvent
|
|
2390
|
+
);
|
|
2391
|
+
const cleanFiles = summary.filesChecked - summary.filesWithViolations;
|
|
2392
|
+
console.log(chalk.bold("\n scan summary\n"));
|
|
2393
|
+
console.log(chalk.dim(` files checked: ${summary.filesChecked}`));
|
|
2394
|
+
console.log(chalk.dim(` files clean: ${cleanFiles}`));
|
|
2395
|
+
console.log(chalk.dim(` files with violations: ${summary.filesWithViolations}`));
|
|
2396
|
+
console.log(chalk.dim(` total violations: ${summary.violations}
|
|
2397
|
+
`));
|
|
2398
|
+
return summary;
|
|
2399
|
+
}
|
|
2400
|
+
async function startWatch(options = {}) {
|
|
2401
|
+
const { projectRoot, watchPath } = resolveWatchPaths(options.path, options.projectRoot);
|
|
2402
|
+
const config2 = loadConfig(projectRoot);
|
|
2403
|
+
const exitOnSetupFailure = options.exitOnSetupFailure ?? true;
|
|
2404
|
+
if (!config2) {
|
|
2405
|
+
const message = "No .memory-core.json found. Run: memory-core init";
|
|
2406
|
+
console.error(chalk.red(`
|
|
2407
|
+
${message}
|
|
2408
|
+
`));
|
|
2409
|
+
options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message });
|
|
2410
|
+
if (exitOnSetupFailure) process.exit(1);
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
const { rules, avoids } = getProfileRules(config2);
|
|
2414
|
+
if (rules.length === 0) {
|
|
2415
|
+
const message = "No architecture rules configured in .memory-core.json \u2014 nothing to watch.";
|
|
2416
|
+
console.log(chalk.yellow(`
|
|
2417
|
+
${message}
|
|
2418
|
+
`));
|
|
2419
|
+
options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message });
|
|
2420
|
+
if (exitOnSetupFailure) process.exit(0);
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
resetLiveStats(projectRoot);
|
|
2424
|
+
console.log(chalk.cyan("\n archmind watch \u2014 real-time rule enforcement\n"));
|
|
2425
|
+
console.log(chalk.dim(` project: ${projectRoot}`));
|
|
2426
|
+
console.log(chalk.dim(` watching: ${watchPath}`));
|
|
2427
|
+
console.log(chalk.dim(` model: ${getChatProviderLabel()}`));
|
|
2428
|
+
console.log(chalk.dim(` rules: ${rules.length}`));
|
|
2429
|
+
if (options.autoFix) console.log(chalk.yellow(" mode: auto-fix enabled \u2014 violations will be rewritten by AI"));
|
|
2430
|
+
console.log(chalk.dim(" ctrl+c to stop\n"));
|
|
2431
|
+
options.onEvent?.({
|
|
2432
|
+
type: "ready",
|
|
2433
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2434
|
+
path: watchPath,
|
|
2435
|
+
model: getChatProviderLabel(),
|
|
2436
|
+
rules: rules.length
|
|
2437
|
+
});
|
|
2438
|
+
if (options.scanOnStart) {
|
|
2439
|
+
console.log(chalk.dim(" running initial snapshot scan before watch events..."));
|
|
2440
|
+
await runSnapshotScan(
|
|
2441
|
+
projectRoot,
|
|
2442
|
+
watchPath,
|
|
2443
|
+
config2,
|
|
2444
|
+
options.verbose ?? false,
|
|
2445
|
+
options.debug ?? false,
|
|
2446
|
+
options.onEvent
|
|
2447
|
+
);
|
|
2448
|
+
console.log(chalk.dim(" initial scan complete.\n"));
|
|
2449
|
+
}
|
|
2450
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2451
|
+
const watcher = watch(watchPath, {
|
|
2452
|
+
ignored: [
|
|
2453
|
+
"**/node_modules/**",
|
|
2454
|
+
"**/.git/**",
|
|
2455
|
+
"**/dist/**",
|
|
2456
|
+
"**/build/**",
|
|
2457
|
+
"**/coverage/**",
|
|
2458
|
+
"**/.memory-core*"
|
|
2459
|
+
],
|
|
2460
|
+
ignoreInitial: true,
|
|
2461
|
+
persistent: true,
|
|
2462
|
+
awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50 }
|
|
2463
|
+
});
|
|
2464
|
+
const keepAlive = setInterval(() => {
|
|
2465
|
+
}, 1 << 30);
|
|
2466
|
+
const handle = (filePath) => {
|
|
2467
|
+
if (!SOURCE_EXTENSIONS2.test(filePath)) return;
|
|
2468
|
+
if (pending.has(filePath)) clearTimeout(pending.get(filePath));
|
|
2469
|
+
const timer = setTimeout(async () => {
|
|
2470
|
+
pending.delete(filePath);
|
|
2471
|
+
const rel = normalizeForGit(relative(projectRoot, filePath));
|
|
2472
|
+
if (rel.startsWith("..")) return;
|
|
2473
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
2474
|
+
console.log(chalk.dim(`
|
|
2475
|
+
[${timestamp.toLocaleTimeString()}] saved: ${rel}`));
|
|
2476
|
+
options.onEvent?.({ type: "saved", timestamp: timestamp.toISOString(), file: rel });
|
|
2477
|
+
const result = await checkFile(
|
|
2478
|
+
filePath,
|
|
2479
|
+
projectRoot,
|
|
2480
|
+
config2,
|
|
2481
|
+
options.verbose ?? false,
|
|
2482
|
+
options.debug ?? false,
|
|
2483
|
+
"diff",
|
|
2484
|
+
options.onEvent
|
|
2485
|
+
);
|
|
2486
|
+
if (result.type === "skipped") {
|
|
2487
|
+
if (result.reason === "No changes compared with HEAD") {
|
|
2488
|
+
recordWatchResult(projectRoot, rel, []);
|
|
2489
|
+
options.onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
options.onEvent?.({ type: "skipped", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, reason: result.reason });
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
if (result.type === "error") {
|
|
2496
|
+
options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message: result.message });
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
const { violations } = result;
|
|
2500
|
+
if (violations.length === 0) {
|
|
2501
|
+
options.onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
options.onEvent?.({ type: "violations", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, violations });
|
|
2505
|
+
if (options.autoFix) {
|
|
2506
|
+
const fixed = await autoFixFile(filePath, projectRoot, violations, rules, avoids, options.debug ?? false);
|
|
2507
|
+
if (fixed) {
|
|
2508
|
+
console.log(chalk.green(` \u2713 Auto-fixed: ${rel} \u2014 re-checking\u2026
|
|
2509
|
+
`));
|
|
2510
|
+
options.onEvent?.({ type: "log", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, message: "auto-fixed by AI" });
|
|
2511
|
+
} else {
|
|
2512
|
+
console.log(chalk.yellow(` \u26A0 Auto-fix failed for ${rel} \u2014 fix manually.
|
|
2513
|
+
`));
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
}, 300);
|
|
2517
|
+
pending.set(filePath, timer);
|
|
2518
|
+
};
|
|
2519
|
+
watcher.on("add", handle);
|
|
2520
|
+
watcher.on("change", handle);
|
|
2521
|
+
watcher.on("unlink", (filePath) => {
|
|
2522
|
+
const rel = normalizeForGit(relative(projectRoot, filePath));
|
|
2523
|
+
if (rel.startsWith("..")) return;
|
|
2524
|
+
recordWatchResult(projectRoot, rel, []);
|
|
2525
|
+
options.onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
|
|
2526
|
+
});
|
|
2527
|
+
watcher.on("error", (err) => {
|
|
2528
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2529
|
+
console.error(chalk.red(` watcher error: ${message}`));
|
|
2530
|
+
options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message });
|
|
2531
|
+
});
|
|
2532
|
+
process.on("SIGINT", () => {
|
|
2533
|
+
console.log(chalk.dim("\n\n archmind watch stopped.\n"));
|
|
2534
|
+
options.onEvent?.({ type: "stopped", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2535
|
+
clearInterval(keepAlive);
|
|
2536
|
+
watcher.close();
|
|
2537
|
+
process.exit(0);
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
// src/infrastructure/filesystem/chokidar-watch-service.ts
|
|
2542
|
+
var ChokidarWatchService = class {
|
|
2543
|
+
async start(options = {}) {
|
|
2544
|
+
await startWatch({
|
|
2545
|
+
path: options.path,
|
|
2546
|
+
verbose: options.verbose,
|
|
2547
|
+
debug: options.debug,
|
|
2548
|
+
scanOnStart: options.scanOnStart,
|
|
2549
|
+
autoFix: options.autoFix
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
async scan(options = {}) {
|
|
2553
|
+
return scanFiles({
|
|
2554
|
+
path: options.path,
|
|
2555
|
+
verbose: options.verbose,
|
|
2556
|
+
debug: options.debug
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
};
|
|
2560
|
+
|
|
2561
|
+
// src/infrastructure/events/in-memory-event-bus.ts
|
|
2562
|
+
var InMemoryEventBus = class {
|
|
2563
|
+
handlers = /* @__PURE__ */ new Map();
|
|
2564
|
+
async publish(event) {
|
|
2565
|
+
const handlers = this.handlers.get(event.type);
|
|
2566
|
+
if (!handlers || handlers.size === 0) return;
|
|
2567
|
+
for (const handler of handlers) {
|
|
2568
|
+
await handler(event);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
subscribe(eventType, handler) {
|
|
2572
|
+
const existing = this.handlers.get(eventType) ?? /* @__PURE__ */ new Set();
|
|
2573
|
+
existing.add(handler);
|
|
2574
|
+
this.handlers.set(eventType, existing);
|
|
2575
|
+
return () => {
|
|
2576
|
+
const current = this.handlers.get(eventType);
|
|
2577
|
+
if (!current) return;
|
|
2578
|
+
current.delete(handler);
|
|
2579
|
+
if (current.size === 0) {
|
|
2580
|
+
this.handlers.delete(eventType);
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
|
|
2586
|
+
// src/modules/memory-engine/application/memory-engine-service.ts
|
|
2587
|
+
var MemoryEngineService = class {
|
|
2588
|
+
constructor(memoryRepository, embeddingProvider) {
|
|
2589
|
+
this.memoryRepository = memoryRepository;
|
|
2590
|
+
this.embeddingProvider = embeddingProvider;
|
|
2591
|
+
}
|
|
2592
|
+
memoryRepository;
|
|
2593
|
+
embeddingProvider;
|
|
2594
|
+
withReason(input) {
|
|
2595
|
+
const reason = input.reason?.trim();
|
|
2596
|
+
return {
|
|
2597
|
+
...input,
|
|
2598
|
+
reason: reason || `Captured as a ${input.type} memory because it should be remembered: ${input.content}`
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
async remember(input) {
|
|
2602
|
+
const normalized = this.withReason(input);
|
|
2603
|
+
const embedding = await this.embeddingProvider.embed(normalized.content);
|
|
2604
|
+
return this.memoryRepository.upsert({ ...normalized, embedding });
|
|
2605
|
+
}
|
|
2606
|
+
async rememberForce(input) {
|
|
2607
|
+
const normalized = this.withReason(input);
|
|
2608
|
+
const embedding = await this.embeddingProvider.embed(normalized.content);
|
|
2609
|
+
await this.memoryRepository.save({ ...normalized, embedding });
|
|
2610
|
+
}
|
|
2611
|
+
async list(filters = {}) {
|
|
2612
|
+
return this.memoryRepository.list(filters);
|
|
2613
|
+
}
|
|
2614
|
+
async getById(id) {
|
|
2615
|
+
return this.memoryRepository.getById(id);
|
|
2616
|
+
}
|
|
2617
|
+
async removeById(id) {
|
|
2618
|
+
return this.memoryRepository.removeById(id);
|
|
2619
|
+
}
|
|
2620
|
+
async removeMany(filters) {
|
|
2621
|
+
return this.memoryRepository.removeMany(filters);
|
|
2622
|
+
}
|
|
2623
|
+
async update(id, patch) {
|
|
2624
|
+
const current = await this.memoryRepository.getById(id);
|
|
2625
|
+
if (!current) return null;
|
|
2626
|
+
const content = patch.content ?? current.content;
|
|
2627
|
+
const embedding = content !== current.content ? await this.embeddingProvider.embed(content) : void 0;
|
|
2628
|
+
return this.memoryRepository.update(id, { ...patch, embedding });
|
|
2629
|
+
}
|
|
2630
|
+
async search(query, architectures, limit = 10) {
|
|
2631
|
+
const embedding = await this.embeddingProvider.embed(query);
|
|
2632
|
+
return this.memoryRepository.searchByEmbedding({ embedding, architectures, limit });
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
|
|
2636
|
+
// src/modules/retrieval-engine/application/retrieval-engine-service.ts
|
|
2637
|
+
var RetrievalEngineService = class {
|
|
2638
|
+
constructor(embeddingProvider, memoryRepository) {
|
|
2639
|
+
this.embeddingProvider = embeddingProvider;
|
|
2640
|
+
this.memoryRepository = memoryRepository;
|
|
2641
|
+
}
|
|
2642
|
+
embeddingProvider;
|
|
2643
|
+
memoryRepository;
|
|
2644
|
+
async retrieve(query) {
|
|
2645
|
+
const limit = query.limit ?? 10;
|
|
2646
|
+
const embedding = await this.embeddingProvider.embed(query.text);
|
|
2647
|
+
const vectorMatches = await this.memoryRepository.searchByEmbedding({
|
|
2648
|
+
embedding,
|
|
2649
|
+
architectures: query.architectures,
|
|
2650
|
+
limit
|
|
2651
|
+
});
|
|
2652
|
+
const lexicalMatches = (await this.memoryRepository.list({
|
|
2653
|
+
architecture: query.architectures,
|
|
2654
|
+
includeGlobal: true,
|
|
2655
|
+
limit: Math.max(limit * 2, 20)
|
|
2656
|
+
})).filter((memory) => {
|
|
2657
|
+
const haystack = `${memory.content} ${memory.title ?? ""} ${(memory.tags ?? []).join(" ")}`.toLowerCase();
|
|
2658
|
+
return haystack.includes(query.text.toLowerCase());
|
|
2659
|
+
});
|
|
2660
|
+
const merged = [...vectorMatches];
|
|
2661
|
+
for (const memory of lexicalMatches) {
|
|
2662
|
+
if (!merged.some((candidate) => candidate.id === memory.id)) {
|
|
2663
|
+
merged.push(memory);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
return { items: merged.slice(0, limit) };
|
|
2667
|
+
}
|
|
2668
|
+
};
|
|
2669
|
+
|
|
2670
|
+
// src/modules/rule-engine/application/rule-engine-service.ts
|
|
2671
|
+
var RuleEngineService = class {
|
|
2672
|
+
constructor(rules, llmProvider) {
|
|
2673
|
+
this.rules = rules;
|
|
2674
|
+
this.llmProvider = llmProvider;
|
|
2675
|
+
}
|
|
2676
|
+
rules;
|
|
2677
|
+
llmProvider;
|
|
2678
|
+
async evaluate(input, includeExplanation = false) {
|
|
2679
|
+
const allViolations = [];
|
|
2680
|
+
for (const rule of this.rules) {
|
|
2681
|
+
allViolations.push(...await rule.evaluate(input));
|
|
2682
|
+
}
|
|
2683
|
+
if (!includeExplanation || allViolations.length === 0 || !this.llmProvider) {
|
|
2684
|
+
return { violations: allViolations };
|
|
2685
|
+
}
|
|
2686
|
+
const explanation = await this.llmProvider.generateText([
|
|
2687
|
+
{
|
|
2688
|
+
role: "system",
|
|
2689
|
+
content: "Summarize architecture violations and provide practical fixes in concise bullet points."
|
|
2690
|
+
},
|
|
2691
|
+
{
|
|
2692
|
+
role: "user",
|
|
2693
|
+
content: JSON.stringify(allViolations, null, 2)
|
|
2694
|
+
}
|
|
2695
|
+
]);
|
|
2696
|
+
return { violations: allViolations, explanation };
|
|
2697
|
+
}
|
|
2698
|
+
};
|
|
2699
|
+
|
|
2700
|
+
// src/modules/graph-engine/application/graph-engine-service.ts
|
|
2701
|
+
import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
2702
|
+
import { join as join8, relative as relative2, resolve as resolve4 } from "path";
|
|
2703
|
+
var SOURCE_EXTENSIONS3 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
2704
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", ".memory-core"]);
|
|
2705
|
+
function isSourceFile(pathValue) {
|
|
2706
|
+
const extension = pathValue.slice(pathValue.lastIndexOf("."));
|
|
2707
|
+
return SOURCE_EXTENSIONS3.has(extension);
|
|
2708
|
+
}
|
|
2709
|
+
function asPosix5(value) {
|
|
2710
|
+
return value.replace(/\\/g, "/");
|
|
2711
|
+
}
|
|
2712
|
+
function normalizeNode(pathValue) {
|
|
2713
|
+
return asPosix5(pathValue);
|
|
2714
|
+
}
|
|
2715
|
+
function edgeKey(edge) {
|
|
2716
|
+
return `${edge.from}\0${edge.to}\0${edge.kind}`;
|
|
2717
|
+
}
|
|
2718
|
+
function generateSnapshotId() {
|
|
2719
|
+
const suffix = Math.random().toString(36).slice(2, 8);
|
|
2720
|
+
return `snapshot-${Date.now()}-${suffix}`;
|
|
2721
|
+
}
|
|
2722
|
+
function toGraph(snapshot) {
|
|
2723
|
+
return {
|
|
2724
|
+
id: snapshot.id,
|
|
2725
|
+
createdAt: snapshot.createdAt,
|
|
2726
|
+
rootPath: snapshot.rootPath,
|
|
2727
|
+
nodes: snapshot.nodes,
|
|
2728
|
+
edges: snapshot.edges
|
|
2729
|
+
};
|
|
1602
2730
|
}
|
|
1603
2731
|
var GraphEngineService = class {
|
|
1604
2732
|
constructor(graphRepository) {
|
|
@@ -1606,19 +2734,19 @@ var GraphEngineService = class {
|
|
|
1606
2734
|
}
|
|
1607
2735
|
graphRepository;
|
|
1608
2736
|
async buildGraph(options = {}) {
|
|
1609
|
-
const cwd =
|
|
2737
|
+
const cwd = resolve4(options.cwd ?? process.cwd());
|
|
1610
2738
|
const files = this.collectSourceFiles(cwd);
|
|
1611
2739
|
const nodes = /* @__PURE__ */ new Set();
|
|
1612
2740
|
const edges = /* @__PURE__ */ new Map();
|
|
1613
2741
|
for (const relativeFile of files) {
|
|
1614
|
-
const absoluteFile =
|
|
2742
|
+
const absoluteFile = resolve4(cwd, relativeFile);
|
|
1615
2743
|
const fromNode = normalizeNode(relativeFile);
|
|
1616
2744
|
nodes.add(fromNode);
|
|
1617
2745
|
const imports = collectResolvedImports(absoluteFile, cwd);
|
|
1618
2746
|
for (const imp of imports) {
|
|
1619
2747
|
let toNode;
|
|
1620
2748
|
if (imp.resolvedPath) {
|
|
1621
|
-
toNode = normalizeNode(
|
|
2749
|
+
toNode = normalizeNode(asPosix5(relative2(cwd, imp.resolvedPath)));
|
|
1622
2750
|
} else if (imp.isExternal) {
|
|
1623
2751
|
toNode = `pkg:${imp.specifier}`;
|
|
1624
2752
|
}
|
|
@@ -1687,15 +2815,15 @@ var GraphEngineService = class {
|
|
|
1687
2815
|
collectSourceFiles(cwd) {
|
|
1688
2816
|
const files = [];
|
|
1689
2817
|
const walk = (dir) => {
|
|
1690
|
-
for (const entry of
|
|
2818
|
+
for (const entry of readdirSync2(dir)) {
|
|
1691
2819
|
if (IGNORED_DIRS.has(entry)) continue;
|
|
1692
|
-
const absolutePath =
|
|
1693
|
-
const stat =
|
|
2820
|
+
const absolutePath = join8(dir, entry);
|
|
2821
|
+
const stat = statSync2(absolutePath);
|
|
1694
2822
|
if (stat.isDirectory()) {
|
|
1695
2823
|
walk(absolutePath);
|
|
1696
2824
|
continue;
|
|
1697
2825
|
}
|
|
1698
|
-
const rel =
|
|
2826
|
+
const rel = asPosix5(relative2(cwd, absolutePath));
|
|
1699
2827
|
if (isSourceFile(rel)) files.push(rel);
|
|
1700
2828
|
}
|
|
1701
2829
|
};
|
|
@@ -1839,8 +2967,8 @@ function getStackReason(memory, activeArchitectures2) {
|
|
|
1839
2967
|
}
|
|
1840
2968
|
function inferProjectArchitectures(cwd = process.cwd(), config2) {
|
|
1841
2969
|
const inferred = /* @__PURE__ */ new Set();
|
|
1842
|
-
if (config2?.backendArchitecture) inferred.add(config2.backendArchitecture);
|
|
1843
|
-
if (config2?.frontendFramework) inferred.add(config2.frontendFramework);
|
|
2970
|
+
if (config2?.backendArchitecture && config2.backendArchitecture !== "custom") inferred.add(config2.backendArchitecture);
|
|
2971
|
+
if (config2?.frontendFramework && config2.frontendFramework !== "custom") inferred.add(config2.frontendFramework);
|
|
1844
2972
|
if (config2?.projectType === "backend" && !config2.backendArchitecture) {
|
|
1845
2973
|
inferred.add("clean-architecture");
|
|
1846
2974
|
}
|
|
@@ -1933,8 +3061,8 @@ async function retrieveMemorySelection(options) {
|
|
|
1933
3061
|
|
|
1934
3062
|
// src/generator.ts
|
|
1935
3063
|
var __filename = fileURLToPath(import.meta.url);
|
|
1936
|
-
var __dirname =
|
|
1937
|
-
var PKG_ROOT =
|
|
3064
|
+
var __dirname = dirname4(__filename);
|
|
3065
|
+
var PKG_ROOT = join9(__dirname, "..");
|
|
1938
3066
|
function stringifyProfileScalar(value) {
|
|
1939
3067
|
if (typeof value === "string") {
|
|
1940
3068
|
const trimmed = value.trim();
|
|
@@ -2022,15 +3150,18 @@ Handlebars.registerHelper("memoryBlock", (memory) => {
|
|
|
2022
3150
|
return new Handlebars.SafeString(lines.join("\n"));
|
|
2023
3151
|
});
|
|
2024
3152
|
function loadProfile(name) {
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
3153
|
+
if (name === "custom") {
|
|
3154
|
+
return { name: "custom", displayName: "Custom", layer: "backend", description: "Custom architecture \u2014 rules added via memory-core remember", rules: [], folders: [], avoid: [] };
|
|
3155
|
+
}
|
|
3156
|
+
const profilePath = join9(PKG_ROOT, "profiles", `${name}.yml`);
|
|
3157
|
+
if (!existsSync8(profilePath)) throw new Error(`Profile not found: ${name}`);
|
|
3158
|
+
return normalizeArchitectureProfile(yaml.load(readFileSync7(profilePath, "utf-8")), name);
|
|
2028
3159
|
}
|
|
2029
3160
|
function listProfiles(layer) {
|
|
2030
|
-
const files =
|
|
3161
|
+
const files = readdirSync3(join9(PKG_ROOT, "profiles")).filter((f) => f.endsWith(".yml"));
|
|
2031
3162
|
const all = files.map(
|
|
2032
3163
|
(f) => normalizeArchitectureProfile(
|
|
2033
|
-
yaml.load(
|
|
3164
|
+
yaml.load(readFileSync7(join9(PKG_ROOT, "profiles", f), "utf-8")),
|
|
2034
3165
|
basename(f, ".yml")
|
|
2035
3166
|
)
|
|
2036
3167
|
);
|
|
@@ -2100,382 +3231,289 @@ function buildTemplateData(options, cwd = process.cwd()) {
|
|
|
2100
3231
|
};
|
|
2101
3232
|
}
|
|
2102
3233
|
function renderTemplate(templateName, data) {
|
|
2103
|
-
const templatePath =
|
|
2104
|
-
if (!
|
|
2105
|
-
return Handlebars.compile(
|
|
3234
|
+
const templatePath = join9(PKG_ROOT, "templates", templateName);
|
|
3235
|
+
if (!existsSync8(templatePath)) throw new Error(`Template not found: ${templateName}`);
|
|
3236
|
+
return Handlebars.compile(readFileSync7(templatePath, "utf-8"))(data);
|
|
2106
3237
|
}
|
|
2107
3238
|
function writeFile(filePath, content) {
|
|
2108
|
-
const dir =
|
|
2109
|
-
if (!
|
|
2110
|
-
if (
|
|
2111
|
-
const existing =
|
|
3239
|
+
const dir = dirname4(filePath);
|
|
3240
|
+
if (!existsSync8(dir)) mkdirSync2(dir, { recursive: true });
|
|
3241
|
+
if (existsSync8(filePath)) {
|
|
3242
|
+
const existing = readFileSync7(filePath, "utf-8");
|
|
2112
3243
|
if (existing === content) return "skipped";
|
|
2113
3244
|
}
|
|
2114
|
-
|
|
3245
|
+
writeFileSync4(filePath, content, "utf-8");
|
|
2115
3246
|
return "written";
|
|
2116
3247
|
}
|
|
2117
3248
|
async function generate(options, cwd = process.cwd(), onlyAgents) {
|
|
2118
|
-
const data = buildTemplateData(options, cwd);
|
|
2119
|
-
const written = [];
|
|
2120
|
-
const skipped = [];
|
|
2121
|
-
const files = onlyAgents ? OUTPUT_FILES.filter((f) => onlyAgents.includes(f.agent)) : OUTPUT_FILES;
|
|
2122
|
-
for (const output of files) {
|
|
2123
|
-
const targetPath =
|
|
2124
|
-
if (output.skipIfExists &&
|
|
2125
|
-
skipped.push(output.path);
|
|
2126
|
-
continue;
|
|
2127
|
-
}
|
|
2128
|
-
try {
|
|
2129
|
-
const content = renderTemplate(output.template, data);
|
|
2130
|
-
const result = writeFile(targetPath, content);
|
|
2131
|
-
if (result === "written") written.push(output.path);
|
|
2132
|
-
else skipped.push(output.path);
|
|
2133
|
-
} catch (err) {
|
|
2134
|
-
if (!(err instanceof Error && err.message.includes("Template not found"))) throw err;
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
return { written, skipped };
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
|
-
// src/modules/rule-engine/infrastructure/ast-deterministic-violations.ts
|
|
2141
|
-
import { resolve as resolve3 } from "path";
|
|
2142
|
-
function asPosix5(value) {
|
|
2143
|
-
return value.replace(/\\/g, "/");
|
|
2144
|
-
}
|
|
2145
|
-
function hasPath(value, pathSegment) {
|
|
2146
|
-
const normalized = asPosix5(value);
|
|
2147
|
-
const trimmed = pathSegment.startsWith("/") ? pathSegment.slice(1) : pathSegment;
|
|
2148
|
-
return normalized.includes(pathSegment) || normalized.includes(trimmed);
|
|
2149
|
-
}
|
|
2150
|
-
function isLegacyOrCompatibilitySpecifier(specifier) {
|
|
2151
|
-
const normalized = asPosix5(specifier);
|
|
2152
|
-
return normalized.includes("/compatibility/") || normalized.includes("compatibility/") || normalized.includes("legacy-");
|
|
2153
|
-
}
|
|
2154
|
-
function isLegacyOrCompatibilityPath(pathValue) {
|
|
2155
|
-
if (!pathValue) return false;
|
|
2156
|
-
const normalized = asPosix5(pathValue);
|
|
2157
|
-
return normalized.includes("/src/compatibility/") || normalized.includes("/legacy-");
|
|
2158
|
-
}
|
|
2159
|
-
function moduleNameFromPath2(pathValue) {
|
|
2160
|
-
const match = asPosix5(pathValue).match(/src\/modules\/([^/]+)\//);
|
|
2161
|
-
return match?.[1];
|
|
2162
|
-
}
|
|
2163
|
-
function isModulePublicPath(pathValue) {
|
|
2164
|
-
const normalized = asPosix5(pathValue);
|
|
2165
|
-
return /src\/modules\/[^/]+\/(public|api)\//.test(normalized) || /src\/modules\/[^/]+\/index\.(ts|tsx|js|jsx|mjs|cjs)$/.test(normalized);
|
|
2166
|
-
}
|
|
2167
|
-
function detectCleanLayer(pathValue) {
|
|
2168
|
-
const normalized = asPosix5(pathValue);
|
|
2169
|
-
if (hasPath(normalized, "/src/domain/") || hasPath(normalized, "/src/core/domain/")) return "domain";
|
|
2170
|
-
if (hasPath(normalized, "/src/application/") || hasPath(normalized, "/src/core/application/")) return "application";
|
|
2171
|
-
if (hasPath(normalized, "/src/infrastructure/")) return "infrastructure";
|
|
2172
|
-
if (hasPath(normalized, "/src/interfaces/")) return "interface";
|
|
2173
|
-
return "unknown";
|
|
2174
|
-
}
|
|
2175
|
-
function detectHexLayer(pathValue) {
|
|
2176
|
-
const normalized = asPosix5(pathValue);
|
|
2177
|
-
if (hasPath(normalized, "/src/core/")) return "core";
|
|
2178
|
-
if (hasPath(normalized, "/src/adapters/inbound/")) return "adapter-inbound";
|
|
2179
|
-
if (hasPath(normalized, "/src/adapters/outbound/")) return "adapter-outbound";
|
|
2180
|
-
if (hasPath(normalized, "/src/adapters/")) return "adapter-other";
|
|
2181
|
-
return "unknown";
|
|
2182
|
-
}
|
|
2183
|
-
function activeArchitectures(config2, rules = []) {
|
|
2184
|
-
const names = /* @__PURE__ */ new Set();
|
|
2185
|
-
if (config2?.backendArchitecture) names.add(config2.backendArchitecture);
|
|
2186
|
-
if (config2?.frontendFramework) names.add(config2.frontendFramework);
|
|
2187
|
-
const text = rules.join("\n").toLowerCase();
|
|
2188
|
-
if (text.includes("modular monolith")) names.add("modular-monolith");
|
|
2189
|
-
if (text.includes("clean architecture")) names.add("clean-architecture");
|
|
2190
|
-
if (text.includes("hexagonal")) names.add("hexagonal");
|
|
2191
|
-
return names;
|
|
2192
|
-
}
|
|
2193
|
-
function pushUnique(target, incoming) {
|
|
2194
|
-
if (target.some(
|
|
2195
|
-
(entry) => entry.rule === incoming.rule && entry.file === incoming.file && entry.line === incoming.line && entry.issue === incoming.issue
|
|
2196
|
-
)) {
|
|
2197
|
-
return;
|
|
2198
|
-
}
|
|
2199
|
-
target.push(incoming);
|
|
2200
|
-
}
|
|
2201
|
-
function evaluateFile(file, options) {
|
|
2202
|
-
const cwd = options.cwd ?? process.cwd();
|
|
2203
|
-
const rules = options.rules ?? [];
|
|
2204
|
-
const architectures = activeArchitectures(options.config, rules);
|
|
2205
|
-
const reasonLookup = options.reasonLookup ?? /* @__PURE__ */ new Map();
|
|
2206
|
-
const violations = [];
|
|
2207
|
-
const absFile = resolve3(cwd, file);
|
|
2208
|
-
const normalizedFile = asPosix5(file);
|
|
2209
|
-
const imports = collectResolvedImports(absFile, cwd);
|
|
2210
|
-
const fromCleanLayer = detectCleanLayer(normalizedFile);
|
|
2211
|
-
const fromHexLayer = detectHexLayer(normalizedFile);
|
|
2212
|
-
for (const imp of imports) {
|
|
2213
|
-
const target = imp.resolvedPath ? asPosix5(imp.resolvedPath) : void 0;
|
|
2214
|
-
if (isLegacyOrCompatibilitySpecifier(imp.specifier) || isLegacyOrCompatibilityPath(target)) {
|
|
2215
|
-
const rule = "Application code must not import compatibility or legacy adapter paths";
|
|
2216
|
-
pushUnique(violations, {
|
|
2217
|
-
rule,
|
|
2218
|
-
file,
|
|
2219
|
-
line: imp.line,
|
|
2220
|
-
issue: `Import references a removed migration path: ${imp.specifier}`,
|
|
2221
|
-
suggestion: "Import module services/ports from src/app, src/modules, src/shared/ports, or current infrastructure adapters.",
|
|
2222
|
-
reason: reasonLookup.get(rule)
|
|
2223
|
-
});
|
|
2224
|
-
}
|
|
2225
|
-
if (architectures.has("modular-monolith")) {
|
|
2226
|
-
const fromModule = moduleNameFromPath2(normalizedFile);
|
|
2227
|
-
const toModule = target ? moduleNameFromPath2(target) : void 0;
|
|
2228
|
-
if (fromModule && toModule && target && fromModule !== toModule && !isModulePublicPath(target)) {
|
|
2229
|
-
const rule = "Modules communicate only through public interfaces or events \u2014 never by importing internals";
|
|
2230
|
-
pushUnique(violations, {
|
|
2231
|
-
rule,
|
|
2232
|
-
file,
|
|
2233
|
-
line: imp.line,
|
|
2234
|
-
issue: `Cross-module import from "${fromModule}" to private path in module "${toModule}"`,
|
|
2235
|
-
suggestion: `Expose a public API from src/modules/${toModule}/index.ts (or public/) and import through it.`,
|
|
2236
|
-
reason: reasonLookup.get(rule)
|
|
2237
|
-
});
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
if (architectures.has("clean-architecture")) {
|
|
2241
|
-
const toCleanLayer = target ? detectCleanLayer(target) : "unknown";
|
|
2242
|
-
if (fromCleanLayer === "domain" && ["application", "infrastructure", "interface"].includes(toCleanLayer)) {
|
|
2243
|
-
const rule = "Entities encapsulate core business logic and have no external dependencies";
|
|
2244
|
-
pushUnique(violations, {
|
|
2245
|
-
rule,
|
|
2246
|
-
file,
|
|
2247
|
-
line: imp.line,
|
|
2248
|
-
issue: `Domain layer imports ${toCleanLayer} layer: ${imp.specifier}`,
|
|
2249
|
-
suggestion: "Keep domain isolated and move orchestration concerns into application layer.",
|
|
2250
|
-
reason: reasonLookup.get(rule)
|
|
2251
|
-
});
|
|
2252
|
-
}
|
|
2253
|
-
if (fromCleanLayer === "application" && (toCleanLayer === "infrastructure" || toCleanLayer === "interface")) {
|
|
2254
|
-
const rule = "Infrastructure layer (DB, HTTP, queues) depends on application \u2014 never the reverse";
|
|
2255
|
-
pushUnique(violations, {
|
|
2256
|
-
rule,
|
|
2257
|
-
file,
|
|
2258
|
-
line: imp.line,
|
|
2259
|
-
issue: `Application layer imports ${toCleanLayer} layer: ${imp.specifier}`,
|
|
2260
|
-
suggestion: "Invert dependency via repository/port interface in application layer.",
|
|
2261
|
-
reason: reasonLookup.get(rule)
|
|
2262
|
-
});
|
|
2263
|
-
}
|
|
2264
|
-
if (fromCleanLayer === "interface" && toCleanLayer === "infrastructure") {
|
|
2265
|
-
const rule = "Controllers must only validate input and delegate to use cases";
|
|
2266
|
-
pushUnique(violations, {
|
|
2267
|
-
rule,
|
|
2268
|
-
file,
|
|
2269
|
-
line: imp.line,
|
|
2270
|
-
issue: `Interface/controller layer imports infrastructure directly: ${imp.specifier}`,
|
|
2271
|
-
suggestion: "Delegate to application use cases instead of calling infrastructure directly.",
|
|
2272
|
-
reason: reasonLookup.get(rule)
|
|
2273
|
-
});
|
|
2274
|
-
}
|
|
2275
|
-
if (fromCleanLayer === "domain" && imp.isExternal && isExternalFrameworkSpecifier(imp.specifier)) {
|
|
2276
|
-
const rule = "Domain layer must not import any framework or library code";
|
|
2277
|
-
pushUnique(violations, {
|
|
2278
|
-
rule,
|
|
2279
|
-
file,
|
|
2280
|
-
line: imp.line,
|
|
2281
|
-
issue: `Domain file imports framework package: ${imp.specifier}`,
|
|
2282
|
-
suggestion: "Keep domain pure. Move framework-specific logic to infrastructure/adapters.",
|
|
2283
|
-
reason: reasonLookup.get(rule)
|
|
2284
|
-
});
|
|
2285
|
-
}
|
|
2286
|
-
}
|
|
2287
|
-
if (architectures.has("hexagonal")) {
|
|
2288
|
-
const toHexLayer = target ? detectHexLayer(target) : "unknown";
|
|
2289
|
-
if (fromHexLayer === "core" && (toHexLayer === "adapter-inbound" || toHexLayer === "adapter-outbound" || toHexLayer === "adapter-other")) {
|
|
2290
|
-
const rule = "Direct imports of adapter code inside the core";
|
|
2291
|
-
pushUnique(violations, {
|
|
2292
|
-
rule,
|
|
2293
|
-
file,
|
|
2294
|
-
line: imp.line,
|
|
2295
|
-
issue: `Core imports adapter path directly: ${imp.specifier}`,
|
|
2296
|
-
suggestion: "Define a core port and resolve adapter at composition root.",
|
|
2297
|
-
reason: reasonLookup.get(rule)
|
|
2298
|
-
});
|
|
2299
|
-
}
|
|
2300
|
-
const crossAdapterBoundary = fromHexLayer === "adapter-inbound" && (toHexLayer === "adapter-outbound" || toHexLayer === "adapter-other") || fromHexLayer === "adapter-outbound" && (toHexLayer === "adapter-inbound" || toHexLayer === "adapter-other");
|
|
2301
|
-
if (crossAdapterBoundary) {
|
|
2302
|
-
const rule = "Adapters implement ports \u2014 one adapter per external system (DB, HTTP, queue, etc.)";
|
|
2303
|
-
pushUnique(violations, {
|
|
2304
|
-
rule,
|
|
2305
|
-
file,
|
|
2306
|
-
line: imp.line,
|
|
2307
|
-
issue: `Adapter imports another adapter layer directly: ${imp.specifier}`,
|
|
2308
|
-
suggestion: "Route adapter collaboration through core ports/use-cases, not direct adapter imports.",
|
|
2309
|
-
reason: reasonLookup.get(rule)
|
|
2310
|
-
});
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
}
|
|
2314
|
-
return violations;
|
|
2315
|
-
}
|
|
2316
|
-
function findAstDeterministicViolationsForFile(file, options = {}) {
|
|
2317
|
-
return evaluateFile(file, options);
|
|
2318
|
-
}
|
|
2319
|
-
function findAstDeterministicViolationsForDiff(diff, options = {}) {
|
|
2320
|
-
const files = parseChangedFilesFromDiff(diff).filter((file) => /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file));
|
|
2321
|
-
const violations = [];
|
|
2322
|
-
for (const file of files) {
|
|
2323
|
-
for (const violation of evaluateFile(file, options)) {
|
|
2324
|
-
pushUnique(violations, violation);
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2327
|
-
const architectures = activeArchitectures(options.config, options.rules ?? []);
|
|
2328
|
-
if (architectures.has("modular-monolith")) {
|
|
2329
|
-
const edges = buildModuleDependencyEdges(files, options.cwd ?? process.cwd());
|
|
2330
|
-
const cycles = detectModuleCycles(edges);
|
|
2331
|
-
for (const cycle of cycles) {
|
|
2332
|
-
const representative = edges.find((edge) => edge.fromModule === cycle[0] && edge.toModule === cycle[1]);
|
|
2333
|
-
if (!representative) continue;
|
|
2334
|
-
const rule = "No circular dependencies between modules";
|
|
2335
|
-
pushUnique(violations, {
|
|
2336
|
-
rule,
|
|
2337
|
-
file: representative.file,
|
|
2338
|
-
line: representative.line,
|
|
2339
|
-
issue: `Module dependency cycle detected: ${cycle.join(" -> ")}`,
|
|
2340
|
-
suggestion: "Break the cycle by introducing a public port/event or moving shared logic into src/shared.",
|
|
2341
|
-
reason: options.reasonLookup?.get(rule)
|
|
2342
|
-
});
|
|
3249
|
+
const data = buildTemplateData(options, cwd);
|
|
3250
|
+
const written = [];
|
|
3251
|
+
const skipped = [];
|
|
3252
|
+
const files = onlyAgents ? OUTPUT_FILES.filter((f) => onlyAgents.includes(f.agent)) : OUTPUT_FILES;
|
|
3253
|
+
for (const output of files) {
|
|
3254
|
+
const targetPath = join9(cwd, output.path);
|
|
3255
|
+
if (output.skipIfExists && existsSync8(targetPath)) {
|
|
3256
|
+
skipped.push(output.path);
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
try {
|
|
3260
|
+
const content = renderTemplate(output.template, data);
|
|
3261
|
+
const result = writeFile(targetPath, content);
|
|
3262
|
+
if (result === "written") written.push(output.path);
|
|
3263
|
+
else skipped.push(output.path);
|
|
3264
|
+
} catch (err) {
|
|
3265
|
+
if (!(err instanceof Error && err.message.includes("Template not found"))) throw err;
|
|
2343
3266
|
}
|
|
2344
3267
|
}
|
|
2345
|
-
return
|
|
3268
|
+
return { written, skipped };
|
|
2346
3269
|
}
|
|
2347
3270
|
|
|
2348
|
-
// src/
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
3271
|
+
// src/hook.ts
|
|
3272
|
+
var reasonMap2 = new Map(
|
|
3273
|
+
seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
|
|
3274
|
+
);
|
|
3275
|
+
var HOOK_PATH = join10(".git", "hooks", "pre-commit");
|
|
3276
|
+
var HOOK_MARKER = "# archmind-memory-core";
|
|
3277
|
+
var COMMIT_MSG_HOOK_PATH = join10(".git", "hooks", "commit-msg");
|
|
3278
|
+
var COMMIT_MSG_HOOK_MARKER = "# archmind-memory-core commit-msg";
|
|
3279
|
+
var RULE_CACHE_FILE = ".memory-core-rules-cache.json";
|
|
3280
|
+
var DB_VERSION_FILE = ".memory-core-db-version";
|
|
3281
|
+
var RULE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
3282
|
+
function buildHookBody(advisory, fast = false) {
|
|
3283
|
+
const suffix = advisory ? " || true" : "";
|
|
3284
|
+
const checkArgs = fast ? "check --staged --fast" : "check --staged";
|
|
3285
|
+
return `${HOOK_MARKER}${advisory ? " advisory" : ""}
|
|
3286
|
+
if [ "\${MEMORY_CORE_SKIP_HOOK:-}" = "1" ] || [ "\${ARCHMIND_SKIP_HOOK:-}" = "1" ] || [ "\${HUSKY:-}" = "0" ] || [ "\${HUSKY_SKIP_HOOKS:-}" = "1" ]; then
|
|
3287
|
+
if command -v memory-core >/dev/null 2>&1 && [ -t 1 ]; then
|
|
3288
|
+
memory-core hook bypass-prompt
|
|
3289
|
+
fi
|
|
3290
|
+
exit 0
|
|
3291
|
+
fi
|
|
3292
|
+
if [ -n "\${SKIP:-}" ] && echo ",$SKIP," | grep -qiE ',(memory-core|archmind),'; then
|
|
3293
|
+
if command -v memory-core >/dev/null 2>&1 && [ -t 1 ]; then
|
|
3294
|
+
memory-core hook bypass-prompt
|
|
3295
|
+
fi
|
|
3296
|
+
exit 0
|
|
3297
|
+
fi
|
|
3298
|
+
if [ -n "\${SKIP_HOOKS:-}" ]; then
|
|
3299
|
+
exit 0
|
|
3300
|
+
fi
|
|
3301
|
+
if command -v memory-core >/dev/null 2>&1; then
|
|
3302
|
+
memory-core ${checkArgs}${suffix}
|
|
3303
|
+
elif [ -f "./node_modules/.bin/memory-core" ]; then
|
|
3304
|
+
./node_modules/.bin/memory-core ${checkArgs}${suffix}
|
|
3305
|
+
elif [ -f "./dist/cli.js" ]; then
|
|
3306
|
+
node ./dist/cli.js ${checkArgs}${suffix}
|
|
3307
|
+
else
|
|
3308
|
+
exit 0
|
|
3309
|
+
fi
|
|
3310
|
+
`;
|
|
2355
3311
|
}
|
|
2356
|
-
function
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
3312
|
+
function buildHookScript(advisory, fast = false) {
|
|
3313
|
+
return `#!/bin/sh
|
|
3314
|
+
|
|
3315
|
+
${buildHookBody(advisory, fast)}`;
|
|
3316
|
+
}
|
|
3317
|
+
function normalizeHookPreamble(content) {
|
|
3318
|
+
const lines = content.split("\n");
|
|
3319
|
+
const normalized = [];
|
|
3320
|
+
let shebangSeen = false;
|
|
3321
|
+
for (const line of lines) {
|
|
3322
|
+
if (/^\s*#!\/bin\/sh\s*$/.test(line)) {
|
|
3323
|
+
if (shebangSeen) continue;
|
|
3324
|
+
shebangSeen = true;
|
|
3325
|
+
normalized.push("#!/bin/sh");
|
|
3326
|
+
continue;
|
|
2369
3327
|
}
|
|
3328
|
+
normalized.push(line);
|
|
2370
3329
|
}
|
|
2371
|
-
|
|
3330
|
+
return normalized.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
2372
3331
|
}
|
|
2373
|
-
function
|
|
2374
|
-
|
|
2375
|
-
if (
|
|
2376
|
-
|
|
2377
|
-
const end = Math.min(lines.length - 1, line - 1 + contextLines);
|
|
2378
|
-
return Array.from({ length: end - start + 1 }, (_, index) => {
|
|
2379
|
-
const current = start + index;
|
|
2380
|
-
const lineNum = String(current + 1).padStart(4, " ");
|
|
2381
|
-
const marker = current === line - 1 ? ">" : " ";
|
|
2382
|
-
return `${lineNum} ${marker} ${lines[current]}`;
|
|
2383
|
-
}).join("\n");
|
|
3332
|
+
function toRuleStatEntry(raw) {
|
|
3333
|
+
if (raw === void 0) return { count: 0, falsePositives: 0 };
|
|
3334
|
+
if (typeof raw === "number") return { count: raw, falsePositives: 0 };
|
|
3335
|
+
return raw;
|
|
2384
3336
|
}
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
function
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
3337
|
+
function readPositiveIntEnv2(name, fallback) {
|
|
3338
|
+
const raw = Number(process.env[name]);
|
|
3339
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
|
|
3340
|
+
}
|
|
3341
|
+
function isFastCheck(options) {
|
|
3342
|
+
return options.fast === true || process.env.MEMORY_CORE_CHECK_FAST === "1";
|
|
3343
|
+
}
|
|
3344
|
+
async function withTimeout(promise, timeoutMs, fallback) {
|
|
3345
|
+
let timer;
|
|
3346
|
+
try {
|
|
3347
|
+
return await Promise.race([
|
|
3348
|
+
promise,
|
|
3349
|
+
new Promise((resolve5) => {
|
|
3350
|
+
timer = setTimeout(() => resolve5(fallback), timeoutMs);
|
|
3351
|
+
})
|
|
3352
|
+
]);
|
|
3353
|
+
} finally {
|
|
3354
|
+
if (timer) clearTimeout(timer);
|
|
2396
3355
|
}
|
|
2397
3356
|
}
|
|
2398
|
-
function
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
}
|
|
3357
|
+
function recordViolations(violations, source = "hook") {
|
|
3358
|
+
const statsPath = join10(process.cwd(), ".memory-core-stats.json");
|
|
3359
|
+
let stats = { rules: {}, files: {} };
|
|
3360
|
+
if (existsSync9(statsPath)) {
|
|
3361
|
+
try {
|
|
3362
|
+
stats = JSON.parse(readFileSync8(statsPath, "utf-8"));
|
|
3363
|
+
} catch {
|
|
3364
|
+
stats = { rules: {}, files: {} };
|
|
3365
|
+
}
|
|
2405
3366
|
}
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
const
|
|
2409
|
-
|
|
3367
|
+
stats.rules ??= {};
|
|
3368
|
+
stats.files ??= {};
|
|
3369
|
+
for (const violation of violations) {
|
|
3370
|
+
const existing = toRuleStatEntry(stats.rules[violation.rule]);
|
|
3371
|
+
stats.rules[violation.rule] = { count: existing.count + 1, falsePositives: existing.falsePositives };
|
|
3372
|
+
if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
|
|
3373
|
+
}
|
|
3374
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3375
|
+
const recent = violations.map((violation) => ({ ...violation, timestamp, source }));
|
|
3376
|
+
stats.recentViolations = [...recent, ...stats.recentViolations ?? []].slice(0, 50);
|
|
3377
|
+
writeFileSync5(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
2410
3378
|
}
|
|
2411
|
-
function
|
|
2412
|
-
|
|
3379
|
+
function resetViolationStats(cwd = process.cwd()) {
|
|
3380
|
+
const statsPath = join10(cwd, ".memory-core-stats.json");
|
|
3381
|
+
if (!existsSync9(statsPath)) return;
|
|
3382
|
+
let stats = {};
|
|
2413
3383
|
try {
|
|
2414
|
-
|
|
3384
|
+
const parsed = JSON.parse(readFileSync8(statsPath, "utf-8"));
|
|
3385
|
+
if (parsed && typeof parsed === "object") {
|
|
3386
|
+
stats = parsed;
|
|
3387
|
+
}
|
|
2415
3388
|
} catch {
|
|
2416
|
-
|
|
3389
|
+
stats = {};
|
|
2417
3390
|
}
|
|
3391
|
+
stats.rules = {};
|
|
3392
|
+
stats.files = {};
|
|
3393
|
+
stats.recentViolations = [];
|
|
3394
|
+
writeFileSync5(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
2418
3395
|
}
|
|
2419
|
-
function
|
|
2420
|
-
const
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
rules[violation.rule] = (rules[violation.rule] ?? 0) + 1;
|
|
3396
|
+
function recordBypass(hadReason, cwd = process.cwd()) {
|
|
3397
|
+
const statsPath = join10(cwd, ".memory-core-stats.json");
|
|
3398
|
+
let stats = { rules: {}, files: {} };
|
|
3399
|
+
if (existsSync9(statsPath)) {
|
|
3400
|
+
try {
|
|
3401
|
+
stats = JSON.parse(readFileSync8(statsPath, "utf-8"));
|
|
3402
|
+
} catch {
|
|
2427
3403
|
}
|
|
2428
3404
|
}
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
stats.rules ??= {};
|
|
2435
|
-
stats.files ??= {};
|
|
2436
|
-
stats.live = {
|
|
2437
|
-
rules: {},
|
|
2438
|
-
files: {},
|
|
2439
|
-
byFile: {}
|
|
3405
|
+
const prev = stats.bypasses ?? { total: 0, withReason: 0, withoutReason: 0 };
|
|
3406
|
+
stats.bypasses = {
|
|
3407
|
+
total: prev.total + 1,
|
|
3408
|
+
withReason: prev.withReason + (hadReason ? 1 : 0),
|
|
3409
|
+
withoutReason: prev.withoutReason + (hadReason ? 0 : 1)
|
|
2440
3410
|
};
|
|
2441
|
-
|
|
3411
|
+
writeFileSync5(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
3412
|
+
return stats.bypasses;
|
|
2442
3413
|
}
|
|
2443
|
-
function
|
|
2444
|
-
const statsPath =
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
delete stats.live.byFile[file];
|
|
2452
|
-
} else {
|
|
2453
|
-
stats.live.byFile[file] = violations;
|
|
3414
|
+
function readBypassStats(cwd = process.cwd()) {
|
|
3415
|
+
const statsPath = join10(cwd, ".memory-core-stats.json");
|
|
3416
|
+
if (!existsSync9(statsPath)) return { total: 0, withReason: 0, withoutReason: 0 };
|
|
3417
|
+
try {
|
|
3418
|
+
const stats = JSON.parse(readFileSync8(statsPath, "utf-8"));
|
|
3419
|
+
return stats.bypasses ?? { total: 0, withReason: 0, withoutReason: 0 };
|
|
3420
|
+
} catch {
|
|
3421
|
+
return { total: 0, withReason: 0, withoutReason: 0 };
|
|
2454
3422
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
3423
|
+
}
|
|
3424
|
+
function accumulateTokenUsage(usage, cwd = process.cwd()) {
|
|
3425
|
+
if (!usage) return;
|
|
3426
|
+
const statsPath = join10(cwd, ".memory-core-stats.json");
|
|
3427
|
+
let stats = { rules: {}, files: {} };
|
|
3428
|
+
if (existsSync9(statsPath)) {
|
|
3429
|
+
try {
|
|
3430
|
+
stats = JSON.parse(readFileSync8(statsPath, "utf-8"));
|
|
3431
|
+
} catch {
|
|
3432
|
+
}
|
|
2461
3433
|
}
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
3434
|
+
const prev = stats.tokens ?? { calls: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
3435
|
+
stats.tokens = {
|
|
3436
|
+
calls: prev.calls + 1,
|
|
3437
|
+
inputTokens: prev.inputTokens + usage.inputTokens,
|
|
3438
|
+
outputTokens: prev.outputTokens + usage.outputTokens,
|
|
3439
|
+
totalTokens: prev.totalTokens + usage.totalTokens
|
|
3440
|
+
};
|
|
3441
|
+
writeFileSync5(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
3442
|
+
}
|
|
3443
|
+
async function promptToSaveViolations(violations) {
|
|
3444
|
+
if (!process.stdin.isTTY || violations.length === 0) return;
|
|
3445
|
+
try {
|
|
3446
|
+
const app = getDefaultApplicationContainer();
|
|
3447
|
+
const { confirm, input } = await import("@inquirer/prompts");
|
|
3448
|
+
const save = await confirm({
|
|
3449
|
+
message: "Save a caught violation as a project rule?",
|
|
3450
|
+
default: false
|
|
3451
|
+
});
|
|
3452
|
+
if (!save) return;
|
|
3453
|
+
const choices = violations.map((violation, index) => `${index + 1}. ${violation.rule}`);
|
|
3454
|
+
const selected = violations.length === 1 ? violations[0] : violations[Number(await input({ message: `Which violation? ${choices.join(" | ")}`, default: "1" })) - 1] ?? violations[0];
|
|
3455
|
+
const reason = await input({
|
|
3456
|
+
message: "Why should this rule exist?",
|
|
3457
|
+
default: selected.reason ?? selected.issue ?? ""
|
|
3458
|
+
});
|
|
3459
|
+
const storedReason = reason.trim() || selected.reason || selected.issue || `Captured from violation: ${selected.rule}`;
|
|
3460
|
+
await app.services.memoryEngine.remember({
|
|
3461
|
+
type: "rule",
|
|
3462
|
+
scope: "project",
|
|
3463
|
+
content: selected.rule,
|
|
3464
|
+
reason: storedReason,
|
|
3465
|
+
tags: ["violation"]
|
|
3466
|
+
});
|
|
3467
|
+
console.log(chalk2.green(" \u2713 Saved as project rule. Run memory-core sync to propagate it.\n"));
|
|
3468
|
+
} catch (err) {
|
|
3469
|
+
console.log(chalk2.yellow(` Could not save violation: ${err.message}
|
|
3470
|
+
`));
|
|
2466
3471
|
}
|
|
2467
|
-
writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
2468
3472
|
}
|
|
2469
|
-
function
|
|
2470
|
-
const
|
|
2471
|
-
|
|
3473
|
+
function readRuleCache(cwd) {
|
|
3474
|
+
const cachePath = join10(cwd, RULE_CACHE_FILE);
|
|
3475
|
+
const configPath = join10(cwd, ".memory-core.json");
|
|
3476
|
+
if (!existsSync9(cachePath) || !existsSync9(configPath)) return null;
|
|
2472
3477
|
try {
|
|
2473
|
-
|
|
3478
|
+
const entry = JSON.parse(readFileSync8(cachePath, "utf-8"));
|
|
3479
|
+
const now = Date.now();
|
|
3480
|
+
if (now - entry.timestamp > RULE_CACHE_TTL_MS) return null;
|
|
3481
|
+
const configMtime = statSync3(configPath).mtimeMs;
|
|
3482
|
+
if (configMtime !== entry.configMtime) return null;
|
|
3483
|
+
const dbVersionPath = join10(cwd, DB_VERSION_FILE);
|
|
3484
|
+
const dbVersionMtime = existsSync9(dbVersionPath) ? statSync3(dbVersionPath).mtimeMs : 0;
|
|
3485
|
+
if (dbVersionMtime !== entry.dbVersionMtime) return null;
|
|
3486
|
+
return entry;
|
|
2474
3487
|
} catch {
|
|
2475
3488
|
return null;
|
|
2476
3489
|
}
|
|
2477
3490
|
}
|
|
2478
|
-
function
|
|
3491
|
+
function saveRuleCache(cwd, data) {
|
|
3492
|
+
const configPath = join10(cwd, ".memory-core.json");
|
|
3493
|
+
try {
|
|
3494
|
+
const configMtime = statSync3(configPath).mtimeMs;
|
|
3495
|
+
const dbVersionPath = join10(cwd, DB_VERSION_FILE);
|
|
3496
|
+
const dbVersionMtime = existsSync9(dbVersionPath) ? statSync3(dbVersionPath).mtimeMs : 0;
|
|
3497
|
+
const entry = {
|
|
3498
|
+
timestamp: Date.now(),
|
|
3499
|
+
configMtime,
|
|
3500
|
+
dbVersionMtime,
|
|
3501
|
+
...data
|
|
3502
|
+
};
|
|
3503
|
+
writeFileSync5(join10(cwd, RULE_CACHE_FILE), JSON.stringify(entry, null, 2) + "\n", "utf-8");
|
|
3504
|
+
} catch {
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
async function loadIgnorePatterns2() {
|
|
3508
|
+
try {
|
|
3509
|
+
const app = getDefaultApplicationContainer();
|
|
3510
|
+
const ignores = await app.services.memoryEngine.list({ type: "ignore", limit: 1e3 });
|
|
3511
|
+
return ignores.map((ignore) => ignore.content);
|
|
3512
|
+
} catch {
|
|
3513
|
+
return [];
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
function getProfileRules2(config2) {
|
|
2479
3517
|
const rules = [];
|
|
2480
3518
|
const avoids = [];
|
|
2481
3519
|
if (config2.backendArchitecture) {
|
|
@@ -2494,10 +3532,10 @@ function getProfileRules(config2) {
|
|
|
2494
3532
|
}
|
|
2495
3533
|
return { rules, avoids };
|
|
2496
3534
|
}
|
|
2497
|
-
async function
|
|
3535
|
+
async function loadRelevantRules2(config2, diff, stagedFiles, fallbackRules) {
|
|
2498
3536
|
try {
|
|
2499
3537
|
const query = buildContextQuery([
|
|
2500
|
-
|
|
3538
|
+
stagedFiles.join("\n"),
|
|
2501
3539
|
diff.slice(0, 1200),
|
|
2502
3540
|
config2.backendArchitecture,
|
|
2503
3541
|
config2.frontendFramework,
|
|
@@ -2505,7 +3543,7 @@ async function loadRelevantRules(cwd, config2, rel, diff, fallbackRules) {
|
|
|
2505
3543
|
]);
|
|
2506
3544
|
const memories = await retrieveContextualMemories({
|
|
2507
3545
|
query,
|
|
2508
|
-
cwd,
|
|
3546
|
+
cwd: process.cwd(),
|
|
2509
3547
|
config: config2,
|
|
2510
3548
|
limit: 15
|
|
2511
3549
|
});
|
|
@@ -2515,7 +3553,7 @@ async function loadRelevantRules(cwd, config2, rel, diff, fallbackRules) {
|
|
|
2515
3553
|
return fallbackRules;
|
|
2516
3554
|
}
|
|
2517
3555
|
}
|
|
2518
|
-
function
|
|
3556
|
+
function applyAllowPatterns2(violations, allowPatterns) {
|
|
2519
3557
|
if (allowPatterns.length === 0) return violations;
|
|
2520
3558
|
return violations.filter((violation) => {
|
|
2521
3559
|
const haystack = `${violation.rule}
|
|
@@ -2524,193 +3562,651 @@ ${violation.file}`.toLowerCase();
|
|
|
2524
3562
|
return !allowPatterns.some((pattern) => haystack.includes(pattern.toLowerCase()));
|
|
2525
3563
|
});
|
|
2526
3564
|
}
|
|
2527
|
-
|
|
2528
|
-
if (
|
|
2529
|
-
const
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
3565
|
+
function normalizeViolation(value) {
|
|
3566
|
+
if (!value || typeof value !== "object") return null;
|
|
3567
|
+
const candidate = value;
|
|
3568
|
+
if (typeof candidate.rule !== "string" || typeof candidate.issue !== "string") return null;
|
|
3569
|
+
return {
|
|
3570
|
+
rule: candidate.rule,
|
|
3571
|
+
file: typeof candidate.file === "string" ? candidate.file : "diff",
|
|
3572
|
+
line: typeof candidate.line === "number" ? candidate.line : void 0,
|
|
3573
|
+
issue: candidate.issue,
|
|
3574
|
+
suggestion: typeof candidate.suggestion === "string" ? candidate.suggestion : void 0,
|
|
3575
|
+
reason: typeof candidate.reason === "string" ? candidate.reason : void 0
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
function parseModelViolations(raw) {
|
|
3579
|
+
const candidates = [
|
|
3580
|
+
raw,
|
|
3581
|
+
raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "")
|
|
3582
|
+
];
|
|
3583
|
+
const objectStart = raw.indexOf("{");
|
|
3584
|
+
const objectEnd = raw.lastIndexOf("}");
|
|
3585
|
+
if (objectStart !== -1 && objectEnd > objectStart) {
|
|
3586
|
+
candidates.push(raw.slice(objectStart, objectEnd + 1));
|
|
3587
|
+
}
|
|
3588
|
+
const arrayStart = raw.indexOf("[");
|
|
3589
|
+
const arrayEnd = raw.lastIndexOf("]");
|
|
3590
|
+
if (arrayStart !== -1 && arrayEnd > arrayStart) {
|
|
3591
|
+
candidates.push(raw.slice(arrayStart, arrayEnd + 1));
|
|
3592
|
+
}
|
|
3593
|
+
for (const candidate of candidates) {
|
|
3594
|
+
try {
|
|
3595
|
+
const parsed = JSON.parse(candidate);
|
|
3596
|
+
const items = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.violations) ? parsed.violations : parsed?.rule ? [parsed] : null;
|
|
3597
|
+
if (!items) continue;
|
|
3598
|
+
return {
|
|
3599
|
+
valid: true,
|
|
3600
|
+
violations: items.map(normalizeViolation).filter((violation) => violation !== null)
|
|
3601
|
+
};
|
|
3602
|
+
} catch {
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
return { valid: false, violations: [] };
|
|
3606
|
+
}
|
|
3607
|
+
function getAddedLines(diff) {
|
|
3608
|
+
const lines = [];
|
|
3609
|
+
let currentFile = "diff";
|
|
3610
|
+
let newLineNumber = 0;
|
|
3611
|
+
for (const line of diff.split("\n")) {
|
|
3612
|
+
if (line.startsWith("+++ b/")) {
|
|
3613
|
+
currentFile = line.slice("+++ b/".length);
|
|
3614
|
+
continue;
|
|
3615
|
+
}
|
|
3616
|
+
const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
3617
|
+
if (hunk) {
|
|
3618
|
+
newLineNumber = Number(hunk[1]);
|
|
3619
|
+
continue;
|
|
3620
|
+
}
|
|
3621
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
3622
|
+
lines.push({
|
|
3623
|
+
file: currentFile,
|
|
3624
|
+
line: Number.isFinite(newLineNumber) ? newLineNumber : void 0,
|
|
3625
|
+
content: line.slice(1)
|
|
3626
|
+
});
|
|
3627
|
+
newLineNumber += 1;
|
|
3628
|
+
continue;
|
|
3629
|
+
}
|
|
3630
|
+
if (!line.startsWith("-") && newLineNumber > 0) {
|
|
3631
|
+
newLineNumber += 1;
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
return lines;
|
|
3635
|
+
}
|
|
3636
|
+
function dedupeViolations(violations) {
|
|
3637
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3638
|
+
const deduped = [];
|
|
3639
|
+
for (const violation of violations) {
|
|
3640
|
+
const key = [
|
|
3641
|
+
violation.rule,
|
|
3642
|
+
violation.file,
|
|
3643
|
+
violation.line ?? "",
|
|
3644
|
+
violation.issue
|
|
3645
|
+
].join("\0");
|
|
3646
|
+
if (seen.has(key)) continue;
|
|
3647
|
+
seen.add(key);
|
|
3648
|
+
deduped.push(violation);
|
|
3649
|
+
}
|
|
3650
|
+
return deduped;
|
|
3651
|
+
}
|
|
3652
|
+
function normalizeKeyPath(value) {
|
|
3653
|
+
return value.replace(/\\/g, "/").replace(/^\.\/+/, "").toLowerCase();
|
|
3654
|
+
}
|
|
3655
|
+
function violationRecurrenceKey(violation) {
|
|
3656
|
+
return [
|
|
3657
|
+
violation.rule.trim().toLowerCase(),
|
|
3658
|
+
normalizeKeyPath(violation.file || "diff"),
|
|
3659
|
+
violation.issue.trim().toLowerCase()
|
|
3660
|
+
].join("\0");
|
|
3661
|
+
}
|
|
3662
|
+
function findRecurringViolations(currentViolations, recentViolations, minCount = 2) {
|
|
3663
|
+
if (currentViolations.length === 0 || recentViolations.length === 0) return [];
|
|
3664
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3665
|
+
for (const recent of recentViolations) {
|
|
3666
|
+
const key = violationRecurrenceKey(recent);
|
|
3667
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
3668
|
+
}
|
|
3669
|
+
return currentViolations.filter((violation) => (counts.get(violationRecurrenceKey(violation)) ?? 0) >= minCount);
|
|
3670
|
+
}
|
|
3671
|
+
function extractIssuePhrase(issue) {
|
|
3672
|
+
const quoted = issue.match(/"([^"]{3,160})"/);
|
|
3673
|
+
if (quoted?.[1]) return quoted[1].trim();
|
|
3674
|
+
const afterColon = issue.split(":").slice(1).join(":").trim();
|
|
3675
|
+
if (afterColon.length >= 3) return afterColon.slice(0, 180);
|
|
3676
|
+
const fallback = issue.trim();
|
|
3677
|
+
return fallback.length >= 3 ? fallback.slice(0, 180) : null;
|
|
3678
|
+
}
|
|
3679
|
+
function buildIgnorePatternFromDecision(decision) {
|
|
3680
|
+
const explicit = decision.pattern?.trim();
|
|
3681
|
+
if (explicit && explicit.length >= 3) return explicit;
|
|
3682
|
+
return extractIssuePhrase(decision.issue);
|
|
3683
|
+
}
|
|
3684
|
+
function parseFalsePositiveDecision(value) {
|
|
3685
|
+
if (!value || typeof value !== "object") return null;
|
|
3686
|
+
const candidate = value;
|
|
3687
|
+
if (typeof candidate.rule !== "string" || typeof candidate.issue !== "string") return null;
|
|
3688
|
+
return {
|
|
3689
|
+
rule: candidate.rule,
|
|
3690
|
+
file: typeof candidate.file === "string" ? candidate.file : void 0,
|
|
3691
|
+
line: typeof candidate.line === "number" ? candidate.line : void 0,
|
|
3692
|
+
issue: candidate.issue,
|
|
3693
|
+
falsePositive: candidate.falsePositive === true,
|
|
3694
|
+
pattern: typeof candidate.pattern === "string" ? candidate.pattern : void 0,
|
|
3695
|
+
reason: typeof candidate.reason === "string" ? candidate.reason : void 0
|
|
3696
|
+
};
|
|
3697
|
+
}
|
|
3698
|
+
function parseFalsePositiveDecisions(raw) {
|
|
3699
|
+
const candidates = [
|
|
3700
|
+
raw,
|
|
3701
|
+
raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "")
|
|
3702
|
+
];
|
|
3703
|
+
const objectStart = raw.indexOf("{");
|
|
3704
|
+
const objectEnd = raw.lastIndexOf("}");
|
|
3705
|
+
if (objectStart !== -1 && objectEnd > objectStart) {
|
|
3706
|
+
candidates.push(raw.slice(objectStart, objectEnd + 1));
|
|
3707
|
+
}
|
|
3708
|
+
const arrayStart = raw.indexOf("[");
|
|
3709
|
+
const arrayEnd = raw.lastIndexOf("]");
|
|
3710
|
+
if (arrayStart !== -1 && arrayEnd > arrayStart) {
|
|
3711
|
+
candidates.push(raw.slice(arrayStart, arrayEnd + 1));
|
|
3712
|
+
}
|
|
3713
|
+
for (const candidate of candidates) {
|
|
3714
|
+
try {
|
|
3715
|
+
const parsed = JSON.parse(candidate);
|
|
3716
|
+
const items = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.decisions) ? parsed.decisions : parsed?.rule ? [parsed] : null;
|
|
3717
|
+
if (!items) continue;
|
|
3718
|
+
return {
|
|
3719
|
+
valid: true,
|
|
3720
|
+
decisions: items.map(parseFalsePositiveDecision).filter((decision) => decision !== null)
|
|
3721
|
+
};
|
|
3722
|
+
} catch {
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
return { valid: false, decisions: [] };
|
|
3726
|
+
}
|
|
3727
|
+
function loadRecentViolationsFromStats(cwd = process.cwd()) {
|
|
3728
|
+
const statsPath = join10(cwd, ".memory-core-stats.json");
|
|
3729
|
+
if (!existsSync9(statsPath)) return [];
|
|
3730
|
+
try {
|
|
3731
|
+
const parsed = JSON.parse(readFileSync8(statsPath, "utf-8"));
|
|
3732
|
+
if (!Array.isArray(parsed.recentViolations)) return [];
|
|
3733
|
+
return parsed.recentViolations.filter(
|
|
3734
|
+
(entry) => Boolean(entry) && typeof entry.rule === "string" && typeof entry.issue === "string" && typeof entry.file === "string" && typeof entry.timestamp === "string"
|
|
3735
|
+
);
|
|
3736
|
+
} catch {
|
|
3737
|
+
return [];
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
function incrementFalsePositivesForPatterns(learnedPatterns, violations, cwd = process.cwd()) {
|
|
3741
|
+
if (learnedPatterns.length === 0 || violations.length === 0) return;
|
|
3742
|
+
const statsPath = join10(cwd, ".memory-core-stats.json");
|
|
3743
|
+
if (!existsSync9(statsPath)) return;
|
|
3744
|
+
let stats;
|
|
3745
|
+
try {
|
|
3746
|
+
stats = JSON.parse(readFileSync8(statsPath, "utf-8"));
|
|
3747
|
+
} catch {
|
|
3748
|
+
return;
|
|
3749
|
+
}
|
|
3750
|
+
stats.rules ??= {};
|
|
3751
|
+
for (const violation of violations) {
|
|
3752
|
+
const haystack = `${violation.rule}
|
|
3753
|
+
${violation.issue}
|
|
3754
|
+
${violation.file}`.toLowerCase();
|
|
3755
|
+
const matched = learnedPatterns.some((p) => haystack.includes(p.toLowerCase()));
|
|
3756
|
+
if (!matched) continue;
|
|
3757
|
+
const existing = toRuleStatEntry(stats.rules[violation.rule]);
|
|
3758
|
+
stats.rules[violation.rule] = { count: existing.count, falsePositives: existing.falsePositives + 1 };
|
|
3759
|
+
}
|
|
3760
|
+
writeFileSync5(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
3761
|
+
}
|
|
3762
|
+
async function learnGlobalIgnoresFromFalsePositives(options) {
|
|
3763
|
+
if (options.currentViolations.length === 0) return [];
|
|
3764
|
+
const recentViolations = loadRecentViolationsFromStats();
|
|
3765
|
+
const recurring = findRecurringViolations(options.currentViolations, recentViolations, 2);
|
|
3766
|
+
if (recurring.length === 0) return [];
|
|
3767
|
+
const systemPrompt = `You are verifying repeated architecture-rule alerts.
|
|
3768
|
+
Mark falsePositive=true ONLY when the alert is clearly a false positive for this staged diff.
|
|
3769
|
+
For each false positive, return a concise ignore pattern that will suppress only this recurring false alert.
|
|
3770
|
+
Prefer exact snippets from the issue text.
|
|
2535
3771
|
|
|
2536
3772
|
Return strict JSON:
|
|
2537
|
-
{"
|
|
2538
|
-
Do not include any text outside
|
|
2539
|
-
const userPrompt =
|
|
2540
|
-
${
|
|
3773
|
+
{"decisions":[{"rule":"...","file":"...","line":1,"issue":"...","falsePositive":true,"pattern":"...","reason":"..."}]}
|
|
3774
|
+
Do not include any text outside JSON.`;
|
|
3775
|
+
const userPrompt = `Staged diff:
|
|
3776
|
+
${options.diff.slice(0, 6e3)}
|
|
2541
3777
|
|
|
2542
|
-
|
|
2543
|
-
${JSON.stringify(
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
console.log(
|
|
3778
|
+
Recurring violations:
|
|
3779
|
+
${JSON.stringify(recurring, null, 2)}
|
|
3780
|
+
|
|
3781
|
+
Existing allow patterns:
|
|
3782
|
+
${JSON.stringify(options.allowPatterns, null, 2)}`;
|
|
3783
|
+
if (options.debug) {
|
|
3784
|
+
console.log(chalk2.gray("\n [debug] false-positive recheck prompt:"));
|
|
3785
|
+
console.log(chalk2.dim(systemPrompt));
|
|
3786
|
+
console.log(chalk2.dim(userPrompt));
|
|
3787
|
+
console.log(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2549
3788
|
}
|
|
2550
3789
|
try {
|
|
2551
|
-
const
|
|
3790
|
+
const recheckTimeoutMs = readPositiveIntEnv2("MEMORY_CORE_FALSE_POSITIVE_TIMEOUT_MS", 6e3);
|
|
3791
|
+
const { content: raw, usage: recheckUsage } = await callChatModel([
|
|
2552
3792
|
{ role: "system", content: systemPrompt },
|
|
2553
3793
|
{ role: "user", content: userPrompt }
|
|
2554
|
-
]);
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
if (
|
|
2558
|
-
|
|
2559
|
-
} catch {
|
|
2560
|
-
return violations;
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
async function loadIgnorePatterns() {
|
|
2564
|
-
try {
|
|
3794
|
+
], { timeoutMs: recheckTimeoutMs });
|
|
3795
|
+
accumulateTokenUsage(recheckUsage);
|
|
3796
|
+
const parsed = parseFalsePositiveDecisions(raw);
|
|
3797
|
+
if (!parsed.valid) return [];
|
|
3798
|
+
const existing = new Set(options.allowPatterns.map((pattern) => pattern.toLowerCase()));
|
|
2565
3799
|
const app = getDefaultApplicationContainer();
|
|
2566
|
-
const
|
|
2567
|
-
|
|
3800
|
+
const inserted = [];
|
|
3801
|
+
for (const decision of parsed.decisions) {
|
|
3802
|
+
if (!decision.falsePositive) continue;
|
|
3803
|
+
const pattern = buildIgnorePatternFromDecision(decision);
|
|
3804
|
+
if (!pattern) continue;
|
|
3805
|
+
const normalized = pattern.toLowerCase();
|
|
3806
|
+
if (existing.has(normalized)) continue;
|
|
3807
|
+
try {
|
|
3808
|
+
await app.services.memoryEngine.remember({
|
|
3809
|
+
type: "ignore",
|
|
3810
|
+
scope: "global",
|
|
3811
|
+
architecture: "global",
|
|
3812
|
+
content: pattern,
|
|
3813
|
+
reason: `Auto-added from repeated false-positive recheck for "${decision.rule}"${decision.reason ? `: ${decision.reason}` : ""}`,
|
|
3814
|
+
tags: ["ignore", "auto-false-positive"]
|
|
3815
|
+
});
|
|
3816
|
+
existing.add(normalized);
|
|
3817
|
+
inserted.push(pattern);
|
|
3818
|
+
} catch {
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
if (inserted.length > 0) {
|
|
3822
|
+
incrementFalsePositivesForPatterns(inserted, options.currentViolations);
|
|
3823
|
+
}
|
|
3824
|
+
return inserted;
|
|
2568
3825
|
} catch {
|
|
2569
3826
|
return [];
|
|
2570
3827
|
}
|
|
2571
3828
|
}
|
|
2572
|
-
function
|
|
2573
|
-
return
|
|
3829
|
+
function normalizePath(value) {
|
|
3830
|
+
return value.replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
2574
3831
|
}
|
|
2575
|
-
function
|
|
2576
|
-
|
|
2577
|
-
const
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
}
|
|
2587
|
-
for (const entry of entries) {
|
|
2588
|
-
const absolute = join7(current, entry);
|
|
2589
|
-
let isDirectory = false;
|
|
2590
|
-
let isFile = false;
|
|
2591
|
-
try {
|
|
2592
|
-
const stats = statSync2(absolute);
|
|
2593
|
-
isDirectory = stats.isDirectory();
|
|
2594
|
-
isFile = stats.isFile();
|
|
2595
|
-
} catch {
|
|
2596
|
-
continue;
|
|
2597
|
-
}
|
|
2598
|
-
if (isDirectory) {
|
|
2599
|
-
if (entry === "node_modules" || entry === ".git" || entry === "dist" || entry === "build" || entry === "coverage") {
|
|
2600
|
-
continue;
|
|
2601
|
-
}
|
|
2602
|
-
stack.push(absolute);
|
|
2603
|
-
continue;
|
|
3832
|
+
function resolveChangedFile(candidate, changedFiles) {
|
|
3833
|
+
const normalizedCandidate = normalizePath(candidate);
|
|
3834
|
+
const candidates = [normalizedCandidate];
|
|
3835
|
+
if (/^(?:a|b)\//.test(normalizedCandidate)) {
|
|
3836
|
+
candidates.push(normalizedCandidate.slice(2));
|
|
3837
|
+
}
|
|
3838
|
+
for (const current of candidates) {
|
|
3839
|
+
if (changedFiles.has(current)) return current;
|
|
3840
|
+
for (const changed of changedFiles) {
|
|
3841
|
+
if (changed.endsWith(`/${current}`) || current.endsWith(`/${changed}`)) {
|
|
3842
|
+
return changed;
|
|
2604
3843
|
}
|
|
2605
|
-
if (isFile && SOURCE_EXTENSIONS3.test(absolute)) files.push(absolute);
|
|
2606
3844
|
}
|
|
2607
3845
|
}
|
|
2608
|
-
return
|
|
2609
|
-
}
|
|
2610
|
-
function listTrackedSourceFiles(projectRoot, watchPath) {
|
|
2611
|
-
const relPrefix = normalizeForGit(relative2(projectRoot, watchPath));
|
|
2612
|
-
const inRoot = relPrefix === "" || relPrefix === ".";
|
|
2613
|
-
const prefixWithSlash = inRoot ? "" : `${relPrefix}/`;
|
|
2614
|
-
const listed = spawnSync("git", ["ls-files"], { encoding: "utf-8", cwd: projectRoot });
|
|
2615
|
-
if (listed.status !== 0) {
|
|
2616
|
-
return listSourceFilesFromFilesystem(watchPath).sort();
|
|
2617
|
-
}
|
|
2618
|
-
const files = (listed.stdout ?? "").split("\n").filter((file) => file.length > 0).filter((file) => SOURCE_EXTENSIONS3.test(file)).filter((file) => inRoot || file.startsWith(prefixWithSlash)).map((file) => join7(projectRoot, file)).filter((file) => existsSync6(file));
|
|
2619
|
-
return [...new Set(files)].sort();
|
|
3846
|
+
return void 0;
|
|
2620
3847
|
}
|
|
2621
|
-
|
|
2622
|
-
const
|
|
2623
|
-
if (
|
|
2624
|
-
|
|
3848
|
+
function buildModelInputFromDiff(diff, maxChars = 8e3) {
|
|
3849
|
+
const addedLines = getAddedLines(diff);
|
|
3850
|
+
if (addedLines.length === 0) {
|
|
3851
|
+
const truncated2 = diff.length > maxChars;
|
|
2625
3852
|
return {
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
3853
|
+
text: truncated2 ? diff.slice(0, maxChars) + "\n\n[diff truncated]" : diff,
|
|
3854
|
+
source: "diff",
|
|
3855
|
+
truncated: truncated2
|
|
2629
3856
|
};
|
|
2630
3857
|
}
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
3858
|
+
const chunks = [];
|
|
3859
|
+
let currentFile = "";
|
|
3860
|
+
for (const addedLine of addedLines) {
|
|
3861
|
+
if (addedLine.file !== currentFile) {
|
|
3862
|
+
currentFile = addedLine.file;
|
|
3863
|
+
chunks.push(`
|
|
3864
|
+
# ${currentFile}`);
|
|
3865
|
+
}
|
|
3866
|
+
const line = addedLine.line ?? "?";
|
|
3867
|
+
chunks.push(`${line}: ${addedLine.content}`);
|
|
3868
|
+
}
|
|
3869
|
+
const summary = chunks.join("\n").trim();
|
|
3870
|
+
const truncated = summary.length > maxChars;
|
|
3871
|
+
return {
|
|
3872
|
+
text: truncated ? summary.slice(0, maxChars) + "\n\n[added lines truncated]" : summary,
|
|
3873
|
+
source: "added-lines",
|
|
3874
|
+
truncated
|
|
2638
3875
|
};
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
3876
|
+
}
|
|
3877
|
+
function findDeterministicViolations(diff, rules, avoids, allowPatterns = []) {
|
|
3878
|
+
const rulePhrases = rules.flatMap(
|
|
3879
|
+
(rule) => extractForbiddenPhrases(rule).map((phrase) => ({ rule, phrase }))
|
|
3880
|
+
);
|
|
3881
|
+
const avoidPhrases = avoids.map((avoid) => ({
|
|
3882
|
+
rule: `Avoid: ${avoid}`,
|
|
3883
|
+
phrase: avoid.toLowerCase()
|
|
3884
|
+
}));
|
|
3885
|
+
const phrases = [...rulePhrases, ...avoidPhrases].filter((item) => item.phrase.length > 0);
|
|
3886
|
+
if (phrases.length === 0) return [];
|
|
3887
|
+
const violations = [];
|
|
3888
|
+
for (const addedLine of getAddedLines(diff)) {
|
|
3889
|
+
const normalizedLine = addedLine.content.toLowerCase();
|
|
3890
|
+
if (allowPatterns.some((pattern) => normalizedLine.includes(pattern.toLowerCase()))) {
|
|
2650
3891
|
continue;
|
|
2651
3892
|
}
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
3893
|
+
for (const { rule, phrase } of phrases) {
|
|
3894
|
+
if (normalizedLine.includes(phrase)) {
|
|
3895
|
+
violations.push({
|
|
3896
|
+
rule,
|
|
3897
|
+
file: addedLine.file,
|
|
3898
|
+
line: addedLine.line,
|
|
3899
|
+
issue: `Added line contains forbidden phrase: "${phrase}"`,
|
|
3900
|
+
suggestion: "Remove this pattern or add an explicit ignore memory if it is intentional.",
|
|
3901
|
+
reason: reasonMap2.get(rule)
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
return dedupeViolations(violations);
|
|
3907
|
+
}
|
|
3908
|
+
function suppressBatchRepetitions(violations, threshold = 3) {
|
|
3909
|
+
const pairCounts = /* @__PURE__ */ new Map();
|
|
3910
|
+
for (const v of violations) {
|
|
3911
|
+
const key = `${v.rule}\0${v.file}`;
|
|
3912
|
+
pairCounts.set(key, (pairCounts.get(key) ?? 0) + 1);
|
|
3913
|
+
}
|
|
3914
|
+
const suppressedKeys = /* @__PURE__ */ new Set();
|
|
3915
|
+
for (const [key, count] of pairCounts) {
|
|
3916
|
+
if (count >= threshold) suppressedKeys.add(key);
|
|
3917
|
+
}
|
|
3918
|
+
if (suppressedKeys.size === 0) return { filtered: violations, suppressedCount: 0 };
|
|
3919
|
+
const filtered = violations.filter((v) => !suppressedKeys.has(`${v.rule}\0${v.file}`));
|
|
3920
|
+
return { filtered, suppressedCount: violations.length - filtered.length };
|
|
3921
|
+
}
|
|
3922
|
+
function groupViolationsByRule(violations) {
|
|
3923
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3924
|
+
for (const v of violations) {
|
|
3925
|
+
const existing = groups.get(v.rule);
|
|
3926
|
+
if (existing) {
|
|
3927
|
+
existing.push(v);
|
|
3928
|
+
} else {
|
|
3929
|
+
groups.set(v.rule, [v]);
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
return groups;
|
|
3933
|
+
}
|
|
3934
|
+
function filterModelViolationsByStagedDiff(violations, stagedFiles, diff) {
|
|
3935
|
+
if (violations.length === 0) return violations;
|
|
3936
|
+
const changedFiles = new Set(stagedFiles.map((file) => normalizePath(file)));
|
|
3937
|
+
if (changedFiles.size === 0) return [];
|
|
3938
|
+
const linesByFile = /* @__PURE__ */ new Map();
|
|
3939
|
+
for (const addedLine of getAddedLines(diff)) {
|
|
3940
|
+
const file = normalizePath(addedLine.file);
|
|
3941
|
+
if (!changedFiles.has(file)) continue;
|
|
3942
|
+
if (typeof addedLine.line !== "number") continue;
|
|
3943
|
+
const list = linesByFile.get(file) ?? [];
|
|
3944
|
+
list.push(addedLine.line);
|
|
3945
|
+
linesByFile.set(file, list);
|
|
3946
|
+
}
|
|
3947
|
+
const LINE_TOLERANCE = 3;
|
|
3948
|
+
const filtered = [];
|
|
3949
|
+
for (const violation of violations) {
|
|
3950
|
+
if (!violation.file || violation.file === "diff") {
|
|
3951
|
+
filtered.push(violation);
|
|
2655
3952
|
continue;
|
|
2656
3953
|
}
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
3954
|
+
const resolvedFile = resolveChangedFile(violation.file, changedFiles);
|
|
3955
|
+
if (!resolvedFile) continue;
|
|
3956
|
+
const candidateLines = linesByFile.get(resolvedFile) ?? [];
|
|
3957
|
+
if (typeof violation.line === "number" && candidateLines.length > 0) {
|
|
3958
|
+
const supported = candidateLines.some((line) => Math.abs(line - violation.line) <= LINE_TOLERANCE);
|
|
3959
|
+
if (!supported) continue;
|
|
3960
|
+
}
|
|
3961
|
+
filtered.push({ ...violation, file: resolvedFile });
|
|
2660
3962
|
}
|
|
2661
|
-
return
|
|
3963
|
+
return filtered;
|
|
2662
3964
|
}
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
3965
|
+
function installHook(advisory = true, fast = false) {
|
|
3966
|
+
if (!existsSync9(".git")) {
|
|
3967
|
+
console.error(chalk2.red("\n Not a git repository. Run from project root.\n"));
|
|
3968
|
+
process.exit(1);
|
|
3969
|
+
}
|
|
3970
|
+
const script = buildHookScript(advisory, fast);
|
|
3971
|
+
const body = buildHookBody(advisory, fast).trimEnd();
|
|
3972
|
+
if (existsSync9(HOOK_PATH)) {
|
|
3973
|
+
const existing = readFileSync8(HOOK_PATH, "utf-8");
|
|
3974
|
+
if (existing.includes(HOOK_MARKER)) {
|
|
3975
|
+
const markerIndex = existing.indexOf(HOOK_MARKER);
|
|
3976
|
+
const beforeRaw = markerIndex > 0 ? existing.slice(0, markerIndex) : "";
|
|
3977
|
+
const normalizedBefore = normalizeHookPreamble(beforeRaw);
|
|
3978
|
+
const preamble = normalizedBefore.length > 0 ? normalizedBefore : "#!/bin/sh";
|
|
3979
|
+
const preambleWithShebang = preamble.startsWith("#!/bin/sh") ? preamble : `#!/bin/sh
|
|
3980
|
+
${preamble}`;
|
|
3981
|
+
writeFileSync5(HOOK_PATH, `${preambleWithShebang}
|
|
3982
|
+
|
|
3983
|
+
${body}
|
|
3984
|
+
`);
|
|
3985
|
+
chmodSync(HOOK_PATH, 493);
|
|
3986
|
+
installCommitMsgHook(advisory);
|
|
3987
|
+
const modeLabel2 = advisory ? chalk2.cyan("advisory") : chalk2.yellow("strict");
|
|
3988
|
+
console.log(chalk2.green("\n \u2713 Pre-commit hook updated") + chalk2.dim(` (${modeLabel2} mode)`));
|
|
3989
|
+
if (fast) console.log(chalk2.gray(` Check mode: fast deterministic checks`));
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
writeFileSync5(HOOK_PATH, existing.trimEnd() + "\n\n" + body + "\n");
|
|
2671
3993
|
} else {
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
3994
|
+
writeFileSync5(HOOK_PATH, script);
|
|
3995
|
+
}
|
|
3996
|
+
chmodSync(HOOK_PATH, 493);
|
|
3997
|
+
installCommitMsgHook(advisory);
|
|
3998
|
+
const modeLabel = advisory ? "advisory (logs violations, never blocks)" : "strict (blocks commits on violations)";
|
|
3999
|
+
console.log(chalk2.green("\n \u2713 Pre-commit hook installed") + chalk2.dim(` \u2014 ${modeLabel}`));
|
|
4000
|
+
console.log(chalk2.gray(fast ? " Check mode: fast deterministic checks" : ` Chat model: ${process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"}`));
|
|
4001
|
+
console.log(chalk2.gray(" Commit message rules: memory-core commit-rules --list"));
|
|
4002
|
+
console.log(chalk2.gray(" To uninstall: memory-core hook uninstall\n"));
|
|
4003
|
+
}
|
|
4004
|
+
function uninstallHook() {
|
|
4005
|
+
if (!existsSync9(HOOK_PATH)) {
|
|
4006
|
+
console.log(chalk2.yellow("\n No pre-commit hook found.\n"));
|
|
4007
|
+
return;
|
|
4008
|
+
}
|
|
4009
|
+
const content = readFileSync8(HOOK_PATH, "utf-8");
|
|
4010
|
+
if (!content.includes(HOOK_MARKER)) {
|
|
4011
|
+
console.log(chalk2.yellow("\n ArchMind hook not found in pre-commit \u2014 nothing to remove.\n"));
|
|
4012
|
+
return;
|
|
4013
|
+
}
|
|
4014
|
+
const markerIndex = content.indexOf(HOOK_MARKER);
|
|
4015
|
+
const before = markerIndex > 1 ? normalizeHookPreamble(content.slice(0, markerIndex)) : "";
|
|
4016
|
+
if (before && before !== "#!/bin/sh") {
|
|
4017
|
+
writeFileSync5(HOOK_PATH, `${before}
|
|
4018
|
+
`);
|
|
4019
|
+
} else {
|
|
4020
|
+
unlinkSync(HOOK_PATH);
|
|
4021
|
+
}
|
|
4022
|
+
uninstallCommitMsgHook();
|
|
4023
|
+
console.log(chalk2.green("\n \u2713 Pre-commit hook removed\n"));
|
|
4024
|
+
}
|
|
4025
|
+
function buildCommitMsgHookBody(advisory) {
|
|
4026
|
+
const suffix = advisory ? " || true" : "";
|
|
4027
|
+
return `${COMMIT_MSG_HOOK_MARKER}${advisory ? " advisory" : ""}
|
|
4028
|
+
if [ "\${MEMORY_CORE_SKIP_HOOK:-}" = "1" ] || [ "\${ARCHMIND_SKIP_HOOK:-}" = "1" ] || [ "\${HUSKY:-}" = "0" ] || [ "\${HUSKY_SKIP_HOOKS:-}" = "1" ]; then
|
|
4029
|
+
exit 0
|
|
4030
|
+
fi
|
|
4031
|
+
if [ -n "\${SKIP:-}" ] && echo ",$SKIP," | grep -qiE ',(memory-core|archmind),'; then
|
|
4032
|
+
exit 0
|
|
4033
|
+
fi
|
|
4034
|
+
if [ -n "\${SKIP_HOOKS:-}" ]; then
|
|
4035
|
+
exit 0
|
|
4036
|
+
fi
|
|
4037
|
+
if command -v memory-core >/dev/null 2>&1; then
|
|
4038
|
+
memory-core check --commit-msg "$1"${suffix}
|
|
4039
|
+
elif [ -f "./node_modules/.bin/memory-core" ]; then
|
|
4040
|
+
./node_modules/.bin/memory-core check --commit-msg "$1"${suffix}
|
|
4041
|
+
elif [ -f "./dist/cli.js" ]; then
|
|
4042
|
+
node ./dist/cli.js check --commit-msg "$1"${suffix}
|
|
4043
|
+
else
|
|
4044
|
+
exit 0
|
|
4045
|
+
fi
|
|
4046
|
+
`;
|
|
4047
|
+
}
|
|
4048
|
+
function installCommitMsgHook(advisory = true) {
|
|
4049
|
+
const body = buildCommitMsgHookBody(advisory).trimEnd();
|
|
4050
|
+
const script = `#!/bin/sh
|
|
4051
|
+
|
|
4052
|
+
${body}
|
|
4053
|
+
`;
|
|
4054
|
+
if (existsSync9(COMMIT_MSG_HOOK_PATH)) {
|
|
4055
|
+
const existing = readFileSync8(COMMIT_MSG_HOOK_PATH, "utf-8");
|
|
4056
|
+
if (existing.includes(COMMIT_MSG_HOOK_MARKER)) {
|
|
4057
|
+
const markerIndex = existing.indexOf(COMMIT_MSG_HOOK_MARKER);
|
|
4058
|
+
const beforeRaw = markerIndex > 0 ? existing.slice(0, markerIndex) : "";
|
|
4059
|
+
const normalizedBefore = normalizeHookPreamble(beforeRaw);
|
|
4060
|
+
const preamble = normalizedBefore.length > 0 ? normalizedBefore : "#!/bin/sh";
|
|
4061
|
+
const preambleWithShebang = preamble.startsWith("#!/bin/sh") ? preamble : `#!/bin/sh
|
|
4062
|
+
${preamble}`;
|
|
4063
|
+
writeFileSync5(COMMIT_MSG_HOOK_PATH, `${preambleWithShebang}
|
|
4064
|
+
|
|
4065
|
+
${body}
|
|
4066
|
+
`);
|
|
2675
4067
|
} else {
|
|
2676
|
-
|
|
2677
|
-
encoding: "utf-8",
|
|
2678
|
-
cwd: projectRoot
|
|
2679
|
-
});
|
|
2680
|
-
inputText = noIndexResult.stdout ?? "";
|
|
4068
|
+
writeFileSync5(COMMIT_MSG_HOOK_PATH, existing.trimEnd() + "\n\n" + body + "\n");
|
|
2681
4069
|
}
|
|
2682
|
-
|
|
4070
|
+
} else {
|
|
4071
|
+
writeFileSync5(COMMIT_MSG_HOOK_PATH, script);
|
|
2683
4072
|
}
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
const
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
4073
|
+
chmodSync(COMMIT_MSG_HOOK_PATH, 493);
|
|
4074
|
+
}
|
|
4075
|
+
function uninstallCommitMsgHook() {
|
|
4076
|
+
if (!existsSync9(COMMIT_MSG_HOOK_PATH)) return;
|
|
4077
|
+
const content = readFileSync8(COMMIT_MSG_HOOK_PATH, "utf-8");
|
|
4078
|
+
if (!content.includes(COMMIT_MSG_HOOK_MARKER)) return;
|
|
4079
|
+
const markerIndex = content.indexOf(COMMIT_MSG_HOOK_MARKER);
|
|
4080
|
+
const before = markerIndex > 1 ? normalizeHookPreamble(content.slice(0, markerIndex)) : "";
|
|
4081
|
+
if (before && before !== "#!/bin/sh") {
|
|
4082
|
+
writeFileSync5(COMMIT_MSG_HOOK_PATH, `${before}
|
|
4083
|
+
`);
|
|
4084
|
+
} else {
|
|
4085
|
+
unlinkSync(COMMIT_MSG_HOOK_PATH);
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
async function checkCommitMsg(msgFile, options = {}) {
|
|
4089
|
+
if (!existsSync9(msgFile)) {
|
|
4090
|
+
if (options.verbose) console.log(chalk2.gray(" No commit message file \u2014 skipping."));
|
|
4091
|
+
return;
|
|
4092
|
+
}
|
|
4093
|
+
const raw = readFileSync8(msgFile, "utf-8");
|
|
4094
|
+
const cleanMsg = raw.split("\n").filter((l) => !l.startsWith("#")).join("\n").trim();
|
|
4095
|
+
if (!cleanMsg) {
|
|
4096
|
+
if (options.verbose) console.log(chalk2.gray(" Empty commit message \u2014 skipping."));
|
|
4097
|
+
return;
|
|
4098
|
+
}
|
|
4099
|
+
const configPath = join10(process.cwd(), ".memory-core.json");
|
|
4100
|
+
if (!existsSync9(configPath)) return;
|
|
4101
|
+
const config2 = JSON.parse(readFileSync8(configPath, "utf-8"));
|
|
4102
|
+
const rules = (config2.commitRules ?? []).filter(Boolean);
|
|
4103
|
+
if (rules.length === 0) return;
|
|
4104
|
+
console.log(chalk2.cyan("\n archmind \u2014 checking commit message\u2026"));
|
|
4105
|
+
const violations = [];
|
|
4106
|
+
for (const rule of rules) {
|
|
4107
|
+
try {
|
|
4108
|
+
const regex = new RegExp(rule.pattern, "im");
|
|
4109
|
+
const matched = regex.test(cleanMsg);
|
|
4110
|
+
const violated = rule.negate ? matched : !matched;
|
|
4111
|
+
if (violated) violations.push({ rule });
|
|
4112
|
+
} catch {
|
|
4113
|
+
if (options.debug) console.log(chalk2.yellow(` [debug] Invalid regex: "${rule.pattern}"`));
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
if (violations.length === 0) {
|
|
4117
|
+
console.log(chalk2.green(" \u2713 Commit message OK.\n"));
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
4120
|
+
const blocking = violations.filter((v) => !v.rule.advisory);
|
|
4121
|
+
violations.forEach(({ rule }) => {
|
|
4122
|
+
const prefix = rule.advisory ? chalk2.yellow(" \u26A0 ") : chalk2.red(" \u2717 ");
|
|
4123
|
+
console.log(prefix + rule.message);
|
|
4124
|
+
const matchLabel = rule.negate ? "(must NOT match)" : "(must match)";
|
|
4125
|
+
console.log(chalk2.dim(` Pattern: ${rule.pattern} ${matchLabel}`));
|
|
4126
|
+
});
|
|
4127
|
+
console.log();
|
|
4128
|
+
if (blocking.length === 0) return;
|
|
4129
|
+
console.log(chalk2.dim(" Fix the commit message, then commit again."));
|
|
4130
|
+
console.log(chalk2.dim(" To bypass: MEMORY_CORE_SKIP_HOOK=1 git commit"));
|
|
4131
|
+
console.log(chalk2.dim(" Manage rules: memory-core commit-rules --list\n"));
|
|
4132
|
+
process.exit(1);
|
|
4133
|
+
}
|
|
4134
|
+
async function checkStaged(options = {}) {
|
|
4135
|
+
const SOURCE_EXTENSIONS4 = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
|
|
4136
|
+
let diff;
|
|
4137
|
+
let stagedFiles = [];
|
|
4138
|
+
try {
|
|
4139
|
+
stagedFiles = execSync("git diff --cached --name-only --diff-filter=ACMRT", { encoding: "utf-8" }).split("\n").filter((f) => f && SOURCE_EXTENSIONS4.test(f)).map((f) => normalizePath(f));
|
|
4140
|
+
if (stagedFiles.length === 0) {
|
|
4141
|
+
if (options.verbose) console.log(chalk2.gray(" No source files staged \u2014 skipping rule check."));
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
const result = spawnSync2(
|
|
4145
|
+
"git",
|
|
4146
|
+
["diff", "--cached", "--unified=0", "--diff-filter=ACMRT", "--", ...stagedFiles],
|
|
4147
|
+
{ encoding: "utf-8" }
|
|
4148
|
+
);
|
|
4149
|
+
diff = result.stdout ?? "";
|
|
4150
|
+
} catch {
|
|
4151
|
+
console.error(chalk2.red(" Failed to read staged diff."));
|
|
4152
|
+
process.exit(1);
|
|
4153
|
+
}
|
|
4154
|
+
if (!diff.trim()) {
|
|
4155
|
+
if (options.verbose) console.log(chalk2.gray(" No staged changes to check."));
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
4158
|
+
const configPath = join10(process.cwd(), ".memory-core.json");
|
|
4159
|
+
if (!existsSync9(configPath)) return;
|
|
4160
|
+
const config2 = JSON.parse(readFileSync8(configPath, "utf-8"));
|
|
4161
|
+
const { rules: fallbackRules, avoids } = getProfileRules2(config2);
|
|
4162
|
+
const fast = isFastCheck(options);
|
|
4163
|
+
const ruleLoadTimeoutMs = readPositiveIntEnv2("MEMORY_CORE_RULE_LOAD_TIMEOUT_MS", 2e3);
|
|
4164
|
+
const ignoreLoadTimeoutMs = readPositiveIntEnv2("MEMORY_CORE_IGNORE_LOAD_TIMEOUT_MS", 1500);
|
|
4165
|
+
let rules;
|
|
4166
|
+
let ignores;
|
|
4167
|
+
let allowPatterns;
|
|
4168
|
+
if (fast) {
|
|
4169
|
+
rules = fallbackRules;
|
|
4170
|
+
ignores = [];
|
|
4171
|
+
allowPatterns = [...new Set(getAllowPatterns(config2))];
|
|
4172
|
+
} else {
|
|
4173
|
+
const cwd = process.cwd();
|
|
4174
|
+
const cached = readRuleCache(cwd);
|
|
4175
|
+
if (cached) {
|
|
4176
|
+
rules = cached.rules;
|
|
4177
|
+
ignores = cached.ignores;
|
|
4178
|
+
allowPatterns = cached.allowPatterns;
|
|
4179
|
+
if (options.debug) {
|
|
4180
|
+
console.log(chalk2.gray(" [debug] using cached rules (TTL valid)"));
|
|
4181
|
+
}
|
|
4182
|
+
} else {
|
|
4183
|
+
const [loadedRules, loadedIgnores] = await Promise.all([
|
|
4184
|
+
withTimeout(loadRelevantRules2(config2, diff, stagedFiles, fallbackRules), ruleLoadTimeoutMs, fallbackRules),
|
|
4185
|
+
withTimeout(loadIgnorePatterns2(), ignoreLoadTimeoutMs, [])
|
|
4186
|
+
]);
|
|
4187
|
+
rules = loadedRules;
|
|
4188
|
+
ignores = loadedIgnores;
|
|
4189
|
+
allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config2), ...loadedIgnores])];
|
|
4190
|
+
saveRuleCache(cwd, { rules, ignores, allowPatterns });
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
if (rules.length === 0) return;
|
|
4194
|
+
const modelInputMaxChars = readPositiveIntEnv2("MEMORY_CORE_MODEL_INPUT_MAX_CHARS", 8e3);
|
|
4195
|
+
const modelInput = buildModelInputFromDiff(diff, modelInputMaxChars);
|
|
4196
|
+
console.log(chalk2.cyan("\n archmind \u2014 checking staged changes against rules\u2026"));
|
|
4197
|
+
if (options.verbose || options.debug) {
|
|
4198
|
+
const sourceLabel = modelInput.source === "added-lines" ? "added lines" : "diff";
|
|
4199
|
+
const modelLabel = fast ? "skipped (--fast)" : getChatProviderLabel();
|
|
4200
|
+
console.log(chalk2.gray(` model: ${modelLabel} rules: ${rules.length} diff: ${diff.length} chars input: ${sourceLabel}${modelInput.truncated ? " (truncated)" : ""}`));
|
|
2694
4201
|
}
|
|
2695
4202
|
const rulesWithReasons = rules.map((r, i) => {
|
|
2696
|
-
const why =
|
|
4203
|
+
const why = reasonMap2.get(r);
|
|
2697
4204
|
return why ? `${i + 1}. ${r}
|
|
2698
4205
|
WHY: ${why}` : `${i + 1}. ${r}`;
|
|
2699
4206
|
}).join("\n");
|
|
2700
|
-
const
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
config: config2,
|
|
2704
|
-
rules,
|
|
2705
|
-
reasonLookup: reasonMap
|
|
2706
|
-
}).map((violation) => ({
|
|
2707
|
-
...violation,
|
|
2708
|
-
severity: "error"
|
|
2709
|
-
}));
|
|
2710
|
-
const analysisTarget = mode === "snapshot" ? "file content" : "file diff";
|
|
2711
|
-
const systemPrompt = `You are a strict code reviewer enforcing architecture rules.
|
|
2712
|
-
Analyze the ${analysisTarget} and identify ONLY clear, definite rule violations.
|
|
2713
|
-
Use the WHY for each rule to understand intent and judge edge cases.
|
|
4207
|
+
const systemPrompt = `You are a strict code reviewer enforcing architecture and framework rules.
|
|
4208
|
+
Analyze the provided staged changes and identify ONLY clear, definite rule violations \u2014 not style preferences.
|
|
4209
|
+
Use the WHY for each rule to understand intent and judge edge cases correctly.
|
|
2714
4210
|
|
|
2715
4211
|
Rules to enforce:
|
|
2716
4212
|
${rulesWithReasons}
|
|
@@ -2721,293 +4217,403 @@ ${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
|
|
|
2721
4217
|
Never flag these accepted project patterns:
|
|
2722
4218
|
${allowPatterns.length ? allowPatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
|
|
2723
4219
|
|
|
2724
|
-
IMPORTANT:
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
4220
|
+
IMPORTANT: You MUST respond with a JSON object that has a "violations" key containing an array.
|
|
4221
|
+
For each violation include a "reason" field \u2014 copy the WHY from the rule to explain to the developer why this matters.
|
|
4222
|
+
Example with violations: {"violations":[{"rule":"Use functional components only","file":"User.tsx","line":3,"issue":"Class component used","suggestion":"Convert to a function component using hooks","reason":"Class components cannot use hooks and the entire React ecosystem now assumes functional components"}]}
|
|
4223
|
+
Example with no violations: {"violations":[]}
|
|
4224
|
+
Do not include any text outside the JSON object.`;
|
|
4225
|
+
if (options.debug) {
|
|
4226
|
+
console.log(chalk2.gray("\n [debug] prompt:"));
|
|
4227
|
+
console.log(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2730
4228
|
console.log(systemPrompt);
|
|
2731
|
-
console.log(
|
|
2732
|
-
console.log(
|
|
2733
|
-
console.log(
|
|
2734
|
-
console.log(
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
4229
|
+
console.log(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
4230
|
+
console.log(chalk2.gray(` [debug] diff length: ${diff.length} chars`));
|
|
4231
|
+
console.log(chalk2.gray(` [debug] model input source: ${modelInput.source}`));
|
|
4232
|
+
console.log(chalk2.dim(modelInput.text));
|
|
4233
|
+
console.log(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
4234
|
+
}
|
|
4235
|
+
const deterministicViolations = findDeterministicViolations(diff, rules, avoids, allowPatterns);
|
|
4236
|
+
const astViolations = findAstDeterministicViolationsForDiff(diff, {
|
|
4237
|
+
cwd: process.cwd(),
|
|
4238
|
+
config: config2,
|
|
4239
|
+
rules,
|
|
4240
|
+
reasonLookup: reasonMap2
|
|
4241
|
+
});
|
|
4242
|
+
const app = getDefaultApplicationContainer();
|
|
4243
|
+
const schemaViolations = await findSchemaViolations({
|
|
4244
|
+
cwd: process.cwd(),
|
|
4245
|
+
memoryEngine: app.services.memoryEngine
|
|
4246
|
+
});
|
|
4247
|
+
let modelViolations = [];
|
|
4248
|
+
let aiFallback = fast;
|
|
4249
|
+
if (fast) {
|
|
4250
|
+
if (options.verbose || options.debug) {
|
|
4251
|
+
console.log(chalk2.gray(" AI check skipped; running deterministic checks only."));
|
|
4252
|
+
}
|
|
4253
|
+
} else try {
|
|
4254
|
+
const checkTimeoutMs = readPositiveIntEnv2("MEMORY_CORE_CHECK_TIMEOUT_MS", readPositiveIntEnv2("CHAT_TIMEOUT_MS", 2e4));
|
|
4255
|
+
const { content: raw, usage: checkUsage } = await callChatModel([
|
|
2744
4256
|
{ role: "system", content: systemPrompt },
|
|
2745
|
-
{ role: "user", content:
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
4257
|
+
{ role: "user", content: `Review these staged changes:
|
|
4258
|
+
|
|
4259
|
+
${modelInput.text}` }
|
|
4260
|
+
], { timeoutMs: checkTimeoutMs });
|
|
4261
|
+
accumulateTokenUsage(checkUsage);
|
|
4262
|
+
if (options.verbose || options.debug) {
|
|
4263
|
+
console.log(chalk2.gray(` raw response: ${options.debug ? raw : raw.slice(0, 200)}`));
|
|
2751
4264
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
4265
|
+
const parsed = parseModelViolations(raw);
|
|
4266
|
+
if (parsed.valid) {
|
|
4267
|
+
modelViolations = parsed.violations;
|
|
4268
|
+
} else {
|
|
4269
|
+
console.log(chalk2.yellow(" \u26A0 AI returned invalid JSON \u2014 using deterministic checks only."));
|
|
4270
|
+
}
|
|
4271
|
+
} catch (err) {
|
|
4272
|
+
if (err.message?.startsWith("MODEL_NOT_FOUND:")) {
|
|
4273
|
+
printModelMissing(err.message.split(":")[1]);
|
|
4274
|
+
aiFallback = true;
|
|
4275
|
+
modelViolations = [];
|
|
4276
|
+
} else if (err.message?.startsWith("TIMEOUT:")) {
|
|
4277
|
+
const timeoutMs = err.message.split(":")[1];
|
|
4278
|
+
console.log(chalk2.yellow(`
|
|
4279
|
+
\u26A0 AI check timed out after ${timeoutMs}ms \u2014 switching to fast deterministic checks for this run.`));
|
|
4280
|
+
console.log(chalk2.gray(" Set MEMORY_CORE_CHECK_TIMEOUT_MS to tune this.\n"));
|
|
4281
|
+
aiFallback = true;
|
|
4282
|
+
modelViolations = [];
|
|
4283
|
+
} else if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
|
|
4284
|
+
console.log(chalk2.yellow("\n \u26A0 Ollama not running \u2014 using deterministic checks only."));
|
|
4285
|
+
console.log(chalk2.gray(" Start it: ollama serve\n"));
|
|
4286
|
+
aiFallback = true;
|
|
4287
|
+
modelViolations = [];
|
|
4288
|
+
} else {
|
|
4289
|
+
console.log(chalk2.yellow(`
|
|
4290
|
+
\u26A0 AI rule check failed: ${err.message}`));
|
|
4291
|
+
console.log(chalk2.gray(" Using deterministic checks only.\n"));
|
|
4292
|
+
aiFallback = true;
|
|
4293
|
+
modelViolations = [];
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
modelViolations = filterModelViolationsByStagedDiff(modelViolations, stagedFiles, diff);
|
|
4297
|
+
let violations = dedupeViolations([...deterministicViolations, ...astViolations, ...schemaViolations, ...modelViolations]);
|
|
4298
|
+
violations = applyAllowPatterns2(violations, allowPatterns);
|
|
4299
|
+
if (violations.length > 0) {
|
|
4300
|
+
const { filtered, suppressedCount } = suppressBatchRepetitions(violations);
|
|
4301
|
+
if (suppressedCount > 0) {
|
|
4302
|
+
console.log(
|
|
4303
|
+
chalk2.dim(
|
|
4304
|
+
` \u2139 Auto-suppressed ${suppressedCount} repetitive violation${suppressedCount > 1 ? "s" : ""} (same rule fired \u22653\xD7 on the same file \u2014 consider tuning the rule)`
|
|
4305
|
+
)
|
|
4306
|
+
);
|
|
4307
|
+
violations = filtered;
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
if (!aiFallback && violations.length > 0) {
|
|
4311
|
+
const learnedPatterns = await learnGlobalIgnoresFromFalsePositives({
|
|
4312
|
+
diff,
|
|
4313
|
+
currentViolations: violations,
|
|
4314
|
+
allowPatterns,
|
|
4315
|
+
debug: options.debug
|
|
4316
|
+
});
|
|
4317
|
+
if (learnedPatterns.length > 0) {
|
|
4318
|
+
if (options.verbose || options.debug) {
|
|
4319
|
+
console.log(chalk2.gray(` learned ${learnedPatterns.length} global ignore pattern${learnedPatterns.length > 1 ? "s" : ""} from false-positive recheck`));
|
|
2761
4320
|
}
|
|
2762
|
-
|
|
2763
|
-
violations =
|
|
4321
|
+
const refinedAllowPatterns = [.../* @__PURE__ */ new Set([...allowPatterns, ...learnedPatterns])];
|
|
4322
|
+
violations = applyAllowPatterns2(violations, refinedAllowPatterns);
|
|
2764
4323
|
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
}));
|
|
2773
|
-
if (violations.length === 0) {
|
|
2774
|
-
recordWatchResult(projectRoot, rel, []);
|
|
2775
|
-
console.log(chalk.green(` \u2713 ${rel}`) + chalk.dim(" \u2014 no violations"));
|
|
2776
|
-
return { type: "checked", violations: [] };
|
|
4324
|
+
}
|
|
4325
|
+
if (violations.length === 0) {
|
|
4326
|
+
resetViolationStats();
|
|
4327
|
+
if (options.dryRun) {
|
|
4328
|
+
console.log(chalk2.green(" \u2713 [dry-run] No rule violations found.\n"));
|
|
4329
|
+
} else {
|
|
4330
|
+
console.log(chalk2.green(" \u2713 No rule violations \u2014 commit allowed.\n"));
|
|
2777
4331
|
}
|
|
4332
|
+
return;
|
|
4333
|
+
}
|
|
4334
|
+
if (options.dryRun) {
|
|
4335
|
+
console.log(
|
|
4336
|
+
chalk2.yellow.bold(
|
|
4337
|
+
`
|
|
4338
|
+
\u26A0 [dry-run] ${violations.length} rule violation${violations.length > 1 ? "s" : ""} would be flagged (commit not blocked)
|
|
4339
|
+
`
|
|
4340
|
+
)
|
|
4341
|
+
);
|
|
4342
|
+
} else {
|
|
2778
4343
|
console.log(
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
4344
|
+
chalk2.red.bold(
|
|
4345
|
+
`
|
|
4346
|
+
\u2717 ${violations.length} rule violation${violations.length > 1 ? "s" : ""} found \u2014 commit blocked
|
|
4347
|
+
`
|
|
4348
|
+
)
|
|
2782
4349
|
);
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
4350
|
+
}
|
|
4351
|
+
let ruleStatsSnapshot = {};
|
|
4352
|
+
{
|
|
4353
|
+
const statsPath = join10(process.cwd(), ".memory-core-stats.json");
|
|
4354
|
+
if (existsSync9(statsPath)) {
|
|
4355
|
+
try {
|
|
4356
|
+
const parsed = JSON.parse(readFileSync8(statsPath, "utf-8"));
|
|
4357
|
+
ruleStatsSnapshot = parsed.rules ?? {};
|
|
4358
|
+
} catch {
|
|
2791
4359
|
}
|
|
2792
|
-
|
|
2793
|
-
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
const MAX_LOCATIONS = 5;
|
|
4363
|
+
const groups = groupViolationsByRule(violations);
|
|
4364
|
+
let groupIndex = 0;
|
|
4365
|
+
for (const [rule, group] of groups) {
|
|
4366
|
+
groupIndex++;
|
|
4367
|
+
const isCluster = group.length > 1;
|
|
4368
|
+
const first = group[0];
|
|
4369
|
+
if (isCluster) {
|
|
4370
|
+
console.log(chalk2.bold.red(`
|
|
4371
|
+
[${groupIndex}] ${rule}`) + chalk2.dim(` \xD7${group.length}`));
|
|
4372
|
+
} else {
|
|
4373
|
+
const loc = first.file ? first.line ? `${first.file}:${first.line}` : first.file : "unknown location";
|
|
4374
|
+
console.log(chalk2.bold(`
|
|
4375
|
+
[${groupIndex}] ${loc}`));
|
|
4376
|
+
console.log(chalk2.yellow(" Rule: ") + rule);
|
|
4377
|
+
}
|
|
4378
|
+
const why = first.reason ?? reasonMap2.get(rule);
|
|
4379
|
+
if (why) console.log(chalk2.dim(" Why: ") + chalk2.dim(why));
|
|
4380
|
+
if (first.suggestion) console.log(chalk2.green(" Fix: ") + first.suggestion);
|
|
4381
|
+
if (isCluster) {
|
|
2794
4382
|
console.log();
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
if (
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
4383
|
+
const shown = group.slice(0, MAX_LOCATIONS);
|
|
4384
|
+
const overflow = group.length - MAX_LOCATIONS;
|
|
4385
|
+
for (const v of shown) {
|
|
4386
|
+
const loc = v.file ? v.line ? `${v.file}:${v.line}` : v.file : "unknown location";
|
|
4387
|
+
const issue = v.issue ? chalk2.dim(` ${v.issue}`) : "";
|
|
4388
|
+
console.log(chalk2.dim(` ${loc}`) + issue);
|
|
4389
|
+
}
|
|
4390
|
+
if (overflow > 0) {
|
|
4391
|
+
console.log(chalk2.dim(` ... and ${overflow} more`));
|
|
4392
|
+
}
|
|
4393
|
+
} else {
|
|
4394
|
+
if (first.issue) console.log(chalk2.red(" Issue: ") + first.issue);
|
|
4395
|
+
}
|
|
4396
|
+
const ruleEntry = toRuleStatEntry(ruleStatsSnapshot[rule]);
|
|
4397
|
+
if (ruleEntry.count > 5 && ruleEntry.falsePositives > 0) {
|
|
4398
|
+
const rate = Math.round(ruleEntry.falsePositives / ruleEntry.count * 100);
|
|
4399
|
+
if (rate > 40) {
|
|
4400
|
+
console.log(chalk2.yellow(`
|
|
4401
|
+
Noisy: ${rate}% historical false-positive rate`));
|
|
4402
|
+
console.log(chalk2.dim(` Silence: memory-core allow "${rule}"`));
|
|
4403
|
+
console.log(chalk2.dim(` Review all: memory-core tune`));
|
|
4404
|
+
} else if (rate > 25) {
|
|
4405
|
+
console.log(chalk2.dim(`
|
|
4406
|
+
Note: ${rate}% false-positive rate \u2014 run: memory-core tune`));
|
|
4407
|
+
}
|
|
2813
4408
|
}
|
|
4409
|
+
console.log();
|
|
4410
|
+
}
|
|
4411
|
+
const noisyRules = Array.from(groups.keys()).filter((rule) => {
|
|
4412
|
+
const entry = toRuleStatEntry(ruleStatsSnapshot[rule]);
|
|
4413
|
+
return entry.count > 3 && entry.falsePositives / entry.count > 0.25;
|
|
4414
|
+
});
|
|
4415
|
+
if (noisyRules.length > 0) {
|
|
2814
4416
|
console.log(
|
|
2815
|
-
|
|
2816
|
-
\u2717 ${violations.length} deterministic violation${violations.length > 1 ? "s" : ""} in ${rel}
|
|
2817
|
-
`)
|
|
4417
|
+
chalk2.yellow(` \u26A0 ${noisyRules.length} of these rule${noisyRules.length > 1 ? "s have" : " has"} a high false-positive rate.`)
|
|
2818
4418
|
);
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
|
|
2825
|
-
if (v.line && existsSync6(filePath)) {
|
|
2826
|
-
printCodeContext(filePath, v.line, 1);
|
|
2827
|
-
}
|
|
2828
|
-
if (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
|
|
2829
|
-
if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
|
|
2830
|
-
console.log();
|
|
2831
|
-
});
|
|
2832
|
-
recordWatchResult(projectRoot, rel, violations);
|
|
2833
|
-
console.log(chalk.dim(' Fix violations or run: memory-core remember "<lesson>"'));
|
|
4419
|
+
console.log(chalk2.dim(" Run: memory-core tune \u2014 to review and silence noisy rules\n"));
|
|
4420
|
+
}
|
|
4421
|
+
if (options.dryRun) {
|
|
4422
|
+
console.log(chalk2.dim(" [dry-run] Commit not blocked. To enforce, run without --dry-run."));
|
|
4423
|
+
console.log(chalk2.dim(' To save as memory: memory-core remember "<lesson>"'));
|
|
2834
4424
|
console.log();
|
|
2835
|
-
return
|
|
4425
|
+
return;
|
|
2836
4426
|
}
|
|
4427
|
+
console.log(chalk2.dim(" Fix the violations above, then commit again."));
|
|
4428
|
+
console.log(chalk2.dim(" To bypass (not recommended): git commit --no-verify"));
|
|
4429
|
+
console.log(chalk2.dim(" Env bypass: MEMORY_CORE_SKIP_HOOK=1 git commit"));
|
|
4430
|
+
console.log(chalk2.dim(' To save as memory: memory-core remember "<lesson>"'));
|
|
4431
|
+
console.log();
|
|
4432
|
+
recordViolations(violations);
|
|
4433
|
+
await promptToSaveViolations(violations);
|
|
4434
|
+
process.exit(1);
|
|
2837
4435
|
}
|
|
2838
|
-
|
|
2839
|
-
const
|
|
2840
|
-
const
|
|
2841
|
-
|
|
2842
|
-
|
|
4436
|
+
function extractForbiddenPhrases(content) {
|
|
4437
|
+
const phrases = [];
|
|
4438
|
+
const normalized = content.replace(/\s+/g, " ");
|
|
4439
|
+
const patterns = [
|
|
4440
|
+
/\bnever\s+([^.;]+)/gi,
|
|
4441
|
+
/\bmust not\s+([^.;]+)/gi,
|
|
4442
|
+
/\bdo not\s+([^.;]+)/gi
|
|
4443
|
+
];
|
|
4444
|
+
for (const pattern of patterns) {
|
|
4445
|
+
for (const match of normalized.matchAll(pattern)) {
|
|
4446
|
+
const phrase = match[1]?.trim();
|
|
4447
|
+
if (phrase && phrase.split(/\s+/).length >= 2) phrases.push(phrase.toLowerCase());
|
|
4448
|
+
}
|
|
2843
4449
|
}
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
4450
|
+
return phrases;
|
|
4451
|
+
}
|
|
4452
|
+
function getCiDiff() {
|
|
4453
|
+
const baseRef = process.env.GITHUB_BASE_REF;
|
|
4454
|
+
const commands = [
|
|
4455
|
+
baseRef ? `git diff --unified=0 --diff-filter=ACMRT origin/${baseRef}...HEAD` : "",
|
|
4456
|
+
"git diff --unified=0 --diff-filter=ACMRT HEAD~1 HEAD",
|
|
4457
|
+
"git diff --cached --unified=0 --diff-filter=ACMRT"
|
|
4458
|
+
].filter(Boolean);
|
|
4459
|
+
for (const command of commands) {
|
|
4460
|
+
try {
|
|
4461
|
+
const diff = execSync(command, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
4462
|
+
if (diff.trim()) return diff;
|
|
4463
|
+
} catch {
|
|
4464
|
+
}
|
|
2852
4465
|
}
|
|
2853
|
-
|
|
2854
|
-
console.log(chalk.cyan("\n archmind scan \u2014 checking tracked source files\n"));
|
|
2855
|
-
console.log(chalk.dim(` project: ${projectRoot}`));
|
|
2856
|
-
console.log(chalk.dim(` path: ${watchPath}`));
|
|
2857
|
-
console.log(chalk.dim(` model: ${getChatProviderLabel()}`));
|
|
2858
|
-
console.log(chalk.dim(` rules: ${rules.length}
|
|
2859
|
-
`));
|
|
2860
|
-
const summary = await runSnapshotScan(
|
|
2861
|
-
projectRoot,
|
|
2862
|
-
watchPath,
|
|
2863
|
-
config2,
|
|
2864
|
-
options.verbose ?? false,
|
|
2865
|
-
options.debug ?? false,
|
|
2866
|
-
options.onEvent
|
|
2867
|
-
);
|
|
2868
|
-
const cleanFiles = summary.filesChecked - summary.filesWithViolations;
|
|
2869
|
-
console.log(chalk.bold("\n scan summary\n"));
|
|
2870
|
-
console.log(chalk.dim(` files checked: ${summary.filesChecked}`));
|
|
2871
|
-
console.log(chalk.dim(` files clean: ${cleanFiles}`));
|
|
2872
|
-
console.log(chalk.dim(` files with violations: ${summary.filesWithViolations}`));
|
|
2873
|
-
console.log(chalk.dim(` total violations: ${summary.violations}
|
|
2874
|
-
`));
|
|
2875
|
-
return summary;
|
|
4466
|
+
return "";
|
|
2876
4467
|
}
|
|
2877
|
-
async function
|
|
2878
|
-
const {
|
|
2879
|
-
const
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
console.error(chalk.red(`
|
|
2884
|
-
${message}
|
|
4468
|
+
async function checkFile2(filePath, options = {}) {
|
|
4469
|
+
const { readFileSync: readFile, existsSync: fileExists } = await import("fs");
|
|
4470
|
+
const resolvedPath = filePath.startsWith("/") ? filePath : join10(process.cwd(), filePath);
|
|
4471
|
+
if (!fileExists(resolvedPath)) {
|
|
4472
|
+
console.error(chalk2.red(`
|
|
4473
|
+
File not found: ${filePath}
|
|
2885
4474
|
`));
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
4475
|
+
process.exit(1);
|
|
4476
|
+
}
|
|
4477
|
+
const content = readFile(resolvedPath, "utf-8");
|
|
4478
|
+
const lines = content.split("\n");
|
|
4479
|
+
const pseudoDiff = [
|
|
4480
|
+
`diff --git a/${filePath} b/${filePath}`,
|
|
4481
|
+
`+++ b/${filePath}`,
|
|
4482
|
+
`@@ -0,0 +1,${lines.length} @@`,
|
|
4483
|
+
...lines.map((l) => `+${l}`)
|
|
4484
|
+
].join("\n");
|
|
4485
|
+
const configPath = join10(process.cwd(), ".memory-core.json");
|
|
4486
|
+
if (!existsSync9(configPath)) {
|
|
4487
|
+
console.error(chalk2.red("\n No .memory-core.json found. Run: memory-core init\n"));
|
|
4488
|
+
process.exit(1);
|
|
4489
|
+
}
|
|
4490
|
+
const config2 = JSON.parse(readFileSync8(configPath, "utf-8"));
|
|
4491
|
+
const { rules: fallbackRules, avoids } = getProfileRules2(config2);
|
|
4492
|
+
const allowPatterns = [...new Set(getAllowPatterns(config2))];
|
|
4493
|
+
const fast = isFastCheck(options);
|
|
4494
|
+
let rules;
|
|
4495
|
+
if (fast) {
|
|
4496
|
+
rules = fallbackRules;
|
|
4497
|
+
} else {
|
|
4498
|
+
const cached = readRuleCache(process.cwd());
|
|
4499
|
+
if (cached) {
|
|
4500
|
+
rules = cached.rules;
|
|
4501
|
+
} else {
|
|
4502
|
+
const ruleLoadTimeoutMs = readPositiveIntEnv2("MEMORY_CORE_RULE_LOAD_TIMEOUT_MS", 2e3);
|
|
4503
|
+
rules = await withTimeout(
|
|
4504
|
+
loadRelevantRules2(config2, pseudoDiff, [filePath], fallbackRules),
|
|
4505
|
+
ruleLoadTimeoutMs,
|
|
4506
|
+
fallbackRules
|
|
4507
|
+
);
|
|
4508
|
+
}
|
|
2889
4509
|
}
|
|
2890
|
-
const { rules } = getProfileRules(config2);
|
|
2891
4510
|
if (rules.length === 0) {
|
|
2892
|
-
|
|
2893
|
-
console.log(chalk.yellow(`
|
|
2894
|
-
${message}
|
|
2895
|
-
`));
|
|
2896
|
-
options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message });
|
|
2897
|
-
if (exitOnSetupFailure) process.exit(0);
|
|
4511
|
+
console.log(chalk2.dim("\n No rules loaded \u2014 nothing to check.\n"));
|
|
2898
4512
|
return;
|
|
2899
4513
|
}
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
type: "ready",
|
|
2909
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2910
|
-
path: watchPath,
|
|
2911
|
-
model: getChatProviderLabel(),
|
|
2912
|
-
rules: rules.length
|
|
4514
|
+
console.log(chalk2.cyan(`
|
|
4515
|
+
archmind \u2014 checking ${filePath} against rules\u2026`));
|
|
4516
|
+
const deterministicViolations = findDeterministicViolations(pseudoDiff, rules, avoids, allowPatterns);
|
|
4517
|
+
const astViolations = findAstDeterministicViolationsForDiff(pseudoDiff, {
|
|
4518
|
+
cwd: process.cwd(),
|
|
4519
|
+
config: config2,
|
|
4520
|
+
rules,
|
|
4521
|
+
reasonLookup: reasonMap2
|
|
2913
4522
|
});
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
config2,
|
|
2920
|
-
options.verbose ?? false,
|
|
2921
|
-
options.debug ?? false,
|
|
2922
|
-
options.onEvent
|
|
2923
|
-
);
|
|
2924
|
-
console.log(chalk.dim(" initial scan complete.\n"));
|
|
4523
|
+
let violations = dedupeViolations([...deterministicViolations, ...astViolations]);
|
|
4524
|
+
violations = applyAllowPatterns2(violations, allowPatterns);
|
|
4525
|
+
if (violations.length === 0) {
|
|
4526
|
+
console.log(chalk2.green(" \u2713 No rule violations found in this file.\n"));
|
|
4527
|
+
return;
|
|
2925
4528
|
}
|
|
2926
|
-
const
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
4529
|
+
const label = options.dryRun ? "[dry-run] " : "";
|
|
4530
|
+
console.log(chalk2.yellow.bold(`
|
|
4531
|
+
${label}${violations.length} violation${violations.length > 1 ? "s" : ""} found in ${filePath}
|
|
4532
|
+
`));
|
|
4533
|
+
const groups = groupViolationsByRule(violations);
|
|
4534
|
+
let idx = 0;
|
|
4535
|
+
for (const [rule, group] of groups) {
|
|
4536
|
+
idx++;
|
|
4537
|
+
const first = group[0];
|
|
4538
|
+
const loc = first.line ? `${filePath}:${first.line}` : filePath;
|
|
4539
|
+
console.log(chalk2.bold(`
|
|
4540
|
+
[${idx}] ${loc}`));
|
|
4541
|
+
console.log(chalk2.yellow(" Rule: ") + rule);
|
|
4542
|
+
if (first.reason) console.log(chalk2.dim(" Why: ") + chalk2.dim(first.reason));
|
|
4543
|
+
if (first.issue) console.log(chalk2.red(" Issue: ") + first.issue);
|
|
4544
|
+
if (first.suggestion) console.log(chalk2.green(" Fix: ") + first.suggestion);
|
|
4545
|
+
console.log();
|
|
4546
|
+
}
|
|
4547
|
+
if (!options.dryRun) process.exit(1);
|
|
4548
|
+
}
|
|
4549
|
+
async function checkCi(options = {}) {
|
|
4550
|
+
let memories;
|
|
4551
|
+
try {
|
|
4552
|
+
memories = readMemoryFile();
|
|
4553
|
+
} catch (err) {
|
|
4554
|
+
console.error(chalk2.red(`
|
|
4555
|
+
CI check failed: ${err.message}
|
|
4556
|
+
`));
|
|
4557
|
+
process.exit(1);
|
|
4558
|
+
}
|
|
4559
|
+
const rules = memories.filter((memory) => memory.type !== "ignore");
|
|
4560
|
+
const ignores = memories.filter((memory) => memory.type === "ignore").map((memory) => memory.content.toLowerCase());
|
|
4561
|
+
const phrases = rules.flatMap(
|
|
4562
|
+
(memory) => extractForbiddenPhrases(memory.content).map((phrase) => ({ rule: memory.content, phrase }))
|
|
4563
|
+
);
|
|
4564
|
+
const diff = getCiDiff();
|
|
4565
|
+
const addedLines = diff.split("\n").filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1));
|
|
4566
|
+
if (options.debug) {
|
|
4567
|
+
console.log(chalk2.gray(`
|
|
4568
|
+
[debug] memories: ${memories.length}`));
|
|
4569
|
+
console.log(chalk2.gray(` [debug] text rules: ${phrases.length}`));
|
|
4570
|
+
console.log(chalk2.gray(` [debug] diff length: ${diff.length} chars
|
|
4571
|
+
`));
|
|
4572
|
+
}
|
|
4573
|
+
const violations = [];
|
|
4574
|
+
for (const line of addedLines) {
|
|
4575
|
+
const normalizedLine = line.toLowerCase();
|
|
4576
|
+
if (ignores.some((ignore) => normalizedLine.includes(ignore))) continue;
|
|
4577
|
+
for (const { rule, phrase } of phrases) {
|
|
4578
|
+
if (normalizedLine.includes(phrase)) {
|
|
4579
|
+
violations.push({
|
|
4580
|
+
rule,
|
|
4581
|
+
file: "diff",
|
|
4582
|
+
issue: `Added line contains forbidden phrase: "${phrase}"`
|
|
4583
|
+
});
|
|
2979
4584
|
}
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
console.
|
|
2995
|
-
|
|
2996
|
-
});
|
|
2997
|
-
process.on("SIGINT", () => {
|
|
2998
|
-
console.log(chalk.dim("\n\n archmind watch stopped.\n"));
|
|
2999
|
-
options.onEvent?.({ type: "stopped", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3000
|
-
clearInterval(keepAlive);
|
|
3001
|
-
watcher.close();
|
|
3002
|
-
process.exit(0);
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
if (violations.length === 0) {
|
|
4588
|
+
console.log(chalk2.green(`
|
|
4589
|
+
\u2713 CI memory check passed (${rules.length} rules loaded from memories.json)
|
|
4590
|
+
`));
|
|
4591
|
+
return;
|
|
4592
|
+
}
|
|
4593
|
+
console.log(chalk2.red.bold(`
|
|
4594
|
+
\u2717 ${violations.length} CI violation${violations.length > 1 ? "s" : ""} found
|
|
4595
|
+
`));
|
|
4596
|
+
violations.forEach((violation, index) => {
|
|
4597
|
+
console.log(chalk2.bold(` [${index + 1}] ${violation.file}`));
|
|
4598
|
+
console.log(chalk2.yellow(" Rule: ") + violation.rule);
|
|
4599
|
+
console.log(chalk2.red(" Issue: ") + violation.issue);
|
|
4600
|
+
console.log();
|
|
3003
4601
|
});
|
|
4602
|
+
recordViolations(violations, "ci");
|
|
4603
|
+
process.exit(1);
|
|
4604
|
+
}
|
|
4605
|
+
function printModelMissing(model) {
|
|
4606
|
+
console.log(chalk2.yellow(`
|
|
4607
|
+
\u26A0 Chat model "${model}" not found in Ollama.`));
|
|
4608
|
+
console.log(chalk2.gray(` Pull a model: ollama pull ${model}`));
|
|
4609
|
+
console.log(chalk2.gray(" Or set OLLAMA_CHAT_MODEL=<model> in .env"));
|
|
4610
|
+
console.log(chalk2.gray(" Recommended: llama3.2 | qwen2.5-coder:3b | mistral\n"));
|
|
3004
4611
|
}
|
|
3005
4612
|
|
|
3006
4613
|
export {
|
|
3007
4614
|
detectProject,
|
|
3008
4615
|
Config,
|
|
3009
4616
|
embed,
|
|
3010
|
-
callChatModel,
|
|
3011
4617
|
getChatProviderLabel,
|
|
3012
4618
|
getPool,
|
|
3013
4619
|
runMigrations,
|
|
@@ -3019,13 +4625,25 @@ export {
|
|
|
3019
4625
|
migrateGraphSnapshots,
|
|
3020
4626
|
probeGraphSnapshotStore,
|
|
3021
4627
|
seeds,
|
|
3022
|
-
|
|
4628
|
+
MEMORY_FILE,
|
|
4629
|
+
toPortableMemory,
|
|
4630
|
+
writeMemoryFile,
|
|
4631
|
+
readMemoryFile,
|
|
4632
|
+
readMemoryFileFromUrl,
|
|
4633
|
+
parseMemoryFile,
|
|
4634
|
+
findSchemaViolations,
|
|
4635
|
+
recordBypass,
|
|
4636
|
+
readBypassStats,
|
|
4637
|
+
installHook,
|
|
4638
|
+
uninstallHook,
|
|
4639
|
+
checkCommitMsg,
|
|
4640
|
+
checkStaged,
|
|
4641
|
+
checkFile2 as checkFile,
|
|
4642
|
+
checkCi,
|
|
3023
4643
|
startWatch,
|
|
3024
4644
|
getDefaultApplicationContainer,
|
|
3025
4645
|
inferProjectArchitectures,
|
|
3026
4646
|
getAllowPatterns,
|
|
3027
|
-
buildContextQuery,
|
|
3028
|
-
retrieveContextualMemories,
|
|
3029
4647
|
retrieveMemorySelection,
|
|
3030
4648
|
OUTPUT_FILES,
|
|
3031
4649
|
AGENT_NAMES,
|