@shahmilsaari/memory-core 1.0.22 → 1.0.26

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.
@@ -1,4 +1,27 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ callChatModel,
4
+ getChatProviderLabel
5
+ } from "./chunk-PQBWHAZN.js";
6
+ import {
7
+ Config,
8
+ deleteMemories,
9
+ deleteMemory,
10
+ getMemory,
11
+ getPool,
12
+ listMemories,
13
+ saveMemory,
14
+ searchMemories,
15
+ updateMemory,
16
+ upsertMemory
17
+ } from "./chunk-M7NKSXFS.js";
18
+ import {
19
+ buildModuleDependencyEdges,
20
+ collectResolvedImports,
21
+ detectModuleCycles,
22
+ isExternalFrameworkSpecifier,
23
+ parseChangedFilesFromDiff
24
+ } from "./chunk-ZZBQEXEO.js";
2
25
 
3
26
  // src/project-detector.ts
4
27
  import { existsSync, readFileSync } from "fs";
@@ -62,33 +85,6 @@ function detectProject(cwd = process.cwd()) {
62
85
  return { language: "Unknown", framework: "Unknown" };
63
86
  }
64
87
 
65
- // src/config.ts
66
- import { config } from "dotenv";
67
- import { existsSync as existsSync2 } from "fs";
68
- import { join as join2 } from "path";
69
- var localEnv = join2(process.cwd(), ".memory-core.env");
70
- config({ path: existsSync2(localEnv) ? localEnv : join2(process.cwd(), ".env") });
71
- var Config = {
72
- get databaseUrl() {
73
- return process.env.DATABASE_URL ?? "";
74
- },
75
- get ollamaUrl() {
76
- return process.env.OLLAMA_URL ?? "http://localhost:11434";
77
- },
78
- get ollamaModel() {
79
- return process.env.OLLAMA_MODEL ?? "nomic-embed-text";
80
- },
81
- get chatModel() {
82
- return process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
83
- },
84
- get chatProvider() {
85
- return process.env.CHAT_PROVIDER ?? "ollama";
86
- },
87
- get chatApiKey() {
88
- return process.env.CHAT_API_KEY ?? "";
89
- }
90
- };
91
-
92
88
  // src/embedding.ts
93
89
  function getEmbeddingTimeoutMs() {
94
90
  const raw = Number(process.env.EMBEDDING_TIMEOUT_MS ?? process.env.MEMORY_CORE_RETRIEVAL_TIMEOUT_MS ?? 5e3);
@@ -116,392 +112,6 @@ async function embed(text) {
116
112
  return data.embedding;
117
113
  }
118
114
 
119
- // src/chat.ts
120
- function getChatConfig() {
121
- const provider = process.env.CHAT_PROVIDER ?? "ollama";
122
- const model = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
123
- return {
124
- provider,
125
- model,
126
- ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
127
- apiKey: process.env.CHAT_API_KEY ?? "",
128
- baseUrl: process.env.CHAT_BASE_URL ?? ""
129
- };
130
- }
131
- function getDefaultTimeoutMs() {
132
- const raw = Number(process.env.CHAT_TIMEOUT_MS ?? 6e4);
133
- return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 6e4;
134
- }
135
- function timeoutSignal(timeoutMs) {
136
- return AbortSignal.timeout(timeoutMs ?? getDefaultTimeoutMs());
137
- }
138
- function normalizeChatError(err, timeoutMs) {
139
- const ms = timeoutMs ?? getDefaultTimeoutMs();
140
- if (err instanceof Error && err.name === "AbortError") {
141
- return new Error(`TIMEOUT:${ms}`);
142
- }
143
- return err instanceof Error ? err : new Error(String(err));
144
- }
145
- async function callOllama(cfg, messages, options = {}) {
146
- const res = await fetch(`${cfg.ollamaUrl}/api/chat`, {
147
- method: "POST",
148
- headers: { "Content-Type": "application/json" },
149
- signal: timeoutSignal(options.timeoutMs),
150
- body: JSON.stringify({ model: cfg.model, messages, stream: false, format: "json" })
151
- });
152
- if (!res.ok) {
153
- const body = await res.text();
154
- if (body.includes("not found") || body.includes("model")) {
155
- throw new Error(`MODEL_NOT_FOUND:${cfg.model}`);
156
- }
157
- throw new Error(body);
158
- }
159
- const data = await res.json();
160
- return data.message.content.trim();
161
- }
162
- async function callOpenAICompat(cfg, messages, options = {}) {
163
- const base = (cfg.baseUrl ?? "").replace(/\/$/, "") || "https://api.openai.com/v1";
164
- const res = await fetch(`${base}/chat/completions`, {
165
- method: "POST",
166
- headers: {
167
- "Content-Type": "application/json",
168
- "Authorization": `Bearer ${cfg.apiKey}`
169
- },
170
- signal: timeoutSignal(options.timeoutMs),
171
- body: JSON.stringify({
172
- model: cfg.model,
173
- messages,
174
- response_format: { type: "json_object" }
175
- })
176
- });
177
- if (!res.ok) throw new Error(`OpenAI API error ${res.status}: ${await res.text()}`);
178
- const data = await res.json();
179
- return data.choices[0].message.content.trim();
180
- }
181
- async function callAnthropic(cfg, messages, options = {}) {
182
- const system = messages.find((m) => m.role === "system")?.content ?? "";
183
- const userMessages = messages.filter((m) => m.role !== "system");
184
- const res = await fetch("https://api.anthropic.com/v1/messages", {
185
- method: "POST",
186
- headers: {
187
- "Content-Type": "application/json",
188
- "x-api-key": cfg.apiKey,
189
- "anthropic-version": "2023-06-01"
190
- },
191
- signal: timeoutSignal(options.timeoutMs),
192
- body: JSON.stringify({
193
- model: cfg.model,
194
- max_tokens: 4096,
195
- system,
196
- messages: userMessages
197
- })
198
- });
199
- if (!res.ok) throw new Error(`Anthropic API error ${res.status}: ${await res.text()}`);
200
- const data = await res.json();
201
- return data.content[0].text.trim();
202
- }
203
- async function callMiniMax(cfg, messages, options = {}) {
204
- const res = await fetch("https://api.minimax.io/v1/chat/completions", {
205
- method: "POST",
206
- headers: {
207
- "Content-Type": "application/json",
208
- "Authorization": `Bearer ${cfg.apiKey}`
209
- },
210
- signal: timeoutSignal(options.timeoutMs),
211
- body: JSON.stringify({
212
- model: cfg.model,
213
- messages,
214
- response_format: { type: "json_object" }
215
- })
216
- });
217
- if (!res.ok) throw new Error(`MiniMax API error ${res.status}: ${await res.text()}`);
218
- const data = await res.json();
219
- return data.choices[0].message.content.trim();
220
- }
221
- async function callChatModel(messages, options = {}) {
222
- const cfg = getChatConfig();
223
- try {
224
- switch (cfg.provider) {
225
- case "openai":
226
- case "openai-compatible":
227
- return await callOpenAICompat(cfg, messages, options);
228
- case "anthropic":
229
- return await callAnthropic(cfg, messages, options);
230
- case "minimax":
231
- return await callMiniMax(cfg, messages, options);
232
- default:
233
- return await callOllama(cfg, messages, options);
234
- }
235
- } catch (err) {
236
- throw normalizeChatError(err, options.timeoutMs);
237
- }
238
- }
239
- function getChatProviderLabel() {
240
- const cfg = getChatConfig();
241
- if (cfg.provider === "ollama") return `ollama (${cfg.model})`;
242
- if (cfg.provider === "openai-compatible") {
243
- const host = cfg.baseUrl ? new URL(cfg.baseUrl).hostname : "custom";
244
- return `openai-compat/${host} (${cfg.model})`;
245
- }
246
- return `${cfg.provider} (${cfg.model})`;
247
- }
248
-
249
- // src/db.ts
250
- import pg from "pg";
251
- import { createHash } from "crypto";
252
- var { Pool } = pg;
253
- var pool = null;
254
- var migrationsRun = false;
255
- function readPositiveIntEnv(name, fallback) {
256
- const raw = Number(process.env[name]);
257
- return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
258
- }
259
- function hashMemoryContent(content) {
260
- return createHash("md5").update(content.trim()).digest("hex");
261
- }
262
- function getPool() {
263
- if (!pool) {
264
- if (!Config.databaseUrl) {
265
- throw new Error("DATABASE_URL is not set. Add it to your .env or .memory-core.env file.");
266
- }
267
- const timeoutMs = readPositiveIntEnv("DATABASE_TIMEOUT_MS", 5e3);
268
- pool = new Pool({
269
- connectionString: Config.databaseUrl,
270
- connectionTimeoutMillis: timeoutMs,
271
- query_timeout: timeoutMs,
272
- statement_timeout: timeoutMs
273
- });
274
- }
275
- return pool;
276
- }
277
- async function runMigrations() {
278
- if (migrationsRun) return;
279
- const client = await getPool().connect();
280
- try {
281
- await client.query("BEGIN");
282
- await client.query(`ALTER TABLE memories ALTER COLUMN scope SET DEFAULT 'project'`);
283
- await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS reason TEXT`);
284
- await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS content_hash TEXT`);
285
- await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS context JSONB NOT NULL DEFAULT '{}'::jsonb`);
286
- await client.query(
287
- `UPDATE memories
288
- SET content_hash = md5(trim(content))
289
- WHERE content_hash IS NULL`
290
- );
291
- await client.query(`CREATE INDEX IF NOT EXISTS memories_content_hash_idx ON memories (content_hash)`);
292
- await client.query("COMMIT");
293
- migrationsRun = true;
294
- } catch (err) {
295
- await client.query("ROLLBACK");
296
- throw err;
297
- } finally {
298
- client.release();
299
- }
300
- }
301
- async function saveMemory(memory) {
302
- await runMigrations();
303
- const { type, scope, architecture, projectName, title, content, reason, context, tags, embedding } = memory;
304
- const contentHash = hashMemoryContent(content);
305
- await getPool().query(
306
- `INSERT INTO memories (type, scope, architecture, project_name, title, content, reason, context, tags, embedding, content_hash)
307
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11)`,
308
- [
309
- type,
310
- scope,
311
- architecture ?? null,
312
- projectName ?? null,
313
- title ?? null,
314
- content,
315
- reason ?? null,
316
- JSON.stringify(context ?? {}),
317
- tags ?? [],
318
- `[${embedding.join(",")}]`,
319
- contentHash
320
- ]
321
- );
322
- }
323
- async function upsertMemory(memory) {
324
- await runMigrations();
325
- const contentHash = hashMemoryContent(memory.content);
326
- const existing = await getPool().query(
327
- `SELECT id FROM memories
328
- WHERE content_hash = $1
329
- AND COALESCE(architecture, '') = COALESCE($2, '')
330
- AND scope = $3
331
- AND type = $4
332
- LIMIT 1`,
333
- [contentHash, memory.architecture ?? null, memory.scope, memory.type]
334
- );
335
- if (existing.rowCount) return "skipped";
336
- await saveMemory(memory);
337
- return "inserted";
338
- }
339
- async function listMemories(filters = {}) {
340
- await runMigrations();
341
- const where = [];
342
- const params = [];
343
- if (filters.type) {
344
- params.push(filters.type);
345
- where.push(`type = $${params.length}`);
346
- }
347
- if (filters.scope) {
348
- params.push(filters.scope);
349
- where.push(`scope = $${params.length}`);
350
- }
351
- if (filters.architecture) {
352
- if (Array.isArray(filters.architecture)) {
353
- params.push(filters.architecture);
354
- where.push(filters.includeGlobal ? `(architecture IS NULL OR architecture = ANY($${params.length}))` : `architecture = ANY($${params.length})`);
355
- } else {
356
- params.push(filters.architecture);
357
- where.push(filters.includeGlobal ? `(architecture IS NULL OR architecture = $${params.length})` : `architecture = $${params.length}`);
358
- }
359
- }
360
- if (filters.projectName) {
361
- params.push(filters.projectName);
362
- where.push(filters.includeGlobal ? `(project_name IS NULL OR project_name = $${params.length})` : `project_name = $${params.length}`);
363
- }
364
- if (filters.tags?.length) {
365
- params.push(filters.tags);
366
- where.push(`tags && $${params.length}::text[]`);
367
- }
368
- const limit = filters.limit ?? 200;
369
- params.push(limit);
370
- const result = await getPool().query(
371
- `SELECT id, type, scope, architecture, project_name, title, content, reason, context, tags, content_hash
372
- FROM memories
373
- ${where.length ? `WHERE ${where.join(" AND ")}` : ""}
374
- ORDER BY id ASC
375
- LIMIT $${params.length}`,
376
- params
377
- );
378
- return result.rows;
379
- }
380
- async function getMemory(id) {
381
- await runMigrations();
382
- const result = await getPool().query(
383
- `SELECT id, type, scope, architecture, project_name, title, content, reason, context, tags, content_hash
384
- FROM memories
385
- WHERE id = $1`,
386
- [id]
387
- );
388
- return result.rows[0] ?? null;
389
- }
390
- async function deleteMemory(id) {
391
- await runMigrations();
392
- const result = await getPool().query(`DELETE FROM memories WHERE id = $1`, [id]);
393
- return (result.rowCount ?? 0) > 0;
394
- }
395
- async function deleteMemories(filters) {
396
- await runMigrations();
397
- const where = [];
398
- const params = [];
399
- if (filters.type) {
400
- params.push(filters.type);
401
- where.push(`type = $${params.length}`);
402
- }
403
- if (filters.scope) {
404
- params.push(filters.scope);
405
- where.push(`scope = $${params.length}`);
406
- }
407
- if (filters.architecture) {
408
- if (Array.isArray(filters.architecture)) {
409
- params.push(filters.architecture);
410
- where.push(`architecture = ANY($${params.length})`);
411
- } else {
412
- params.push(filters.architecture);
413
- where.push(`architecture = $${params.length}`);
414
- }
415
- }
416
- if (filters.tag) {
417
- params.push(filters.tag);
418
- where.push(`$${params.length} = ANY(tags)`);
419
- }
420
- if (where.length === 0) {
421
- throw new Error("Refusing to bulk-delete without filters");
422
- }
423
- const result = await getPool().query(
424
- `DELETE FROM memories WHERE ${where.join(" AND ")}`,
425
- params
426
- );
427
- return result.rowCount ?? 0;
428
- }
429
- async function updateMemory(id, patch) {
430
- await runMigrations();
431
- const current = await getMemory(id);
432
- if (!current) return null;
433
- const content = patch.content ?? current.content;
434
- const contentHash = hashMemoryContent(content);
435
- const embedding = patch.embedding ? `[${patch.embedding.join(",")}]` : null;
436
- const result = await getPool().query(
437
- `UPDATE memories
438
- SET type = $2,
439
- scope = $3,
440
- title = $4,
441
- content = $5,
442
- reason = $6,
443
- context = $7::jsonb,
444
- tags = $8,
445
- content_hash = $9,
446
- embedding = COALESCE($10::vector, embedding)
447
- WHERE id = $1
448
- RETURNING id, type, scope, architecture, project_name, title, content, reason, context, tags, content_hash`,
449
- [
450
- id,
451
- patch.type ?? current.type,
452
- patch.scope ?? current.scope,
453
- patch.title ?? current.title ?? null,
454
- content,
455
- patch.reason ?? current.reason ?? null,
456
- JSON.stringify(patch.context ?? current.context ?? {}),
457
- patch.tags ?? current.tags ?? [],
458
- contentHash,
459
- embedding
460
- ]
461
- );
462
- return result.rows[0] ?? null;
463
- }
464
- async function searchMemories(embedding, architectures, limit = 10) {
465
- await runMigrations();
466
- const vector = `[${embedding.join(",")}]`;
467
- const params = [vector];
468
- let whereClause = "";
469
- const selectedArchitectures = architectures ? (Array.isArray(architectures) ? architectures : [architectures]).filter(Boolean) : [];
470
- if (selectedArchitectures.length > 0) {
471
- whereClause = `WHERE (
472
- architecture = ANY($2)
473
- OR architecture IS NULL
474
- OR architecture = 'global'
475
- )`;
476
- params.push(selectedArchitectures);
477
- }
478
- const client = await getPool().connect();
479
- try {
480
- await client.query("BEGIN");
481
- await client.query("SET LOCAL ivfflat.probes = 10");
482
- const result = await client.query(
483
- `SELECT id, type, scope, architecture, project_name, title, content, reason, context, tags,
484
- 1 - (embedding <=> $1) AS similarity
485
- FROM memories
486
- ${whereClause}
487
- ORDER BY embedding <=> $1
488
- LIMIT $${params.length + 1}`,
489
- [...params, limit]
490
- );
491
- await client.query("COMMIT");
492
- return result.rows;
493
- } finally {
494
- client.release();
495
- }
496
- }
497
- async function closePool() {
498
- if (pool) {
499
- await pool.end();
500
- pool = null;
501
- migrationsRun = false;
502
- }
503
- }
504
-
505
115
  // src/infrastructure/persistence/postgres/postgres-graph-repository.ts
506
116
  var graphMigrationsRun = false;
507
117
  function asPosix(value) {
@@ -1008,16 +618,180 @@ var seeds = [
1008
618
  { 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
619
  ];
1010
620
 
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";
621
+ // src/memory-file.ts
622
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
623
+ import { join as join2 } from "path";
624
+ var MEMORY_FILE = "memories.json";
625
+ function toPortableMemory(memory) {
626
+ return {
627
+ type: memory.type,
628
+ scope: memory.scope,
629
+ architecture: memory.architecture,
630
+ projectName: memory.project_name,
631
+ title: memory.title,
632
+ content: memory.content,
633
+ reason: memory.reason,
634
+ context: memory.context,
635
+ tags: memory.tags ?? []
636
+ };
637
+ }
638
+ function normalizeStringArray(value) {
639
+ if (!Array.isArray(value)) return void 0;
640
+ const entries = value.filter((entry) => typeof entry === "string" && entry.trim() !== "");
641
+ return entries.length ? entries : void 0;
642
+ }
643
+ function parseContext(value) {
644
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
645
+ const record = value;
646
+ const context = {};
647
+ const appliesTo = normalizeStringArray(record.appliesTo);
648
+ const avoidWhen = normalizeStringArray(record.avoidWhen);
649
+ const examples = normalizeStringArray(record.examples);
650
+ if (appliesTo) context.appliesTo = appliesTo;
651
+ if (avoidWhen) context.avoidWhen = avoidWhen;
652
+ if (examples) context.examples = examples;
653
+ if (typeof record.source === "string" && record.source.trim() !== "") context.source = record.source;
654
+ return Object.keys(context).length ? context : void 0;
655
+ }
656
+ function writeMemoryFile(memories, cwd = process.cwd()) {
657
+ const path = join2(cwd, MEMORY_FILE);
658
+ writeFileSync(path, JSON.stringify(memories, null, 2) + "\n", "utf-8");
659
+ return path;
660
+ }
661
+ function readMemoryFile(cwd = process.cwd()) {
662
+ const path = join2(cwd, MEMORY_FILE);
663
+ if (!existsSync2(path)) {
664
+ throw new Error(`${MEMORY_FILE} not found. Run: memory-core export`);
665
+ }
666
+ return parseMemoryFile(readFileSync2(path, "utf-8"));
667
+ }
668
+ async function readMemoryFileFromUrl(url) {
669
+ const res = await fetch(url, { signal: AbortSignal.timeout(15e3) });
670
+ if (!res.ok) throw new Error(`Failed to download ${url}: HTTP ${res.status}`);
671
+ return parseMemoryFile(await res.text());
672
+ }
673
+ function parseMemoryFile(raw) {
674
+ const parsed = JSON.parse(raw);
675
+ if (!Array.isArray(parsed)) {
676
+ throw new Error(`${MEMORY_FILE} must be a JSON array`);
677
+ }
678
+ return parsed.map((item, index) => {
679
+ if (!item || typeof item !== "object") {
680
+ throw new Error(`Memory at index ${index} must be an object`);
681
+ }
682
+ const record = item;
683
+ if (typeof record.content !== "string" || record.content.trim() === "") {
684
+ throw new Error(`Memory at index ${index} is missing content`);
685
+ }
686
+ return {
687
+ type: typeof record.type === "string" ? record.type : "rule",
688
+ scope: typeof record.scope === "string" ? record.scope : "project",
689
+ architecture: typeof record.architecture === "string" ? record.architecture : void 0,
690
+ projectName: typeof record.projectName === "string" ? record.projectName : void 0,
691
+ title: typeof record.title === "string" ? record.title : void 0,
692
+ content: record.content,
693
+ reason: typeof record.reason === "string" ? record.reason : void 0,
694
+ context: parseContext(record.context),
695
+ tags: Array.isArray(record.tags) ? record.tags.filter((tag) => typeof tag === "string") : []
696
+ };
697
+ });
698
+ }
699
+
700
+ // src/modules/rule-engine/infrastructure/schema-violations.ts
701
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
702
+ import { join as join3 } from "path";
703
+ function parseSchemaRule(content) {
704
+ try {
705
+ const parsed = JSON.parse(content);
706
+ if (parsed !== null && typeof parsed === "object" && "tsFile" in parsed && "goFile" in parsed && typeof parsed.tsFile === "string" && typeof parsed.goFile === "string") {
707
+ return parsed;
708
+ }
709
+ } catch {
710
+ }
711
+ return null;
712
+ }
713
+ function extractTsFields(source) {
714
+ const fields = [];
715
+ const bodyMatch = source.match(/(?:interface|type)\s+\w+(?:<[^>]*>)?\s*(?:=\s*)?\{([^}]+)\}/s);
716
+ if (!bodyMatch) return fields;
717
+ for (const line of bodyMatch[1].split("\n")) {
718
+ const match = line.match(/^\s*(\w+)\??:/);
719
+ if (match) fields.push(match[1]);
720
+ }
721
+ return fields;
722
+ }
723
+ function extractGoFields(source) {
724
+ const fields = [];
725
+ const bodyMatch = source.match(/type\s+\w+\s+struct\s*\{([^}]+)\}/s);
726
+ if (!bodyMatch) return fields;
727
+ for (const line of bodyMatch[1].split("\n")) {
728
+ const jsonTag = line.match(/json:"([^",\s]+)/);
729
+ if (jsonTag) {
730
+ if (jsonTag[1] !== "-") fields.push(jsonTag[1]);
731
+ continue;
732
+ }
733
+ const fieldMatch = line.match(/^\s+([A-Z]\w*)/);
734
+ if (fieldMatch) {
735
+ const name = fieldMatch[1];
736
+ fields.push(name.charAt(0).toLowerCase() + name.slice(1));
737
+ }
738
+ }
739
+ return fields;
740
+ }
741
+ async function findSchemaViolations(opts) {
742
+ if (!opts.memoryEngine) return [];
743
+ let schemaMemories;
744
+ try {
745
+ schemaMemories = await opts.memoryEngine.list({ type: "schema", limit: 100 });
746
+ } catch {
747
+ return [];
748
+ }
749
+ const violations = [];
750
+ for (const memory of schemaMemories) {
751
+ const rule = parseSchemaRule(memory.content);
752
+ if (!rule) continue;
753
+ const tsPath = join3(opts.cwd, rule.tsFile);
754
+ const goPath = join3(opts.cwd, rule.goFile);
755
+ if (!existsSync3(tsPath) || !existsSync3(goPath)) continue;
756
+ const tsSource = readFileSync3(tsPath, "utf-8");
757
+ const goSource = readFileSync3(goPath, "utf-8");
758
+ const tsFields = new Set(extractTsFields(tsSource));
759
+ const goFields = new Set(extractGoFields(goSource));
760
+ for (const field of goFields) {
761
+ if (!tsFields.has(field)) {
762
+ violations.push({
763
+ rule: `Schema alignment: ${rule.tsFile} must match ${rule.goFile}`,
764
+ file: rule.tsFile,
765
+ issue: `Go field "${field}" missing in TypeScript file`,
766
+ suggestion: `Add "${field}" to the TypeScript interface/type in ${rule.tsFile}`,
767
+ reason: "Schema drift between TypeScript and Go causes runtime mismatches"
768
+ });
769
+ }
770
+ }
771
+ for (const field of tsFields) {
772
+ if (!goFields.has(field)) {
773
+ violations.push({
774
+ rule: `Schema alignment: ${rule.tsFile} must match ${rule.goFile}`,
775
+ file: rule.goFile,
776
+ issue: `TypeScript field "${field}" missing in Go struct`,
777
+ suggestion: `Add "${field}" to the Go struct in ${rule.goFile}`,
778
+ reason: "Schema drift between TypeScript and Go causes runtime mismatches"
779
+ });
780
+ }
781
+ }
782
+ }
783
+ return violations;
784
+ }
785
+
786
+ // src/hook.ts
787
+ import { execSync, spawnSync as spawnSync2 } from "child_process";
788
+ import { writeFileSync as writeFileSync5, existsSync as existsSync7, unlinkSync, readFileSync as readFileSync7, chmodSync, statSync as statSync3 } from "fs";
789
+ import { join as join8 } from "path";
790
+ import chalk2 from "chalk";
1017
791
 
1018
792
  // 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";
793
+ import { readFileSync as readFileSync6, readdirSync as readdirSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync6 } from "fs";
794
+ import { join as join7, dirname as dirname3, basename } from "path";
1021
795
  import { fileURLToPath } from "url";
1022
796
  import Handlebars from "handlebars";
1023
797
  import yaml from "js-yaml";
@@ -1038,7 +812,8 @@ var ChatLlmProvider = class {
1038
812
  return getChatProviderLabel();
1039
813
  }
1040
814
  async generateText(messages) {
1041
- return callChatModel(messages);
815
+ const result = await callChatModel(messages);
816
+ return result.content;
1042
817
  }
1043
818
  };
1044
819
 
@@ -1125,9 +900,9 @@ var PostgresMemoryRepository = class {
1125
900
  };
1126
901
 
1127
902
  // 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");
903
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
904
+ import { dirname, join as join4 } from "path";
905
+ var DEFAULT_FILE = join4(".memory-core", "graph-snapshots.json");
1131
906
  function asPosix2(value) {
1132
907
  return value.replace(/\\/g, "/");
1133
908
  }
@@ -1166,11 +941,11 @@ var FileGraphRepository = class {
1166
941
  }
1167
942
  readData() {
1168
943
  const filePath = this.absolutePath();
1169
- if (!existsSync3(filePath)) {
944
+ if (!existsSync4(filePath)) {
1170
945
  return { version: 1, snapshots: [] };
1171
946
  }
1172
947
  try {
1173
- const parsed = JSON.parse(readFileSync2(filePath, "utf-8"));
948
+ const parsed = JSON.parse(readFileSync4(filePath, "utf-8"));
1174
949
  if (!Array.isArray(parsed.snapshots)) {
1175
950
  return { version: 1, snapshots: [] };
1176
951
  }
@@ -1187,11 +962,11 @@ var FileGraphRepository = class {
1187
962
  writeData(data) {
1188
963
  const filePath = this.absolutePath();
1189
964
  mkdirSync(dirname(filePath), { recursive: true });
1190
- writeFileSync(filePath, `${JSON.stringify(data, null, 2)}
965
+ writeFileSync2(filePath, `${JSON.stringify(data, null, 2)}
1191
966
  `, "utf-8");
1192
967
  }
