@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.
@@ -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
- return data.message.content.trim();
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
- return data.choices[0].message.content.trim();
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
- return data.content[0].text.trim();
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
- return data.choices[0].message.content.trim();
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/watcher.ts
1012
- import { watch } from "chokidar";
1013
- import { spawnSync } from "child_process";
1014
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
1015
- import { dirname as dirname4, join as join7, relative as relative2, resolve as resolve4, sep } from "path";
1016
- import chalk from "chalk";
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 readFileSync4, readdirSync as readdirSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
1020
- import { join as join6, dirname as dirname3, basename } from "path";
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
- return callChatModel(messages);
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 existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1129
- import { dirname, join as join3 } from "path";
1130
- var DEFAULT_FILE = join3(".memory-core", "graph-snapshots.json");
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 (!existsSync3(filePath)) {
1351
+ if (!existsSync5(filePath)) {
1170
1352
  return { version: 1, snapshots: [] };
1171
1353
  }
1172
1354
  try {
1173
- const parsed = JSON.parse(readFileSync2(filePath, "utf-8"));
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
- writeFileSync(filePath, `${JSON.stringify(data, null, 2)}
1372
+ writeFileSync2(filePath, `${JSON.stringify(data, null, 2)}
1191
1373
  `, "utf-8");
1192
1374
  }
1193
1375
  absolutePath() {
1194
- return join3(this.rootPath, this.relativeFilePath);
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/infrastructure/filesystem/chokidar-watch-service.ts
1240
- var ChokidarWatchService = class {
1241
- async start(options = {}) {
1242
- await startWatch({
1243
- path: options.path,
1244
- verbose: options.verbose,
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/graph-engine/application/graph-engine-service.ts
1398
- import { readdirSync, statSync } from "fs";
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 existsSync4, readFileSync as readFileSync3 } from "fs";
1404
- import { dirname as dirname2, extname, isAbsolute, join as join4, normalize, resolve } from "path";
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 (existsSync4(candidate)) return normalize(candidate);
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 (existsSync4(withExt)) return normalize(withExt);
1469
+ if (existsSync6(withExt)) return normalize(withExt);
1440
1470
  }
1441
1471
  for (const ext of SOURCE_EXTENSIONS) {
1442
- const indexFile = join4(candidate, `index${ext}`);
1443
- if (existsSync4(indexFile)) return normalize(indexFile);
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 (!existsSync4(filePath)) return [];
1468
- const source = readFileSync3(filePath, "utf-8");
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/graph-engine/application/graph-engine-service.ts
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 normalizeNode(pathValue) {
1585
- return asPosix4(pathValue);
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 edgeKey(edge) {
1588
- return `${edge.from}\0${edge.to}\0${edge.kind}`;
1613
+ function isLegacyOrCompatibilitySpecifier(specifier) {
1614
+ const normalized = asPosix4(specifier);
1615
+ return normalized.includes("/compatibility/") || normalized.includes("compatibility/") || normalized.includes("legacy-");
1589
1616
  }
1590
- function generateSnapshotId() {
1591
- const suffix = Math.random().toString(36).slice(2, 8);
1592
- return `snapshot-${Date.now()}-${suffix}`;
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 toGraph(snapshot) {
1595
- return {
1596
- id: snapshot.id,
1597
- createdAt: snapshot.createdAt,
1598
- rootPath: snapshot.rootPath,
1599
- nodes: snapshot.nodes,
1600
- edges: snapshot.edges
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 = resolve2(options.cwd ?? process.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 = resolve2(cwd, relativeFile);
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(asPosix4(relative(cwd, imp.resolvedPath)));
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 readdirSync(dir)) {
2818
+ for (const entry of readdirSync2(dir)) {
1691
2819
  if (IGNORED_DIRS.has(entry)) continue;
1692
- const absolutePath = join5(dir, entry);
1693
- const stat = statSync(absolutePath);
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 = asPosix4(relative(cwd, absolutePath));
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 = dirname3(__filename);
1937
- var PKG_ROOT = join6(__dirname, "..");
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
- const profilePath = join6(PKG_ROOT, "profiles", `${name}.yml`);
2026
- if (!existsSync5(profilePath)) throw new Error(`Profile not found: ${name}`);
2027
- return normalizeArchitectureProfile(yaml.load(readFileSync4(profilePath, "utf-8")), name);
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 = readdirSync2(join6(PKG_ROOT, "profiles")).filter((f) => f.endsWith(".yml"));
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(readFileSync4(join6(PKG_ROOT, "profiles", f), "utf-8")),
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 = join6(PKG_ROOT, "templates", templateName);
2104
- if (!existsSync5(templatePath)) throw new Error(`Template not found: ${templateName}`);
2105
- return Handlebars.compile(readFileSync4(templatePath, "utf-8"))(data);
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 = dirname3(filePath);
2109
- if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
2110
- if (existsSync5(filePath)) {
2111
- const existing = readFileSync4(filePath, "utf-8");
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
- writeFileSync2(filePath, content, "utf-8");
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 = join6(cwd, output.path);
2124
- if (output.skipIfExists && existsSync5(targetPath)) {
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 violations;
3268
+ return { written, skipped };
2346
3269
  }
2347
3270
 
2348
- // src/watcher.ts
2349
- function getFileLines(filePath) {
2350
- try {
2351
- return readFileSync5(filePath, "utf-8").split("\n");
2352
- } catch {
2353
- return [];
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 printCodeContext(filePath, line, contextLines = 2) {
2357
- const lines = getFileLines(filePath);
2358
- if (lines.length === 0) return;
2359
- const start = Math.max(0, line - 1 - contextLines);
2360
- const end = Math.min(lines.length - 1, line - 1 + contextLines);
2361
- 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"));
2362
- for (let i = start; i <= end; i++) {
2363
- const lineNum = String(i + 1).padStart(4, " ");
2364
- const isViolation = i === line - 1;
2365
- if (isViolation) {
2366
- console.log(chalk.red(` \u2502 ${lineNum} \u25B6 ${lines[i]}`));
2367
- } else {
2368
- console.log(chalk.dim(` \u2502 ${lineNum} ${lines[i]}`));
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
- 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"));
3330
+ return normalized.join("\n").replace(/\n{3,}/g, "\n\n").trim();
2372
3331
  }
2373
- function formatCodeContext(filePath, line, contextLines = 2) {
2374
- const lines = getFileLines(filePath);
2375
- if (lines.length === 0) return void 0;
2376
- const start = Math.max(0, line - 1 - contextLines);
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
- var SOURCE_EXTENSIONS3 = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
2386
- var reasonMap = new Map(
2387
- seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
2388
- );
2389
- function findProjectRoot(startPath) {
2390
- let current = resolve4(startPath);
2391
- while (true) {
2392
- if (existsSync6(join7(current, ".memory-core.json"))) return current;
2393
- const parent = dirname4(current);
2394
- if (parent === current) return null;
2395
- current = parent;
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 resolveWatchPaths(pathOption, projectRootOption) {
2399
- if (projectRootOption) {
2400
- const projectRoot2 = resolve4(projectRootOption);
2401
- return {
2402
- projectRoot: projectRoot2,
2403
- watchPath: resolve4(projectRoot2, pathOption ?? ".")
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
- const cwdRoot = resolve4(process.cwd());
2407
- const watchPath = resolve4(cwdRoot, pathOption ?? ".");
2408
- const projectRoot = findProjectRoot(watchPath) ?? findProjectRoot(cwdRoot) ?? cwdRoot;
2409
- return { projectRoot, watchPath };
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 readStatsFile(statsPath) {
2412
- if (!existsSync6(statsPath)) return { rules: {}, files: {} };
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
- return JSON.parse(readFileSync5(statsPath, "utf-8"));
3384
+ const parsed = JSON.parse(readFileSync8(statsPath, "utf-8"));
3385
+ if (parsed && typeof parsed === "object") {
3386
+ stats = parsed;
3387
+ }
2415
3388
  } catch {
2416
- return { rules: {}, files: {} };
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 rebuildLiveCounters(byFile) {
2420
- const rules = {};
2421
- const files = {};
2422
- for (const [file, violations] of Object.entries(byFile)) {
2423
- if (!Array.isArray(violations) || violations.length === 0) continue;
2424
- files[file] = violations.length;
2425
- for (const violation of violations) {
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
- return { rules, files };
2430
- }
2431
- function resetLiveStats(cwd) {
2432
- const statsPath = join7(cwd, ".memory-core-stats.json");
2433
- const stats = readStatsFile(statsPath);
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
- writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
3411
+ writeFileSync5(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
3412
+ return stats.bypasses;
2442
3413
  }
2443
- function recordWatchResult(cwd, file, violations) {
2444
- const statsPath = join7(cwd, ".memory-core-stats.json");
2445
- const stats = readStatsFile(statsPath);
2446
- stats.rules ??= {};
2447
- stats.files ??= {};
2448
- stats.live ??= { rules: {}, files: {}, byFile: {} };
2449
- stats.live.byFile ??= {};
2450
- if (violations.length === 0) {
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
- const live = rebuildLiveCounters(stats.live.byFile);
2456
- stats.live.rules = live.rules;
2457
- stats.live.files = live.files;
2458
- for (const violation of violations) {
2459
- stats.rules[violation.rule] = (stats.rules[violation.rule] ?? 0) + 1;
2460
- if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
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
- if (violations.length > 0) {
2463
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2464
- const recent = violations.map((violation) => ({ ...violation, timestamp, source: "watch" }));
2465
- stats.recentViolations = [...recent, ...stats.recentViolations ?? []].slice(0, 50);
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 loadConfig(cwd) {
2470
- const configPath = join7(cwd, ".memory-core.json");
2471
- if (!existsSync6(configPath)) return null;
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
- return JSON.parse(readFileSync5(configPath, "utf-8"));
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 getProfileRules(config2) {
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 loadRelevantRules(cwd, config2, rel, diff, fallbackRules) {
3535
+ async function loadRelevantRules2(config2, diff, stagedFiles, fallbackRules) {
2498
3536
  try {
2499
3537
  const query = buildContextQuery([
2500
- rel,
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 applyAllowPatterns(violations, allowPatterns) {
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
- async function verifyViolations(inputText, violations, allowPatterns, debug, mode = "diff") {
2528
- if (violations.length === 0) return violations;
2529
- const sourceLabel = mode === "snapshot" ? "file content" : "diff";
2530
- const systemPrompt = `You are verifying candidate architecture violations.
2531
- Only keep violations that are directly supported by the ${sourceLabel}.
2532
- Reject speculative or weak matches.
2533
- Treat these allowlisted patterns as intentional and valid:
2534
- ${allowPatterns.length ? allowPatterns.map((pattern, index) => `${index + 1}. ${pattern}`).join("\n") : "(none)"}
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
- {"violations":[{"rule":"...","file":"...","line":1,"issue":"...","suggestion":"...","reason":"..."}]}
2538
- Do not include any text outside the JSON.`;
2539
- const userPrompt = `${mode === "snapshot" ? "File content" : "Diff"}:
2540
- ${inputText.slice(0, 6e3)}
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
- Candidate violations:
2543
- ${JSON.stringify(violations, null, 2)}`;
2544
- if (debug) {
2545
- console.log(chalk.gray("\n [debug] verifier prompt:"));
2546
- console.log(chalk.dim(systemPrompt));
2547
- console.log(chalk.dim(userPrompt));
2548
- 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"));
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 raw = await callChatModel([
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
- const parsed = JSON.parse(raw);
2556
- if (Array.isArray(parsed?.violations)) return parsed.violations;
2557
- if (Array.isArray(parsed)) return parsed;
2558
- return violations;
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 ignores = await app.services.memoryEngine.list({ type: "ignore", limit: 1e3 });
2567
- return ignores.map((ignore) => ignore.content);
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 normalizeForGit(pathLike) {
2573
- return pathLike.split(sep).join("/");
3829
+ function normalizePath(value) {
3830
+ return value.replace(/\\/g, "/").replace(/^\.\/+/, "");
2574
3831
  }
2575
- function listSourceFilesFromFilesystem(dir) {
2576
- if (!existsSync6(dir)) return [];
2577
- const files = [];
2578
- const stack = [dir];
2579
- while (stack.length > 0) {
2580
- const current = stack.pop();
2581
- let entries = [];
2582
- try {
2583
- entries = readdirSync3(current);
2584
- } catch {
2585
- continue;
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 files;
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
- async function runSnapshotScan(projectRoot, watchPath, config2, verbose, debug, onEvent) {
2622
- const files = listTrackedSourceFiles(projectRoot, watchPath);
2623
- if (files.length === 0) {
2624
- console.log(chalk.yellow("\n No tracked source files found for scan.\n"));
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
- filesChecked: 0,
2627
- filesWithViolations: 0,
2628
- violations: 0
3853
+ text: truncated2 ? diff.slice(0, maxChars) + "\n\n[diff truncated]" : diff,
3854
+ source: "diff",
3855
+ truncated: truncated2
2629
3856
  };
2630
3857
  }
2631
- console.log(chalk.dim(`
2632
- scanning ${files.length} tracked source files...
2633
- `));
2634
- const summary = {
2635
- filesChecked: 0,
2636
- filesWithViolations: 0,
2637
- violations: 0
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
- for (const filePath of files) {
2640
- const rel = normalizeForGit(relative2(projectRoot, filePath));
2641
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2642
- onEvent?.({ type: "saved", timestamp, file: rel });
2643
- const result = await checkFile(filePath, projectRoot, config2, verbose, debug, "snapshot", onEvent);
2644
- if (result.type !== "checked") {
2645
- if (result.type === "skipped") {
2646
- onEvent?.({ type: "skipped", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, reason: result.reason });
2647
- } else {
2648
- onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message: result.message });
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
- summary.filesChecked += 1;
2653
- if (result.violations.length === 0) {
2654
- onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
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
- summary.filesWithViolations += 1;
2658
- summary.violations += result.violations.length;
2659
- onEvent?.({ type: "violations", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, violations: result.violations });
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 summary;
3963
+ return filtered;
2662
3964
  }
2663
- async function checkFile(filePath, projectRoot, config2, verbose, debug, mode = "diff", onEvent) {
2664
- const rel = relative2(projectRoot, filePath).split(sep).join("/");
2665
- if (rel.startsWith("..")) return { type: "skipped", reason: "File is outside project root" };
2666
- let inputText;
2667
- if (mode === "snapshot") {
2668
- if (!existsSync6(filePath)) return { type: "skipped", reason: "File no longer exists" };
2669
- inputText = readFileSync5(filePath, "utf-8");
2670
- if (!inputText.trim()) return { type: "skipped", reason: "File is empty" };
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
- const headResult = spawnSync("git", ["diff", "HEAD", "--", rel], { encoding: "utf-8", cwd: projectRoot });
2673
- if (headResult.stdout?.trim()) {
2674
- inputText = headResult.stdout;
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
- const noIndexResult = spawnSync("git", ["diff", "--no-index", "/dev/null", rel], {
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
- if (!inputText.trim()) return { type: "skipped", reason: "No changes compared with HEAD" };
4070
+ } else {
4071
+ writeFileSync5(COMMIT_MSG_HOOK_PATH, script);
2683
4072
  }
2684
- const { rules: fallbackRules, avoids } = getProfileRules(config2);
2685
- const rules = await loadRelevantRules(projectRoot, config2, rel, inputText, fallbackRules);
2686
- if (rules.length === 0) return { type: "skipped", reason: "No applicable architecture rules" };
2687
- const MAX_INPUT = 6e3;
2688
- const truncated = inputText.length > MAX_INPUT;
2689
- const inputToSend = truncated ? inputText.slice(0, MAX_INPUT) + "\n\n[input truncated]" : inputText;
2690
- if (verbose || debug) {
2691
- const label = mode === "snapshot" ? "snapshot" : `${inputText.length} chars`;
2692
- console.log(chalk.dim(`
2693
- [watch] checking ${rel} (${label})\u2026`));
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 = reasonMap.get(r);
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 allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config2), ...await loadIgnorePatterns()])];
2701
- const astViolations = findAstDeterministicViolationsForFile(rel, {
2702
- cwd: projectRoot,
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: Respond with JSON: {"violations":[...]} or {"violations":[]}.
2725
- Each violation: {"rule":"...","file":"...","line":N,"issue":"...","suggestion":"...","reason":"..."}.
2726
- No text outside the JSON.`;
2727
- if (debug) {
2728
- console.log(chalk.gray("\n [debug] prompt:"));
2729
- 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"));
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(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"));
2732
- console.log(chalk.gray(` [debug] input length: ${inputText.length} chars`));
2733
- console.log(chalk.dim(inputToSend));
2734
- 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"));
2735
- }
2736
- try {
2737
- const reviewPrompt = mode === "snapshot" ? `Review this file ${rel}:
2738
-
2739
- ${inputToSend}` : `Review this diff for ${rel}:
2740
-
2741
- ${inputToSend}`;
2742
- onEvent?.({ type: "log", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, message: `checking with ${getChatProviderLabel()} | ${rules.length} rule${rules.length === 1 ? "" : "s"}` });
2743
- const raw = await callChatModel([
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: reviewPrompt }
2746
- ]);
2747
- if (debug) {
2748
- console.log(chalk.gray(" [debug] raw response:"));
2749
- console.log(chalk.dim(raw));
2750
- 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"));
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
- let violations = [];
2753
- try {
2754
- const parsed = JSON.parse(raw);
2755
- if (Array.isArray(parsed)) {
2756
- violations = parsed;
2757
- } else if (Array.isArray(parsed?.violations)) {
2758
- violations = parsed.violations;
2759
- } else if (parsed?.rule) {
2760
- violations = [parsed];
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
- } catch {
2763
- violations = [];
4321
+ const refinedAllowPatterns = [.../* @__PURE__ */ new Set([...allowPatterns, ...learnedPatterns])];
4322
+ violations = applyAllowPatterns2(violations, refinedAllowPatterns);
2764
4323
  }
2765
- onEvent?.({ type: "log", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, message: `model returned ${violations.length} candidate violation${violations.length === 1 ? "" : "s"}` });
2766
- violations = await verifyViolations(inputText, violations, allowPatterns, debug, mode);
2767
- violations = [...astViolations, ...violations];
2768
- violations = applyAllowPatterns(violations, allowPatterns);
2769
- violations = violations.map((violation) => ({
2770
- ...violation,
2771
- code: violation.code ?? (violation.line ? formatCodeContext(filePath, violation.line, 1) : void 0)
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
- chalk.red.bold(`
2780
- \u2717 ${violations.length} violation${violations.length > 1 ? "s" : ""} in ${rel}
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
- violations.forEach((v, i) => {
2784
- const loc = v.line ? `${v.file ?? rel}:${v.line}` : v.file ?? rel;
2785
- console.log(chalk.bold(` [${i + 1}] ${loc}`));
2786
- console.log(chalk.yellow(" Rule: ") + v.rule);
2787
- const why = v.reason ?? reasonMap.get(v.rule);
2788
- if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
2789
- if (v.line && existsSync6(filePath)) {
2790
- printCodeContext(filePath, v.line, 1);
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
- if (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
2793
- if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
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
- recordWatchResult(projectRoot, rel, violations);
2797
- console.log(chalk.dim(' Fix violations or run: memory-core remember "<lesson>"'));
2798
- console.log();
2799
- return { type: "checked", violations };
2800
- } catch (err) {
2801
- const aiUnavailable = err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED");
2802
- const message = aiUnavailable ? `Model unreachable for ${rel}; using deterministic checks only.` : `AI check failed for ${rel}; using deterministic checks only.`;
2803
- console.log(chalk.yellow(` \u26A0 ${message}`));
2804
- let violations = applyAllowPatterns(astViolations, allowPatterns);
2805
- violations = violations.map((violation) => ({
2806
- ...violation,
2807
- code: violation.code ?? (violation.line ? formatCodeContext(filePath, violation.line, 1) : void 0)
2808
- }));
2809
- if (violations.length === 0) {
2810
- recordWatchResult(projectRoot, rel, []);
2811
- console.log(chalk.green(` \u2713 ${rel}`) + chalk.dim(" \u2014 no deterministic violations"));
2812
- return { type: "checked", violations: [] };
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
- chalk.red.bold(`
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
- violations.forEach((v, i) => {
2820
- const loc = v.line ? `${v.file ?? rel}:${v.line}` : v.file ?? rel;
2821
- console.log(chalk.bold(` [${i + 1}] ${loc}`));
2822
- console.log(chalk.yellow(" Rule: ") + v.rule);
2823
- const why = v.reason ?? reasonMap.get(v.rule);
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 { type: "checked", violations };
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
- async function scanFiles(options = {}) {
2839
- const { projectRoot, watchPath } = resolveWatchPaths(options.path, options.projectRoot);
2840
- const config2 = loadConfig(projectRoot);
2841
- if (!config2) {
2842
- throw new Error("No .memory-core.json found. Run: memory-core init");
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
- const { rules } = getProfileRules(config2);
2845
- if (rules.length === 0) {
2846
- console.log(chalk.yellow("\n No architecture rules configured in .memory-core.json \u2014 nothing to scan.\n"));
2847
- return {
2848
- filesChecked: 0,
2849
- filesWithViolations: 0,
2850
- violations: 0
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
- resetLiveStats(projectRoot);
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 startWatch(options = {}) {
2878
- const { projectRoot, watchPath } = resolveWatchPaths(options.path, options.projectRoot);
2879
- const config2 = loadConfig(projectRoot);
2880
- const exitOnSetupFailure = options.exitOnSetupFailure ?? true;
2881
- if (!config2) {
2882
- const message = "No .memory-core.json found. Run: memory-core init";
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
- options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message });
2887
- if (exitOnSetupFailure) process.exit(1);
2888
- return;
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
- const message = "No architecture rules configured in .memory-core.json \u2014 nothing to watch.";
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
- resetLiveStats(projectRoot);
2901
- console.log(chalk.cyan("\n archmind watch \u2014 real-time rule enforcement\n"));
2902
- console.log(chalk.dim(` project: ${projectRoot}`));
2903
- console.log(chalk.dim(` watching: ${watchPath}`));
2904
- console.log(chalk.dim(` model: ${getChatProviderLabel()}`));
2905
- console.log(chalk.dim(` rules: ${rules.length}`));
2906
- console.log(chalk.dim(" ctrl+c to stop\n"));
2907
- options.onEvent?.({
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
- if (options.scanOnStart) {
2915
- console.log(chalk.dim(" running initial snapshot scan before watch events..."));
2916
- await runSnapshotScan(
2917
- projectRoot,
2918
- watchPath,
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 pending = /* @__PURE__ */ new Map();
2927
- const watcher = watch(watchPath, {
2928
- ignored: [
2929
- "**/node_modules/**",
2930
- "**/.git/**",
2931
- "**/dist/**",
2932
- "**/build/**",
2933
- "**/coverage/**",
2934
- "**/.memory-core*"
2935
- ],
2936
- ignoreInitial: true,
2937
- persistent: true,
2938
- awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50 }
2939
- });
2940
- const keepAlive = setInterval(() => {
2941
- }, 1 << 30);
2942
- const handle = (filePath) => {
2943
- if (!SOURCE_EXTENSIONS3.test(filePath)) return;
2944
- if (pending.has(filePath)) clearTimeout(pending.get(filePath));
2945
- const timer = setTimeout(async () => {
2946
- pending.delete(filePath);
2947
- const rel = normalizeForGit(relative2(projectRoot, filePath));
2948
- if (rel.startsWith("..")) return;
2949
- const timestamp = /* @__PURE__ */ new Date();
2950
- console.log(chalk.dim(`
2951
- [${timestamp.toLocaleTimeString()}] saved: ${rel}`));
2952
- options.onEvent?.({ type: "saved", timestamp: timestamp.toISOString(), file: rel });
2953
- const result = await checkFile(
2954
- filePath,
2955
- projectRoot,
2956
- config2,
2957
- options.verbose ?? false,
2958
- options.debug ?? false,
2959
- "diff",
2960
- options.onEvent
2961
- );
2962
- if (result.type === "skipped") {
2963
- if (result.reason === "No changes compared with HEAD") {
2964
- recordWatchResult(projectRoot, rel, []);
2965
- options.onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
2966
- return;
2967
- }
2968
- options.onEvent?.({ type: "skipped", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, reason: result.reason });
2969
- return;
2970
- }
2971
- if (result.type === "error") {
2972
- options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message: result.message });
2973
- return;
2974
- }
2975
- const { violations } = result;
2976
- if (violations.length === 0) {
2977
- options.onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
2978
- return;
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
- options.onEvent?.({ type: "violations", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, violations });
2981
- }, 300);
2982
- pending.set(filePath, timer);
2983
- };
2984
- watcher.on("add", handle);
2985
- watcher.on("change", handle);
2986
- watcher.on("unlink", (filePath) => {
2987
- const rel = normalizeForGit(relative2(projectRoot, filePath));
2988
- if (rel.startsWith("..")) return;
2989
- recordWatchResult(projectRoot, rel, []);
2990
- options.onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
2991
- });
2992
- watcher.on("error", (err) => {
2993
- const message = err instanceof Error ? err.message : String(err);
2994
- console.error(chalk.red(` watcher error: ${message}`));
2995
- options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message });
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
- findAstDeterministicViolationsForDiff,
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,