1193
968
  absolutePath() {
1194
- return join3(this.rootPath, this.relativeFilePath);
969
+ return join4(this.rootPath, this.relativeFilePath);
1195
970
  }
1196
971
  };
1197
972
 
@@ -1236,77 +1011,1023 @@ var ResilientGraphRepository = class {
1236
1011
  }
1237
1012
  };
1238
1013
 
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
- };
1014
+ // src/watcher.ts
1015
+ import { watch } from "chokidar";
1016
+ import { spawnSync } from "child_process";
1017
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync3 } from "fs";
1018
+ import { dirname as dirname2, join as join5, relative, resolve as resolve2, sep } from "path";
1019
+ import chalk from "chalk";
1282
1020
 
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);
1021
+ // src/modules/rule-engine/infrastructure/ast-deterministic-violations.ts
1022
+ import { resolve } from "path";
1023
+ function asPosix3(value) {
1024
+ return value.replace(/\\/g, "/");
1025
+ }
1026
+ function hasPath(value, pathSegment) {
1027
+ const normalized = asPosix3(value);
1028
+ const trimmed = pathSegment.startsWith("/") ? pathSegment.slice(1) : pathSegment;
1029
+ return normalized.includes(pathSegment) || normalized.includes(trimmed);
1030
+ }
1031
+ function isLegacyOrCompatibilitySpecifier(specifier) {
1032
+ const normalized = asPosix3(specifier);
1033
+ return normalized.includes("/compatibility/") || normalized.includes("compatibility/") || normalized.includes("legacy-");
1034
+ }
1035
+ function isLegacyOrCompatibilityPath(pathValue) {
1036
+ if (!pathValue) return false;
1037
+ const normalized = asPosix3(pathValue);
1038
+ return normalized.includes("/src/compatibility/") || normalized.includes("/legacy-");
1039
+ }
1040
+ function moduleNameFromPath(pathValue) {
1041
+ const match = asPosix3(pathValue).match(/src\/modules\/([^/]+)\//);
1042
+ return match?.[1];
1043
+ }
1044
+ function isModulePublicPath(pathValue) {
1045
+ const normalized = asPosix3(pathValue);
1046
+ return /src\/modules\/[^/]+\/(public|api)\//.test(normalized) || /src\/modules\/[^/]+\/index\.(ts|tsx|js|jsx|mjs|cjs)$/.test(normalized);
1047
+ }
1048
+ function detectCleanLayer(pathValue) {
1049
+ const normalized = asPosix3(pathValue);
1050
+ if (hasPath(normalized, "/src/domain/") || hasPath(normalized, "/src/core/domain/")) return "domain";
1051
+ if (hasPath(normalized, "/src/application/") || hasPath(normalized, "/src/core/application/")) return "application";
1052
+ if (hasPath(normalized, "/src/infrastructure/")) return "infrastructure";
1053
+ if (hasPath(normalized, "/src/interfaces/")) return "interface";
1054
+ return "unknown";
1055
+ }
1056
+ function detectHexLayer(pathValue) {
1057
+ const normalized = asPosix3(pathValue);
1058
+ if (hasPath(normalized, "/src/core/")) return "core";
1059
+ if (hasPath(normalized, "/src/adapters/inbound/")) return "adapter-inbound";
1060
+ if (hasPath(normalized, "/src/adapters/outbound/")) return "adapter-outbound";
1061
+ if (hasPath(normalized, "/src/adapters/")) return "adapter-other";
1062
+ return "unknown";
1063
+ }
1064
+ function activeArchitectures(config, rules = []) {
1065
+ const names = /* @__PURE__ */ new Set();
1066
+ if (config?.backendArchitecture) names.add(config.backendArchitecture);
1067
+ if (config?.frontendFramework) names.add(config.frontendFramework);
1068
+ const text = rules.join("\n").toLowerCase();
1069
+ if (text.includes("modular monolith")) names.add("modular-monolith");
1070
+ if (text.includes("clean architecture")) names.add("clean-architecture");
1071
+ if (text.includes("hexagonal")) names.add("hexagonal");
1072
+ return names;
1073
+ }
1074
+ function pushUnique(target, incoming) {
1075
+ if (target.some(
1076
+ (entry) => entry.rule === incoming.rule && entry.file === incoming.file && entry.line === incoming.line && entry.issue === incoming.issue
1077
+ )) {
1078
+ return;
1079
+ }
1080
+ target.push(incoming);
1081
+ }
1082
+ function evaluateFile(file, options) {
1083
+ const cwd = options.cwd ?? process.cwd();
1084
+ const rules = options.rules ?? [];
1085
+ const architectures = activeArchitectures(options.config, rules);
1086
+ const reasonLookup = options.reasonLookup ?? /* @__PURE__ */ new Map();
1087
+ const violations = [];
1088
+ const absFile = resolve(cwd, file);
1089
+ const normalizedFile = asPosix3(file);
1090
+ const imports = collectResolvedImports(absFile, cwd);
1091
+ const fromCleanLayer = detectCleanLayer(normalizedFile);
1092
+ const fromHexLayer = detectHexLayer(normalizedFile);
1093
+ for (const imp of imports) {
1094
+ const target = imp.resolvedPath ? asPosix3(imp.resolvedPath) : void 0;
1095
+ if (isLegacyOrCompatibilitySpecifier(imp.specifier) || isLegacyOrCompatibilityPath(target)) {
1096
+ const rule = "Application code must not import compatibility or legacy adapter paths";
1097
+ pushUnique(violations, {
1098
+ rule,
1099
+ file,
1100
+ line: imp.line,
1101
+ issue: `Import references a removed migration path: ${imp.specifier}`,
1102
+ suggestion: "Import module services/ports from src/app, src/modules, src/shared/ports, or current infrastructure adapters.",
1103
+ reason: reasonLookup.get(rule)
1104
+ });
1105
+ }
1106
+ if (architectures.has("modular-monolith")) {
1107
+ const fromModule = moduleNameFromPath(normalizedFile);
1108
+ const toModule = target ? moduleNameFromPath(target) : void 0;
1109
+ if (fromModule && toModule && target && fromModule !== toModule && !isModulePublicPath(target)) {
1110
+ const rule = "Modules communicate only through public interfaces or events \u2014 never by importing internals";
1111
+ pushUnique(violations, {
1112
+ rule,
1113
+ file,
1114
+ line: imp.line,
1115
+ issue: `Cross-module import from "${fromModule}" to private path in module "${toModule}"`,
1116
+ suggestion: `Expose a public API from src/modules/${toModule}/index.ts (or public/) and import through it.`,
1117
+ reason: reasonLookup.get(rule)
1118
+ });
1119
+ }
1120
+ }
1121
+ if (architectures.has("clean-architecture")) {
1122
+ const toCleanLayer = target ? detectCleanLayer(target) : "unknown";
1123
+ if (fromCleanLayer === "domain" && ["application", "infrastructure", "interface"].includes(toCleanLayer)) {
1124
+ const rule = "Entities encapsulate core business logic and have no external dependencies";
1125
+ pushUnique(violations, {
1126
+ rule,
1127
+ file,
1128
+ line: imp.line,
1129
+ issue: `Domain layer imports ${toCleanLayer} layer: ${imp.specifier}`,
1130
+ suggestion: "Keep domain isolated and move orchestration concerns into application layer.",
1131
+ reason: reasonLookup.get(rule)
1132
+ });
1133
+ }
1134
+ if (fromCleanLayer === "application" && (toCleanLayer === "infrastructure" || toCleanLayer === "interface")) {
1135
+ const rule = "Infrastructure layer (DB, HTTP, queues) depends on application \u2014 never the reverse";
1136
+ pushUnique(violations, {
1137
+ rule,
1138
+ file,
1139
+ line: imp.line,
1140
+ issue: `Application layer imports ${toCleanLayer} layer: ${imp.specifier}`,
1141
+ suggestion: "Invert dependency via repository/port interface in application layer.",
1142
+ reason: reasonLookup.get(rule)
1143
+ });
1144
+ }
1145
+ if (fromCleanLayer === "interface" && toCleanLayer === "infrastructure") {
1146
+ const rule = "Controllers must only validate input and delegate to use cases";
1147
+ pushUnique(violations, {
1148
+ rule,
1149
+ file,
1150
+ line: imp.line,
1151
+ issue: `Interface/controller layer imports infrastructure directly: ${imp.specifier}`,
1152
+ suggestion: "Delegate to application use cases instead of calling infrastructure directly.",
1153
+ reason: reasonLookup.get(rule)
1154
+ });
1155
+ }
1156
+ if (fromCleanLayer === "domain" && imp.isExternal && isExternalFrameworkSpecifier(imp.specifier)) {
1157
+ const rule = "Domain layer must not import any framework or library code";
1158
+ pushUnique(violations, {
1159
+ rule,
1160
+ file,
1161
+ line: imp.line,
1162
+ issue: `Domain file imports framework package: ${imp.specifier}`,
1163
+ suggestion: "Keep domain pure. Move framework-specific logic to infrastructure/adapters.",
1164
+ reason: reasonLookup.get(rule)
1165
+ });
1166
+ }
1167
+ }
1168
+ if (architectures.has("hexagonal")) {
1169
+ const toHexLayer = target ? detectHexLayer(target) : "unknown";
1170
+ if (fromHexLayer === "core" && (toHexLayer === "adapter-inbound" || toHexLayer === "adapter-outbound" || toHexLayer === "adapter-other")) {
1171
+ const rule = "Direct imports of adapter code inside the core";
1172
+ pushUnique(violations, {
1173
+ rule,
1174
+ file,
1175
+ line: imp.line,
1176
+ issue: `Core imports adapter path directly: ${imp.specifier}`,
1177
+ suggestion: "Define a core port and resolve adapter at composition root.",
1178
+ reason: reasonLookup.get(rule)
1179
+ });
1180
+ }
1181
+ const crossAdapterBoundary = fromHexLayer === "adapter-inbound" && (toHexLayer === "adapter-outbound" || toHexLayer === "adapter-other") || fromHexLayer === "adapter-outbound" && (toHexLayer === "adapter-inbound" || toHexLayer === "adapter-other");
1182
+ if (crossAdapterBoundary) {
1183
+ const rule = "Adapters implement ports \u2014 one adapter per external system (DB, HTTP, queue, etc.)";
1184
+ pushUnique(violations, {
1185
+ rule,
1186
+ file,
1187
+ line: imp.line,
1188
+ issue: `Adapter imports another adapter layer directly: ${imp.specifier}`,
1189
+ suggestion: "Route adapter collaboration through core ports/use-cases, not direct adapter imports.",
1190
+ reason: reasonLookup.get(rule)
1191
+ });
1192
+ }
1193
+ }
1194
+ }
1195
+ return violations;
1196
+ }
1197
+ function findAstDeterministicViolationsForFile(file, options = {}) {
1198
+ return evaluateFile(file, options);
1199
+ }
1200
+ function findAstDeterministicViolationsForDiff(diff, options = {}) {
1201
+ const files = parseChangedFilesFromDiff(diff).filter((file) => /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file));
1202
+ const violations = [];
1203
+ for (const file of files) {
1204
+ for (const violation of evaluateFile(file, options)) {
1205
+ pushUnique(violations, violation);
1206
+ }
1207
+ }
1208
+ const architectures = activeArchitectures(options.config, options.rules ?? []);
1209
+ if (architectures.has("modular-monolith")) {
1210
+ const edges = buildModuleDependencyEdges(files, options.cwd ?? process.cwd());
1211
+ const cycles = detectModuleCycles(edges);
1212
+ for (const cycle of cycles) {
1213
+ const representative = edges.find((edge) => edge.fromModule === cycle[0] && edge.toModule === cycle[1]);
1214
+ if (!representative) continue;
1215
+ const rule = "No circular dependencies between modules";
1216
+ pushUnique(violations, {
1217
+ rule,
1218
+ file: representative.file,
1219
+ line: representative.line,
1220
+ issue: `Module dependency cycle detected: ${cycle.join(" -> ")}`,
1221
+ suggestion: "Break the cycle by introducing a public port/event or moving shared logic into src/shared.",
1222
+ reason: options.reasonLookup?.get(rule)
1223
+ });
1224
+ }
1225
+ }
1226
+ return violations;
1227
+ }
1228
+
1229
+ // src/watcher.ts
1230
+ function getFileLines(filePath) {
1231
+ try {
1232
+ return readFileSync5(filePath, "utf-8").split("\n");
1233
+ } catch {
1234
+ return [];
1235
+ }
1236
+ }
1237
+ function printCodeContext(filePath, line, contextLines = 2) {
1238
+ const lines = getFileLines(filePath);
1239
+ if (lines.length === 0) return;
1240
+ const start = Math.max(0, line - 1 - contextLines);
1241
+ const end = Math.min(lines.length - 1, line - 1 + contextLines);
1242
+ 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"));
1243
+ for (let i = start; i <= end; i++) {
1244
+ const lineNum = String(i + 1).padStart(4, " ");
1245
+ const isViolation = i === line - 1;
1246
+ if (isViolation) {
1247
+ console.log(chalk.red(` \u2502 ${lineNum} \u25B6 ${lines[i]}`));
1248
+ } else {
1249
+ console.log(chalk.dim(` \u2502 ${lineNum} ${lines[i]}`));
1250
+ }
1251
+ }
1252
+ 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"));
1253
+ }
1254
+ function formatCodeContext(filePath, line, contextLines = 2) {
1255
+ const lines = getFileLines(filePath);
1256
+ if (lines.length === 0) return void 0;
1257
+ const start = Math.max(0, line - 1 - contextLines);
1258
+ const end = Math.min(lines.length - 1, line - 1 + contextLines);
1259
+ return Array.from({ length: end - start + 1 }, (_, index) => {
1260
+ const current = start + index;
1261
+ const lineNum = String(current + 1).padStart(4, " ");
1262
+ const marker = current === line - 1 ? ">" : " ";
1263
+ return `${lineNum} ${marker} ${lines[current]}`;
1264
+ }).join("\n");
1265
+ }
1266
+ var SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
1267
+ var reasonMap = new Map(
1268
+ seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
1269
+ );
1270
+ function findProjectRoot(startPath) {
1271
+ let current = resolve2(startPath);
1272
+ while (true) {
1273
+ if (existsSync5(join5(current, ".memory-core.json"))) return current;
1274
+ const parent = dirname2(current);
1275
+ if (parent === current) return null;
1276
+ current = parent;
1277
+ }
1278
+ }
1279
+ function resolveWatchPaths(pathOption, projectRootOption) {
1280
+ if (projectRootOption) {
1281
+ const projectRoot2 = resolve2(projectRootOption);
1282
+ return {
1283
+ projectRoot: projectRoot2,
1284
+ watchPath: resolve2(projectRoot2, pathOption ?? ".")
1285
+ };
1286
+ }
1287
+ const cwdRoot = resolve2(process.cwd());
1288
+ const watchPath = resolve2(cwdRoot, pathOption ?? ".");
1289
+ const projectRoot = findProjectRoot(watchPath) ?? findProjectRoot(cwdRoot) ?? cwdRoot;
1290
+ return { projectRoot, watchPath };
1291
+ }
1292
+ function readStatsFile(statsPath) {
1293
+ if (!existsSync5(statsPath)) return { rules: {}, files: {} };
1294
+ try {
1295
+ return JSON.parse(readFileSync5(statsPath, "utf-8"));
1296
+ } catch {
1297
+ return { rules: {}, files: {} };
1298
+ }
1299
+ }
1300
+ function rebuildLiveCounters(byFile) {
1301
+ const rules = {};
1302
+ const files = {};
1303
+ for (const [file, violations] of Object.entries(byFile)) {
1304
+ if (!Array.isArray(violations) || violations.length === 0) continue;
1305
+ files[file] = violations.length;
1306
+ for (const violation of violations) {
1307
+ rules[violation.rule] = (rules[violation.rule] ?? 0) + 1;
1308
+ }
1309
+ }
1310
+ return { rules, files };
1311
+ }
1312
+ function resetLiveStats(cwd) {
1313
+ const statsPath = join5(cwd, ".memory-core-stats.json");
1314
+ const stats = readStatsFile(statsPath);
1315
+ stats.rules ??= {};
1316
+ stats.files ??= {};
1317
+ stats.live = {
1318
+ rules: {},
1319
+ files: {},
1320
+ byFile: {}
1321
+ };
1322
+ writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
1323
+ }
1324
+ function recordWatchResult(cwd, file, violations) {
1325
+ const statsPath = join5(cwd, ".memory-core-stats.json");
1326
+ const stats = readStatsFile(statsPath);
1327
+ stats.rules ??= {};
1328
+ stats.files ??= {};
1329
+ stats.live ??= { rules: {}, files: {}, byFile: {} };
1330
+ stats.live.byFile ??= {};
1331
+ if (violations.length === 0) {
1332
+ delete stats.live.byFile[file];
1333
+ } else {
1334
+ stats.live.byFile[file] = violations;
1335
+ }
1336
+ const live = rebuildLiveCounters(stats.live.byFile);
1337
+ stats.live.rules = live.rules;
1338
+ stats.live.files = live.files;
1339
+ for (const violation of violations) {
1340
+ stats.rules[violation.rule] = (stats.rules[violation.rule] ?? 0) + 1;
1341
+ if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
1342
+ }
1343
+ if (violations.length > 0) {
1344
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1345
+ const recent = violations.map((violation) => ({ ...violation, timestamp, source: "watch" }));
1346
+ stats.recentViolations = [...recent, ...stats.recentViolations ?? []].slice(0, 50);
1347
+ }
1348
+ writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
1349
+ }
1350
+ function loadConfig(cwd) {
1351
+ const configPath = join5(cwd, ".memory-core.json");
1352
+ if (!existsSync5(configPath)) return null;
1353
+ try {
1354
+ return JSON.parse(readFileSync5(configPath, "utf-8"));
1355
+ } catch {
1356
+ return null;
1357
+ }
1358
+ }
1359
+ function getProfileRules(config) {
1360
+ const rules = [];
1361
+ const avoids = [];
1362
+ if (config.backendArchitecture) {
1363
+ const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
1364
+ if (profile) {
1365
+ rules.push(...profile.rules);
1366
+ avoids.push(...profile.avoid);
1367
+ }
1368
+ }
1369
+ if (config.frontendFramework) {
1370
+ const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
1371
+ if (profile) {
1372
+ rules.push(...profile.rules);
1373
+ avoids.push(...profile.avoid);
1374
+ }
1375
+ }
1376
+ return { rules, avoids };
1377
+ }
1378
+ async function loadRelevantRules(cwd, config, rel, diff, fallbackRules) {
1379
+ try {
1380
+ const query = buildContextQuery([
1381
+ rel,
1382
+ diff.slice(0, 1200),
1383
+ config.backendArchitecture,
1384
+ config.frontendFramework,
1385
+ config.language
1386
+ ]);
1387
+ const memories = await retrieveContextualMemories({
1388
+ query,
1389
+ cwd,
1390
+ config,
1391
+ limit: 15
1392
+ });
1393
+ const selected = memories.filter((memory) => ["rule", "pattern", "decision"].includes(memory.type)).map((memory) => memory.content);
1394
+ return selected.length > 0 ? selected : fallbackRules;
1395
+ } catch {
1396
+ return fallbackRules;
1397
+ }
1398
+ }
1399
+ function applyAllowPatterns(violations, allowPatterns) {
1400
+ if (allowPatterns.length === 0) return violations;
1401
+ return violations.filter((violation) => {
1402
+ const haystack = `${violation.rule}
1403
+ ${violation.issue}
1404
+ ${violation.file}`.toLowerCase();
1405
+ return !allowPatterns.some((pattern) => haystack.includes(pattern.toLowerCase()));
1406
+ });
1407
+ }
1408
+ async function verifyViolations(inputText, violations, allowPatterns, debug, mode = "diff") {
1409
+ if (violations.length === 0) return violations;
1410
+ const sourceLabel = mode === "snapshot" ? "file content" : "diff";
1411
+ const systemPrompt = `You are verifying candidate architecture violations.
1412
+ Only keep violations that are directly supported by the ${sourceLabel}.
1413
+ Reject speculative or weak matches.
1414
+ Treat these allowlisted patterns as intentional and valid:
1415
+ ${allowPatterns.length ? allowPatterns.map((pattern, index) => `${index + 1}. ${pattern}`).join("\n") : "(none)"}
1416
+
1417
+ Return strict JSON:
1418
+ {"violations":[{"rule":"...","file":"...","line":1,"issue":"...","suggestion":"...","reason":"..."}]}
1419
+ Do not include any text outside the JSON.`;
1420
+ const userPrompt = `${mode === "snapshot" ? "File content" : "Diff"}:
1421
+ ${inputText.slice(0, 6e3)}
1422
+
1423
+ Candidate violations:
1424
+ ${JSON.stringify(violations, null, 2)}`;
1425
+ if (debug) {
1426
+ console.log(chalk.gray("\n [debug] verifier prompt:"));
1427
+ console.log(chalk.dim(systemPrompt));
1428
+ console.log(chalk.dim(userPrompt));
1429
+ 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"));
1430
+ }
1431
+ try {
1432
+ const { content: raw, usage: verifyUsage } = await callChatModel([
1433
+ { role: "system", content: systemPrompt },
1434
+ { role: "user", content: userPrompt }
1435
+ ]);
1436
+ accumulateTokenUsage(verifyUsage);
1437
+ const parsed = JSON.parse(raw);
1438
+ if (Array.isArray(parsed?.violations)) return parsed.violations;
1439
+ if (Array.isArray(parsed)) return parsed;
1440
+ return violations;
1441
+ } catch {
1442
+ return violations;
1443
+ }
1444
+ }
1445
+ async function loadIgnorePatterns() {
1446
+ try {
1447
+ const app = getDefaultApplicationContainer();
1448
+ const ignores = await app.services.memoryEngine.list({ type: "ignore", limit: 1e3 });
1449
+ return ignores.map((ignore) => ignore.content);
1450
+ } catch {
1451
+ return [];
1452
+ }
1453
+ }
1454
+ function normalizeForGit(pathLike) {
1455
+ return pathLike.split(sep).join("/");
1456
+ }
1457
+ function listSourceFilesFromFilesystem(dir) {
1458
+ if (!existsSync5(dir)) return [];
1459
+ const files = [];
1460
+ const stack = [dir];
1461
+ while (stack.length > 0) {
1462
+ const current = stack.pop();
1463
+ let entries = [];
1464
+ try {
1465
+ entries = readdirSync(current);
1466
+ } catch {
1467
+ continue;
1468
+ }
1469
+ for (const entry of entries) {
1470
+ const absolute = join5(current, entry);
1471
+ let isDirectory = false;
1472
+ let isFile = false;
1473
+ try {
1474
+ const stats = statSync(absolute);
1475
+ isDirectory = stats.isDirectory();
1476
+ isFile = stats.isFile();
1477
+ } catch {
1478
+ continue;
1479
+ }
1480
+ if (isDirectory) {
1481
+ if (entry === "node_modules" || entry === ".git" || entry === "dist" || entry === "build" || entry === "coverage") {
1482
+ continue;
1483
+ }
1484
+ stack.push(absolute);
1485
+ continue;
1486
+ }
1487
+ if (isFile && SOURCE_EXTENSIONS.test(absolute)) files.push(absolute);
1488
+ }
1489
+ }
1490
+ return files;
1491
+ }
1492
+ function listTrackedSourceFiles(projectRoot, watchPath) {
1493
+ const relPrefix = normalizeForGit(relative(projectRoot, watchPath));
1494
+ const inRoot = relPrefix === "" || relPrefix === ".";
1495
+ const prefixWithSlash = inRoot ? "" : `${relPrefix}/`;
1496
+ const listed = spawnSync("git", ["ls-files"], { encoding: "utf-8", cwd: projectRoot });
1497
+ if (listed.status !== 0) {
1498
+ return listSourceFilesFromFilesystem(watchPath).sort();
1499
+ }
1500
+ const files = (listed.stdout ?? "").split("\n").filter((file) => file.length > 0).filter((file) => SOURCE_EXTENSIONS.test(file)).filter((file) => inRoot || file.startsWith(prefixWithSlash)).map((file) => join5(projectRoot, file)).filter((file) => existsSync5(file));
1501
+ return [...new Set(files)].sort();
1502
+ }
1503
+ async function runSnapshotScan(projectRoot, watchPath, config, verbose, debug, onEvent) {
1504
+ const files = listTrackedSourceFiles(projectRoot, watchPath);
1505
+ if (files.length === 0) {
1506
+ console.log(chalk.yellow("\n No tracked source files found for scan.\n"));
1507
+ return {
1508
+ filesChecked: 0,
1509
+ filesWithViolations: 0,
1510
+ violations: 0
1511
+ };
1512
+ }
1513
+ console.log(chalk.dim(`
1514
+ scanning ${files.length} tracked source files...
1515
+ `));
1516
+ const summary = {
1517
+ filesChecked: 0,
1518
+ filesWithViolations: 0,
1519
+ violations: 0
1520
+ };
1521
+ for (const filePath of files) {
1522
+ const rel = normalizeForGit(relative(projectRoot, filePath));
1523
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1524
+ onEvent?.({ type: "saved", timestamp, file: rel });
1525
+ const result = await checkFile(filePath, projectRoot, config, verbose, debug, "snapshot", onEvent);
1526
+ if (result.type !== "checked") {
1527
+ if (result.type === "skipped") {
1528
+ onEvent?.({ type: "skipped", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, reason: result.reason });
1529
+ } else {
1530
+ onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message: result.message });
1531
+ }
1532
+ continue;
1533
+ }
1534
+ summary.filesChecked += 1;
1535
+ if (result.violations.length === 0) {
1536
+ onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
1537
+ continue;
1538
+ }
1539
+ summary.filesWithViolations += 1;
1540
+ summary.violations += result.violations.length;
1541
+ onEvent?.({ type: "violations", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, violations: result.violations });
1542
+ }
1543
+ return summary;
1544
+ }
1545
+ async function autoFixFile(filePath, projectRoot, violations, rules, avoids, debug) {
1546
+ if (!existsSync5(filePath)) return false;
1547
+ const content = readFileSync5(filePath, "utf-8");
1548
+ const rel = relative(projectRoot, filePath).split(sep).join("/");
1549
+ const violationSummary = violations.map(
1550
+ (v, i) => `${i + 1}. Rule: "${v.rule}"
1551
+ Issue: ${v.issue}${v.suggestion ? `
1552
+ Fix: ${v.suggestion}` : ""}${v.line ? `
1553
+ Line: ${v.line}` : ""}`
1554
+ ).join("\n\n");
1555
+ const systemPrompt = `You are an expert code fixer. You will be given a file with architecture violations.
1556
+ Fix ONLY the violations listed below. Do not change anything else.
1557
+ Return ONLY the complete fixed file content \u2014 no markdown, no explanation, no code blocks.`;
1558
+ const userPrompt = `File: ${rel}
1559
+
1560
+ Violations to fix:
1561
+ ${violationSummary}
1562
+
1563
+ Rules being enforced:
1564
+ ${rules.slice(0, 10).join("\n")}
1565
+ ${avoids.length > 0 ? `
1566
+ Things that must never appear:
1567
+ ${avoids.slice(0, 5).join("\n")}` : ""}
1568
+
1569
+ Current file content:
1570
+ ${content}`;
1571
+ if (debug) {
1572
+ console.log(chalk.gray(" [debug] auto-fix prompt:"));
1573
+ console.log(chalk.dim(userPrompt.slice(0, 500) + "..."));
1574
+ }
1575
+ try {
1576
+ console.log(chalk.cyan(` \u26A1 Auto-fixing ${violations.length} violation${violations.length > 1 ? "s" : ""} in ${rel}\u2026`));
1577
+ const { content: fixed, usage: fixUsage } = await callChatModel([
1578
+ { role: "system", content: systemPrompt },
1579
+ { role: "user", content: userPrompt }
1580
+ ]);
1581
+ accumulateTokenUsage(fixUsage);
1582
+ if (!fixed.trim()) return false;
1583
+ writeFileSync3(filePath, fixed, "utf-8");
1584
+ try {
1585
+ const app = getDefaultApplicationContainer();
1586
+ for (const v of violations) {
1587
+ await app.services.memoryEngine.remember({
1588
+ type: "rule",
1589
+ scope: "project",
1590
+ content: v.rule,
1591
+ reason: `Auto-fixed by AI: ${v.issue}`,
1592
+ tags: ["auto-fix"]
1593
+ });
1594
+ }
1595
+ } catch {
1596
+ }
1597
+ return true;
1598
+ } catch (err) {
1599
+ if (debug) console.log(chalk.yellow(` [debug] auto-fix failed: ${err.message}`));
1600
+ return false;
1601
+ }
1602
+ }
1603
+ async function checkFile(filePath, projectRoot, config, verbose, debug, mode = "diff", onEvent) {
1604
+ const rel = relative(projectRoot, filePath).split(sep).join("/");
1605
+ if (rel.startsWith("..")) return { type: "skipped", reason: "File is outside project root" };
1606
+ let inputText;
1607
+ if (mode === "snapshot") {
1608
+ if (!existsSync5(filePath)) return { type: "skipped", reason: "File no longer exists" };
1609
+ inputText = readFileSync5(filePath, "utf-8");
1610
+ if (!inputText.trim()) return { type: "skipped", reason: "File is empty" };
1611
+ } else {
1612
+ const headResult = spawnSync("git", ["diff", "HEAD", "--", rel], { encoding: "utf-8", cwd: projectRoot });
1613
+ if (headResult.stdout?.trim()) {
1614
+ inputText = headResult.stdout;
1615
+ } else {
1616
+ const noIndexResult = spawnSync("git", ["diff", "--no-index", "/dev/null", rel], {
1617
+ encoding: "utf-8",
1618
+ cwd: projectRoot
1619
+ });
1620
+ inputText = noIndexResult.stdout ?? "";
1621
+ }
1622
+ if (!inputText.trim()) return { type: "skipped", reason: "No changes compared with HEAD" };
1623
+ }
1624
+ const { rules: fallbackRules, avoids } = getProfileRules(config);
1625
+ const rules = await loadRelevantRules(projectRoot, config, rel, inputText, fallbackRules);
1626
+ if (rules.length === 0) return { type: "skipped", reason: "No applicable architecture rules" };
1627
+ const MAX_INPUT = 6e3;
1628
+ const truncated = inputText.length > MAX_INPUT;
1629
+ const inputToSend = truncated ? inputText.slice(0, MAX_INPUT) + "\n\n[input truncated]" : inputText;
1630
+ if (verbose || debug) {
1631
+ const label = mode === "snapshot" ? "snapshot" : `${inputText.length} chars`;
1632
+ console.log(chalk.dim(`
1633
+ [watch] checking ${rel} (${label})\u2026`));
1634
+ }
1635
+ const rulesWithReasons = rules.map((r, i) => {
1636
+ const why = reasonMap.get(r);
1637
+ return why ? `${i + 1}. ${r}
1638
+ WHY: ${why}` : `${i + 1}. ${r}`;
1639
+ }).join("\n");
1640
+ const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...await loadIgnorePatterns()])];
1641
+ const astViolations = findAstDeterministicViolationsForFile(rel, {
1642
+ cwd: projectRoot,
1643
+ config,
1644
+ rules,
1645
+ reasonLookup: reasonMap
1646
+ }).map((violation) => ({
1647
+ ...violation,
1648
+ severity: "error"
1649
+ }));
1650
+ const analysisTarget = mode === "snapshot" ? "file content" : "file diff";
1651
+ const systemPrompt = `You are a strict code reviewer enforcing architecture rules.
1652
+ Analyze the ${analysisTarget} and identify ONLY clear, definite rule violations.
1653
+ Use the WHY for each rule to understand intent and judge edge cases.
1654
+
1655
+ Rules to enforce:
1656
+ ${rulesWithReasons}
1657
+
1658
+ Things that must never appear:
1659
+ ${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
1660
+
1661
+ Never flag these accepted project patterns:
1662
+ ${allowPatterns.length ? allowPatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
1663
+
1664
+ IMPORTANT: Respond with JSON: {"violations":[...]} or {"violations":[]}.
1665
+ Each violation: {"rule":"...","file":"...","line":N,"issue":"...","suggestion":"...","reason":"..."}.
1666
+ No text outside the JSON.`;
1667
+ if (debug) {
1668
+ console.log(chalk.gray("\n [debug] prompt:"));
1669
+ 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"));
1670
+ console.log(systemPrompt);
1671
+ 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"));
1672
+ console.log(chalk.gray(` [debug] input length: ${inputText.length} chars`));
1673
+ console.log(chalk.dim(inputToSend));
1674
+ 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"));
1675
+ }
1676
+ try {
1677
+ const reviewPrompt = mode === "snapshot" ? `Review this file ${rel}:
1678
+
1679
+ ${inputToSend}` : `Review this diff for ${rel}:
1680
+
1681
+ ${inputToSend}`;
1682
+ onEvent?.({ type: "log", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, message: `checking with ${getChatProviderLabel()} | ${rules.length} rule${rules.length === 1 ? "" : "s"}` });
1683
+ const { content: raw, usage: reviewUsage } = await callChatModel([
1684
+ { role: "system", content: systemPrompt },
1685
+ { role: "user", content: reviewPrompt }
1686
+ ]);
1687
+ accumulateTokenUsage(reviewUsage);
1688
+ if (debug) {
1689
+ console.log(chalk.gray(" [debug] raw response:"));
1690
+ console.log(chalk.dim(raw));
1691
+ 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"));
1692
+ }
1693
+ let violations = [];
1694
+ try {
1695
+ const parsed = JSON.parse(raw);
1696
+ if (Array.isArray(parsed)) {
1697
+ violations = parsed;
1698
+ } else if (Array.isArray(parsed?.violations)) {
1699
+ violations = parsed.violations;
1700
+ } else if (parsed?.rule) {
1701
+ violations = [parsed];
1702
+ }
1703
+ } catch {
1704
+ violations = [];
1705
+ }
1706
+ onEvent?.({ type: "log", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, message: `model returned ${violations.length} candidate violation${violations.length === 1 ? "" : "s"}` });
1707
+ violations = await verifyViolations(inputText, violations, allowPatterns, debug, mode);
1708
+ violations = [...astViolations, ...violations];
1709
+ violations = applyAllowPatterns(violations, allowPatterns);
1710
+ violations = violations.map((violation) => ({
1711
+ ...violation,
1712
+ code: violation.code ?? (violation.line ? formatCodeContext(filePath, violation.line, 1) : void 0)
1713
+ }));
1714
+ if (violations.length === 0) {
1715
+ recordWatchResult(projectRoot, rel, []);
1716
+ console.log(chalk.green(` \u2713 ${rel}`) + chalk.dim(" \u2014 no violations"));
1717
+ return { type: "checked", violations: [] };
1718
+ }
1719
+ console.log(
1720
+ chalk.red.bold(`
1721
+ \u2717 ${violations.length} violation${violations.length > 1 ? "s" : ""} in ${rel}
1722
+ `)
1723
+ );
1724
+ violations.forEach((v, i) => {
1725
+ const loc = v.line ? `${v.file ?? rel}:${v.line}` : v.file ?? rel;
1726
+ console.log(chalk.bold(` [${i + 1}] ${loc}`));
1727
+ console.log(chalk.yellow(" Rule: ") + v.rule);
1728
+ const why = v.reason ?? reasonMap.get(v.rule);
1729
+ if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
1730
+ if (v.line && existsSync5(filePath)) {
1731
+ printCodeContext(filePath, v.line, 1);
1732
+ }
1733
+ if (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
1734
+ if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
1735
+ console.log();
1736
+ });
1737
+ recordWatchResult(projectRoot, rel, violations);
1738
+ console.log(chalk.dim(' Fix violations or run: memory-core remember "<lesson>"'));
1739
+ console.log();
1740
+ return { type: "checked", violations };
1741
+ } catch (err) {
1742
+ const aiUnavailable = err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED");
1743
+ const message = aiUnavailable ? `Model unreachable for ${rel}; using deterministic checks only.` : `AI check failed for ${rel}; using deterministic checks only.`;
1744
+ console.log(chalk.yellow(` \u26A0 ${message}`));
1745
+ let violations = applyAllowPatterns(astViolations, allowPatterns);
1746
+ violations = violations.map((violation) => ({
1747
+ ...violation,
1748
+ code: violation.code ?? (violation.line ? formatCodeContext(filePath, violation.line, 1) : void 0)
1749
+ }));
1750
+ if (violations.length === 0) {
1751
+ recordWatchResult(projectRoot, rel, []);
1752
+ console.log(chalk.green(` \u2713 ${rel}`) + chalk.dim(" \u2014 no deterministic violations"));
1753
+ return { type: "checked", violations: [] };
1754
+ }
1755
+ console.log(
1756
+ chalk.red.bold(`
1757
+ \u2717 ${violations.length} deterministic violation${violations.length > 1 ? "s" : ""} in ${rel}
1758
+ `)
1759
+ );
1760
+ violations.forEach((v, i) => {
1761
+ const loc = v.line ? `${v.file ?? rel}:${v.line}` : v.file ?? rel;
1762
+ console.log(chalk.bold(` [${i + 1}] ${loc}`));
1763
+ console.log(chalk.yellow(" Rule: ") + v.rule);
1764
+ const why = v.reason ?? reasonMap.get(v.rule);
1765
+ if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
1766
+ if (v.line && existsSync5(filePath)) {
1767
+ printCodeContext(filePath, v.line, 1);
1768
+ }
1769
+ if (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
1770
+ if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
1771
+ console.log();
1772
+ });
1773
+ recordWatchResult(projectRoot, rel, violations);
1774
+ console.log(chalk.dim(' Fix violations or run: memory-core remember "<lesson>"'));
1775
+ console.log();
1776
+ return { type: "checked", violations };
1777
+ }
1778
+ }
1779
+ async function scanFiles(options = {}) {
1780
+ const { projectRoot, watchPath } = resolveWatchPaths(options.path, options.projectRoot);
1781
+ const config = loadConfig(projectRoot);
1782
+ if (!config) {
1783
+ throw new Error("No .memory-core.json found. Run: memory-core init");
1784
+ }
1785
+ const { rules } = getProfileRules(config);
1786
+ if (rules.length === 0) {
1787
+ console.log(chalk.yellow("\n No architecture rules configured in .memory-core.json \u2014 nothing to scan.\n"));
1788
+ return {
1789
+ filesChecked: 0,
1790
+ filesWithViolations: 0,
1791
+ violations: 0
1792
+ };
1793
+ }
1794
+ resetLiveStats(projectRoot);
1795
+ console.log(chalk.cyan("\n archmind scan \u2014 checking tracked source files\n"));
1796
+ console.log(chalk.dim(` project: ${projectRoot}`));
1797
+ console.log(chalk.dim(` path: ${watchPath}`));
1798
+ console.log(chalk.dim(` model: ${getChatProviderLabel()}`));
1799
+ console.log(chalk.dim(` rules: ${rules.length}
1800
+ `));
1801
+ const summary = await runSnapshotScan(
1802
+ projectRoot,
1803
+ watchPath,
1804
+ config,
1805
+ options.verbose ?? false,
1806
+ options.debug ?? false,
1807
+ options.onEvent
1808
+ );
1809
+ const cleanFiles = summary.filesChecked - summary.filesWithViolations;
1810
+ console.log(chalk.bold("\n scan summary\n"));
1811
+ console.log(chalk.dim(` files checked: ${summary.filesChecked}`));
1812
+ console.log(chalk.dim(` files clean: ${cleanFiles}`));
1813
+ console.log(chalk.dim(` files with violations: ${summary.filesWithViolations}`));
1814
+ console.log(chalk.dim(` total violations: ${summary.violations}
1815
+ `));
1816
+ return summary;
1817
+ }
1818
+ async function startWatch(options = {}) {
1819
+ const { projectRoot, watchPath } = resolveWatchPaths(options.path, options.projectRoot);
1820
+ const config = loadConfig(projectRoot);
1821
+ const exitOnSetupFailure = options.exitOnSetupFailure ?? true;
1822
+ if (!config) {
1823
+ const message = "No .memory-core.json found. Run: memory-core init";
1824
+ console.error(chalk.red(`
1825
+ ${message}
1826
+ `));
1827
+ options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message });
1828
+ if (exitOnSetupFailure) process.exit(1);
1829
+ return;
1830
+ }
1831
+ const { rules, avoids } = getProfileRules(config);
1832
+ if (rules.length === 0) {
1833
+ const message = "No architecture rules configured in .memory-core.json \u2014 nothing to watch.";
1834
+ console.log(chalk.yellow(`
1835
+ ${message}
1836
+ `));
1837
+ options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message });
1838
+ if (exitOnSetupFailure) process.exit(0);
1839
+ return;
1840
+ }
1841
+ resetLiveStats(projectRoot);
1842
+ console.log(chalk.cyan("\n archmind watch \u2014 real-time rule enforcement\n"));
1843
+ console.log(chalk.dim(` project: ${projectRoot}`));
1844
+ console.log(chalk.dim(` watching: ${watchPath}`));
1845
+ console.log(chalk.dim(` model: ${getChatProviderLabel()}`));
1846
+ console.log(chalk.dim(` rules: ${rules.length}`));
1847
+ if (options.autoFix) console.log(chalk.yellow(" mode: auto-fix enabled \u2014 violations will be rewritten by AI"));
1848
+ console.log(chalk.dim(" ctrl+c to stop\n"));
1849
+ options.onEvent?.({
1850
+ type: "ready",
1851
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1852
+ path: watchPath,
1853
+ model: getChatProviderLabel(),
1854
+ rules: rules.length
1855
+ });
1856
+ if (options.scanOnStart) {
1857
+ console.log(chalk.dim(" running initial snapshot scan before watch events..."));
1858
+ await runSnapshotScan(
1859
+ projectRoot,
1860
+ watchPath,
1861
+ config,
1862
+ options.verbose ?? false,
1863
+ options.debug ?? false,
1864
+ options.onEvent
1865
+ );
1866
+ console.log(chalk.dim(" initial scan complete.\n"));
1867
+ }
1868
+ const pending = /* @__PURE__ */ new Map();
1869
+ const watcher = watch(watchPath, {
1870
+ ignored: [
1871
+ "**/node_modules/**",
1872
+ "**/.git/**",
1873
+ "**/dist/**",
1874
+ "**/build/**",
1875
+ "**/coverage/**",
1876
+ "**/.memory-core*"
1877
+ ],
1878
+ ignoreInitial: true,
1879
+ persistent: true,
1880
+ awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50 }
1881
+ });
1882
+ const keepAlive = setInterval(() => {
1883
+ }, 1 << 30);
1884
+ const handle = (filePath) => {
1885
+ if (!SOURCE_EXTENSIONS.test(filePath)) return;
1886
+ if (pending.has(filePath)) clearTimeout(pending.get(filePath));
1887
+ const timer = setTimeout(async () => {
1888
+ pending.delete(filePath);
1889
+ const rel = normalizeForGit(relative(projectRoot, filePath));
1890
+ if (rel.startsWith("..")) return;
1891
+ const timestamp = /* @__PURE__ */ new Date();
1892
+ console.log(chalk.dim(`
1893
+ [${timestamp.toLocaleTimeString()}] saved: ${rel}`));
1894
+ options.onEvent?.({ type: "saved", timestamp: timestamp.toISOString(), file: rel });
1895
+ const result = await checkFile(
1896
+ filePath,
1897
+ projectRoot,
1898
+ config,
1899
+ options.verbose ?? false,
1900
+ options.debug ?? false,
1901
+ "diff",
1902
+ options.onEvent
1903
+ );
1904
+ if (result.type === "skipped") {
1905
+ if (result.reason === "No changes compared with HEAD") {
1906
+ recordWatchResult(projectRoot, rel, []);
1907
+ options.onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
1908
+ return;
1909
+ }
1910
+ options.onEvent?.({ type: "skipped", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, reason: result.reason });
1911
+ return;
1912
+ }
1913
+ if (result.type === "error") {
1914
+ options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message: result.message });
1915
+ return;
1916
+ }
1917
+ const { violations } = result;
1918
+ if (violations.length === 0) {
1919
+ options.onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
1920
+ return;
1921
+ }
1922
+ options.onEvent?.({ type: "violations", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, violations });
1923
+ if (options.autoFix) {
1924
+ const fixed = await autoFixFile(filePath, projectRoot, violations, rules, avoids, options.debug ?? false);
1925
+ if (fixed) {
1926
+ console.log(chalk.green(` \u2713 Auto-fixed: ${rel} \u2014 re-checking\u2026
1927
+ `));
1928
+ options.onEvent?.({ type: "log", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, message: "auto-fixed by AI" });
1929
+ } else {
1930
+ console.log(chalk.yellow(` \u26A0 Auto-fix failed for ${rel} \u2014 fix manually.
1931
+ `));
1932
+ }
1933
+ }
1934
+ }, 300);
1935
+ pending.set(filePath, timer);
1936
+ };
1937
+ watcher.on("add", handle);
1938
+ watcher.on("change", handle);
1939
+ watcher.on("unlink", (filePath) => {
1940
+ const rel = normalizeForGit(relative(projectRoot, filePath));
1941
+ if (rel.startsWith("..")) return;
1942
+ recordWatchResult(projectRoot, rel, []);
1943
+ options.onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
1944
+ });
1945
+ watcher.on("error", (err) => {
1946
+ const message = err instanceof Error ? err.message : String(err);
1947
+ console.error(chalk.red(` watcher error: ${message}`));
1948
+ options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message });
1949
+ });
1950
+ process.on("SIGINT", () => {
1951
+ console.log(chalk.dim("\n\n archmind watch stopped.\n"));
1952
+ options.onEvent?.({ type: "stopped", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
1953
+ clearInterval(keepAlive);
1954
+ watcher.close();
1955
+ process.exit(0);
1956
+ });
1957
+ }
1958
+
1959
+ // src/infrastructure/filesystem/chokidar-watch-service.ts
1960
+ var ChokidarWatchService = class {
1961
+ async start(options = {}) {
1962
+ await startWatch({
1963
+ path: options.path,
1964
+ verbose: options.verbose,
1965
+ debug: options.debug,
1966
+ scanOnStart: options.scanOnStart,
1967
+ autoFix: options.autoFix
1968
+ });
1969
+ }
1970
+ async scan(options = {}) {
1971
+ return scanFiles({
1972
+ path: options.path,
1973
+ verbose: options.verbose,
1974
+ debug: options.debug
1975
+ });
1976
+ }
1977
+ };
1978
+
1979
+ // src/infrastructure/events/in-memory-event-bus.ts
1980
+ var InMemoryEventBus = class {
1981
+ handlers = /* @__PURE__ */ new Map();
1982
+ async publish(event) {
1983
+ const handlers = this.handlers.get(event.type);
1984
+ if (!handlers || handlers.size === 0) return;
1985
+ for (const handler of handlers) {
1986
+ await handler(event);
1987
+ }
1988
+ }
1989
+ subscribe(eventType, handler) {
1990
+ const existing = this.handlers.get(eventType) ?? /* @__PURE__ */ new Set();
1991
+ existing.add(handler);
1992
+ this.handlers.set(eventType, existing);
1993
+ return () => {
1994
+ const current = this.handlers.get(eventType);
1995
+ if (!current) return;
1996
+ current.delete(handler);
1997
+ if (current.size === 0) {
1998
+ this.handlers.delete(eventType);
1999
+ }
2000
+ };
2001
+ }
2002
+ };
2003
+
2004
+ // src/modules/memory-engine/application/memory-engine-service.ts
2005
+ var MemoryEngineService = class {
2006
+ constructor(memoryRepository, embeddingProvider) {
2007
+ this.memoryRepository = memoryRepository;
2008
+ this.embeddingProvider = embeddingProvider;
2009
+ }
2010
+ memoryRepository;
2011
+ embeddingProvider;
2012
+ withReason(input) {
2013
+ const reason = input.reason?.trim();
2014
+ return {
2015
+ ...input,
2016
+ reason: reason || `Captured as a ${input.type} memory because it should be remembered: ${input.content}`
2017
+ };
2018
+ }
2019
+ async remember(input) {
2020
+ const normalized = this.withReason(input);
2021
+ const embedding = await this.embeddingProvider.embed(normalized.content);
2022
+ return this.memoryRepository.upsert({ ...normalized, embedding });
2023
+ }
2024
+ async rememberForce(input) {
2025
+ const normalized = this.withReason(input);
2026
+ const embedding = await this.embeddingProvider.embed(normalized.content);
2027
+ await this.memoryRepository.save({ ...normalized, embedding });
2028
+ }
2029
+ async list(filters = {}) {
2030
+ return this.memoryRepository.list(filters);
1310
2031
  }
1311
2032
  async getById(id) {
1312
2033
  return this.memoryRepository.getById(id);
@@ -1395,183 +2116,8 @@ var RuleEngineService = class {
1395
2116
  };
1396
2117
 
1397
2118
  // 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";
1400
-
1401
- // src/infrastructure/ast/import-analysis.ts
1402
- 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";
1405
- var SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
1406
- var NODE_BUILTINS = /* @__PURE__ */ new Set([...builtinModules, ...builtinModules.map((entry) => `node:${entry}`)]);
1407
- function countNewlines(value) {
1408
- let count = 0;
1409
- for (const char of value) {
1410
- if (char === "\n") count += 1;
1411
- }
1412
- return count;
1413
- }
1414
- function parseImports(source) {
1415
- const imports = [];
1416
- const patterns = [
1417
- { kind: "import", regex: /(^|\n)\s*import\s+(?:type\s+)?(?:[^'"\n]+?\s+from\s+)?['"]([^'"\n]+)['"]/g },
1418
- { kind: "export-from", regex: /(^|\n)\s*export\s+(?:type\s+)?(?:[^'"\n]+?\s+from\s+)['"]([^'"\n]+)['"]/g },
1419
- { kind: "require", regex: /(^|\n)[^\n]*?\brequire\(\s*['"]([^'"\n]+)['"]\s*\)/g },
1420
- { kind: "dynamic-import", regex: /(^|\n)[^\n]*?\bimport\(\s*['"]([^'"\n]+)['"]\s*\)/g }
1421
- ];
1422
- for (const { kind, regex } of patterns) {
1423
- for (const match of source.matchAll(regex)) {
1424
- const prefix = source.slice(0, match.index ?? 0);
1425
- imports.push({
1426
- kind,
1427
- specifier: match[2],
1428
- line: countNewlines(prefix) + 1
1429
- });
1430
- }
1431
- }
1432
- return imports;
1433
- }
1434
- function tryResolveFilePath(candidate) {
1435
- if (existsSync4(candidate)) return normalize(candidate);
1436
- if (!extname(candidate)) {
1437
- for (const ext of SOURCE_EXTENSIONS) {
1438
- const withExt = `${candidate}${ext}`;
1439
- if (existsSync4(withExt)) return normalize(withExt);
1440
- }
1441
- for (const ext of SOURCE_EXTENSIONS) {
1442
- const indexFile = join4(candidate, `index${ext}`);
1443
- if (existsSync4(indexFile)) return normalize(indexFile);
1444
- }
1445
- }
1446
- return void 0;
1447
- }
1448
- function looksLikeExternal(specifier) {
1449
- return !specifier.startsWith(".") && !specifier.startsWith("/") && !specifier.startsWith("@/");
1450
- }
1451
- function resolveImportPath(fromFile, specifier, cwd = process.cwd()) {
1452
- if (specifier.startsWith(".")) {
1453
- return tryResolveFilePath(resolve(dirname2(fromFile), specifier));
1454
- }
1455
- if (specifier.startsWith("/")) {
1456
- return tryResolveFilePath(resolve(cwd, `.${specifier}`));
1457
- }
1458
- if (specifier.startsWith("@/")) {
1459
- return tryResolveFilePath(resolve(cwd, "src", specifier.slice(2)));
1460
- }
1461
- if (isAbsolute(specifier)) {
1462
- return tryResolveFilePath(specifier);
1463
- }
1464
- return void 0;
1465
- }
1466
- function collectResolvedImports(filePath, cwd = process.cwd()) {
1467
- if (!existsSync4(filePath)) return [];
1468
- const source = readFileSync3(filePath, "utf-8");
1469
- const imports = parseImports(source);
1470
- return imports.map((entry) => {
1471
- if (looksLikeExternal(entry.specifier) || NODE_BUILTINS.has(entry.specifier)) {
1472
- return { ...entry, isExternal: true };
1473
- }
1474
- return {
1475
- ...entry,
1476
- isExternal: false,
1477
- resolvedPath: resolveImportPath(filePath, entry.specifier, cwd)
1478
- };
1479
- });
1480
- }
1481
- function asPosix3(value) {
1482
- return value.replace(/\\/g, "/");
1483
- }
1484
- function moduleNameFromPath(absPath, cwd = process.cwd()) {
1485
- const rel = asPosix3(normalize(absPath)).replace(asPosix3(normalize(cwd)) + "/", "");
1486
- const match = rel.match(/^src\/modules\/([^/]+)\//);
1487
- return match?.[1];
1488
- }
1489
- function buildModuleDependencyEdges(files, cwd = process.cwd()) {
1490
- const edges = [];
1491
- for (const file of files) {
1492
- const absoluteFile = resolve(cwd, file);
1493
- const fromModule = moduleNameFromPath(absoluteFile, cwd);
1494
- if (!fromModule) continue;
1495
- const imports = collectResolvedImports(absoluteFile, cwd);
1496
- for (const imp of imports) {
1497
- if (!imp.resolvedPath) continue;
1498
- const toModule = moduleNameFromPath(imp.resolvedPath, cwd);
1499
- if (!toModule || toModule === fromModule) continue;
1500
- edges.push({
1501
- fromModule,
1502
- toModule,
1503
- file,
1504
- line: imp.line
1505
- });
1506
- }
1507
- }
1508
- return edges;
1509
- }
1510
- function parseChangedFilesFromDiff(diff) {
1511
- const files = /* @__PURE__ */ new Set();
1512
- for (const line of diff.split("\n")) {
1513
- if (!line.startsWith("+++ b/")) continue;
1514
- const file = line.slice("+++ b/".length).trim();
1515
- if (!file || file === "/dev/null") continue;
1516
- files.add(file);
1517
- }
1518
- return [...files];
1519
- }
1520
- function detectModuleCycles(edges) {
1521
- const graph = /* @__PURE__ */ new Map();
1522
- for (const edge of edges) {
1523
- const next = graph.get(edge.fromModule) ?? /* @__PURE__ */ new Set();
1524
- next.add(edge.toModule);
1525
- graph.set(edge.fromModule, next);
1526
- }
1527
- const visited = /* @__PURE__ */ new Set();
1528
- const stack = /* @__PURE__ */ new Set();
1529
- const cycles = /* @__PURE__ */ new Set();
1530
- const visit = (node, path) => {
1531
- visited.add(node);
1532
- stack.add(node);
1533
- const next = graph.get(node) ?? /* @__PURE__ */ new Set();
1534
- for (const target of next) {
1535
- if (!visited.has(target)) {
1536
- visit(target, [...path, target]);
1537
- continue;
1538
- }
1539
- if (stack.has(target)) {
1540
- const start = path.indexOf(target);
1541
- const cycle = start >= 0 ? path.slice(start).concat(target) : [node, target, node];
1542
- cycles.add(cycle.join("->"));
1543
- }
1544
- }
1545
- stack.delete(node);
1546
- };
1547
- for (const node of graph.keys()) {
1548
- if (!visited.has(node)) {
1549
- visit(node, [node]);
1550
- }
1551
- }
1552
- return [...cycles].map((value) => value.split("->"));
1553
- }
1554
- function isExternalFrameworkSpecifier(specifier) {
1555
- const frameworkPrefixes = [
1556
- "express",
1557
- "fastify",
1558
- "@nestjs/",
1559
- "react",
1560
- "vue",
1561
- "svelte",
1562
- "@angular/",
1563
- "next/",
1564
- "nuxt",
1565
- "typeorm",
1566
- "@prisma/",
1567
- "mongoose",
1568
- "sequelize",
1569
- "axios"
1570
- ];
1571
- return frameworkPrefixes.some((prefix) => specifier === prefix || specifier.startsWith(`${prefix}/`));
1572
- }
1573
-
1574
- // src/modules/graph-engine/application/graph-engine-service.ts
2119
+ import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
2120
+ import { join as join6, relative as relative2, resolve as resolve3 } from "path";
1575
2121
  var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
1576
2122
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", ".memory-core"]);
1577
2123
  function isSourceFile(pathValue) {
@@ -1606,19 +2152,19 @@ var GraphEngineService = class {
1606
2152
  }
1607
2153
  graphRepository;
1608
2154
  async buildGraph(options = {}) {
1609
- const cwd = resolve2(options.cwd ?? process.cwd());
2155
+ const cwd = resolve3(options.cwd ?? process.cwd());
1610
2156
  const files = this.collectSourceFiles(cwd);
1611
2157
  const nodes = /* @__PURE__ */ new Set();
1612
2158
  const edges = /* @__PURE__ */ new Map();
1613
2159
  for (const relativeFile of files) {
1614
- const absoluteFile = resolve2(cwd, relativeFile);
2160
+ const absoluteFile = resolve3(cwd, relativeFile);
1615
2161
  const fromNode = normalizeNode(relativeFile);
1616
2162
  nodes.add(fromNode);
1617
2163
  const imports = collectResolvedImports(absoluteFile, cwd);
1618
2164
  for (const imp of imports) {
1619
2165
  let toNode;
1620
2166
  if (imp.resolvedPath) {
1621
- toNode = normalizeNode(asPosix4(relative(cwd, imp.resolvedPath)));
2167
+ toNode = normalizeNode(asPosix4(relative2(cwd, imp.resolvedPath)));
1622
2168
  } else if (imp.isExternal) {
1623
2169
  toNode = `pkg:${imp.specifier}`;
1624
2170
  }
@@ -1687,15 +2233,15 @@ var GraphEngineService = class {
1687
2233
  collectSourceFiles(cwd) {
1688
2234
  const files = [];
1689
2235
  const walk = (dir) => {
1690
- for (const entry of readdirSync(dir)) {
2236
+ for (const entry of readdirSync2(dir)) {
1691
2237
  if (IGNORED_DIRS.has(entry)) continue;
1692
- const absolutePath = join5(dir, entry);
1693
- const stat = statSync(absolutePath);
2238
+ const absolutePath = join6(dir, entry);
2239
+ const stat = statSync2(absolutePath);
1694
2240
  if (stat.isDirectory()) {
1695
2241
  walk(absolutePath);
1696
2242
  continue;
1697
2243
  }
1698
- const rel = asPosix4(relative(cwd, absolutePath));
2244
+ const rel = asPosix4(relative2(cwd, absolutePath));
1699
2245
  if (isSourceFile(rel)) files.push(rel);
1700
2246
  }
1701
2247
  };
@@ -1837,11 +2383,11 @@ function getStackReason(memory, activeArchitectures2) {
1837
2383
  reason: `excluded: tagged for ${architectureKeys.join(", ")}; active stack is ${active}`
1838
2384
  };
1839
2385
  }
1840
- function inferProjectArchitectures(cwd = process.cwd(), config2) {
2386
+ function inferProjectArchitectures(cwd = process.cwd(), config) {
1841
2387
  const inferred = /* @__PURE__ */ new Set();
1842
- if (config2?.backendArchitecture) inferred.add(config2.backendArchitecture);
1843
- if (config2?.frontendFramework) inferred.add(config2.frontendFramework);
1844
- if (config2?.projectType === "backend" && !config2.backendArchitecture) {
2388
+ if (config?.backendArchitecture && config.backendArchitecture !== "custom") inferred.add(config.backendArchitecture);
2389
+ if (config?.frontendFramework && config.frontendFramework !== "custom") inferred.add(config.frontendFramework);
2390
+ if (config?.projectType === "backend" && !config.backendArchitecture) {
1845
2391
  inferred.add("clean-architecture");
1846
2392
  }
1847
2393
  const detected = detectProject(cwd);
@@ -1850,14 +2396,14 @@ function inferProjectArchitectures(cwd = process.cwd(), config2) {
1850
2396
  }
1851
2397
  return [...inferred];
1852
2398
  }
1853
- function getAllowPatterns(config2) {
1854
- return [...new Set(config2?.allowPatterns?.filter(Boolean) ?? [])];
2399
+ function getAllowPatterns(config) {
2400
+ return [...new Set(config?.allowPatterns?.filter(Boolean) ?? [])];
1855
2401
  }
1856
- function filterRelevantMemories(memories, config2, cwd = process.cwd()) {
1857
- return explainMemorySelection(memories, config2, cwd).included;
2402
+ function filterRelevantMemories(memories, config, cwd = process.cwd()) {
2403
+ return explainMemorySelection(memories, config, cwd).included;
1858
2404
  }
1859
- function explainMemorySelection(memories, config2, cwd = process.cwd(), threshold = 0.8) {
1860
- const activeArchitectures2 = inferProjectArchitectures(cwd, config2);
2405
+ function explainMemorySelection(memories, config, cwd = process.cwd(), threshold = 0.8) {
2406
+ const activeArchitectures2 = inferProjectArchitectures(cwd, config);
1861
2407
  const activeSet = new Set(activeArchitectures2);
1862
2408
  const included = [];
1863
2409
  const decisions = [];
@@ -1934,7 +2480,7 @@ async function retrieveMemorySelection(options) {
1934
2480
  // src/generator.ts
1935
2481
  var __filename = fileURLToPath(import.meta.url);
1936
2482
  var __dirname = dirname3(__filename);
1937
- var PKG_ROOT = join6(__dirname, "..");
2483
+ var PKG_ROOT = join7(__dirname, "..");
1938
2484
  function stringifyProfileScalar(value) {
1939
2485
  if (typeof value === "string") {
1940
2486
  const trimmed = value.trim();
@@ -2022,15 +2568,18 @@ Handlebars.registerHelper("memoryBlock", (memory) => {
2022
2568
  return new Handlebars.SafeString(lines.join("\n"));
2023
2569
  });
2024
2570
  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);
2571
+ if (name === "custom") {
2572
+ return { name: "custom", displayName: "Custom", layer: "backend", description: "Custom architecture \u2014 rules added via memory-core remember", rules: [], folders: [], avoid: [] };
2573
+ }
2574
+ const profilePath = join7(PKG_ROOT, "profiles", `${name}.yml`);
2575
+ if (!existsSync6(profilePath)) throw new Error(`Profile not found: ${name}`);
2576
+ return normalizeArchitectureProfile(yaml.load(readFileSync6(profilePath, "utf-8")), name);
2028
2577
  }
2029
2578
  function listProfiles(layer) {
2030
- const files = readdirSync2(join6(PKG_ROOT, "profiles")).filter((f) => f.endsWith(".yml"));
2579
+ const files = readdirSync3(join7(PKG_ROOT, "profiles")).filter((f) => f.endsWith(".yml"));
2031
2580
  const all = files.map(
2032
2581
  (f) => normalizeArchitectureProfile(
2033
- yaml.load(readFileSync4(join6(PKG_ROOT, "profiles", f), "utf-8")),
2582
+ yaml.load(readFileSync6(join7(PKG_ROOT, "profiles", f), "utf-8")),
2034
2583
  basename(f, ".yml")
2035
2584
  )
2036
2585
  );
@@ -2100,18 +2649,18 @@ function buildTemplateData(options, cwd = process.cwd()) {
2100
2649
  };
2101
2650
  }
2102
2651
  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);
2652
+ const templatePath = join7(PKG_ROOT, "templates", templateName);
2653
+ if (!existsSync6(templatePath)) throw new Error(`Template not found: ${templateName}`);
2654
+ return Handlebars.compile(readFileSync6(templatePath, "utf-8"))(data);
2106
2655
  }
2107
2656
  function writeFile(filePath, content) {
2108
2657
  const dir = dirname3(filePath);
2109
- if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
2110
- if (existsSync5(filePath)) {
2111
- const existing = readFileSync4(filePath, "utf-8");
2658
+ if (!existsSync6(dir)) mkdirSync2(dir, { recursive: true });
2659
+ if (existsSync6(filePath)) {
2660
+ const existing = readFileSync6(filePath, "utf-8");
2112
2661
  if (existing === content) return "skipped";
2113
2662
  }
2114
- writeFileSync2(filePath, content, "utf-8");
2663
+ writeFileSync4(filePath, content, "utf-8");
2115
2664
  return "written";
2116
2665
  }
2117
2666
  async function generate(options, cwd = process.cwd(), onlyAgents) {
@@ -2120,373 +2669,280 @@ async function generate(options, cwd = process.cwd(), onlyAgents) {
2120
2669
  const skipped = [];
2121
2670
  const files = onlyAgents ? OUTPUT_FILES.filter((f) => onlyAgents.includes(f.agent)) : OUTPUT_FILES;
2122
2671
  for (const output of files) {
2123
- const targetPath = join6(cwd, output.path);
2124
- if (output.skipIfExists && existsSync5(targetPath)) {
2672
+ const targetPath = join7(cwd, output.path);
2673
+ if (output.skipIfExists && existsSync6(targetPath)) {
2125
2674
  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
- }
2675
+ continue;
2312
2676
  }
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);
2677
+ try {
2678
+ const content = renderTemplate(output.template, data);
2679
+ const result = writeFile(targetPath, content);
2680
+ if (result === "written") written.push(output.path);
2681
+ else skipped.push(output.path);
2682
+ } catch (err) {
2683
+ if (!(err instanceof Error && err.message.includes("Template not found"))) throw err;
2325
2684
  }
2326
2685
  }
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
- });
2686
+ return { written, skipped };
2687
+ }
2688
+
2689
+ // src/hook.ts
2690
+ var reasonMap2 = new Map(
2691
+ seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
2692
+ );
2693
+ var HOOK_PATH = join8(".git", "hooks", "pre-commit");
2694
+ var HOOK_MARKER = "# archmind-memory-core";
2695
+ var COMMIT_MSG_HOOK_PATH = join8(".git", "hooks", "commit-msg");
2696
+ var COMMIT_MSG_HOOK_MARKER = "# archmind-memory-core commit-msg";
2697
+ var RULE_CACHE_FILE = ".memory-core-rules-cache.json";
2698
+ var DB_VERSION_FILE = ".memory-core-db-version";
2699
+ var RULE_CACHE_TTL_MS = 5 * 60 * 1e3;
2700
+ function buildHookBody(advisory, fast = false) {
2701
+ const suffix = advisory ? " || true" : "";
2702
+ const checkArgs = fast ? "check --staged --fast" : "check --staged";
2703
+ return `${HOOK_MARKER}${advisory ? " advisory" : ""}
2704
+ if [ "\${MEMORY_CORE_SKIP_HOOK:-}" = "1" ] || [ "\${ARCHMIND_SKIP_HOOK:-}" = "1" ] || [ "\${HUSKY:-}" = "0" ] || [ "\${HUSKY_SKIP_HOOKS:-}" = "1" ]; then
2705
+ if command -v memory-core >/dev/null 2>&1 && [ -t 1 ]; then
2706
+ memory-core hook bypass-prompt
2707
+ fi
2708
+ exit 0
2709
+ fi
2710
+ if [ -n "\${SKIP:-}" ] && echo ",$SKIP," | grep -qiE ',(memory-core|archmind),'; then
2711
+ if command -v memory-core >/dev/null 2>&1 && [ -t 1 ]; then
2712
+ memory-core hook bypass-prompt
2713
+ fi
2714
+ exit 0
2715
+ fi
2716
+ if [ -n "\${SKIP_HOOKS:-}" ]; then
2717
+ exit 0
2718
+ fi
2719
+ if command -v memory-core >/dev/null 2>&1; then
2720
+ memory-core ${checkArgs}${suffix}
2721
+ elif [ -f "./node_modules/.bin/memory-core" ]; then
2722
+ ./node_modules/.bin/memory-core ${checkArgs}${suffix}
2723
+ elif [ -f "./dist/cli.js" ]; then
2724
+ node ./dist/cli.js ${checkArgs}${suffix}
2725
+ else
2726
+ exit 0
2727
+ fi
2728
+ `;
2729
+ }
2730
+ function buildHookScript(advisory, fast = false) {
2731
+ return `#!/bin/sh
2732
+
2733
+ ${buildHookBody(advisory, fast)}`;
2734
+ }
2735
+ function normalizeHookPreamble(content) {
2736
+ const lines = content.split("\n");
2737
+ const normalized = [];
2738
+ let shebangSeen = false;
2739
+ for (const line of lines) {
2740
+ if (/^\s*#!\/bin\/sh\s*$/.test(line)) {
2741
+ if (shebangSeen) continue;
2742
+ shebangSeen = true;
2743
+ normalized.push("#!/bin/sh");
2744
+ continue;
2343
2745
  }
2746
+ normalized.push(line);
2344
2747
  }
2345
- return violations;
2748
+ return normalized.join("\n").replace(/\n{3,}/g, "\n\n").trim();
2346
2749
  }
2347
-
2348
- // src/watcher.ts
2349
- function getFileLines(filePath) {
2750
+ function toRuleStatEntry(raw) {
2751
+ if (raw === void 0) return { count: 0, falsePositives: 0 };
2752
+ if (typeof raw === "number") return { count: raw, falsePositives: 0 };
2753
+ return raw;
2754
+ }
2755
+ function readPositiveIntEnv(name, fallback) {
2756
+ const raw = Number(process.env[name]);
2757
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
2758
+ }
2759
+ function isFastCheck(options) {
2760
+ return options.fast === true || process.env.MEMORY_CORE_CHECK_FAST === "1";
2761
+ }
2762
+ async function withTimeout(promise, timeoutMs, fallback) {
2763
+ let timer;
2350
2764
  try {
2351
- return readFileSync5(filePath, "utf-8").split("\n");
2352
- } catch {
2353
- return [];
2765
+ return await Promise.race([
2766
+ promise,
2767
+ new Promise((resolve4) => {
2768
+ timer = setTimeout(() => resolve4(fallback), timeoutMs);
2769
+ })
2770
+ ]);
2771
+ } finally {
2772
+ if (timer) clearTimeout(timer);
2354
2773
  }
2355
2774
  }
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]}`));
2775
+ function recordViolations(violations, source = "hook") {
2776
+ const statsPath = join8(process.cwd(), ".memory-core-stats.json");
2777
+ let stats = { rules: {}, files: {} };
2778
+ if (existsSync7(statsPath)) {
2779
+ try {
2780
+ stats = JSON.parse(readFileSync7(statsPath, "utf-8"));
2781
+ } catch {
2782
+ stats = { rules: {}, files: {} };
2369
2783
  }
2370
2784
  }
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"));
2372
- }
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");
2785
+ stats.rules ??= {};
2786
+ stats.files ??= {};
2787
+ for (const violation of violations) {
2788
+ const existing = toRuleStatEntry(stats.rules[violation.rule]);
2789
+ stats.rules[violation.rule] = { count: existing.count + 1, falsePositives: existing.falsePositives };
2790
+ if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
2791
+ }
2792
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2793
+ const recent = violations.map((violation) => ({ ...violation, timestamp, source }));
2794
+ stats.recentViolations = [...recent, ...stats.recentViolations ?? []].slice(0, 50);
2795
+ writeFileSync5(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
2384
2796
  }
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;
2797
+ function resetViolationStats(cwd = process.cwd()) {
2798
+ const statsPath = join8(cwd, ".memory-core-stats.json");
2799
+ if (!existsSync7(statsPath)) return;
2800
+ let stats = {};
2801
+ try {
2802
+ const parsed = JSON.parse(readFileSync7(statsPath, "utf-8"));
2803
+ if (parsed && typeof parsed === "object") {
2804
+ stats = parsed;
2805
+ }
2806
+ } catch {
2807
+ stats = {};
2396
2808
  }
2809
+ stats.rules = {};
2810
+ stats.files = {};
2811
+ stats.recentViolations = [];
2812
+ writeFileSync5(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
2397
2813
  }
2398
- function resolveWatchPaths(pathOption, projectRootOption) {
2399
- if (projectRootOption) {
2400
- const projectRoot2 = resolve4(projectRootOption);
2401
- return {
2402
- projectRoot: projectRoot2,
2403
- watchPath: resolve4(projectRoot2, pathOption ?? ".")
2404
- };
2814
+ function recordBypass(hadReason, cwd = process.cwd()) {
2815
+ const statsPath = join8(cwd, ".memory-core-stats.json");
2816
+ let stats = { rules: {}, files: {} };
2817
+ if (existsSync7(statsPath)) {
2818
+ try {
2819
+ stats = JSON.parse(readFileSync7(statsPath, "utf-8"));
2820
+ } catch {
2821
+ }
2405
2822
  }
2406
- const cwdRoot = resolve4(process.cwd());
2407
- const watchPath = resolve4(cwdRoot, pathOption ?? ".");
2408
- const projectRoot = findProjectRoot(watchPath) ?? findProjectRoot(cwdRoot) ?? cwdRoot;
2409
- return { projectRoot, watchPath };
2823
+ const prev = stats.bypasses ?? { total: 0, withReason: 0, withoutReason: 0 };
2824
+ stats.bypasses = {
2825
+ total: prev.total + 1,
2826
+ withReason: prev.withReason + (hadReason ? 1 : 0),
2827
+ withoutReason: prev.withoutReason + (hadReason ? 0 : 1)
2828
+ };
2829
+ writeFileSync5(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
2830
+ return stats.bypasses;
2410
2831
  }
2411
- function readStatsFile(statsPath) {
2412
- if (!existsSync6(statsPath)) return { rules: {}, files: {} };
2832
+ function readBypassStats(cwd = process.cwd()) {
2833
+ const statsPath = join8(cwd, ".memory-core-stats.json");
2834
+ if (!existsSync7(statsPath)) return { total: 0, withReason: 0, withoutReason: 0 };
2413
2835
  try {
2414
- return JSON.parse(readFileSync5(statsPath, "utf-8"));
2836
+ const stats = JSON.parse(readFileSync7(statsPath, "utf-8"));
2837
+ return stats.bypasses ?? { total: 0, withReason: 0, withoutReason: 0 };
2415
2838
  } catch {
2416
- return { rules: {}, files: {} };
2839
+ return { total: 0, withReason: 0, withoutReason: 0 };
2417
2840
  }
2418
2841
  }
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;
2842
+ function accumulateTokenUsage(usage, cwd = process.cwd()) {
2843
+ if (!usage) return;
2844
+ const statsPath = join8(cwd, ".memory-core-stats.json");
2845
+ let stats = { rules: {}, files: {} };
2846
+ if (existsSync7(statsPath)) {
2847
+ try {
2848
+ stats = JSON.parse(readFileSync7(statsPath, "utf-8"));
2849
+ } catch {
2427
2850
  }
2428
2851
  }
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: {}
2852
+ const prev = stats.tokens ?? { calls: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0 };
2853
+ stats.tokens = {
2854
+ calls: prev.calls + 1,
2855
+ inputTokens: prev.inputTokens + usage.inputTokens,
2856
+ outputTokens: prev.outputTokens + usage.outputTokens,
2857
+ totalTokens: prev.totalTokens + usage.totalTokens
2440
2858
  };
2441
- writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
2859
+ writeFileSync5(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
2442
2860
  }
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;
2861
+ async function promptToSaveViolations(violations) {
2862
+ if (!process.stdin.isTTY || violations.length === 0) return;
2863
+ try {
2864
+ const app = getDefaultApplicationContainer();
2865
+ const { confirm, input } = await import("@inquirer/prompts");
2866
+ const save = await confirm({
2867
+ message: "Save a caught violation as a project rule?",
2868
+ default: false
2869
+ });
2870
+ if (!save) return;
2871
+ const choices = violations.map((violation, index) => `${index + 1}. ${violation.rule}`);
2872
+ const selected = violations.length === 1 ? violations[0] : violations[Number(await input({ message: `Which violation? ${choices.join(" | ")}`, default: "1" })) - 1] ?? violations[0];
2873
+ const reason = await input({
2874
+ message: "Why should this rule exist?",
2875
+ default: selected.reason ?? selected.issue ?? ""
2876
+ });
2877
+ const storedReason = reason.trim() || selected.reason || selected.issue || `Captured from violation: ${selected.rule}`;
2878
+ await app.services.memoryEngine.remember({
2879
+ type: "rule",
2880
+ scope: "project",
2881
+ content: selected.rule,
2882
+ reason: storedReason,
2883
+ tags: ["violation"]
2884
+ });
2885
+ console.log(chalk2.green(" \u2713 Saved as project rule. Run memory-core sync to propagate it.\n"));
2886
+ } catch (err) {
2887
+ console.log(chalk2.yellow(` Could not save violation: ${err.message}
2888
+ `));
2454
2889
  }
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;
2890
+ }
2891
+ function readRuleCache(cwd) {
2892
+ const cachePath = join8(cwd, RULE_CACHE_FILE);
2893
+ const configPath = join8(cwd, ".memory-core.json");
2894
+ if (!existsSync7(cachePath) || !existsSync7(configPath)) return null;
2895
+ try {
2896
+ const entry = JSON.parse(readFileSync7(cachePath, "utf-8"));
2897
+ const now = Date.now();
2898
+ if (now - entry.timestamp > RULE_CACHE_TTL_MS) return null;
2899
+ const configMtime = statSync3(configPath).mtimeMs;
2900
+ if (configMtime !== entry.configMtime) return null;
2901
+ const dbVersionPath = join8(cwd, DB_VERSION_FILE);
2902
+ const dbVersionMtime = existsSync7(dbVersionPath) ? statSync3(dbVersionPath).mtimeMs : 0;
2903
+ if (dbVersionMtime !== entry.dbVersionMtime) return null;
2904
+ return entry;
2905
+ } catch {
2906
+ return null;
2461
2907
  }
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);
2908
+ }
2909
+ function saveRuleCache(cwd, data) {
2910
+ const configPath = join8(cwd, ".memory-core.json");
2911
+ try {
2912
+ const configMtime = statSync3(configPath).mtimeMs;
2913
+ const dbVersionPath = join8(cwd, DB_VERSION_FILE);
2914
+ const dbVersionMtime = existsSync7(dbVersionPath) ? statSync3(dbVersionPath).mtimeMs : 0;
2915
+ const entry = {
2916
+ timestamp: Date.now(),
2917
+ configMtime,
2918
+ dbVersionMtime,
2919
+ ...data
2920
+ };
2921
+ writeFileSync5(join8(cwd, RULE_CACHE_FILE), JSON.stringify(entry, null, 2) + "\n", "utf-8");
2922
+ } catch {
2466
2923
  }
2467
- writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
2468
2924
  }
2469
- function loadConfig(cwd) {
2470
- const configPath = join7(cwd, ".memory-core.json");
2471
- if (!existsSync6(configPath)) return null;
2925
+ async function loadIgnorePatterns2() {
2472
2926
  try {
2473
- return JSON.parse(readFileSync5(configPath, "utf-8"));
2927
+ const app = getDefaultApplicationContainer();
2928
+ const ignores = await app.services.memoryEngine.list({ type: "ignore", limit: 1e3 });
2929
+ return ignores.map((ignore) => ignore.content);
2474
2930
  } catch {
2475
- return null;
2931
+ return [];
2476
2932
  }
2477
2933
  }
2478
- function getProfileRules(config2) {
2934
+ function getProfileRules2(config) {
2479
2935
  const rules = [];
2480
2936
  const avoids = [];
2481
- if (config2.backendArchitecture) {
2482
- const profile = listProfiles("backend").find((p) => p.name === config2.backendArchitecture);
2937
+ if (config.backendArchitecture) {
2938
+ const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
2483
2939
  if (profile) {
2484
2940
  rules.push(...profile.rules);
2485
2941
  avoids.push(...profile.avoid);
2486
2942
  }
2487
2943
  }
2488
- if (config2.frontendFramework) {
2489
- const profile = listProfiles("frontend").find((p) => p.name === config2.frontendFramework);
2944
+ if (config.frontendFramework) {
2945
+ const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
2490
2946
  if (profile) {
2491
2947
  rules.push(...profile.rules);
2492
2948
  avoids.push(...profile.avoid);
@@ -2494,19 +2950,19 @@ function getProfileRules(config2) {
2494
2950
  }
2495
2951
  return { rules, avoids };
2496
2952
  }
2497
- async function loadRelevantRules(cwd, config2, rel, diff, fallbackRules) {
2953
+ async function loadRelevantRules2(config, diff, stagedFiles, fallbackRules) {
2498
2954
  try {
2499
2955
  const query = buildContextQuery([
2500
- rel,
2956
+ stagedFiles.join("\n"),
2501
2957
  diff.slice(0, 1200),
2502
- config2.backendArchitecture,
2503
- config2.frontendFramework,
2504
- config2.language
2958
+ config.backendArchitecture,
2959
+ config.frontendFramework,
2960
+ config.language
2505
2961
  ]);
2506
2962
  const memories = await retrieveContextualMemories({
2507
2963
  query,
2508
- cwd,
2509
- config: config2,
2964
+ cwd: process.cwd(),
2965
+ config,
2510
2966
  limit: 15
2511
2967
  });
2512
2968
  const selected = memories.filter((memory) => ["rule", "pattern", "decision"].includes(memory.type)).map((memory) => memory.content);
@@ -2515,7 +2971,7 @@ async function loadRelevantRules(cwd, config2, rel, diff, fallbackRules) {
2515
2971
  return fallbackRules;
2516
2972
  }
2517
2973
  }
2518
- function applyAllowPatterns(violations, allowPatterns) {
2974
+ function applyAllowPatterns2(violations, allowPatterns) {
2519
2975
  if (allowPatterns.length === 0) return violations;
2520
2976
  return violations.filter((violation) => {
2521
2977
  const haystack = `${violation.rule}
@@ -2524,193 +2980,651 @@ ${violation.file}`.toLowerCase();
2524
2980
  return !allowPatterns.some((pattern) => haystack.includes(pattern.toLowerCase()));
2525
2981
  });
2526
2982
  }
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)"}
2535
-
2536
- 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)}
2541
-
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"));
2983
+ function normalizeViolation(value) {
2984
+ if (!value || typeof value !== "object") return null;
2985
+ const candidate = value;
2986
+ if (typeof candidate.rule !== "string" || typeof candidate.issue !== "string") return null;
2987
+ return {
2988
+ rule: candidate.rule,
2989
+ file: typeof candidate.file === "string" ? candidate.file : "diff",
2990
+ line: typeof candidate.line === "number" ? candidate.line : void 0,
2991
+ issue: candidate.issue,
2992
+ suggestion: typeof candidate.suggestion === "string" ? candidate.suggestion : void 0,
2993
+ reason: typeof candidate.reason === "string" ? candidate.reason : void 0
2994
+ };
2995
+ }
2996
+ function parseModelViolations(raw) {
2997
+ const candidates = [
2998
+ raw,
2999
+ raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "")
3000
+ ];
3001
+ const objectStart = raw.indexOf("{");
3002
+ const objectEnd = raw.lastIndexOf("}");
3003
+ if (objectStart !== -1 && objectEnd > objectStart) {
3004
+ candidates.push(raw.slice(objectStart, objectEnd + 1));
3005
+ }
3006
+ const arrayStart = raw.indexOf("[");
3007
+ const arrayEnd = raw.lastIndexOf("]");
3008
+ if (arrayStart !== -1 && arrayEnd > arrayStart) {
3009
+ candidates.push(raw.slice(arrayStart, arrayEnd + 1));
3010
+ }
3011
+ for (const candidate of candidates) {
3012
+ try {
3013
+ const parsed = JSON.parse(candidate);
3014
+ const items = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.violations) ? parsed.violations : parsed?.rule ? [parsed] : null;
3015
+ if (!items) continue;
3016
+ return {
3017
+ valid: true,
3018
+ violations: items.map(normalizeViolation).filter((violation) => violation !== null)
3019
+ };
3020
+ } catch {
3021
+ }
3022
+ }
3023
+ return { valid: false, violations: [] };
3024
+ }
3025
+ function getAddedLines(diff) {
3026
+ const lines = [];
3027
+ let currentFile = "diff";
3028
+ let newLineNumber = 0;
3029
+ for (const line of diff.split("\n")) {
3030
+ if (line.startsWith("+++ b/")) {
3031
+ currentFile = line.slice("+++ b/".length);
3032
+ continue;
3033
+ }
3034
+ const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
3035
+ if (hunk) {
3036
+ newLineNumber = Number(hunk[1]);
3037
+ continue;
3038
+ }
3039
+ if (line.startsWith("+") && !line.startsWith("+++")) {
3040
+ lines.push({
3041
+ file: currentFile,
3042
+ line: Number.isFinite(newLineNumber) ? newLineNumber : void 0,
3043
+ content: line.slice(1)
3044
+ });
3045
+ newLineNumber += 1;
3046
+ continue;
3047
+ }
3048
+ if (!line.startsWith("-") && newLineNumber > 0) {
3049
+ newLineNumber += 1;
3050
+ }
3051
+ }
3052
+ return lines;
3053
+ }
3054
+ function dedupeViolations(violations) {
3055
+ const seen = /* @__PURE__ */ new Set();
3056
+ const deduped = [];
3057
+ for (const violation of violations) {
3058
+ const key = [
3059
+ violation.rule,
3060
+ violation.file,
3061
+ violation.line ?? "",
3062
+ violation.issue
3063
+ ].join("\0");
3064
+ if (seen.has(key)) continue;
3065
+ seen.add(key);
3066
+ deduped.push(violation);
3067
+ }
3068
+ return deduped;
3069
+ }
3070
+ function normalizeKeyPath(value) {
3071
+ return value.replace(/\\/g, "/").replace(/^\.\/+/, "").toLowerCase();
3072
+ }
3073
+ function violationRecurrenceKey(violation) {
3074
+ return [
3075
+ violation.rule.trim().toLowerCase(),
3076
+ normalizeKeyPath(violation.file || "diff"),
3077
+ violation.issue.trim().toLowerCase()
3078
+ ].join("\0");
3079
+ }
3080
+ function findRecurringViolations(currentViolations, recentViolations, minCount = 2) {
3081
+ if (currentViolations.length === 0 || recentViolations.length === 0) return [];
3082
+ const counts = /* @__PURE__ */ new Map();
3083
+ for (const recent of recentViolations) {
3084
+ const key = violationRecurrenceKey(recent);
3085
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3086
+ }
3087
+ return currentViolations.filter((violation) => (counts.get(violationRecurrenceKey(violation)) ?? 0) >= minCount);
3088
+ }
3089
+ function extractIssuePhrase(issue) {
3090
+ const quoted = issue.match(/"([^"]{3,160})"/);
3091
+ if (quoted?.[1]) return quoted[1].trim();
3092
+ const afterColon = issue.split(":").slice(1).join(":").trim();
3093
+ if (afterColon.length >= 3) return afterColon.slice(0, 180);
3094
+ const fallback = issue.trim();
3095
+ return fallback.length >= 3 ? fallback.slice(0, 180) : null;
3096
+ }
3097
+ function buildIgnorePatternFromDecision(decision) {
3098
+ const explicit = decision.pattern?.trim();
3099
+ if (explicit && explicit.length >= 3) return explicit;
3100
+ return extractIssuePhrase(decision.issue);
3101
+ }
3102
+ function parseFalsePositiveDecision(value) {
3103
+ if (!value || typeof value !== "object") return null;
3104
+ const candidate = value;
3105
+ if (typeof candidate.rule !== "string" || typeof candidate.issue !== "string") return null;
3106
+ return {
3107
+ rule: candidate.rule,
3108
+ file: typeof candidate.file === "string" ? candidate.file : void 0,
3109
+ line: typeof candidate.line === "number" ? candidate.line : void 0,
3110
+ issue: candidate.issue,
3111
+ falsePositive: candidate.falsePositive === true,
3112
+ pattern: typeof candidate.pattern === "string" ? candidate.pattern : void 0,
3113
+ reason: typeof candidate.reason === "string" ? candidate.reason : void 0
3114
+ };
3115
+ }
3116
+ function parseFalsePositiveDecisions(raw) {
3117
+ const candidates = [
3118
+ raw,
3119
+ raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "")
3120
+ ];
3121
+ const objectStart = raw.indexOf("{");
3122
+ const objectEnd = raw.lastIndexOf("}");
3123
+ if (objectStart !== -1 && objectEnd > objectStart) {
3124
+ candidates.push(raw.slice(objectStart, objectEnd + 1));
3125
+ }
3126
+ const arrayStart = raw.indexOf("[");
3127
+ const arrayEnd = raw.lastIndexOf("]");
3128
+ if (arrayStart !== -1 && arrayEnd > arrayStart) {
3129
+ candidates.push(raw.slice(arrayStart, arrayEnd + 1));
3130
+ }
3131
+ for (const candidate of candidates) {
3132
+ try {
3133
+ const parsed = JSON.parse(candidate);
3134
+ const items = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.decisions) ? parsed.decisions : parsed?.rule ? [parsed] : null;
3135
+ if (!items) continue;
3136
+ return {
3137
+ valid: true,
3138
+ decisions: items.map(parseFalsePositiveDecision).filter((decision) => decision !== null)
3139
+ };
3140
+ } catch {
3141
+ }
3142
+ }
3143
+ return { valid: false, decisions: [] };
3144
+ }
3145
+ function loadRecentViolationsFromStats(cwd = process.cwd()) {
3146
+ const statsPath = join8(cwd, ".memory-core-stats.json");
3147
+ if (!existsSync7(statsPath)) return [];
3148
+ try {
3149
+ const parsed = JSON.parse(readFileSync7(statsPath, "utf-8"));
3150
+ if (!Array.isArray(parsed.recentViolations)) return [];
3151
+ return parsed.recentViolations.filter(
3152
+ (entry) => Boolean(entry) && typeof entry.rule === "string" && typeof entry.issue === "string" && typeof entry.file === "string" && typeof entry.timestamp === "string"
3153
+ );
3154
+ } catch {
3155
+ return [];
3156
+ }
3157
+ }
3158
+ function incrementFalsePositivesForPatterns(learnedPatterns, violations, cwd = process.cwd()) {
3159
+ if (learnedPatterns.length === 0 || violations.length === 0) return;
3160
+ const statsPath = join8(cwd, ".memory-core-stats.json");
3161
+ if (!existsSync7(statsPath)) return;
3162
+ let stats;
3163
+ try {
3164
+ stats = JSON.parse(readFileSync7(statsPath, "utf-8"));
3165
+ } catch {
3166
+ return;
3167
+ }
3168
+ stats.rules ??= {};
3169
+ for (const violation of violations) {
3170
+ const haystack = `${violation.rule}
3171
+ ${violation.issue}
3172
+ ${violation.file}`.toLowerCase();
3173
+ const matched = learnedPatterns.some((p) => haystack.includes(p.toLowerCase()));
3174
+ if (!matched) continue;
3175
+ const existing = toRuleStatEntry(stats.rules[violation.rule]);
3176
+ stats.rules[violation.rule] = { count: existing.count, falsePositives: existing.falsePositives + 1 };
3177
+ }
3178
+ writeFileSync5(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
3179
+ }
3180
+ async function learnGlobalIgnoresFromFalsePositives(options) {
3181
+ if (options.currentViolations.length === 0) return [];
3182
+ const recentViolations = loadRecentViolationsFromStats();
3183
+ const recurring = findRecurringViolations(options.currentViolations, recentViolations, 2);
3184
+ if (recurring.length === 0) return [];
3185
+ const systemPrompt = `You are verifying repeated architecture-rule alerts.
3186
+ Mark falsePositive=true ONLY when the alert is clearly a false positive for this staged diff.
3187
+ For each false positive, return a concise ignore pattern that will suppress only this recurring false alert.
3188
+ Prefer exact snippets from the issue text.
3189
+
3190
+ Return strict JSON:
3191
+ {"decisions":[{"rule":"...","file":"...","line":1,"issue":"...","falsePositive":true,"pattern":"...","reason":"..."}]}
3192
+ Do not include any text outside JSON.`;
3193
+ const userPrompt = `Staged diff:
3194
+ ${options.diff.slice(0, 6e3)}
3195
+
3196
+ Recurring violations:
3197
+ ${JSON.stringify(recurring, null, 2)}
3198
+
3199
+ Existing allow patterns:
3200
+ ${JSON.stringify(options.allowPatterns, null, 2)}`;
3201
+ if (options.debug) {
3202
+ console.log(chalk2.gray("\n [debug] false-positive recheck prompt:"));
3203
+ console.log(chalk2.dim(systemPrompt));
3204
+ console.log(chalk2.dim(userPrompt));
3205
+ 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
3206
  }
2550
3207
  try {
2551
- const raw = await callChatModel([
3208
+ const recheckTimeoutMs = readPositiveIntEnv("MEMORY_CORE_FALSE_POSITIVE_TIMEOUT_MS", 6e3);
3209
+ const { content: raw, usage: recheckUsage } = await callChatModel([
2552
3210
  { role: "system", content: systemPrompt },
2553
3211
  { 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 {
3212
+ ], { timeoutMs: recheckTimeoutMs });
3213
+ accumulateTokenUsage(recheckUsage);
3214
+ const parsed = parseFalsePositiveDecisions(raw);
3215
+ if (!parsed.valid) return [];
3216
+ const existing = new Set(options.allowPatterns.map((pattern) => pattern.toLowerCase()));
2565
3217
  const app = getDefaultApplicationContainer();
2566
- const ignores = await app.services.memoryEngine.list({ type: "ignore", limit: 1e3 });
2567
- return ignores.map((ignore) => ignore.content);
3218
+ const inserted = [];
3219
+ for (const decision of parsed.decisions) {
3220
+ if (!decision.falsePositive) continue;
3221
+ const pattern = buildIgnorePatternFromDecision(decision);
3222
+ if (!pattern) continue;
3223
+ const normalized = pattern.toLowerCase();
3224
+ if (existing.has(normalized)) continue;
3225
+ try {
3226
+ await app.services.memoryEngine.remember({
3227
+ type: "ignore",
3228
+ scope: "global",
3229
+ architecture: "global",
3230
+ content: pattern,
3231
+ reason: `Auto-added from repeated false-positive recheck for "${decision.rule}"${decision.reason ? `: ${decision.reason}` : ""}`,
3232
+ tags: ["ignore", "auto-false-positive"]
3233
+ });
3234
+ existing.add(normalized);
3235
+ inserted.push(pattern);
3236
+ } catch {
3237
+ }
3238
+ }
3239
+ if (inserted.length > 0) {
3240
+ incrementFalsePositivesForPatterns(inserted, options.currentViolations);
3241
+ }
3242
+ return inserted;
2568
3243
  } catch {
2569
3244
  return [];
2570
3245
  }
2571
3246
  }
2572
- function normalizeForGit(pathLike) {
2573
- return pathLike.split(sep).join("/");
3247
+ function normalizePath(value) {
3248
+ return value.replace(/\\/g, "/").replace(/^\.\/+/, "");
2574
3249
  }
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;
3250
+ function resolveChangedFile(candidate, changedFiles) {
3251
+ const normalizedCandidate = normalizePath(candidate);
3252
+ const candidates = [normalizedCandidate];
3253
+ if (/^(?:a|b)\//.test(normalizedCandidate)) {
3254
+ candidates.push(normalizedCandidate.slice(2));
3255
+ }
3256
+ for (const current of candidates) {
3257
+ if (changedFiles.has(current)) return current;
3258
+ for (const changed of changedFiles) {
3259
+ if (changed.endsWith(`/${current}`) || current.endsWith(`/${changed}`)) {
3260
+ return changed;
2604
3261
  }
2605
- if (isFile && SOURCE_EXTENSIONS3.test(absolute)) files.push(absolute);
2606
3262
  }
2607
3263
  }
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();
3264
+ return void 0;
2620
3265
  }
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"));
3266
+ function buildModelInputFromDiff(diff, maxChars = 8e3) {
3267
+ const addedLines = getAddedLines(diff);
3268
+ if (addedLines.length === 0) {
3269
+ const truncated2 = diff.length > maxChars;
2625
3270
  return {
2626
- filesChecked: 0,
2627
- filesWithViolations: 0,
2628
- violations: 0
3271
+ text: truncated2 ? diff.slice(0, maxChars) + "\n\n[diff truncated]" : diff,
3272
+ source: "diff",
3273
+ truncated: truncated2
2629
3274
  };
2630
3275
  }
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
3276
+ const chunks = [];
3277
+ let currentFile = "";
3278
+ for (const addedLine of addedLines) {
3279
+ if (addedLine.file !== currentFile) {
3280
+ currentFile = addedLine.file;
3281
+ chunks.push(`
3282
+ # ${currentFile}`);
3283
+ }
3284
+ const line = addedLine.line ?? "?";
3285
+ chunks.push(`${line}: ${addedLine.content}`);
3286
+ }
3287
+ const summary = chunks.join("\n").trim();
3288
+ const truncated = summary.length > maxChars;
3289
+ return {
3290
+ text: truncated ? summary.slice(0, maxChars) + "\n\n[added lines truncated]" : summary,
3291
+ source: "added-lines",
3292
+ truncated
2638
3293
  };
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
- }
3294
+ }
3295
+ function findDeterministicViolations(diff, rules, avoids, allowPatterns = []) {
3296
+ const rulePhrases = rules.flatMap(
3297
+ (rule) => extractForbiddenPhrases(rule).map((phrase) => ({ rule, phrase }))
3298
+ );
3299
+ const avoidPhrases = avoids.map((avoid) => ({
3300
+ rule: `Avoid: ${avoid}`,
3301
+ phrase: avoid.toLowerCase()
3302
+ }));
3303
+ const phrases = [...rulePhrases, ...avoidPhrases].filter((item) => item.phrase.length > 0);
3304
+ if (phrases.length === 0) return [];
3305
+ const violations = [];
3306
+ for (const addedLine of getAddedLines(diff)) {
3307
+ const normalizedLine = addedLine.content.toLowerCase();
3308
+ if (allowPatterns.some((pattern) => normalizedLine.includes(pattern.toLowerCase()))) {
2650
3309
  continue;
2651
3310
  }
2652
- summary.filesChecked += 1;
2653
- if (result.violations.length === 0) {
2654
- onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
3311
+ for (const { rule, phrase } of phrases) {
3312
+ if (normalizedLine.includes(phrase)) {
3313
+ violations.push({
3314
+ rule,
3315
+ file: addedLine.file,
3316
+ line: addedLine.line,
3317
+ issue: `Added line contains forbidden phrase: "${phrase}"`,
3318
+ suggestion: "Remove this pattern or add an explicit ignore memory if it is intentional.",
3319
+ reason: reasonMap2.get(rule)
3320
+ });
3321
+ }
3322
+ }
3323
+ }
3324
+ return dedupeViolations(violations);
3325
+ }
3326
+ function suppressBatchRepetitions(violations, threshold = 3) {
3327
+ const pairCounts = /* @__PURE__ */ new Map();
3328
+ for (const v of violations) {
3329
+ const key = `${v.rule}\0${v.file}`;
3330
+ pairCounts.set(key, (pairCounts.get(key) ?? 0) + 1);
3331
+ }
3332
+ const suppressedKeys = /* @__PURE__ */ new Set();
3333
+ for (const [key, count] of pairCounts) {
3334
+ if (count >= threshold) suppressedKeys.add(key);
3335
+ }
3336
+ if (suppressedKeys.size === 0) return { filtered: violations, suppressedCount: 0 };
3337
+ const filtered = violations.filter((v) => !suppressedKeys.has(`${v.rule}\0${v.file}`));
3338
+ return { filtered, suppressedCount: violations.length - filtered.length };
3339
+ }
3340
+ function groupViolationsByRule(violations) {
3341
+ const groups = /* @__PURE__ */ new Map();
3342
+ for (const v of violations) {
3343
+ const existing = groups.get(v.rule);
3344
+ if (existing) {
3345
+ existing.push(v);
3346
+ } else {
3347
+ groups.set(v.rule, [v]);
3348
+ }
3349
+ }
3350
+ return groups;
3351
+ }
3352
+ function filterModelViolationsByStagedDiff(violations, stagedFiles, diff) {
3353
+ if (violations.length === 0) return violations;
3354
+ const changedFiles = new Set(stagedFiles.map((file) => normalizePath(file)));
3355
+ if (changedFiles.size === 0) return [];
3356
+ const linesByFile = /* @__PURE__ */ new Map();
3357
+ for (const addedLine of getAddedLines(diff)) {
3358
+ const file = normalizePath(addedLine.file);
3359
+ if (!changedFiles.has(file)) continue;
3360
+ if (typeof addedLine.line !== "number") continue;
3361
+ const list = linesByFile.get(file) ?? [];
3362
+ list.push(addedLine.line);
3363
+ linesByFile.set(file, list);
3364
+ }
3365
+ const LINE_TOLERANCE = 3;
3366
+ const filtered = [];
3367
+ for (const violation of violations) {
3368
+ if (!violation.file || violation.file === "diff") {
3369
+ filtered.push(violation);
2655
3370
  continue;
2656
3371
  }
2657
- summary.filesWithViolations += 1;
2658
- summary.violations += result.violations.length;
2659
- onEvent?.({ type: "violations", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, violations: result.violations });
3372
+ const resolvedFile = resolveChangedFile(violation.file, changedFiles);
3373
+ if (!resolvedFile) continue;
3374
+ const candidateLines = linesByFile.get(resolvedFile) ?? [];
3375
+ if (typeof violation.line === "number" && candidateLines.length > 0) {
3376
+ const supported = candidateLines.some((line) => Math.abs(line - violation.line) <= LINE_TOLERANCE);
3377
+ if (!supported) continue;
3378
+ }
3379
+ filtered.push({ ...violation, file: resolvedFile });
3380
+ }
3381
+ return filtered;
3382
+ }
3383
+ function installHook(advisory = true, fast = false) {
3384
+ if (!existsSync7(".git")) {
3385
+ console.error(chalk2.red("\n Not a git repository. Run from project root.\n"));
3386
+ process.exit(1);
3387
+ }
3388
+ const script = buildHookScript(advisory, fast);
3389
+ const body = buildHookBody(advisory, fast).trimEnd();
3390
+ if (existsSync7(HOOK_PATH)) {
3391
+ const existing = readFileSync7(HOOK_PATH, "utf-8");
3392
+ if (existing.includes(HOOK_MARKER)) {
3393
+ const markerIndex = existing.indexOf(HOOK_MARKER);
3394
+ const beforeRaw = markerIndex > 0 ? existing.slice(0, markerIndex) : "";
3395
+ const normalizedBefore = normalizeHookPreamble(beforeRaw);
3396
+ const preamble = normalizedBefore.length > 0 ? normalizedBefore : "#!/bin/sh";
3397
+ const preambleWithShebang = preamble.startsWith("#!/bin/sh") ? preamble : `#!/bin/sh
3398
+ ${preamble}`;
3399
+ writeFileSync5(HOOK_PATH, `${preambleWithShebang}
3400
+
3401
+ ${body}
3402
+ `);
3403
+ chmodSync(HOOK_PATH, 493);
3404
+ installCommitMsgHook(advisory);
3405
+ const modeLabel2 = advisory ? chalk2.cyan("advisory") : chalk2.yellow("strict");
3406
+ console.log(chalk2.green("\n \u2713 Pre-commit hook updated") + chalk2.dim(` (${modeLabel2} mode)`));
3407
+ if (fast) console.log(chalk2.gray(` Check mode: fast deterministic checks`));
3408
+ return;
3409
+ }
3410
+ writeFileSync5(HOOK_PATH, existing.trimEnd() + "\n\n" + body + "\n");
3411
+ } else {
3412
+ writeFileSync5(HOOK_PATH, script);
3413
+ }
3414
+ chmodSync(HOOK_PATH, 493);
3415
+ installCommitMsgHook(advisory);
3416
+ const modeLabel = advisory ? "advisory (logs violations, never blocks)" : "strict (blocks commits on violations)";
3417
+ console.log(chalk2.green("\n \u2713 Pre-commit hook installed") + chalk2.dim(` \u2014 ${modeLabel}`));
3418
+ console.log(chalk2.gray(fast ? " Check mode: fast deterministic checks" : ` Chat model: ${process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"}`));
3419
+ console.log(chalk2.gray(" Commit message rules: memory-core commit-rules --list"));
3420
+ console.log(chalk2.gray(" To uninstall: memory-core hook uninstall\n"));
3421
+ }
3422
+ function uninstallHook() {
3423
+ if (!existsSync7(HOOK_PATH)) {
3424
+ console.log(chalk2.yellow("\n No pre-commit hook found.\n"));
3425
+ return;
3426
+ }
3427
+ const content = readFileSync7(HOOK_PATH, "utf-8");
3428
+ if (!content.includes(HOOK_MARKER)) {
3429
+ console.log(chalk2.yellow("\n ArchMind hook not found in pre-commit \u2014 nothing to remove.\n"));
3430
+ return;
3431
+ }
3432
+ const markerIndex = content.indexOf(HOOK_MARKER);
3433
+ const before = markerIndex > 1 ? normalizeHookPreamble(content.slice(0, markerIndex)) : "";
3434
+ if (before && before !== "#!/bin/sh") {
3435
+ writeFileSync5(HOOK_PATH, `${before}
3436
+ `);
3437
+ } else {
3438
+ unlinkSync(HOOK_PATH);
3439
+ }
3440
+ uninstallCommitMsgHook();
3441
+ console.log(chalk2.green("\n \u2713 Pre-commit hook removed\n"));
3442
+ }
3443
+ function buildCommitMsgHookBody(advisory) {
3444
+ const suffix = advisory ? " || true" : "";
3445
+ return `${COMMIT_MSG_HOOK_MARKER}${advisory ? " advisory" : ""}
3446
+ if [ "\${MEMORY_CORE_SKIP_HOOK:-}" = "1" ] || [ "\${ARCHMIND_SKIP_HOOK:-}" = "1" ] || [ "\${HUSKY:-}" = "0" ] || [ "\${HUSKY_SKIP_HOOKS:-}" = "1" ]; then
3447
+ exit 0
3448
+ fi
3449
+ if [ -n "\${SKIP:-}" ] && echo ",$SKIP," | grep -qiE ',(memory-core|archmind),'; then
3450
+ exit 0
3451
+ fi
3452
+ if [ -n "\${SKIP_HOOKS:-}" ]; then
3453
+ exit 0
3454
+ fi
3455
+ if command -v memory-core >/dev/null 2>&1; then
3456
+ memory-core check --commit-msg "$1"${suffix}
3457
+ elif [ -f "./node_modules/.bin/memory-core" ]; then
3458
+ ./node_modules/.bin/memory-core check --commit-msg "$1"${suffix}
3459
+ elif [ -f "./dist/cli.js" ]; then
3460
+ node ./dist/cli.js check --commit-msg "$1"${suffix}
3461
+ else
3462
+ exit 0
3463
+ fi
3464
+ `;
3465
+ }
3466
+ function installCommitMsgHook(advisory = true) {
3467
+ const body = buildCommitMsgHookBody(advisory).trimEnd();
3468
+ const script = `#!/bin/sh
3469
+
3470
+ ${body}
3471
+ `;
3472
+ if (existsSync7(COMMIT_MSG_HOOK_PATH)) {
3473
+ const existing = readFileSync7(COMMIT_MSG_HOOK_PATH, "utf-8");
3474
+ if (existing.includes(COMMIT_MSG_HOOK_MARKER)) {
3475
+ const markerIndex = existing.indexOf(COMMIT_MSG_HOOK_MARKER);
3476
+ const beforeRaw = markerIndex > 0 ? existing.slice(0, markerIndex) : "";
3477
+ const normalizedBefore = normalizeHookPreamble(beforeRaw);
3478
+ const preamble = normalizedBefore.length > 0 ? normalizedBefore : "#!/bin/sh";
3479
+ const preambleWithShebang = preamble.startsWith("#!/bin/sh") ? preamble : `#!/bin/sh
3480
+ ${preamble}`;
3481
+ writeFileSync5(COMMIT_MSG_HOOK_PATH, `${preambleWithShebang}
3482
+
3483
+ ${body}
3484
+ `);
3485
+ } else {
3486
+ writeFileSync5(COMMIT_MSG_HOOK_PATH, existing.trimEnd() + "\n\n" + body + "\n");
3487
+ }
3488
+ } else {
3489
+ writeFileSync5(COMMIT_MSG_HOOK_PATH, script);
3490
+ }
3491
+ chmodSync(COMMIT_MSG_HOOK_PATH, 493);
3492
+ }
3493
+ function uninstallCommitMsgHook() {
3494
+ if (!existsSync7(COMMIT_MSG_HOOK_PATH)) return;
3495
+ const content = readFileSync7(COMMIT_MSG_HOOK_PATH, "utf-8");
3496
+ if (!content.includes(COMMIT_MSG_HOOK_MARKER)) return;
3497
+ const markerIndex = content.indexOf(COMMIT_MSG_HOOK_MARKER);
3498
+ const before = markerIndex > 1 ? normalizeHookPreamble(content.slice(0, markerIndex)) : "";
3499
+ if (before && before !== "#!/bin/sh") {
3500
+ writeFileSync5(COMMIT_MSG_HOOK_PATH, `${before}
3501
+ `);
3502
+ } else {
3503
+ unlinkSync(COMMIT_MSG_HOOK_PATH);
2660
3504
  }
2661
- return summary;
2662
3505
  }
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" };
3506
+ async function checkCommitMsg(msgFile, options = {}) {
3507
+ if (!existsSync7(msgFile)) {
3508
+ if (options.verbose) console.log(chalk2.gray(" No commit message file \u2014 skipping."));
3509
+ return;
3510
+ }
3511
+ const raw = readFileSync7(msgFile, "utf-8");
3512
+ const cleanMsg = raw.split("\n").filter((l) => !l.startsWith("#")).join("\n").trim();
3513
+ if (!cleanMsg) {
3514
+ if (options.verbose) console.log(chalk2.gray(" Empty commit message \u2014 skipping."));
3515
+ return;
3516
+ }
3517
+ const configPath = join8(process.cwd(), ".memory-core.json");
3518
+ if (!existsSync7(configPath)) return;
3519
+ const config = JSON.parse(readFileSync7(configPath, "utf-8"));
3520
+ const rules = (config.commitRules ?? []).filter(Boolean);
3521
+ if (rules.length === 0) return;
3522
+ console.log(chalk2.cyan("\n archmind \u2014 checking commit message\u2026"));
3523
+ const violations = [];
3524
+ for (const rule of rules) {
3525
+ try {
3526
+ const regex = new RegExp(rule.pattern, "im");
3527
+ const matched = regex.test(cleanMsg);
3528
+ const violated = rule.negate ? matched : !matched;
3529
+ if (violated) violations.push({ rule });
3530
+ } catch {
3531
+ if (options.debug) console.log(chalk2.yellow(` [debug] Invalid regex: "${rule.pattern}"`));
3532
+ }
3533
+ }
3534
+ if (violations.length === 0) {
3535
+ console.log(chalk2.green(" \u2713 Commit message OK.\n"));
3536
+ return;
3537
+ }
3538
+ const blocking = violations.filter((v) => !v.rule.advisory);
3539
+ violations.forEach(({ rule }) => {
3540
+ const prefix = rule.advisory ? chalk2.yellow(" \u26A0 ") : chalk2.red(" \u2717 ");
3541
+ console.log(prefix + rule.message);
3542
+ const matchLabel = rule.negate ? "(must NOT match)" : "(must match)";
3543
+ console.log(chalk2.dim(` Pattern: ${rule.pattern} ${matchLabel}`));
3544
+ });
3545
+ console.log();
3546
+ if (blocking.length === 0) return;
3547
+ console.log(chalk2.dim(" Fix the commit message, then commit again."));
3548
+ console.log(chalk2.dim(" To bypass: MEMORY_CORE_SKIP_HOOK=1 git commit"));
3549
+ console.log(chalk2.dim(" Manage rules: memory-core commit-rules --list\n"));
3550
+ process.exit(1);
3551
+ }
3552
+ async function checkStaged(options = {}) {
3553
+ const SOURCE_EXTENSIONS3 = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
3554
+ let diff;
3555
+ let stagedFiles = [];
3556
+ try {
3557
+ stagedFiles = execSync("git diff --cached --name-only --diff-filter=ACMRT", { encoding: "utf-8" }).split("\n").filter((f) => f && SOURCE_EXTENSIONS3.test(f)).map((f) => normalizePath(f));
3558
+ if (stagedFiles.length === 0) {
3559
+ if (options.verbose) console.log(chalk2.gray(" No source files staged \u2014 skipping rule check."));
3560
+ return;
3561
+ }
3562
+ const result = spawnSync2(
3563
+ "git",
3564
+ ["diff", "--cached", "--unified=0", "--diff-filter=ACMRT", "--", ...stagedFiles],
3565
+ { encoding: "utf-8" }
3566
+ );
3567
+ diff = result.stdout ?? "";
3568
+ } catch {
3569
+ console.error(chalk2.red(" Failed to read staged diff."));
3570
+ process.exit(1);
3571
+ }
3572
+ if (!diff.trim()) {
3573
+ if (options.verbose) console.log(chalk2.gray(" No staged changes to check."));
3574
+ return;
3575
+ }
3576
+ const configPath = join8(process.cwd(), ".memory-core.json");
3577
+ if (!existsSync7(configPath)) return;
3578
+ const config = JSON.parse(readFileSync7(configPath, "utf-8"));
3579
+ const { rules: fallbackRules, avoids } = getProfileRules2(config);
3580
+ const fast = isFastCheck(options);
3581
+ const ruleLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_RULE_LOAD_TIMEOUT_MS", 2e3);
3582
+ const ignoreLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_IGNORE_LOAD_TIMEOUT_MS", 1500);
3583
+ let rules;
3584
+ let ignores;
3585
+ let allowPatterns;
3586
+ if (fast) {
3587
+ rules = fallbackRules;
3588
+ ignores = [];
3589
+ allowPatterns = [...new Set(getAllowPatterns(config))];
2671
3590
  } else {
2672
- const headResult = spawnSync("git", ["diff", "HEAD", "--", rel], { encoding: "utf-8", cwd: projectRoot });
2673
- if (headResult.stdout?.trim()) {
2674
- inputText = headResult.stdout;
3591
+ const cwd = process.cwd();
3592
+ const cached = readRuleCache(cwd);
3593
+ if (cached) {
3594
+ rules = cached.rules;
3595
+ ignores = cached.ignores;
3596
+ allowPatterns = cached.allowPatterns;
3597
+ if (options.debug) {
3598
+ console.log(chalk2.gray(" [debug] using cached rules (TTL valid)"));
3599
+ }
2675
3600
  } else {
2676
- const noIndexResult = spawnSync("git", ["diff", "--no-index", "/dev/null", rel], {
2677
- encoding: "utf-8",
2678
- cwd: projectRoot
2679
- });
2680
- inputText = noIndexResult.stdout ?? "";
3601
+ const [loadedRules, loadedIgnores] = await Promise.all([
3602
+ withTimeout(loadRelevantRules2(config, diff, stagedFiles, fallbackRules), ruleLoadTimeoutMs, fallbackRules),
3603
+ withTimeout(loadIgnorePatterns2(), ignoreLoadTimeoutMs, [])
3604
+ ]);
3605
+ rules = loadedRules;
3606
+ ignores = loadedIgnores;
3607
+ allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...loadedIgnores])];
3608
+ saveRuleCache(cwd, { rules, ignores, allowPatterns });
2681
3609
  }
2682
- if (!inputText.trim()) return { type: "skipped", reason: "No changes compared with HEAD" };
2683
3610
  }
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`));
3611
+ if (rules.length === 0) return;
3612
+ const modelInputMaxChars = readPositiveIntEnv("MEMORY_CORE_MODEL_INPUT_MAX_CHARS", 8e3);
3613
+ const modelInput = buildModelInputFromDiff(diff, modelInputMaxChars);
3614
+ console.log(chalk2.cyan("\n archmind \u2014 checking staged changes against rules\u2026"));
3615
+ if (options.verbose || options.debug) {
3616
+ const sourceLabel = modelInput.source === "added-lines" ? "added lines" : "diff";
3617
+ const modelLabel = fast ? "skipped (--fast)" : getChatProviderLabel();
3618
+ console.log(chalk2.gray(` model: ${modelLabel} rules: ${rules.length} diff: ${diff.length} chars input: ${sourceLabel}${modelInput.truncated ? " (truncated)" : ""}`));
2694
3619
  }
2695
3620
  const rulesWithReasons = rules.map((r, i) => {
2696
- const why = reasonMap.get(r);
3621
+ const why = reasonMap2.get(r);
2697
3622
  return why ? `${i + 1}. ${r}
2698
3623
  WHY: ${why}` : `${i + 1}. ${r}`;
2699
3624
  }).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.
3625
+ const systemPrompt = `You are a strict code reviewer enforcing architecture and framework rules.
3626
+ Analyze the provided staged changes and identify ONLY clear, definite rule violations \u2014 not style preferences.
3627
+ Use the WHY for each rule to understand intent and judge edge cases correctly.
2714
3628
 
2715
3629
  Rules to enforce:
2716
3630
  ${rulesWithReasons}
@@ -2721,311 +3635,424 @@ ${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
2721
3635
  Never flag these accepted project patterns:
2722
3636
  ${allowPatterns.length ? allowPatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
2723
3637
 
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"));
3638
+ IMPORTANT: You MUST respond with a JSON object that has a "violations" key containing an array.
3639
+ For each violation include a "reason" field \u2014 copy the WHY from the rule to explain to the developer why this matters.
3640
+ 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"}]}
3641
+ Example with no violations: {"violations":[]}
3642
+ Do not include any text outside the JSON object.`;
3643
+ if (options.debug) {
3644
+ console.log(chalk2.gray("\n [debug] prompt:"));
3645
+ 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
3646
  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([
3647
+ 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"));
3648
+ console.log(chalk2.gray(` [debug] diff length: ${diff.length} chars`));
3649
+ console.log(chalk2.gray(` [debug] model input source: ${modelInput.source}`));
3650
+ console.log(chalk2.dim(modelInput.text));
3651
+ 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"));
3652
+ }
3653
+ const deterministicViolations = findDeterministicViolations(diff, rules, avoids, allowPatterns);
3654
+ const astViolations = findAstDeterministicViolationsForDiff(diff, {
3655
+ cwd: process.cwd(),
3656
+ config,
3657
+ rules,
3658
+ reasonLookup: reasonMap2
3659
+ });
3660
+ const app = getDefaultApplicationContainer();
3661
+ const schemaViolations = await findSchemaViolations({
3662
+ cwd: process.cwd(),
3663
+ memoryEngine: app.services.memoryEngine
3664
+ });
3665
+ let modelViolations = [];
3666
+ let aiFallback = fast;
3667
+ if (fast) {
3668
+ if (options.verbose || options.debug) {
3669
+ console.log(chalk2.gray(" AI check skipped; running deterministic checks only."));
3670
+ }
3671
+ } else try {
3672
+ const checkTimeoutMs = readPositiveIntEnv("MEMORY_CORE_CHECK_TIMEOUT_MS", readPositiveIntEnv("CHAT_TIMEOUT_MS", 2e4));
3673
+ const { content: raw, usage: checkUsage } = await callChatModel([
2744
3674
  { 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"));
3675
+ { role: "user", content: `Review these staged changes:
3676
+
3677
+ ${modelInput.text}` }
3678
+ ], { timeoutMs: checkTimeoutMs });
3679
+ accumulateTokenUsage(checkUsage);
3680
+ if (options.verbose || options.debug) {
3681
+ console.log(chalk2.gray(` raw response: ${options.debug ? raw : raw.slice(0, 200)}`));
2751
3682
  }
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];
3683
+ const parsed = parseModelViolations(raw);
3684
+ if (parsed.valid) {
3685
+ modelViolations = parsed.violations;
3686
+ } else {
3687
+ console.log(chalk2.yellow(" \u26A0 AI returned invalid JSON \u2014 using deterministic checks only."));
3688
+ }
3689
+ } catch (err) {
3690
+ if (err.message?.startsWith("MODEL_NOT_FOUND:")) {
3691
+ printModelMissing(err.message.split(":")[1]);
3692
+ aiFallback = true;
3693
+ modelViolations = [];
3694
+ } else if (err.message?.startsWith("TIMEOUT:")) {
3695
+ const timeoutMs = err.message.split(":")[1];
3696
+ console.log(chalk2.yellow(`
3697
+ \u26A0 AI check timed out after ${timeoutMs}ms \u2014 switching to fast deterministic checks for this run.`));
3698
+ console.log(chalk2.gray(" Set MEMORY_CORE_CHECK_TIMEOUT_MS to tune this.\n"));
3699
+ aiFallback = true;
3700
+ modelViolations = [];
3701
+ } else if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
3702
+ console.log(chalk2.yellow("\n \u26A0 Ollama not running \u2014 using deterministic checks only."));
3703
+ console.log(chalk2.gray(" Start it: ollama serve\n"));
3704
+ aiFallback = true;
3705
+ modelViolations = [];
3706
+ } else {
3707
+ console.log(chalk2.yellow(`
3708
+ \u26A0 AI rule check failed: ${err.message}`));
3709
+ console.log(chalk2.gray(" Using deterministic checks only.\n"));
3710
+ aiFallback = true;
3711
+ modelViolations = [];
3712
+ }
3713
+ }
3714
+ modelViolations = filterModelViolationsByStagedDiff(modelViolations, stagedFiles, diff);
3715
+ let violations = dedupeViolations([...deterministicViolations, ...astViolations, ...schemaViolations, ...modelViolations]);
3716
+ violations = applyAllowPatterns2(violations, allowPatterns);
3717
+ if (violations.length > 0) {
3718
+ const { filtered, suppressedCount } = suppressBatchRepetitions(violations);
3719
+ if (suppressedCount > 0) {
3720
+ console.log(
3721
+ chalk2.dim(
3722
+ ` \u2139 Auto-suppressed ${suppressedCount} repetitive violation${suppressedCount > 1 ? "s" : ""} (same rule fired \u22653\xD7 on the same file \u2014 consider tuning the rule)`
3723
+ )
3724
+ );
3725
+ violations = filtered;
3726
+ }
3727
+ }
3728
+ if (!aiFallback && violations.length > 0) {
3729
+ const learnedPatterns = await learnGlobalIgnoresFromFalsePositives({
3730
+ diff,
3731
+ currentViolations: violations,
3732
+ allowPatterns,
3733
+ debug: options.debug
3734
+ });
3735
+ if (learnedPatterns.length > 0) {
3736
+ if (options.verbose || options.debug) {
3737
+ console.log(chalk2.gray(` learned ${learnedPatterns.length} global ignore pattern${learnedPatterns.length > 1 ? "s" : ""} from false-positive recheck`));
2761
3738
  }
2762
- } catch {
2763
- violations = [];
3739
+ const refinedAllowPatterns = [.../* @__PURE__ */ new Set([...allowPatterns, ...learnedPatterns])];
3740
+ violations = applyAllowPatterns2(violations, refinedAllowPatterns);
2764
3741
  }
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: [] };
3742
+ }
3743
+ if (violations.length === 0) {
3744
+ resetViolationStats();
3745
+ if (options.dryRun) {
3746
+ console.log(chalk2.green(" \u2713 [dry-run] No rule violations found.\n"));
3747
+ } else {
3748
+ console.log(chalk2.green(" \u2713 No rule violations \u2014 commit allowed.\n"));
2777
3749
  }
3750
+ return;
3751
+ }
3752
+ if (options.dryRun) {
3753
+ console.log(
3754
+ chalk2.yellow.bold(
3755
+ `
3756
+ \u26A0 [dry-run] ${violations.length} rule violation${violations.length > 1 ? "s" : ""} would be flagged (commit not blocked)
3757
+ `
3758
+ )
3759
+ );
3760
+ } else {
2778
3761
  console.log(
2779
- chalk.red.bold(`
2780
- \u2717 ${violations.length} violation${violations.length > 1 ? "s" : ""} in ${rel}
2781
- `)
3762
+ chalk2.red.bold(
3763
+ `
3764
+ \u2717 ${violations.length} rule violation${violations.length > 1 ? "s" : ""} found \u2014 commit blocked
3765
+ `
3766
+ )
2782
3767
  );
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);
3768
+ }
3769
+ let ruleStatsSnapshot = {};
3770
+ {
3771
+ const statsPath = join8(process.cwd(), ".memory-core-stats.json");
3772
+ if (existsSync7(statsPath)) {
3773
+ try {
3774
+ const parsed = JSON.parse(readFileSync7(statsPath, "utf-8"));
3775
+ ruleStatsSnapshot = parsed.rules ?? {};
3776
+ } catch {
2791
3777
  }
2792
- if (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
2793
- if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
3778
+ }
3779
+ }
3780
+ const MAX_LOCATIONS = 5;
3781
+ const groups = groupViolationsByRule(violations);
3782
+ let groupIndex = 0;
3783
+ for (const [rule, group] of groups) {
3784
+ groupIndex++;
3785
+ const isCluster = group.length > 1;
3786
+ const first = group[0];
3787
+ if (isCluster) {
3788
+ console.log(chalk2.bold.red(`
3789
+ [${groupIndex}] ${rule}`) + chalk2.dim(` \xD7${group.length}`));
3790
+ } else {
3791
+ const loc = first.file ? first.line ? `${first.file}:${first.line}` : first.file : "unknown location";
3792
+ console.log(chalk2.bold(`
3793
+ [${groupIndex}] ${loc}`));
3794
+ console.log(chalk2.yellow(" Rule: ") + rule);
3795
+ }
3796
+ const why = first.reason ?? reasonMap2.get(rule);
3797
+ if (why) console.log(chalk2.dim(" Why: ") + chalk2.dim(why));
3798
+ if (first.suggestion) console.log(chalk2.green(" Fix: ") + first.suggestion);
3799
+ if (isCluster) {
2794
3800
  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: [] };
3801
+ const shown = group.slice(0, MAX_LOCATIONS);
3802
+ const overflow = group.length - MAX_LOCATIONS;
3803
+ for (const v of shown) {
3804
+ const loc = v.file ? v.line ? `${v.file}:${v.line}` : v.file : "unknown location";
3805
+ const issue = v.issue ? chalk2.dim(` ${v.issue}`) : "";
3806
+ console.log(chalk2.dim(` ${loc}`) + issue);
3807
+ }
3808
+ if (overflow > 0) {
3809
+ console.log(chalk2.dim(` ... and ${overflow} more`));
3810
+ }
3811
+ } else {
3812
+ if (first.issue) console.log(chalk2.red(" Issue: ") + first.issue);
2813
3813
  }
3814
+ const ruleEntry = toRuleStatEntry(ruleStatsSnapshot[rule]);
3815
+ if (ruleEntry.count > 5 && ruleEntry.falsePositives > 0) {
3816
+ const rate = Math.round(ruleEntry.falsePositives / ruleEntry.count * 100);
3817
+ if (rate > 40) {
3818
+ console.log(chalk2.yellow(`
3819
+ Noisy: ${rate}% historical false-positive rate`));
3820
+ console.log(chalk2.dim(` Silence: memory-core allow "${rule}"`));
3821
+ console.log(chalk2.dim(` Review all: memory-core tune`));
3822
+ } else if (rate > 25) {
3823
+ console.log(chalk2.dim(`
3824
+ Note: ${rate}% false-positive rate \u2014 run: memory-core tune`));
3825
+ }
3826
+ }
3827
+ console.log();
3828
+ }
3829
+ const noisyRules = Array.from(groups.keys()).filter((rule) => {
3830
+ const entry = toRuleStatEntry(ruleStatsSnapshot[rule]);
3831
+ return entry.count > 3 && entry.falsePositives / entry.count > 0.25;
3832
+ });
3833
+ if (noisyRules.length > 0) {
2814
3834
  console.log(
2815
- chalk.red.bold(`
2816
- \u2717 ${violations.length} deterministic violation${violations.length > 1 ? "s" : ""} in ${rel}
2817
- `)
3835
+ chalk2.yellow(` \u26A0 ${noisyRules.length} of these rule${noisyRules.length > 1 ? "s have" : " has"} a high false-positive rate.`)
2818
3836
  );
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>"'));
3837
+ console.log(chalk2.dim(" Run: memory-core tune \u2014 to review and silence noisy rules\n"));
3838
+ }
3839
+ if (options.dryRun) {
3840
+ console.log(chalk2.dim(" [dry-run] Commit not blocked. To enforce, run without --dry-run."));
3841
+ console.log(chalk2.dim(' To save as memory: memory-core remember "<lesson>"'));
2834
3842
  console.log();
2835
- return { type: "checked", violations };
3843
+ return;
2836
3844
  }
2837
- }
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");
3845
+ console.log(chalk2.dim(" Fix the violations above, then commit again."));
3846
+ console.log(chalk2.dim(" To bypass (not recommended): git commit --no-verify"));
3847
+ console.log(chalk2.dim(" Env bypass: MEMORY_CORE_SKIP_HOOK=1 git commit"));
3848
+ console.log(chalk2.dim(' To save as memory: memory-core remember "<lesson>"'));
3849
+ console.log();
3850
+ recordViolations(violations);
3851
+ await promptToSaveViolations(violations);
3852
+ process.exit(1);
3853
+ }
3854
+ function extractForbiddenPhrases(content) {
3855
+ const phrases = [];
3856
+ const normalized = content.replace(/\s+/g, " ");
3857
+ const patterns = [
3858
+ /\bnever\s+([^.;]+)/gi,
3859
+ /\bmust not\s+([^.;]+)/gi,
3860
+ /\bdo not\s+([^.;]+)/gi
3861
+ ];
3862
+ for (const pattern of patterns) {
3863
+ for (const match of normalized.matchAll(pattern)) {
3864
+ const phrase = match[1]?.trim();
3865
+ if (phrase && phrase.split(/\s+/).length >= 2) phrases.push(phrase.toLowerCase());
3866
+ }
2843
3867
  }
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
- };
3868
+ return phrases;
3869
+ }
3870
+ function getCiDiff() {
3871
+ const baseRef = process.env.GITHUB_BASE_REF;
3872
+ const commands = [
3873
+ baseRef ? `git diff --unified=0 --diff-filter=ACMRT origin/${baseRef}...HEAD` : "",
3874
+ "git diff --unified=0 --diff-filter=ACMRT HEAD~1 HEAD",
3875
+ "git diff --cached --unified=0 --diff-filter=ACMRT"
3876
+ ].filter(Boolean);
3877
+ for (const command of commands) {
3878
+ try {
3879
+ const diff = execSync(command, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
3880
+ if (diff.trim()) return diff;
3881
+ } catch {
3882
+ }
2852
3883
  }
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;
3884
+ return "";
2876
3885
  }
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}
3886
+ async function checkFile2(filePath, options = {}) {
3887
+ const { readFileSync: readFile, existsSync: fileExists } = await import("fs");
3888
+ const resolvedPath = filePath.startsWith("/") ? filePath : join8(process.cwd(), filePath);
3889
+ if (!fileExists(resolvedPath)) {
3890
+ console.error(chalk2.red(`
3891
+ File not found: ${filePath}
2885
3892
  `));
2886
- options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message });
2887
- if (exitOnSetupFailure) process.exit(1);
2888
- return;
3893
+ process.exit(1);
3894
+ }
3895
+ const content = readFile(resolvedPath, "utf-8");
3896
+ const lines = content.split("\n");
3897
+ const pseudoDiff = [
3898
+ `diff --git a/${filePath} b/${filePath}`,
3899
+ `+++ b/${filePath}`,
3900
+ `@@ -0,0 +1,${lines.length} @@`,
3901
+ ...lines.map((l) => `+${l}`)
3902
+ ].join("\n");
3903
+ const configPath = join8(process.cwd(), ".memory-core.json");
3904
+ if (!existsSync7(configPath)) {
3905
+ console.error(chalk2.red("\n No .memory-core.json found. Run: memory-core init\n"));
3906
+ process.exit(1);
3907
+ }
3908
+ const config = JSON.parse(readFileSync7(configPath, "utf-8"));
3909
+ const { rules: fallbackRules, avoids } = getProfileRules2(config);
3910
+ const allowPatterns = [...new Set(getAllowPatterns(config))];
3911
+ const fast = isFastCheck(options);
3912
+ let rules;
3913
+ if (fast) {
3914
+ rules = fallbackRules;
3915
+ } else {
3916
+ const cached = readRuleCache(process.cwd());
3917
+ if (cached) {
3918
+ rules = cached.rules;
3919
+ } else {
3920
+ const ruleLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_RULE_LOAD_TIMEOUT_MS", 2e3);
3921
+ rules = await withTimeout(
3922
+ loadRelevantRules2(config, pseudoDiff, [filePath], fallbackRules),
3923
+ ruleLoadTimeoutMs,
3924
+ fallbackRules
3925
+ );
3926
+ }
2889
3927
  }
2890
- const { rules } = getProfileRules(config2);
2891
3928
  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);
3929
+ console.log(chalk2.dim("\n No rules loaded \u2014 nothing to check.\n"));
2898
3930
  return;
2899
3931
  }
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
3932
+ console.log(chalk2.cyan(`
3933
+ archmind \u2014 checking ${filePath} against rules\u2026`));
3934
+ const deterministicViolations = findDeterministicViolations(pseudoDiff, rules, avoids, allowPatterns);
3935
+ const astViolations = findAstDeterministicViolationsForDiff(pseudoDiff, {
3936
+ cwd: process.cwd(),
3937
+ config,
3938
+ rules,
3939
+ reasonLookup: reasonMap2
2913
3940
  });
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"));
3941
+ let violations = dedupeViolations([...deterministicViolations, ...astViolations]);
3942
+ violations = applyAllowPatterns2(violations, allowPatterns);
3943
+ if (violations.length === 0) {
3944
+ console.log(chalk2.green(" \u2713 No rule violations found in this file.\n"));
3945
+ return;
2925
3946
  }
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;
3947
+ const label = options.dryRun ? "[dry-run] " : "";
3948
+ console.log(chalk2.yellow.bold(`
3949
+ ${label}${violations.length} violation${violations.length > 1 ? "s" : ""} found in ${filePath}
3950
+ `));
3951
+ const groups = groupViolationsByRule(violations);
3952
+ let idx = 0;
3953
+ for (const [rule, group] of groups) {
3954
+ idx++;
3955
+ const first = group[0];
3956
+ const loc = first.line ? `${filePath}:${first.line}` : filePath;
3957
+ console.log(chalk2.bold(`
3958
+ [${idx}] ${loc}`));
3959
+ console.log(chalk2.yellow(" Rule: ") + rule);
3960
+ if (first.reason) console.log(chalk2.dim(" Why: ") + chalk2.dim(first.reason));
3961
+ if (first.issue) console.log(chalk2.red(" Issue: ") + first.issue);
3962
+ if (first.suggestion) console.log(chalk2.green(" Fix: ") + first.suggestion);
3963
+ console.log();
3964
+ }
3965
+ if (!options.dryRun) process.exit(1);
3966
+ }
3967
+ async function checkCi(options = {}) {
3968
+ let memories;
3969
+ try {
3970
+ memories = readMemoryFile();
3971
+ } catch (err) {
3972
+ console.error(chalk2.red(`
3973
+ CI check failed: ${err.message}
3974
+ `));
3975
+ process.exit(1);
3976
+ }
3977
+ const rules = memories.filter((memory) => memory.type !== "ignore");
3978
+ const ignores = memories.filter((memory) => memory.type === "ignore").map((memory) => memory.content.toLowerCase());
3979
+ const phrases = rules.flatMap(
3980
+ (memory) => extractForbiddenPhrases(memory.content).map((phrase) => ({ rule: memory.content, phrase }))
3981
+ );
3982
+ const diff = getCiDiff();
3983
+ const addedLines = diff.split("\n").filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1));
3984
+ if (options.debug) {
3985
+ console.log(chalk2.gray(`
3986
+ [debug] memories: ${memories.length}`));
3987
+ console.log(chalk2.gray(` [debug] text rules: ${phrases.length}`));
3988
+ console.log(chalk2.gray(` [debug] diff length: ${diff.length} chars
3989
+ `));
3990
+ }
3991
+ const violations = [];
3992
+ for (const line of addedLines) {
3993
+ const normalizedLine = line.toLowerCase();
3994
+ if (ignores.some((ignore) => normalizedLine.includes(ignore))) continue;
3995
+ for (const { rule, phrase } of phrases) {
3996
+ if (normalizedLine.includes(phrase)) {
3997
+ violations.push({
3998
+ rule,
3999
+ file: "diff",
4000
+ issue: `Added line contains forbidden phrase: "${phrase}"`
4001
+ });
2979
4002
  }
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);
4003
+ }
4004
+ }
4005
+ if (violations.length === 0) {
4006
+ console.log(chalk2.green(`
4007
+ \u2713 CI memory check passed (${rules.length} rules loaded from memories.json)
4008
+ `));
4009
+ return;
4010
+ }
4011
+ console.log(chalk2.red.bold(`
4012
+ \u2717 ${violations.length} CI violation${violations.length > 1 ? "s" : ""} found
4013
+ `));
4014
+ violations.forEach((violation, index) => {
4015
+ console.log(chalk2.bold(` [${index + 1}] ${violation.file}`));
4016
+ console.log(chalk2.yellow(" Rule: ") + violation.rule);
4017
+ console.log(chalk2.red(" Issue: ") + violation.issue);
4018
+ console.log();
3003
4019
  });
4020
+ recordViolations(violations, "ci");
4021
+ process.exit(1);
4022
+ }
4023
+ function printModelMissing(model) {
4024
+ console.log(chalk2.yellow(`
4025
+ \u26A0 Chat model "${model}" not found in Ollama.`));
4026
+ console.log(chalk2.gray(` Pull a model: ollama pull ${model}`));
4027
+ console.log(chalk2.gray(" Or set OLLAMA_CHAT_MODEL=<model> in .memory-core.env"));
4028
+ console.log(chalk2.gray(" Recommended: llama3.2 | qwen2.5-coder:3b | mistral\n"));
3004
4029
  }
3005
4030
 
3006
4031
  export {
3007
4032
  detectProject,
3008
- Config,
3009
4033
  embed,
3010
- callChatModel,
3011
- getChatProviderLabel,
3012
- getPool,
3013
- runMigrations,
3014
- saveMemory,
3015
- listMemories,
3016
- deleteMemory,
3017
- updateMemory,
3018
- closePool,
3019
4034
  migrateGraphSnapshots,
3020
4035
  probeGraphSnapshotStore,
3021
4036
  seeds,
3022
- findAstDeterministicViolationsForDiff,
4037
+ MEMORY_FILE,
4038
+ toPortableMemory,
4039
+ writeMemoryFile,
4040
+ readMemoryFile,
4041
+ readMemoryFileFromUrl,
4042
+ parseMemoryFile,
4043
+ findSchemaViolations,
4044
+ recordBypass,
4045
+ readBypassStats,
4046
+ installHook,
4047
+ uninstallHook,
4048
+ checkCommitMsg,
4049
+ checkStaged,
4050
+ checkFile2 as checkFile,
4051
+ checkCi,
3023
4052
  startWatch,
3024
4053
  getDefaultApplicationContainer,
3025
4054
  inferProjectArchitectures,
3026
4055
  getAllowPatterns,
3027
- buildContextQuery,
3028
- retrieveContextualMemories,
3029
4056
  retrieveMemorySelection,
3030
4057
  OUTPUT_FILES,
3031
4058
  AGENT_NAMES,