@shahmilsaari/memory-core 1.0.0 → 1.0.2

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,10 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- embed
4
- } from "./chunk-HAGRPKR3.js";
5
- import {
6
- searchMemories
7
- } from "./chunk-WUL7HLAA.js";
8
2
 
9
3
  // src/project-detector.ts
10
4
  import { existsSync, readFileSync } from "fs";
@@ -68,350 +62,525 @@ function detectProject(cwd = process.cwd()) {
68
62
  return { language: "Unknown", framework: "Unknown" };
69
63
  }
70
64
 
71
- // src/retriever.ts
72
- async function retrieve(query, architecture, limit = 10) {
73
- const embedding = await embed(query);
74
- return searchMemories(embedding, architecture, limit);
75
- }
76
-
77
- // src/memory-selection.ts
78
- var FRAMEWORK_ARCHITECTURE_MAP = {
79
- Laravel: ["laravel-service-repository"],
80
- "Next.js": ["nextjs"],
81
- "Nuxt.js": ["nuxt"],
82
- Go: ["go-api"],
83
- NestJS: ["nestjs"],
84
- React: ["react"],
85
- "Vue.js": ["vue"],
86
- Svelte: ["svelte"]
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
+ }
87
90
  };
88
- var KNOWN_ARCHITECTURE_KEYS = /* @__PURE__ */ new Set([
89
- ...Object.values(FRAMEWORK_ARCHITECTURE_MAP).flat(),
90
- "angular",
91
- "clean-architecture",
92
- "express",
93
- "fastify",
94
- "hexagonal",
95
- "modular-monolith",
96
- "mvc",
97
- "react-native"
98
- ]);
99
- function normalizeText(value) {
100
- return value.toLowerCase().replace(/[`"'()[\]{}.,:;!?/\\<>|=*+-]/g, " ").replace(/\s+/g, " ").trim();
101
- }
102
- function tokenSet(value) {
103
- return new Set(
104
- normalizeText(value).split(" ").filter((token) => token.length > 2)
105
- );
106
- }
107
- function similarityScore(a, b) {
108
- const left = tokenSet(a);
109
- const right = tokenSet(b);
110
- if (left.size === 0 || right.size === 0) return 0;
111
- let intersection = 0;
112
- for (const token of left) {
113
- if (right.has(token)) intersection++;
91
+
92
+ // src/embedding.ts
93
+ async function embed(text) {
94
+ let response;
95
+ try {
96
+ response = await fetch(`${Config.ollamaUrl}/api/embeddings`, {
97
+ method: "POST",
98
+ headers: { "Content-Type": "application/json" },
99
+ body: JSON.stringify({ model: Config.ollamaModel, prompt: text })
100
+ });
101
+ } catch {
102
+ throw new Error(
103
+ `Cannot reach Ollama at ${Config.ollamaUrl}. Run: ollama serve`
104
+ );
114
105
  }
115
- return 2 * intersection / (left.size + right.size);
106
+ if (!response.ok) {
107
+ const body = await response.text();
108
+ throw new Error(`Ollama embedding failed (${response.status}): ${body}`);
109
+ }
110
+ const data = await response.json();
111
+ return data.embedding;
116
112
  }
117
- function mergeMemory(primary, secondary) {
118
- const mergedTags = [.../* @__PURE__ */ new Set([...primary.tags ?? [], ...secondary.tags ?? []])];
119
- const reason = [primary.reason, secondary.reason].filter(Boolean).join(" | ") || void 0;
113
+
114
+ // src/chat.ts
115
+ function getChatConfig() {
116
+ const provider = process.env.CHAT_PROVIDER ?? "ollama";
117
+ const model = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
120
118
  return {
121
- ...primary,
122
- tags: mergedTags,
123
- reason
119
+ provider,
120
+ model,
121
+ ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
122
+ apiKey: process.env.CHAT_API_KEY ?? ""
124
123
  };
125
124
  }
126
- function memoryArchitectureKeys(memory) {
127
- if (memory.architecture && memory.architecture !== "global") {
128
- return [memory.architecture];
125
+ async function callOllama(cfg, messages) {
126
+ const res = await fetch(`${cfg.ollamaUrl}/api/chat`, {
127
+ method: "POST",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({ model: cfg.model, messages, stream: false, format: "json" })
130
+ });
131
+ if (!res.ok) {
132
+ const body = await res.text();
133
+ if (body.includes("not found") || body.includes("model")) {
134
+ throw new Error(`MODEL_NOT_FOUND:${cfg.model}`);
135
+ }
136
+ throw new Error(body);
129
137
  }
130
- return (memory.tags ?? []).filter((tag) => KNOWN_ARCHITECTURE_KEYS.has(tag));
138
+ const data = await res.json();
139
+ return data.message.content.trim();
131
140
  }
132
- function getStackReason(memory, activeArchitectures) {
133
- const architectureKeys = memoryArchitectureKeys(memory);
134
- if (architectureKeys.length === 0) {
135
- return {
136
- included: true,
137
- reason: "global memory: no architecture-specific tag"
138
- };
139
- }
140
- const matched = architectureKeys.filter((architecture) => activeArchitectures.has(architecture));
141
- if (matched.length > 0) {
142
- return {
143
- included: true,
144
- reason: `matched active architecture: ${matched.join(", ")}`
145
- };
141
+ async function callOpenAI(cfg, messages) {
142
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
143
+ method: "POST",
144
+ headers: {
145
+ "Content-Type": "application/json",
146
+ "Authorization": `Bearer ${cfg.apiKey}`
147
+ },
148
+ body: JSON.stringify({
149
+ model: cfg.model,
150
+ messages,
151
+ response_format: { type: "json_object" }
152
+ })
153
+ });
154
+ if (!res.ok) throw new Error(`OpenAI API error ${res.status}: ${await res.text()}`);
155
+ const data = await res.json();
156
+ return data.choices[0].message.content.trim();
157
+ }
158
+ async function callAnthropic(cfg, messages) {
159
+ const system = messages.find((m) => m.role === "system")?.content ?? "";
160
+ const userMessages = messages.filter((m) => m.role !== "system");
161
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
162
+ method: "POST",
163
+ headers: {
164
+ "Content-Type": "application/json",
165
+ "x-api-key": cfg.apiKey,
166
+ "anthropic-version": "2023-06-01"
167
+ },
168
+ body: JSON.stringify({
169
+ model: cfg.model,
170
+ max_tokens: 4096,
171
+ system,
172
+ messages: userMessages
173
+ })
174
+ });
175
+ if (!res.ok) throw new Error(`Anthropic API error ${res.status}: ${await res.text()}`);
176
+ const data = await res.json();
177
+ return data.content[0].text.trim();
178
+ }
179
+ async function callMiniMax(cfg, messages) {
180
+ const res = await fetch("https://api.minimax.io/v1/chat/completions", {
181
+ method: "POST",
182
+ headers: {
183
+ "Content-Type": "application/json",
184
+ "Authorization": `Bearer ${cfg.apiKey}`
185
+ },
186
+ body: JSON.stringify({
187
+ model: cfg.model,
188
+ messages,
189
+ response_format: { type: "json_object" }
190
+ })
191
+ });
192
+ if (!res.ok) throw new Error(`MiniMax API error ${res.status}: ${await res.text()}`);
193
+ const data = await res.json();
194
+ return data.choices[0].message.content.trim();
195
+ }
196
+ async function callChatModel(messages) {
197
+ const cfg = getChatConfig();
198
+ switch (cfg.provider) {
199
+ case "openai":
200
+ return callOpenAI(cfg, messages);
201
+ case "anthropic":
202
+ return callAnthropic(cfg, messages);
203
+ case "minimax":
204
+ return callMiniMax(cfg, messages);
205
+ default:
206
+ return callOllama(cfg, messages);
146
207
  }
147
- const active = [...activeArchitectures].join(", ") || "none detected";
148
- return {
149
- included: false,
150
- reason: `excluded: tagged for ${architectureKeys.join(", ")}; active stack is ${active}`
151
- };
152
208
  }
153
- function dedupeMemories(memories, threshold = 0.8) {
154
- const deduped = [];
155
- for (const memory of memories) {
156
- const existingIndex = deduped.findIndex((candidate) => {
157
- if (candidate.content_hash && memory.content_hash && candidate.content_hash === memory.content_hash) {
158
- return true;
159
- }
160
- return similarityScore(candidate.content, memory.content) >= threshold;
161
- });
162
- if (existingIndex === -1) {
163
- deduped.push(memory);
164
- continue;
209
+ function getChatProviderLabel() {
210
+ const cfg = getChatConfig();
211
+ if (cfg.provider === "ollama") return `ollama (${cfg.model})`;
212
+ return `${cfg.provider} (${cfg.model})`;
213
+ }
214
+
215
+ // src/db.ts
216
+ import pg from "pg";
217
+ import { createHash } from "crypto";
218
+ var { Pool } = pg;
219
+ var pool = null;
220
+ var migrationsRun = false;
221
+ function hashMemoryContent(content) {
222
+ return createHash("md5").update(content.trim()).digest("hex");
223
+ }
224
+ function getPool() {
225
+ if (!pool) {
226
+ if (!Config.databaseUrl) {
227
+ throw new Error("DATABASE_URL is not set. Add it to your .env or .memory-core.env file.");
165
228
  }
166
- deduped[existingIndex] = mergeMemory(deduped[existingIndex], memory);
229
+ pool = new Pool({ connectionString: Config.databaseUrl });
167
230
  }
168
- return deduped;
231
+ return pool;
169
232
  }
170
- function inferProjectArchitectures(cwd = process.cwd(), config) {
171
- const inferred = /* @__PURE__ */ new Set();
172
- if (config?.backendArchitecture) inferred.add(config.backendArchitecture);
173
- if (config?.frontendFramework) inferred.add(config.frontendFramework);
174
- if (config?.projectType === "backend" && !config.backendArchitecture) {
175
- inferred.add("clean-architecture");
176
- }
177
- const detected = detectProject(cwd);
178
- for (const architecture of FRAMEWORK_ARCHITECTURE_MAP[detected.framework] ?? []) {
179
- inferred.add(architecture);
233
+ async function runMigrations() {
234
+ if (migrationsRun) return;
235
+ const client = await getPool().connect();
236
+ try {
237
+ await client.query("BEGIN");
238
+ await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS reason TEXT`);
239
+ await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS content_hash TEXT`);
240
+ await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS context JSONB NOT NULL DEFAULT '{}'::jsonb`);
241
+ await client.query(
242
+ `UPDATE memories
243
+ SET content_hash = md5(trim(content))
244
+ WHERE content_hash IS NULL`
245
+ );
246
+ await client.query(`CREATE INDEX IF NOT EXISTS memories_content_hash_idx ON memories (content_hash)`);
247
+ await client.query("COMMIT");
248
+ migrationsRun = true;
249
+ } catch (err) {
250
+ await client.query("ROLLBACK");
251
+ throw err;
252
+ } finally {
253
+ client.release();
180
254
  }
181
- return [...inferred];
182
255
  }
183
- function getAllowPatterns(config) {
184
- return [...new Set(config?.allowPatterns?.filter(Boolean) ?? [])];
256
+ async function saveMemory(memory) {
257
+ await runMigrations();
258
+ const { type, scope, architecture, projectName, title, content, reason, context, tags, embedding } = memory;
259
+ const contentHash = hashMemoryContent(content);
260
+ await getPool().query(
261
+ `INSERT INTO memories (type, scope, architecture, project_name, title, content, reason, context, tags, embedding, content_hash)
262
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11)`,
263
+ [
264
+ type,
265
+ scope,
266
+ architecture ?? null,
267
+ projectName ?? null,
268
+ title ?? null,
269
+ content,
270
+ reason ?? null,
271
+ JSON.stringify(context ?? {}),
272
+ tags ?? [],
273
+ `[${embedding.join(",")}]`,
274
+ contentHash
275
+ ]
276
+ );
185
277
  }
186
- function filterRelevantMemories(memories, config, cwd = process.cwd()) {
187
- return explainMemorySelection(memories, config, cwd).included;
278
+ async function upsertMemory(memory) {
279
+ await runMigrations();
280
+ const contentHash = hashMemoryContent(memory.content);
281
+ const existing = await getPool().query(
282
+ `SELECT id FROM memories
283
+ WHERE content_hash = $1
284
+ AND COALESCE(architecture, '') = COALESCE($2, '')
285
+ AND scope = $3
286
+ AND type = $4
287
+ LIMIT 1`,
288
+ [contentHash, memory.architecture ?? null, memory.scope, memory.type]
289
+ );
290
+ if (existing.rowCount) return "skipped";
291
+ await saveMemory(memory);
292
+ return "inserted";
188
293
  }
189
- function explainMemorySelection(memories, config, cwd = process.cwd(), threshold = 0.8) {
190
- const activeArchitectures = inferProjectArchitectures(cwd, config);
191
- const activeSet = new Set(activeArchitectures);
192
- const included = [];
193
- const decisions = [];
194
- for (const memory of memories) {
195
- const stackDecision = getStackReason(memory, activeSet);
196
- if (!stackDecision.included) {
197
- decisions.push({
198
- memory,
199
- status: "excluded",
200
- reason: stackDecision.reason
201
- });
202
- continue;
294
+ async function listMemories(filters = {}) {
295
+ await runMigrations();
296
+ const where = [];
297
+ const params = [];
298
+ if (filters.type) {
299
+ params.push(filters.type);
300
+ where.push(`type = $${params.length}`);
301
+ }
302
+ if (filters.scope) {
303
+ params.push(filters.scope);
304
+ where.push(`scope = $${params.length}`);
305
+ }
306
+ if (filters.architecture) {
307
+ if (Array.isArray(filters.architecture)) {
308
+ params.push(filters.architecture);
309
+ where.push(filters.includeGlobal ? `(architecture IS NULL OR architecture = ANY($${params.length}))` : `architecture = ANY($${params.length})`);
310
+ } else {
311
+ params.push(filters.architecture);
312
+ where.push(filters.includeGlobal ? `(architecture IS NULL OR architecture = $${params.length})` : `architecture = $${params.length}`);
203
313
  }
204
- const existingIndex = included.findIndex((candidate) => {
205
- if (candidate.content_hash && memory.content_hash && candidate.content_hash === memory.content_hash) {
206
- return true;
207
- }
208
- return similarityScore(candidate.content, memory.content) >= threshold;
209
- });
210
- if (existingIndex === -1) {
211
- included.push(memory);
212
- decisions.push({
213
- memory,
214
- status: "included",
215
- reason: stackDecision.reason
216
- });
217
- continue;
314
+ }
315
+ if (filters.projectName) {
316
+ params.push(filters.projectName);
317
+ where.push(filters.includeGlobal ? `(project_name IS NULL OR project_name = $${params.length})` : `project_name = $${params.length}`);
318
+ }
319
+ if (filters.tags?.length) {
320
+ params.push(filters.tags);
321
+ where.push(`tags && $${params.length}::text[]`);
322
+ }
323
+ const limit = filters.limit ?? 200;
324
+ params.push(limit);
325
+ const result = await getPool().query(
326
+ `SELECT id, type, scope, architecture, project_name, title, content, reason, context, tags, content_hash
327
+ FROM memories
328
+ ${where.length ? `WHERE ${where.join(" AND ")}` : ""}
329
+ ORDER BY id ASC
330
+ LIMIT $${params.length}`,
331
+ params
332
+ );
333
+ return result.rows;
334
+ }
335
+ async function getMemory(id) {
336
+ await runMigrations();
337
+ const result = await getPool().query(
338
+ `SELECT id, type, scope, architecture, project_name, title, content, reason, context, tags, content_hash
339
+ FROM memories
340
+ WHERE id = $1`,
341
+ [id]
342
+ );
343
+ return result.rows[0] ?? null;
344
+ }
345
+ async function deleteMemory(id) {
346
+ await runMigrations();
347
+ const result = await getPool().query(`DELETE FROM memories WHERE id = $1`, [id]);
348
+ return (result.rowCount ?? 0) > 0;
349
+ }
350
+ async function deleteMemories(filters) {
351
+ await runMigrations();
352
+ const where = [];
353
+ const params = [];
354
+ if (filters.type) {
355
+ params.push(filters.type);
356
+ where.push(`type = $${params.length}`);
357
+ }
358
+ if (filters.scope) {
359
+ params.push(filters.scope);
360
+ where.push(`scope = $${params.length}`);
361
+ }
362
+ if (filters.architecture) {
363
+ if (Array.isArray(filters.architecture)) {
364
+ params.push(filters.architecture);
365
+ where.push(`architecture = ANY($${params.length})`);
366
+ } else {
367
+ params.push(filters.architecture);
368
+ where.push(`architecture = $${params.length}`);
218
369
  }
219
- included[existingIndex] = mergeMemory(included[existingIndex], memory);
220
- decisions.push({
221
- memory,
222
- status: "excluded",
223
- reason: `duplicate or near-duplicate of memory #${included[existingIndex].id}`
224
- });
225
370
  }
226
- return {
227
- included,
228
- excluded: decisions.filter((decision) => decision.status === "excluded"),
229
- decisions,
230
- activeArchitectures
231
- };
371
+ if (filters.tag) {
372
+ params.push(filters.tag);
373
+ where.push(`$${params.length} = ANY(tags)`);
374
+ }
375
+ if (where.length === 0) {
376
+ throw new Error("Refusing to bulk-delete without filters");
377
+ }
378
+ const result = await getPool().query(
379
+ `DELETE FROM memories WHERE ${where.join(" AND ")}`,
380
+ params
381
+ );
382
+ return result.rowCount ?? 0;
232
383
  }
233
- function buildContextQuery(parts, maxLength = 1200) {
234
- return parts.filter(Boolean).join("\n").slice(0, maxLength);
384
+ async function updateMemory(id, patch) {
385
+ await runMigrations();
386
+ const current = await getMemory(id);
387
+ if (!current) return null;
388
+ const content = patch.content ?? current.content;
389
+ const contentHash = hashMemoryContent(content);
390
+ const embedding = patch.embedding ? `[${patch.embedding.join(",")}]` : null;
391
+ const result = await getPool().query(
392
+ `UPDATE memories
393
+ SET type = $2,
394
+ scope = $3,
395
+ title = $4,
396
+ content = $5,
397
+ reason = $6,
398
+ context = $7::jsonb,
399
+ tags = $8,
400
+ content_hash = $9,
401
+ embedding = COALESCE($10::vector, embedding)
402
+ WHERE id = $1
403
+ RETURNING id, type, scope, architecture, project_name, title, content, reason, context, tags, content_hash`,
404
+ [
405
+ id,
406
+ patch.type ?? current.type,
407
+ patch.scope ?? current.scope,
408
+ patch.title ?? current.title ?? null,
409
+ content,
410
+ patch.reason ?? current.reason ?? null,
411
+ JSON.stringify(patch.context ?? current.context ?? {}),
412
+ patch.tags ?? current.tags ?? [],
413
+ contentHash,
414
+ embedding
415
+ ]
416
+ );
417
+ return result.rows[0] ?? null;
235
418
  }
236
- async function retrieveContextualMemories(options) {
237
- return (await retrieveMemorySelection(options)).included;
419
+ async function searchMemories(embedding, architectures, limit = 10) {
420
+ await runMigrations();
421
+ const vector = `[${embedding.join(",")}]`;
422
+ const params = [vector];
423
+ let whereClause = "";
424
+ const selectedArchitectures = architectures ? (Array.isArray(architectures) ? architectures : [architectures]).filter(Boolean) : [];
425
+ if (selectedArchitectures.length > 0) {
426
+ whereClause = `WHERE (
427
+ architecture = ANY($2)
428
+ OR architecture IS NULL
429
+ OR architecture = 'global'
430
+ )`;
431
+ params.push(selectedArchitectures);
432
+ }
433
+ const client = await getPool().connect();
434
+ try {
435
+ await client.query("BEGIN");
436
+ await client.query("SET LOCAL ivfflat.probes = 10");
437
+ const result = await client.query(
438
+ `SELECT id, type, scope, architecture, project_name, title, content, reason, context, tags,
439
+ 1 - (embedding <=> $1) AS similarity
440
+ FROM memories
441
+ ${whereClause}
442
+ ORDER BY embedding <=> $1
443
+ LIMIT $${params.length + 1}`,
444
+ [...params, limit]
445
+ );
446
+ await client.query("COMMIT");
447
+ return result.rows;
448
+ } finally {
449
+ client.release();
450
+ }
238
451
  }
239
- async function retrieveMemorySelection(options) {
240
- const architectures = inferProjectArchitectures(options.cwd, options.config);
241
- const memories = await retrieve(options.query, architectures, options.limit ?? 15);
242
- return explainMemorySelection(memories, options.config, options.cwd);
452
+ async function closePool() {
453
+ if (pool) {
454
+ await pool.end();
455
+ pool = null;
456
+ migrationsRun = false;
457
+ }
243
458
  }
244
459
 
245
- // src/generator.ts
246
- import { readFileSync as readFileSync2, readdirSync, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
247
- import { join as join2, dirname } from "path";
248
- import { fileURLToPath } from "url";
249
- import Handlebars from "handlebars";
250
- import yaml from "js-yaml";
251
- var __filename = fileURLToPath(import.meta.url);
252
- var __dirname = dirname(__filename);
253
- var PKG_ROOT = join2(__dirname, "..");
254
- var OUTPUT_FILES = [
255
- { template: "CLAUDE.md.hbs", path: "CLAUDE.md", agent: "Claude Code" },
256
- { template: "copilot-instructions.md.hbs", path: ".github/copilot-instructions.md", agent: "GitHub Copilot" },
257
- { template: "cursorrules.hbs", path: ".cursorrules", agent: "Cursor" },
258
- { template: "cursor-rule.mdc.hbs", path: ".cursor/rules/memory-core.mdc", agent: "Cursor" },
259
- { template: "windsurfrules.hbs", path: ".windsurfrules", agent: "Windsurf" },
260
- { template: "clinerules.hbs", path: ".clinerules", agent: "Cline" },
261
- { template: "roo-rule.md.hbs", path: ".roo/rules/memory-core.md", agent: "Roo Code" },
262
- { template: "aider.conf.yml.hbs", path: ".aider.conf.yml", agent: "Aider" },
263
- { template: "continue-config.json.hbs", path: ".continue/config.json", agent: "Continue.dev", skipIfExists: true },
264
- { template: "DEVIN.md.hbs", path: "DEVIN.md", agent: "Devin" },
265
- { template: "amazonq-guidelines.md.hbs", path: ".amazonq/dev/guidelines.md", agent: "Amazon Q" },
266
- { template: "gemini-styleguide.md.hbs", path: ".gemini/styleguide.md", agent: "Gemini Code Assist" },
267
- { template: "zed-settings.json.hbs", path: ".zed/settings.json", agent: "Zed AI", skipIfExists: true },
268
- { template: "jetbrains-ai.md.hbs", path: ".idea/ai-instructions.md", agent: "JetBrains AI" },
269
- { template: "AGENTS.md.hbs", path: "AGENTS.md", agent: "OpenHands" },
270
- { template: "AI_RULES.md.hbs", path: "AI_RULES.md", agent: "Shared" },
271
- { template: "ARCHITECTURE.md.hbs", path: "ARCHITECTURE.md", agent: "Shared" },
272
- { template: "PROJECT_MEMORY.md.hbs", path: "PROJECT_MEMORY.md", agent: "Shared" }
273
- ];
274
- var AGENT_NAMES = [...new Set(OUTPUT_FILES.map((f) => f.agent))];
275
- Handlebars.registerHelper(
276
- "join",
277
- (arr, sep) => Array.isArray(arr) ? arr.join(sep) : ""
278
- );
279
- Handlebars.registerHelper(
280
- "bullet",
281
- (arr) => Array.isArray(arr) ? arr.map((i) => `- ${i}`).join("\n") : ""
282
- );
283
- Handlebars.registerHelper(
284
- "numbered",
285
- (arr) => Array.isArray(arr) ? arr.map((i, idx) => `${idx + 1}. ${i}`).join("\n") : ""
286
- );
287
- Handlebars.registerHelper("json", (val) => JSON.stringify(val, null, 2));
288
- Handlebars.registerHelper("memoryBlock", (memory) => {
289
- const meta = [memory.type, memory.architecture].filter(Boolean).join(" \xB7 ");
290
- const label = memory.title ? `${memory.title}: ${memory.content}` : memory.content;
291
- const lines = [`- [${meta || "memory"}] ${label}`];
292
- if (memory.reason) lines.push(` Why: ${memory.reason}`);
293
- if (memory.context?.appliesTo?.length) lines.push(` Use when: ${memory.context.appliesTo.join("; ")}`);
294
- if (memory.context?.avoidWhen?.length) lines.push(` Avoid when: ${memory.context.avoidWhen.join("; ")}`);
295
- if (memory.context?.examples?.length) {
296
- lines.push(" Examples:");
297
- for (const example of memory.context.examples) lines.push(` - ${example}`);
298
- }
299
- if (memory.tags?.length) lines.push(` Tags: ${memory.tags.join(", ")}`);
300
- if (memory.project_name || memory.context?.source) {
301
- lines.push(` Source: ${memory.context?.source ?? memory.project_name}`);
302
- }
303
- return new Handlebars.SafeString(lines.join("\n"));
304
- });
305
- function loadProfile(name) {
306
- const profilePath = join2(PKG_ROOT, "profiles", `${name}.yml`);
307
- if (!existsSync2(profilePath)) throw new Error(`Profile not found: ${name}`);
308
- return yaml.load(readFileSync2(profilePath, "utf-8"));
460
+ // src/infrastructure/persistence/postgres/postgres-graph-repository.ts
461
+ var graphMigrationsRun = false;
462
+ function asPosix(value) {
463
+ return value.replace(/\\/g, "/");
309
464
  }
310
- function listProfiles(layer) {
311
- const files = readdirSync(join2(PKG_ROOT, "profiles")).filter((f) => f.endsWith(".yml"));
312
- const all = files.map((f) => yaml.load(readFileSync2(join2(PKG_ROOT, "profiles", f), "utf-8")));
313
- if (!layer) return all;
314
- if (layer === "backend") return all.filter((p) => p.layer === "backend" || p.layer === "fullstack");
315
- if (layer === "frontend") return all.filter((p) => p.layer === "frontend" || p.layer === "fullstack");
316
- return all;
465
+ function isEdge(value) {
466
+ if (!value || typeof value !== "object") return false;
467
+ const edge = value;
468
+ return typeof edge.from === "string" && typeof edge.to === "string" && (edge.kind === "import" || edge.kind === "dynamic-import" || edge.kind === "require");
317
469
  }
318
- function buildTemplateData(options, cwd = process.cwd()) {
319
- const backend = options.backendArchitecture ? loadProfile(options.backendArchitecture) : null;
320
- const frontend = options.frontendFramework ? loadProfile(options.frontendFramework) : null;
321
- const dedupedMemories = filterRelevantMemories(options.memories, {
322
- projectType: options.projectType,
323
- backendArchitecture: options.backendArchitecture,
324
- frontendFramework: options.frontendFramework,
325
- language: options.language
326
- }, cwd);
327
- const allRules = [
328
- ...backend?.rules ?? [],
329
- ...frontend?.rules ?? []
330
- ];
331
- const allFolders = [
332
- ...backend?.folders ?? [],
333
- ...frontend?.folders ?? []
334
- ];
335
- const allAvoid = [
336
- ...backend?.avoid ?? [],
337
- ...frontend?.avoid ?? []
338
- ];
339
- const archLabel = [
340
- backend ? `Backend: ${backend.displayName}` : null,
341
- frontend ? `Frontend: ${frontend.displayName}` : null
342
- ].filter(Boolean).join(" \xB7 ");
470
+ function parseStringArray(value) {
471
+ if (!Array.isArray(value)) return [];
472
+ return value.filter((entry) => typeof entry === "string");
473
+ }
474
+ function parseEdgeArray(value) {
475
+ if (!Array.isArray(value)) return [];
476
+ return value.filter(isEdge);
477
+ }
478
+ function toSnapshot(row) {
343
479
  return {
344
- projectName: options.projectName,
345
- projectType: options.projectType,
346
- isBackend: options.projectType === "backend" || options.projectType === "fullstack",
347
- isFrontend: options.projectType === "frontend" || options.projectType === "fullstack",
348
- isFullstack: options.projectType === "fullstack",
349
- // backend
350
- hasBackend: !!backend,
351
- backendArchitecture: backend?.displayName,
352
- backendDescription: backend?.description,
353
- backendRules: backend?.rules ?? [],
354
- backendFolders: backend?.folders ?? [],
355
- backendAvoid: backend?.avoid ?? [],
356
- // frontend
357
- hasFrontend: !!frontend,
358
- frontendFramework: frontend?.displayName,
359
- frontendDescription: frontend?.description,
360
- frontendRules: frontend?.rules ?? [],
361
- frontendFolders: frontend?.folders ?? [],
362
- frontendAvoid: frontend?.avoid ?? [],
363
- // combined — used by simple templates
364
- architecture: archLabel,
365
- rules: allRules,
366
- folders: allFolders,
367
- avoid: allAvoid,
368
- description: [backend?.description, frontend?.description].filter(Boolean).join(" | "),
369
- // memories
370
- memories: dedupedMemories,
371
- hasMemories: dedupedMemories.length > 0,
372
- // misc
373
- language: options.language,
374
- caveman: options.caveman,
375
- generatedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
480
+ id: row.id,
481
+ rootPath: asPosix(row.root_path),
482
+ createdAt: new Date(row.created_at).toISOString(),
483
+ nodes: parseStringArray(row.nodes),
484
+ edges: parseEdgeArray(row.edges)
376
485
  };
377
486
  }
378
- function renderTemplate(templateName, data) {
379
- const templatePath = join2(PKG_ROOT, "templates", templateName);
380
- if (!existsSync2(templatePath)) throw new Error(`Template not found: ${templateName}`);
381
- return Handlebars.compile(readFileSync2(templatePath, "utf-8"))(data);
382
- }
383
- function writeFile(filePath, content) {
384
- const dir = dirname(filePath);
385
- if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
386
- if (existsSync2(filePath)) {
387
- const existing = readFileSync2(filePath, "utf-8");
388
- if (existing === content) return "skipped";
487
+ async function ensureGraphMigrations() {
488
+ if (graphMigrationsRun) return;
489
+ const client = await getPool().connect();
490
+ try {
491
+ await client.query("BEGIN");
492
+ await client.query(`
493
+ CREATE TABLE IF NOT EXISTS graph_snapshots (
494
+ id TEXT PRIMARY KEY,
495
+ root_path TEXT NOT NULL,
496
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
497
+ nodes JSONB NOT NULL DEFAULT '[]'::jsonb,
498
+ edges JSONB NOT NULL DEFAULT '[]'::jsonb
499
+ )
500
+ `);
501
+ await client.query("CREATE INDEX IF NOT EXISTS graph_snapshots_root_path_idx ON graph_snapshots (root_path)");
502
+ await client.query("CREATE INDEX IF NOT EXISTS graph_snapshots_created_at_idx ON graph_snapshots (created_at DESC)");
503
+ await client.query("COMMIT");
504
+ graphMigrationsRun = true;
505
+ } catch (err) {
506
+ await client.query("ROLLBACK");
507
+ throw err;
508
+ } finally {
509
+ client.release();
389
510
  }
390
- writeFileSync(filePath, content, "utf-8");
391
- return "written";
392
511
  }
393
- async function generate(options, cwd = process.cwd(), onlyAgents) {
394
- const data = buildTemplateData(options, cwd);
395
- const written = [];
396
- const skipped = [];
397
- const files = onlyAgents ? OUTPUT_FILES.filter((f) => onlyAgents.includes(f.agent)) : OUTPUT_FILES;
398
- for (const output of files) {
399
- const targetPath = join2(cwd, output.path);
400
- if (output.skipIfExists && existsSync2(targetPath)) {
401
- skipped.push(output.path);
402
- continue;
403
- }
404
- try {
405
- const content = renderTemplate(output.template, data);
406
- const result = writeFile(targetPath, content);
407
- if (result === "written") written.push(output.path);
408
- else skipped.push(output.path);
409
- } catch (err) {
410
- if (!(err instanceof Error && err.message.includes("Template not found"))) throw err;
411
- }
412
- }
413
- return { written, skipped };
512
+ async function migrateGraphSnapshots() {
513
+ await ensureGraphMigrations();
514
+ }
515
+ async function probeGraphSnapshotStore(rootPath) {
516
+ await ensureGraphMigrations();
517
+ const result = await getPool().query(
518
+ `SELECT COUNT(*)::text AS count
519
+ FROM graph_snapshots
520
+ WHERE root_path = $1`,
521
+ [asPosix(rootPath)]
522
+ );
523
+ return { snapshotCount: Number(result.rows[0]?.count ?? 0) };
414
524
  }
525
+ var PostgresGraphRepository = class {
526
+ async saveSnapshot(snapshot) {
527
+ await ensureGraphMigrations();
528
+ await getPool().query(
529
+ `INSERT INTO graph_snapshots (id, root_path, created_at, nodes, edges)
530
+ VALUES ($1, $2, $3::timestamptz, $4::jsonb, $5::jsonb)
531
+ ON CONFLICT (id)
532
+ DO UPDATE SET
533
+ root_path = EXCLUDED.root_path,
534
+ created_at = EXCLUDED.created_at,
535
+ nodes = EXCLUDED.nodes,
536
+ edges = EXCLUDED.edges`,
537
+ [
538
+ snapshot.id,
539
+ asPosix(snapshot.rootPath),
540
+ snapshot.createdAt,
541
+ JSON.stringify(snapshot.nodes),
542
+ JSON.stringify(snapshot.edges)
543
+ ]
544
+ );
545
+ }
546
+ async latestSnapshot(rootPath) {
547
+ await ensureGraphMigrations();
548
+ const result = await getPool().query(
549
+ `SELECT id, root_path, created_at, nodes, edges
550
+ FROM graph_snapshots
551
+ WHERE root_path = $1
552
+ ORDER BY created_at DESC
553
+ LIMIT 1`,
554
+ [asPosix(rootPath)]
555
+ );
556
+ const row = result.rows[0];
557
+ return row ? toSnapshot(row) : null;
558
+ }
559
+ async getSnapshot(rootPath, snapshotId) {
560
+ await ensureGraphMigrations();
561
+ const result = await getPool().query(
562
+ `SELECT id, root_path, created_at, nodes, edges
563
+ FROM graph_snapshots
564
+ WHERE root_path = $1 AND id = $2
565
+ LIMIT 1`,
566
+ [asPosix(rootPath), snapshotId]
567
+ );
568
+ const row = result.rows[0];
569
+ return row ? toSnapshot(row) : null;
570
+ }
571
+ async listSnapshots(rootPath, limit = 20) {
572
+ await ensureGraphMigrations();
573
+ const result = await getPool().query(
574
+ `SELECT id, root_path, created_at, nodes, edges
575
+ FROM graph_snapshots
576
+ WHERE root_path = $1
577
+ ORDER BY created_at DESC
578
+ LIMIT $2`,
579
+ [asPosix(rootPath), Math.max(1, limit)]
580
+ );
581
+ return result.rows.map(toSnapshot);
582
+ }
583
+ };
415
584
 
416
585
  // src/seeds.ts
417
586
  var seeds = [
@@ -794,116 +963,1290 @@ var seeds = [
794
963
  { 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"] }
795
964
  ];
796
965
 
797
- // src/chat.ts
798
- function getChatConfig() {
799
- const provider = process.env.CHAT_PROVIDER ?? "ollama";
800
- const model = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
966
+ // src/watcher.ts
967
+ import { watch } from "chokidar";
968
+ import { spawnSync } from "child_process";
969
+ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
970
+ import { join as join7, relative as relative2 } from "path";
971
+ import chalk from "chalk";
972
+
973
+ // src/generator.ts
974
+ import { readFileSync as readFileSync4, readdirSync as readdirSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
975
+ import { join as join6, dirname as dirname3 } from "path";
976
+ import { fileURLToPath } from "url";
977
+ import Handlebars from "handlebars";
978
+ import yaml from "js-yaml";
979
+
980
+ // src/infrastructure/providers/embedding/ollama-embedding-provider.ts
981
+ var OllamaEmbeddingProvider = class {
982
+ providerName() {
983
+ return "ollama";
984
+ }
985
+ async embed(text) {
986
+ return embed(text);
987
+ }
988
+ };
989
+
990
+ // src/infrastructure/providers/llm/chat-llm-provider.ts
991
+ var ChatLlmProvider = class {
992
+ providerName() {
993
+ return getChatProviderLabel();
994
+ }
995
+ async generateText(messages) {
996
+ return callChatModel(messages);
997
+ }
998
+ };
999
+
1000
+ // src/infrastructure/persistence/postgres/postgres-memory-repository.ts
1001
+ function toRecord(memory) {
1002
+ return {
1003
+ id: memory.id,
1004
+ type: memory.type,
1005
+ scope: memory.scope,
1006
+ architecture: memory.architecture,
1007
+ projectName: memory.project_name,
1008
+ title: memory.title,
1009
+ content: memory.content,
1010
+ reason: memory.reason,
1011
+ context: memory.context,
1012
+ tags: memory.tags,
1013
+ contentHash: memory.content_hash,
1014
+ similarity: memory.similarity
1015
+ };
1016
+ }
1017
+ function toDbContext(context) {
1018
+ return context;
1019
+ }
1020
+ function toDbInput(input) {
1021
+ return {
1022
+ type: input.type,
1023
+ scope: input.scope,
1024
+ architecture: input.architecture,
1025
+ projectName: input.projectName,
1026
+ title: input.title,
1027
+ content: input.content,
1028
+ reason: input.reason,
1029
+ context: toDbContext(input.context),
1030
+ tags: input.tags,
1031
+ embedding: input.embedding
1032
+ };
1033
+ }
1034
+ var PostgresMemoryRepository = class {
1035
+ async save(input) {
1036
+ await saveMemory(toDbInput(input));
1037
+ }
1038
+ async upsert(input) {
1039
+ return upsertMemory(toDbInput(input));
1040
+ }
1041
+ async list(filters = {}) {
1042
+ const rows = await listMemories({
1043
+ type: filters.type,
1044
+ scope: filters.scope,
1045
+ architecture: filters.architecture,
1046
+ projectName: filters.projectName,
1047
+ includeGlobal: filters.includeGlobal,
1048
+ limit: filters.limit,
1049
+ tags: filters.tags
1050
+ });
1051
+ return rows.map(toRecord);
1052
+ }
1053
+ async getById(id) {
1054
+ const row = await getMemory(id);
1055
+ return row ? toRecord(row) : null;
1056
+ }
1057
+ async update(id, patch) {
1058
+ const row = await updateMemory(id, {
1059
+ type: patch.type,
1060
+ scope: patch.scope,
1061
+ title: patch.title,
1062
+ content: patch.content,
1063
+ reason: patch.reason,
1064
+ context: patch.context,
1065
+ tags: patch.tags,
1066
+ embedding: patch.embedding
1067
+ });
1068
+ return row ? toRecord(row) : null;
1069
+ }
1070
+ async removeById(id) {
1071
+ return deleteMemory(id);
1072
+ }
1073
+ async removeMany(filters) {
1074
+ return deleteMemories(filters);
1075
+ }
1076
+ async searchByEmbedding(input) {
1077
+ const rows = await searchMemories(input.embedding, input.architectures, input.limit ?? 10);
1078
+ return rows.map(toRecord);
1079
+ }
1080
+ };
1081
+
1082
+ // src/infrastructure/persistence/filesystem/file-graph-repository.ts
1083
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1084
+ import { dirname, join as join3 } from "path";
1085
+ var DEFAULT_FILE = join3(".memory-core", "graph-snapshots.json");
1086
+ function asPosix2(value) {
1087
+ return value.replace(/\\/g, "/");
1088
+ }
1089
+ var FileGraphRepository = class {
1090
+ constructor(rootPath = process.cwd(), relativeFilePath = DEFAULT_FILE) {
1091
+ this.rootPath = rootPath;
1092
+ this.relativeFilePath = relativeFilePath;
1093
+ }
1094
+ rootPath;
1095
+ relativeFilePath;
1096
+ async saveSnapshot(snapshot) {
1097
+ const data = this.readData();
1098
+ data.snapshots.push({
1099
+ ...snapshot,
1100
+ rootPath: asPosix2(snapshot.rootPath),
1101
+ nodes: [...new Set(snapshot.nodes.map(asPosix2))],
1102
+ edges: snapshot.edges.map((edge) => ({ ...edge, from: asPosix2(edge.from), to: asPosix2(edge.to) }))
1103
+ });
1104
+ this.writeData(data);
1105
+ }
1106
+ async latestSnapshot(rootPath) {
1107
+ const snapshots = this.snapshotsForRoot(rootPath);
1108
+ return snapshots.length ? snapshots[snapshots.length - 1] : null;
1109
+ }
1110
+ async getSnapshot(rootPath, snapshotId) {
1111
+ const snapshots = this.snapshotsForRoot(rootPath);
1112
+ return snapshots.find((snapshot) => snapshot.id === snapshotId) ?? null;
1113
+ }
1114
+ async listSnapshots(rootPath, limit = 20) {
1115
+ const snapshots = this.snapshotsForRoot(rootPath);
1116
+ return [...snapshots].slice(-Math.max(1, limit)).reverse();
1117
+ }
1118
+ snapshotsForRoot(rootPath) {
1119
+ const normalized = asPosix2(rootPath);
1120
+ return this.readData().snapshots.filter((snapshot) => asPosix2(snapshot.rootPath) === normalized);
1121
+ }
1122
+ readData() {
1123
+ const filePath = this.absolutePath();
1124
+ if (!existsSync3(filePath)) {
1125
+ return { version: 1, snapshots: [] };
1126
+ }
1127
+ try {
1128
+ const parsed = JSON.parse(readFileSync2(filePath, "utf-8"));
1129
+ if (!Array.isArray(parsed.snapshots)) {
1130
+ return { version: 1, snapshots: [] };
1131
+ }
1132
+ return {
1133
+ version: 1,
1134
+ snapshots: parsed.snapshots.filter(
1135
+ (snapshot) => typeof snapshot?.id === "string" && typeof snapshot?.createdAt === "string" && typeof snapshot?.rootPath === "string" && Array.isArray(snapshot?.nodes) && Array.isArray(snapshot?.edges)
1136
+ )
1137
+ };
1138
+ } catch {
1139
+ return { version: 1, snapshots: [] };
1140
+ }
1141
+ }
1142
+ writeData(data) {
1143
+ const filePath = this.absolutePath();
1144
+ mkdirSync(dirname(filePath), { recursive: true });
1145
+ writeFileSync(filePath, `${JSON.stringify(data, null, 2)}
1146
+ `, "utf-8");
1147
+ }
1148
+ absolutePath() {
1149
+ return join3(this.rootPath, this.relativeFilePath);
1150
+ }
1151
+ };
1152
+
1153
+ // src/infrastructure/persistence/postgres/resilient-graph-repository.ts
1154
+ var ResilientGraphRepository = class {
1155
+ constructor(primary, fallback) {
1156
+ this.primary = primary;
1157
+ this.fallback = fallback;
1158
+ }
1159
+ primary;
1160
+ fallback;
1161
+ async saveSnapshot(snapshot) {
1162
+ try {
1163
+ await this.primary.saveSnapshot(snapshot);
1164
+ } catch {
1165
+ await this.fallback.saveSnapshot(snapshot);
1166
+ }
1167
+ }
1168
+ async latestSnapshot(rootPath) {
1169
+ try {
1170
+ const value = await this.primary.latestSnapshot(rootPath);
1171
+ if (value) return value;
1172
+ } catch {
1173
+ }
1174
+ return this.fallback.latestSnapshot(rootPath);
1175
+ }
1176
+ async getSnapshot(rootPath, snapshotId) {
1177
+ try {
1178
+ const value = await this.primary.getSnapshot(rootPath, snapshotId);
1179
+ if (value) return value;
1180
+ } catch {
1181
+ }
1182
+ return this.fallback.getSnapshot(rootPath, snapshotId);
1183
+ }
1184
+ async listSnapshots(rootPath, limit = 20) {
1185
+ try {
1186
+ const value = await this.primary.listSnapshots(rootPath, limit);
1187
+ if (value.length > 0) return value;
1188
+ } catch {
1189
+ }
1190
+ return this.fallback.listSnapshots(rootPath, limit);
1191
+ }
1192
+ };
1193
+
1194
+ // src/infrastructure/filesystem/chokidar-watch-service.ts
1195
+ var ChokidarWatchService = class {
1196
+ async start(options = {}) {
1197
+ await startWatch({
1198
+ path: options.path,
1199
+ verbose: options.verbose,
1200
+ debug: options.debug
1201
+ });
1202
+ }
1203
+ };
1204
+
1205
+ // src/infrastructure/events/in-memory-event-bus.ts
1206
+ var InMemoryEventBus = class {
1207
+ handlers = /* @__PURE__ */ new Map();
1208
+ async publish(event) {
1209
+ const handlers = this.handlers.get(event.type);
1210
+ if (!handlers || handlers.size === 0) return;
1211
+ for (const handler of handlers) {
1212
+ await handler(event);
1213
+ }
1214
+ }
1215
+ subscribe(eventType, handler) {
1216
+ const existing = this.handlers.get(eventType) ?? /* @__PURE__ */ new Set();
1217
+ existing.add(handler);
1218
+ this.handlers.set(eventType, existing);
1219
+ return () => {
1220
+ const current = this.handlers.get(eventType);
1221
+ if (!current) return;
1222
+ current.delete(handler);
1223
+ if (current.size === 0) {
1224
+ this.handlers.delete(eventType);
1225
+ }
1226
+ };
1227
+ }
1228
+ };
1229
+
1230
+ // src/modules/memory-engine/application/memory-engine-service.ts
1231
+ var MemoryEngineService = class {
1232
+ constructor(memoryRepository, embeddingProvider) {
1233
+ this.memoryRepository = memoryRepository;
1234
+ this.embeddingProvider = embeddingProvider;
1235
+ }
1236
+ memoryRepository;
1237
+ embeddingProvider;
1238
+ async remember(input) {
1239
+ const embedding = await this.embeddingProvider.embed(input.content);
1240
+ return this.memoryRepository.upsert({ ...input, embedding });
1241
+ }
1242
+ async rememberForce(input) {
1243
+ const embedding = await this.embeddingProvider.embed(input.content);
1244
+ await this.memoryRepository.save({ ...input, embedding });
1245
+ }
1246
+ async list(filters = {}) {
1247
+ return this.memoryRepository.list(filters);
1248
+ }
1249
+ async getById(id) {
1250
+ return this.memoryRepository.getById(id);
1251
+ }
1252
+ async removeById(id) {
1253
+ return this.memoryRepository.removeById(id);
1254
+ }
1255
+ async removeMany(filters) {
1256
+ return this.memoryRepository.removeMany(filters);
1257
+ }
1258
+ async update(id, patch) {
1259
+ const current = await this.memoryRepository.getById(id);
1260
+ if (!current) return null;
1261
+ const content = patch.content ?? current.content;
1262
+ const embedding = content !== current.content ? await this.embeddingProvider.embed(content) : void 0;
1263
+ return this.memoryRepository.update(id, { ...patch, embedding });
1264
+ }
1265
+ async search(query, architectures, limit = 10) {
1266
+ const embedding = await this.embeddingProvider.embed(query);
1267
+ return this.memoryRepository.searchByEmbedding({ embedding, architectures, limit });
1268
+ }
1269
+ };
1270
+
1271
+ // src/modules/retrieval-engine/application/retrieval-engine-service.ts
1272
+ var RetrievalEngineService = class {
1273
+ constructor(embeddingProvider, memoryRepository) {
1274
+ this.embeddingProvider = embeddingProvider;
1275
+ this.memoryRepository = memoryRepository;
1276
+ }
1277
+ embeddingProvider;
1278
+ memoryRepository;
1279
+ async retrieve(query) {
1280
+ const limit = query.limit ?? 10;
1281
+ const embedding = await this.embeddingProvider.embed(query.text);
1282
+ const vectorMatches = await this.memoryRepository.searchByEmbedding({
1283
+ embedding,
1284
+ architectures: query.architectures,
1285
+ limit
1286
+ });
1287
+ const lexicalMatches = (await this.memoryRepository.list({
1288
+ architecture: query.architectures,
1289
+ includeGlobal: true,
1290
+ limit: Math.max(limit * 2, 20)
1291
+ })).filter((memory) => {
1292
+ const haystack = `${memory.content} ${memory.title ?? ""} ${(memory.tags ?? []).join(" ")}`.toLowerCase();
1293
+ return haystack.includes(query.text.toLowerCase());
1294
+ });
1295
+ const merged = [...vectorMatches];
1296
+ for (const memory of lexicalMatches) {
1297
+ if (!merged.some((candidate) => candidate.id === memory.id)) {
1298
+ merged.push(memory);
1299
+ }
1300
+ }
1301
+ return { items: merged.slice(0, limit) };
1302
+ }
1303
+ };
1304
+
1305
+ // src/modules/rule-engine/application/rule-engine-service.ts
1306
+ var RuleEngineService = class {
1307
+ constructor(rules, llmProvider) {
1308
+ this.rules = rules;
1309
+ this.llmProvider = llmProvider;
1310
+ }
1311
+ rules;
1312
+ llmProvider;
1313
+ async evaluate(input, includeExplanation = false) {
1314
+ const allViolations = [];
1315
+ for (const rule of this.rules) {
1316
+ allViolations.push(...await rule.evaluate(input));
1317
+ }
1318
+ if (!includeExplanation || allViolations.length === 0 || !this.llmProvider) {
1319
+ return { violations: allViolations };
1320
+ }
1321
+ const explanation = await this.llmProvider.generateText([
1322
+ {
1323
+ role: "system",
1324
+ content: "Summarize architecture violations and provide practical fixes in concise bullet points."
1325
+ },
1326
+ {
1327
+ role: "user",
1328
+ content: JSON.stringify(allViolations, null, 2)
1329
+ }
1330
+ ]);
1331
+ return { violations: allViolations, explanation };
1332
+ }
1333
+ };
1334
+
1335
+ // src/modules/graph-engine/application/graph-engine-service.ts
1336
+ import { readdirSync, statSync } from "fs";
1337
+ import { join as join5, relative, resolve as resolve2 } from "path";
1338
+
1339
+ // src/infrastructure/ast/import-analysis.ts
1340
+ import { builtinModules } from "module";
1341
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1342
+ import { dirname as dirname2, extname, isAbsolute, join as join4, normalize, resolve } from "path";
1343
+ var SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
1344
+ var NODE_BUILTINS = /* @__PURE__ */ new Set([...builtinModules, ...builtinModules.map((entry) => `node:${entry}`)]);
1345
+ function countNewlines(value) {
1346
+ let count = 0;
1347
+ for (const char of value) {
1348
+ if (char === "\n") count += 1;
1349
+ }
1350
+ return count;
1351
+ }
1352
+ function parseImports(source) {
1353
+ const imports = [];
1354
+ const patterns = [
1355
+ { kind: "import", regex: /(^|\n)\s*import\s+(?:type\s+)?(?:[^'"\n]+?\s+from\s+)?['"]([^'"\n]+)['"]/g },
1356
+ { kind: "export-from", regex: /(^|\n)\s*export\s+(?:type\s+)?(?:[^'"\n]+?\s+from\s+)['"]([^'"\n]+)['"]/g },
1357
+ { kind: "require", regex: /(^|\n)[^\n]*?\brequire\(\s*['"]([^'"\n]+)['"]\s*\)/g },
1358
+ { kind: "dynamic-import", regex: /(^|\n)[^\n]*?\bimport\(\s*['"]([^'"\n]+)['"]\s*\)/g }
1359
+ ];
1360
+ for (const { kind, regex } of patterns) {
1361
+ for (const match of source.matchAll(regex)) {
1362
+ const prefix = source.slice(0, match.index ?? 0);
1363
+ imports.push({
1364
+ kind,
1365
+ specifier: match[2],
1366
+ line: countNewlines(prefix) + 1
1367
+ });
1368
+ }
1369
+ }
1370
+ return imports;
1371
+ }
1372
+ function tryResolveFilePath(candidate) {
1373
+ if (existsSync4(candidate)) return normalize(candidate);
1374
+ if (!extname(candidate)) {
1375
+ for (const ext of SOURCE_EXTENSIONS) {
1376
+ const withExt = `${candidate}${ext}`;
1377
+ if (existsSync4(withExt)) return normalize(withExt);
1378
+ }
1379
+ for (const ext of SOURCE_EXTENSIONS) {
1380
+ const indexFile = join4(candidate, `index${ext}`);
1381
+ if (existsSync4(indexFile)) return normalize(indexFile);
1382
+ }
1383
+ }
1384
+ return void 0;
1385
+ }
1386
+ function looksLikeExternal(specifier) {
1387
+ return !specifier.startsWith(".") && !specifier.startsWith("/") && !specifier.startsWith("@/");
1388
+ }
1389
+ function resolveImportPath(fromFile, specifier, cwd = process.cwd()) {
1390
+ if (specifier.startsWith(".")) {
1391
+ return tryResolveFilePath(resolve(dirname2(fromFile), specifier));
1392
+ }
1393
+ if (specifier.startsWith("/")) {
1394
+ return tryResolveFilePath(resolve(cwd, `.${specifier}`));
1395
+ }
1396
+ if (specifier.startsWith("@/")) {
1397
+ return tryResolveFilePath(resolve(cwd, "src", specifier.slice(2)));
1398
+ }
1399
+ if (isAbsolute(specifier)) {
1400
+ return tryResolveFilePath(specifier);
1401
+ }
1402
+ return void 0;
1403
+ }
1404
+ function collectResolvedImports(filePath, cwd = process.cwd()) {
1405
+ if (!existsSync4(filePath)) return [];
1406
+ const source = readFileSync3(filePath, "utf-8");
1407
+ const imports = parseImports(source);
1408
+ return imports.map((entry) => {
1409
+ if (looksLikeExternal(entry.specifier) || NODE_BUILTINS.has(entry.specifier)) {
1410
+ return { ...entry, isExternal: true };
1411
+ }
1412
+ return {
1413
+ ...entry,
1414
+ isExternal: false,
1415
+ resolvedPath: resolveImportPath(filePath, entry.specifier, cwd)
1416
+ };
1417
+ });
1418
+ }
1419
+ function asPosix3(value) {
1420
+ return value.replace(/\\/g, "/");
1421
+ }
1422
+ function moduleNameFromPath(absPath, cwd = process.cwd()) {
1423
+ const rel = asPosix3(normalize(absPath)).replace(asPosix3(normalize(cwd)) + "/", "");
1424
+ const match = rel.match(/^src\/modules\/([^/]+)\//);
1425
+ return match?.[1];
1426
+ }
1427
+ function buildModuleDependencyEdges(files, cwd = process.cwd()) {
1428
+ const edges = [];
1429
+ for (const file of files) {
1430
+ const absoluteFile = resolve(cwd, file);
1431
+ const fromModule = moduleNameFromPath(absoluteFile, cwd);
1432
+ if (!fromModule) continue;
1433
+ const imports = collectResolvedImports(absoluteFile, cwd);
1434
+ for (const imp of imports) {
1435
+ if (!imp.resolvedPath) continue;
1436
+ const toModule = moduleNameFromPath(imp.resolvedPath, cwd);
1437
+ if (!toModule || toModule === fromModule) continue;
1438
+ edges.push({
1439
+ fromModule,
1440
+ toModule,
1441
+ file,
1442
+ line: imp.line
1443
+ });
1444
+ }
1445
+ }
1446
+ return edges;
1447
+ }
1448
+ function parseChangedFilesFromDiff(diff) {
1449
+ const files = /* @__PURE__ */ new Set();
1450
+ for (const line of diff.split("\n")) {
1451
+ if (!line.startsWith("+++ b/")) continue;
1452
+ const file = line.slice("+++ b/".length).trim();
1453
+ if (!file || file === "/dev/null") continue;
1454
+ files.add(file);
1455
+ }
1456
+ return [...files];
1457
+ }
1458
+ function detectModuleCycles(edges) {
1459
+ const graph = /* @__PURE__ */ new Map();
1460
+ for (const edge of edges) {
1461
+ const next = graph.get(edge.fromModule) ?? /* @__PURE__ */ new Set();
1462
+ next.add(edge.toModule);
1463
+ graph.set(edge.fromModule, next);
1464
+ }
1465
+ const visited = /* @__PURE__ */ new Set();
1466
+ const stack = /* @__PURE__ */ new Set();
1467
+ const cycles = /* @__PURE__ */ new Set();
1468
+ const visit = (node, path) => {
1469
+ visited.add(node);
1470
+ stack.add(node);
1471
+ const next = graph.get(node) ?? /* @__PURE__ */ new Set();
1472
+ for (const target of next) {
1473
+ if (!visited.has(target)) {
1474
+ visit(target, [...path, target]);
1475
+ continue;
1476
+ }
1477
+ if (stack.has(target)) {
1478
+ const start = path.indexOf(target);
1479
+ const cycle = start >= 0 ? path.slice(start).concat(target) : [node, target, node];
1480
+ cycles.add(cycle.join("->"));
1481
+ }
1482
+ }
1483
+ stack.delete(node);
1484
+ };
1485
+ for (const node of graph.keys()) {
1486
+ if (!visited.has(node)) {
1487
+ visit(node, [node]);
1488
+ }
1489
+ }
1490
+ return [...cycles].map((value) => value.split("->"));
1491
+ }
1492
+ function isExternalFrameworkSpecifier(specifier) {
1493
+ const frameworkPrefixes = [
1494
+ "express",
1495
+ "fastify",
1496
+ "@nestjs/",
1497
+ "react",
1498
+ "vue",
1499
+ "svelte",
1500
+ "@angular/",
1501
+ "next/",
1502
+ "nuxt",
1503
+ "typeorm",
1504
+ "@prisma/",
1505
+ "mongoose",
1506
+ "sequelize",
1507
+ "axios"
1508
+ ];
1509
+ return frameworkPrefixes.some((prefix) => specifier === prefix || specifier.startsWith(`${prefix}/`));
1510
+ }
1511
+
1512
+ // src/modules/graph-engine/application/graph-engine-service.ts
1513
+ var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
1514
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", ".memory-core"]);
1515
+ function isSourceFile(pathValue) {
1516
+ const extension = pathValue.slice(pathValue.lastIndexOf("."));
1517
+ return SOURCE_EXTENSIONS2.has(extension);
1518
+ }
1519
+ function asPosix4(value) {
1520
+ return value.replace(/\\/g, "/");
1521
+ }
1522
+ function normalizeNode(pathValue) {
1523
+ return asPosix4(pathValue);
1524
+ }
1525
+ function edgeKey(edge) {
1526
+ return `${edge.from}\0${edge.to}\0${edge.kind}`;
1527
+ }
1528
+ function generateSnapshotId() {
1529
+ const suffix = Math.random().toString(36).slice(2, 8);
1530
+ return `snapshot-${Date.now()}-${suffix}`;
1531
+ }
1532
+ function toGraph(snapshot) {
1533
+ return {
1534
+ id: snapshot.id,
1535
+ createdAt: snapshot.createdAt,
1536
+ rootPath: snapshot.rootPath,
1537
+ nodes: snapshot.nodes,
1538
+ edges: snapshot.edges
1539
+ };
1540
+ }
1541
+ var GraphEngineService = class {
1542
+ constructor(graphRepository) {
1543
+ this.graphRepository = graphRepository;
1544
+ }
1545
+ graphRepository;
1546
+ async buildGraph(options = {}) {
1547
+ const cwd = resolve2(options.cwd ?? process.cwd());
1548
+ const files = this.collectSourceFiles(cwd);
1549
+ const nodes = /* @__PURE__ */ new Set();
1550
+ const edges = /* @__PURE__ */ new Map();
1551
+ for (const relativeFile of files) {
1552
+ const absoluteFile = resolve2(cwd, relativeFile);
1553
+ const fromNode = normalizeNode(relativeFile);
1554
+ nodes.add(fromNode);
1555
+ const imports = collectResolvedImports(absoluteFile, cwd);
1556
+ for (const imp of imports) {
1557
+ let toNode;
1558
+ if (imp.resolvedPath) {
1559
+ toNode = normalizeNode(asPosix4(relative(cwd, imp.resolvedPath)));
1560
+ } else if (imp.isExternal) {
1561
+ toNode = `pkg:${imp.specifier}`;
1562
+ }
1563
+ if (!toNode) continue;
1564
+ nodes.add(toNode);
1565
+ const edge = {
1566
+ from: fromNode,
1567
+ to: toNode,
1568
+ kind: imp.kind === "export-from" ? "import" : imp.kind
1569
+ };
1570
+ edges.set(edgeKey(edge), edge);
1571
+ }
1572
+ }
1573
+ return {
1574
+ rootPath: cwd,
1575
+ nodes: [...nodes].sort(),
1576
+ edges: [...edges.values()].sort(
1577
+ (a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || a.kind.localeCompare(b.kind)
1578
+ )
1579
+ };
1580
+ }
1581
+ async buildAndStoreSnapshot(options = {}) {
1582
+ const graph = await this.buildGraph(options);
1583
+ const snapshot = {
1584
+ ...graph,
1585
+ id: generateSnapshotId(),
1586
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1587
+ };
1588
+ await this.storeSnapshot(snapshot);
1589
+ return { snapshot };
1590
+ }
1591
+ async storeSnapshot(graph) {
1592
+ const snapshotId = graph.id ?? generateSnapshotId();
1593
+ const createdAt = graph.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
1594
+ await this.graphRepository.saveSnapshot({
1595
+ id: snapshotId,
1596
+ createdAt,
1597
+ rootPath: graph.rootPath,
1598
+ nodes: graph.nodes,
1599
+ edges: graph.edges
1600
+ });
1601
+ }
1602
+ async latest(rootPath) {
1603
+ const snapshot = await this.graphRepository.latestSnapshot(rootPath);
1604
+ return snapshot ? toGraph(snapshot) : null;
1605
+ }
1606
+ async getSnapshot(rootPath, snapshotId) {
1607
+ const snapshot = await this.graphRepository.getSnapshot(rootPath, snapshotId);
1608
+ return snapshot ? toGraph(snapshot) : null;
1609
+ }
1610
+ async listSnapshots(rootPath, limit = 20) {
1611
+ const snapshots = await this.graphRepository.listSnapshots(rootPath, limit);
1612
+ return snapshots.map(toGraph);
1613
+ }
1614
+ diffGraphs(left, right) {
1615
+ const leftNodes = new Set(left.nodes);
1616
+ const rightNodes = new Set(right.nodes);
1617
+ const leftEdges = new Map(left.edges.map((edge) => [edgeKey(edge), edge]));
1618
+ const rightEdges = new Map(right.edges.map((edge) => [edgeKey(edge), edge]));
1619
+ const addedNodes = right.nodes.filter((node) => !leftNodes.has(node));
1620
+ const removedNodes = left.nodes.filter((node) => !rightNodes.has(node));
1621
+ const addedEdges = right.edges.filter((edge) => !leftEdges.has(edgeKey(edge)));
1622
+ const removedEdges = left.edges.filter((edge) => !rightEdges.has(edgeKey(edge)));
1623
+ return { addedNodes, removedNodes, addedEdges, removedEdges };
1624
+ }
1625
+ collectSourceFiles(cwd) {
1626
+ const files = [];
1627
+ const walk = (dir) => {
1628
+ for (const entry of readdirSync(dir)) {
1629
+ if (IGNORED_DIRS.has(entry)) continue;
1630
+ const absolutePath = join5(dir, entry);
1631
+ const stat = statSync(absolutePath);
1632
+ if (stat.isDirectory()) {
1633
+ walk(absolutePath);
1634
+ continue;
1635
+ }
1636
+ const rel = asPosix4(relative(cwd, absolutePath));
1637
+ if (isSourceFile(rel)) files.push(rel);
1638
+ }
1639
+ };
1640
+ walk(cwd);
1641
+ return files;
1642
+ }
1643
+ };
1644
+
1645
+ // src/modules/agent-engine/application/agent-engine-service.ts
1646
+ var AgentEngineService = class {
1647
+ constructor(eventBus) {
1648
+ this.eventBus = eventBus;
1649
+ }
1650
+ eventBus;
1651
+ async dispatch(task) {
1652
+ await this.eventBus.publish({
1653
+ type: "agent.task.dispatched",
1654
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
1655
+ payload: task
1656
+ });
1657
+ }
1658
+ };
1659
+
1660
+ // src/app/build-application-container.ts
1661
+ function buildApplicationContainer(cwd = process.cwd()) {
1662
+ const embeddingProvider = new OllamaEmbeddingProvider();
1663
+ const llmProvider = new ChatLlmProvider();
1664
+ const memoryRepository = new PostgresMemoryRepository();
1665
+ const fileGraphRepository = new FileGraphRepository(cwd);
1666
+ const graphRepository = Config.databaseUrl ? new ResilientGraphRepository(new PostgresGraphRepository(), fileGraphRepository) : fileGraphRepository;
1667
+ const eventBus = new InMemoryEventBus();
1668
+ const watchService = new ChokidarWatchService();
1669
+ const memoryEngine = new MemoryEngineService(memoryRepository, embeddingProvider);
1670
+ const retrievalEngine = new RetrievalEngineService(embeddingProvider, memoryRepository);
1671
+ const ruleEngine = new RuleEngineService([], llmProvider);
1672
+ const graphEngine = new GraphEngineService(graphRepository);
1673
+ const agentEngine = new AgentEngineService(eventBus);
1674
+ return {
1675
+ providers: {
1676
+ embeddingProvider,
1677
+ llmProvider,
1678
+ memoryRepository,
1679
+ graphRepository,
1680
+ eventBus,
1681
+ watchService
1682
+ },
1683
+ services: {
1684
+ memoryEngine,
1685
+ retrievalEngine,
1686
+ ruleEngine,
1687
+ graphEngine,
1688
+ agentEngine
1689
+ }
1690
+ };
1691
+ }
1692
+
1693
+ // src/app/index.ts
1694
+ var defaultContainer;
1695
+ function getDefaultApplicationContainer() {
1696
+ if (!defaultContainer) {
1697
+ defaultContainer = buildApplicationContainer(process.cwd());
1698
+ }
1699
+ return defaultContainer;
1700
+ }
1701
+
1702
+ // src/memory-selection.ts
1703
+ var FRAMEWORK_ARCHITECTURE_MAP = {
1704
+ Laravel: ["laravel-service-repository"],
1705
+ "Next.js": ["nextjs"],
1706
+ "Nuxt.js": ["nuxt"],
1707
+ Go: ["go-api"],
1708
+ NestJS: ["nestjs"],
1709
+ React: ["react"],
1710
+ "Vue.js": ["vue"],
1711
+ Svelte: ["svelte"]
1712
+ };
1713
+ var KNOWN_ARCHITECTURE_KEYS = /* @__PURE__ */ new Set([
1714
+ ...Object.values(FRAMEWORK_ARCHITECTURE_MAP).flat(),
1715
+ "angular",
1716
+ "clean-architecture",
1717
+ "express",
1718
+ "fastify",
1719
+ "hexagonal",
1720
+ "modular-monolith",
1721
+ "mvc",
1722
+ "react-native"
1723
+ ]);
1724
+ function normalizeText(value) {
1725
+ return value.toLowerCase().replace(/[`"'()[\]{}.,:;!?/\\<>|=*+-]/g, " ").replace(/\s+/g, " ").trim();
1726
+ }
1727
+ function tokenSet(value) {
1728
+ return new Set(
1729
+ normalizeText(value).split(" ").filter((token) => token.length > 2)
1730
+ );
1731
+ }
1732
+ function similarityScore(a, b) {
1733
+ const left = tokenSet(a);
1734
+ const right = tokenSet(b);
1735
+ if (left.size === 0 || right.size === 0) return 0;
1736
+ let intersection = 0;
1737
+ for (const token of left) {
1738
+ if (right.has(token)) intersection++;
1739
+ }
1740
+ return 2 * intersection / (left.size + right.size);
1741
+ }
1742
+ function mergeMemory(primary, secondary) {
1743
+ const mergedTags = [.../* @__PURE__ */ new Set([...primary.tags ?? [], ...secondary.tags ?? []])];
1744
+ const reason = [primary.reason, secondary.reason].filter(Boolean).join(" | ") || void 0;
1745
+ return {
1746
+ ...primary,
1747
+ tags: mergedTags,
1748
+ reason
1749
+ };
1750
+ }
1751
+ function memoryArchitectureKeys(memory) {
1752
+ if (memory.architecture && memory.architecture !== "global") {
1753
+ return [memory.architecture];
1754
+ }
1755
+ return (memory.tags ?? []).filter((tag) => KNOWN_ARCHITECTURE_KEYS.has(tag));
1756
+ }
1757
+ function getStackReason(memory, activeArchitectures2) {
1758
+ const architectureKeys = memoryArchitectureKeys(memory);
1759
+ if (architectureKeys.length === 0) {
1760
+ return {
1761
+ included: true,
1762
+ reason: "global memory: no architecture-specific tag"
1763
+ };
1764
+ }
1765
+ const matched = architectureKeys.filter((architecture) => activeArchitectures2.has(architecture));
1766
+ if (matched.length > 0) {
1767
+ return {
1768
+ included: true,
1769
+ reason: `matched active architecture: ${matched.join(", ")}`
1770
+ };
1771
+ }
1772
+ const active = [...activeArchitectures2].join(", ") || "none detected";
1773
+ return {
1774
+ included: false,
1775
+ reason: `excluded: tagged for ${architectureKeys.join(", ")}; active stack is ${active}`
1776
+ };
1777
+ }
1778
+ function inferProjectArchitectures(cwd = process.cwd(), config2) {
1779
+ const inferred = /* @__PURE__ */ new Set();
1780
+ if (config2?.backendArchitecture) inferred.add(config2.backendArchitecture);
1781
+ if (config2?.frontendFramework) inferred.add(config2.frontendFramework);
1782
+ if (config2?.projectType === "backend" && !config2.backendArchitecture) {
1783
+ inferred.add("clean-architecture");
1784
+ }
1785
+ const detected = detectProject(cwd);
1786
+ for (const architecture of FRAMEWORK_ARCHITECTURE_MAP[detected.framework] ?? []) {
1787
+ inferred.add(architecture);
1788
+ }
1789
+ return [...inferred];
1790
+ }
1791
+ function getAllowPatterns(config2) {
1792
+ return [...new Set(config2?.allowPatterns?.filter(Boolean) ?? [])];
1793
+ }
1794
+ function filterRelevantMemories(memories, config2, cwd = process.cwd()) {
1795
+ return explainMemorySelection(memories, config2, cwd).included;
1796
+ }
1797
+ function explainMemorySelection(memories, config2, cwd = process.cwd(), threshold = 0.8) {
1798
+ const activeArchitectures2 = inferProjectArchitectures(cwd, config2);
1799
+ const activeSet = new Set(activeArchitectures2);
1800
+ const included = [];
1801
+ const decisions = [];
1802
+ for (const memory of memories) {
1803
+ const stackDecision = getStackReason(memory, activeSet);
1804
+ if (!stackDecision.included) {
1805
+ decisions.push({
1806
+ memory,
1807
+ status: "excluded",
1808
+ reason: stackDecision.reason
1809
+ });
1810
+ continue;
1811
+ }
1812
+ const existingIndex = included.findIndex((candidate) => {
1813
+ if (candidate.content_hash && memory.content_hash && candidate.content_hash === memory.content_hash) {
1814
+ return true;
1815
+ }
1816
+ return similarityScore(candidate.content, memory.content) >= threshold;
1817
+ });
1818
+ if (existingIndex === -1) {
1819
+ included.push(memory);
1820
+ decisions.push({
1821
+ memory,
1822
+ status: "included",
1823
+ reason: stackDecision.reason
1824
+ });
1825
+ continue;
1826
+ }
1827
+ included[existingIndex] = mergeMemory(included[existingIndex], memory);
1828
+ decisions.push({
1829
+ memory,
1830
+ status: "excluded",
1831
+ reason: `duplicate or near-duplicate of memory #${included[existingIndex].id}`
1832
+ });
1833
+ }
1834
+ return {
1835
+ included,
1836
+ excluded: decisions.filter((decision) => decision.status === "excluded"),
1837
+ decisions,
1838
+ activeArchitectures: activeArchitectures2
1839
+ };
1840
+ }
1841
+ function buildContextQuery(parts, maxLength = 1200) {
1842
+ return parts.filter(Boolean).join("\n").slice(0, maxLength);
1843
+ }
1844
+ async function retrieveContextualMemories(options) {
1845
+ return (await retrieveMemorySelection(options)).included;
1846
+ }
1847
+ async function retrieveMemorySelection(options) {
1848
+ const architectures = inferProjectArchitectures(options.cwd, options.config);
1849
+ const retrievalEngine = options.retrievalEngine ?? getDefaultApplicationContainer().services.retrievalEngine;
1850
+ const result = await retrievalEngine.retrieve({
1851
+ text: options.query,
1852
+ architectures,
1853
+ limit: options.limit ?? 15
1854
+ });
1855
+ const memories = result.items.map((memory) => ({
1856
+ id: memory.id,
1857
+ type: memory.type,
1858
+ scope: memory.scope,
1859
+ architecture: memory.architecture,
1860
+ project_name: memory.projectName,
1861
+ title: memory.title,
1862
+ content: memory.content,
1863
+ reason: memory.reason,
1864
+ context: memory.context,
1865
+ tags: memory.tags ?? [],
1866
+ content_hash: memory.contentHash,
1867
+ similarity: memory.similarity
1868
+ }));
1869
+ return explainMemorySelection(memories, options.config, options.cwd);
1870
+ }
1871
+
1872
+ // src/generator.ts
1873
+ var __filename = fileURLToPath(import.meta.url);
1874
+ var __dirname = dirname3(__filename);
1875
+ var PKG_ROOT = join6(__dirname, "..");
1876
+ var OUTPUT_FILES = [
1877
+ { template: "CLAUDE.md.hbs", path: "CLAUDE.md", agent: "Claude Code" },
1878
+ { template: "copilot-instructions.md.hbs", path: ".github/copilot-instructions.md", agent: "GitHub Copilot" },
1879
+ { template: "cursorrules.hbs", path: ".cursorrules", agent: "Cursor" },
1880
+ { template: "cursor-rule.mdc.hbs", path: ".cursor/rules/memory-core.mdc", agent: "Cursor" },
1881
+ { template: "windsurfrules.hbs", path: ".windsurfrules", agent: "Windsurf" },
1882
+ { template: "clinerules.hbs", path: ".clinerules", agent: "Cline" },
1883
+ { template: "roo-rule.md.hbs", path: ".roo/rules/memory-core.md", agent: "Roo Code" },
1884
+ { template: "aider.conf.yml.hbs", path: ".aider.conf.yml", agent: "Aider" },
1885
+ { template: "continue-config.json.hbs", path: ".continue/config.json", agent: "Continue.dev", skipIfExists: true },
1886
+ { template: "DEVIN.md.hbs", path: "DEVIN.md", agent: "Devin" },
1887
+ { template: "amazonq-guidelines.md.hbs", path: ".amazonq/dev/guidelines.md", agent: "Amazon Q" },
1888
+ { template: "gemini-styleguide.md.hbs", path: ".gemini/styleguide.md", agent: "Gemini Code Assist" },
1889
+ { template: "zed-settings.json.hbs", path: ".zed/settings.json", agent: "Zed AI", skipIfExists: true },
1890
+ { template: "jetbrains-ai.md.hbs", path: ".idea/ai-instructions.md", agent: "JetBrains AI" },
1891
+ { template: "AGENTS.md.hbs", path: "AGENTS.md", agent: "OpenHands" },
1892
+ { template: "AI_RULES.md.hbs", path: "AI_RULES.md", agent: "Shared" },
1893
+ { template: "ARCHITECTURE.md.hbs", path: "ARCHITECTURE.md", agent: "Shared" },
1894
+ { template: "PROJECT_MEMORY.md.hbs", path: "PROJECT_MEMORY.md", agent: "Shared" }
1895
+ ];
1896
+ var AGENT_NAMES = [...new Set(OUTPUT_FILES.map((f) => f.agent))];
1897
+ Handlebars.registerHelper(
1898
+ "join",
1899
+ (arr, sep) => Array.isArray(arr) ? arr.join(sep) : ""
1900
+ );
1901
+ Handlebars.registerHelper(
1902
+ "bullet",
1903
+ (arr) => Array.isArray(arr) ? arr.map((i) => `- ${i}`).join("\n") : ""
1904
+ );
1905
+ Handlebars.registerHelper(
1906
+ "numbered",
1907
+ (arr) => Array.isArray(arr) ? arr.map((i, idx) => `${idx + 1}. ${i}`).join("\n") : ""
1908
+ );
1909
+ Handlebars.registerHelper("json", (val) => JSON.stringify(val, null, 2));
1910
+ Handlebars.registerHelper("memoryBlock", (memory) => {
1911
+ const meta = [memory.type, memory.architecture].filter(Boolean).join(" \xB7 ");
1912
+ const label = memory.title ? `${memory.title}: ${memory.content}` : memory.content;
1913
+ const lines = [`- [${meta || "memory"}] ${label}`];
1914
+ if (memory.reason) lines.push(` Why: ${memory.reason}`);
1915
+ if (memory.context?.appliesTo?.length) lines.push(` Use when: ${memory.context.appliesTo.join("; ")}`);
1916
+ if (memory.context?.avoidWhen?.length) lines.push(` Avoid when: ${memory.context.avoidWhen.join("; ")}`);
1917
+ if (memory.context?.examples?.length) {
1918
+ lines.push(" Examples:");
1919
+ for (const example of memory.context.examples) lines.push(` - ${example}`);
1920
+ }
1921
+ if (memory.tags?.length) lines.push(` Tags: ${memory.tags.join(", ")}`);
1922
+ if (memory.project_name || memory.context?.source) {
1923
+ lines.push(` Source: ${memory.context?.source ?? memory.project_name}`);
1924
+ }
1925
+ return new Handlebars.SafeString(lines.join("\n"));
1926
+ });
1927
+ function loadProfile(name) {
1928
+ const profilePath = join6(PKG_ROOT, "profiles", `${name}.yml`);
1929
+ if (!existsSync5(profilePath)) throw new Error(`Profile not found: ${name}`);
1930
+ return yaml.load(readFileSync4(profilePath, "utf-8"));
1931
+ }
1932
+ function listProfiles(layer) {
1933
+ const files = readdirSync2(join6(PKG_ROOT, "profiles")).filter((f) => f.endsWith(".yml"));
1934
+ const all = files.map((f) => yaml.load(readFileSync4(join6(PKG_ROOT, "profiles", f), "utf-8")));
1935
+ if (!layer) return all;
1936
+ if (layer === "backend") return all.filter((p) => p.layer === "backend" || p.layer === "fullstack");
1937
+ if (layer === "frontend") return all.filter((p) => p.layer === "frontend" || p.layer === "fullstack");
1938
+ return all;
1939
+ }
1940
+ function buildTemplateData(options, cwd = process.cwd()) {
1941
+ const backend = options.backendArchitecture ? loadProfile(options.backendArchitecture) : null;
1942
+ const frontend = options.frontendFramework ? loadProfile(options.frontendFramework) : null;
1943
+ const dedupedMemories = filterRelevantMemories(options.memories, {
1944
+ projectType: options.projectType,
1945
+ backendArchitecture: options.backendArchitecture,
1946
+ frontendFramework: options.frontendFramework,
1947
+ language: options.language
1948
+ }, cwd);
1949
+ const allRules = [
1950
+ ...backend?.rules ?? [],
1951
+ ...frontend?.rules ?? []
1952
+ ];
1953
+ const allFolders = [
1954
+ ...backend?.folders ?? [],
1955
+ ...frontend?.folders ?? []
1956
+ ];
1957
+ const allAvoid = [
1958
+ ...backend?.avoid ?? [],
1959
+ ...frontend?.avoid ?? []
1960
+ ];
1961
+ const archLabel = [
1962
+ backend ? `Backend: ${backend.displayName}` : null,
1963
+ frontend ? `Frontend: ${frontend.displayName}` : null
1964
+ ].filter(Boolean).join(" \xB7 ");
801
1965
  return {
802
- provider,
803
- model,
804
- ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
805
- apiKey: process.env.CHAT_API_KEY ?? ""
1966
+ projectName: options.projectName,
1967
+ projectType: options.projectType,
1968
+ isBackend: options.projectType === "backend" || options.projectType === "fullstack",
1969
+ isFrontend: options.projectType === "frontend" || options.projectType === "fullstack",
1970
+ isFullstack: options.projectType === "fullstack",
1971
+ // backend
1972
+ hasBackend: !!backend,
1973
+ backendArchitecture: backend?.displayName,
1974
+ backendDescription: backend?.description,
1975
+ backendRules: backend?.rules ?? [],
1976
+ backendFolders: backend?.folders ?? [],
1977
+ backendAvoid: backend?.avoid ?? [],
1978
+ // frontend
1979
+ hasFrontend: !!frontend,
1980
+ frontendFramework: frontend?.displayName,
1981
+ frontendDescription: frontend?.description,
1982
+ frontendRules: frontend?.rules ?? [],
1983
+ frontendFolders: frontend?.folders ?? [],
1984
+ frontendAvoid: frontend?.avoid ?? [],
1985
+ // combined — used by simple templates
1986
+ architecture: archLabel,
1987
+ rules: allRules,
1988
+ folders: allFolders,
1989
+ avoid: allAvoid,
1990
+ description: [backend?.description, frontend?.description].filter(Boolean).join(" | "),
1991
+ // memories
1992
+ memories: dedupedMemories,
1993
+ hasMemories: dedupedMemories.length > 0,
1994
+ // misc
1995
+ language: options.language,
1996
+ caveman: options.caveman,
1997
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
806
1998
  };
807
1999
  }
808
- async function callOllama(cfg, messages) {
809
- const res = await fetch(`${cfg.ollamaUrl}/api/chat`, {
810
- method: "POST",
811
- headers: { "Content-Type": "application/json" },
812
- body: JSON.stringify({ model: cfg.model, messages, stream: false, format: "json" })
813
- });
814
- if (!res.ok) {
815
- const body = await res.text();
816
- if (body.includes("not found") || body.includes("model")) {
817
- throw new Error(`MODEL_NOT_FOUND:${cfg.model}`);
2000
+ function renderTemplate(templateName, data) {
2001
+ const templatePath = join6(PKG_ROOT, "templates", templateName);
2002
+ if (!existsSync5(templatePath)) throw new Error(`Template not found: ${templateName}`);
2003
+ return Handlebars.compile(readFileSync4(templatePath, "utf-8"))(data);
2004
+ }
2005
+ function writeFile(filePath, content) {
2006
+ const dir = dirname3(filePath);
2007
+ if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
2008
+ if (existsSync5(filePath)) {
2009
+ const existing = readFileSync4(filePath, "utf-8");
2010
+ if (existing === content) return "skipped";
2011
+ }
2012
+ writeFileSync2(filePath, content, "utf-8");
2013
+ return "written";
2014
+ }
2015
+ async function generate(options, cwd = process.cwd(), onlyAgents) {
2016
+ const data = buildTemplateData(options, cwd);
2017
+ const written = [];
2018
+ const skipped = [];
2019
+ const files = onlyAgents ? OUTPUT_FILES.filter((f) => onlyAgents.includes(f.agent)) : OUTPUT_FILES;
2020
+ for (const output of files) {
2021
+ const targetPath = join6(cwd, output.path);
2022
+ if (output.skipIfExists && existsSync5(targetPath)) {
2023
+ skipped.push(output.path);
2024
+ continue;
2025
+ }
2026
+ try {
2027
+ const content = renderTemplate(output.template, data);
2028
+ const result = writeFile(targetPath, content);
2029
+ if (result === "written") written.push(output.path);
2030
+ else skipped.push(output.path);
2031
+ } catch (err) {
2032
+ if (!(err instanceof Error && err.message.includes("Template not found"))) throw err;
818
2033
  }
819
- throw new Error(body);
820
2034
  }
821
- const data = await res.json();
822
- return data.message.content.trim();
2035
+ return { written, skipped };
823
2036
  }
824
- async function callOpenAI(cfg, messages) {
825
- const res = await fetch("https://api.openai.com/v1/chat/completions", {
826
- method: "POST",
827
- headers: {
828
- "Content-Type": "application/json",
829
- "Authorization": `Bearer ${cfg.apiKey}`
830
- },
831
- body: JSON.stringify({
832
- model: cfg.model,
833
- messages,
834
- response_format: { type: "json_object" }
835
- })
836
- });
837
- if (!res.ok) throw new Error(`OpenAI API error ${res.status}: ${await res.text()}`);
838
- const data = await res.json();
839
- return data.choices[0].message.content.trim();
2037
+
2038
+ // src/modules/rule-engine/infrastructure/ast-deterministic-violations.ts
2039
+ import { resolve as resolve3 } from "path";
2040
+ function asPosix5(value) {
2041
+ return value.replace(/\\/g, "/");
840
2042
  }
841
- async function callAnthropic(cfg, messages) {
842
- const system = messages.find((m) => m.role === "system")?.content ?? "";
843
- const userMessages = messages.filter((m) => m.role !== "system");
844
- const res = await fetch("https://api.anthropic.com/v1/messages", {
845
- method: "POST",
846
- headers: {
847
- "Content-Type": "application/json",
848
- "x-api-key": cfg.apiKey,
849
- "anthropic-version": "2023-06-01"
850
- },
851
- body: JSON.stringify({
852
- model: cfg.model,
853
- max_tokens: 4096,
854
- system,
855
- messages: userMessages
856
- })
857
- });
858
- if (!res.ok) throw new Error(`Anthropic API error ${res.status}: ${await res.text()}`);
859
- const data = await res.json();
860
- return data.content[0].text.trim();
2043
+ function hasPath(value, pathSegment) {
2044
+ const normalized = asPosix5(value);
2045
+ const trimmed = pathSegment.startsWith("/") ? pathSegment.slice(1) : pathSegment;
2046
+ return normalized.includes(pathSegment) || normalized.includes(trimmed);
861
2047
  }
862
- async function callMiniMax(cfg, messages) {
863
- const res = await fetch("https://api.minimax.io/v1/chat/completions", {
864
- method: "POST",
865
- headers: {
866
- "Content-Type": "application/json",
867
- "Authorization": `Bearer ${cfg.apiKey}`
868
- },
869
- body: JSON.stringify({
870
- model: cfg.model,
871
- messages,
872
- response_format: { type: "json_object" }
873
- })
874
- });
875
- if (!res.ok) throw new Error(`MiniMax API error ${res.status}: ${await res.text()}`);
876
- const data = await res.json();
877
- return data.choices[0].message.content.trim();
2048
+ function isLegacyOrCompatibilitySpecifier(specifier) {
2049
+ const normalized = asPosix5(specifier);
2050
+ return normalized.includes("/compatibility/") || normalized.includes("compatibility/") || normalized.includes("legacy-");
878
2051
  }
879
- async function callChatModel(messages) {
880
- const cfg = getChatConfig();
881
- switch (cfg.provider) {
882
- case "openai":
883
- return callOpenAI(cfg, messages);
884
- case "anthropic":
885
- return callAnthropic(cfg, messages);
886
- case "minimax":
887
- return callMiniMax(cfg, messages);
888
- default:
889
- return callOllama(cfg, messages);
2052
+ function isLegacyOrCompatibilityPath(pathValue) {
2053
+ if (!pathValue) return false;
2054
+ const normalized = asPosix5(pathValue);
2055
+ return normalized.includes("/src/compatibility/") || normalized.includes("/legacy-");
2056
+ }
2057
+ function moduleNameFromPath2(pathValue) {
2058
+ const match = asPosix5(pathValue).match(/src\/modules\/([^/]+)\//);
2059
+ return match?.[1];
2060
+ }
2061
+ function isModulePublicPath(pathValue) {
2062
+ const normalized = asPosix5(pathValue);
2063
+ return /src\/modules\/[^/]+\/(public|api)\//.test(normalized) || /src\/modules\/[^/]+\/index\.(ts|tsx|js|jsx|mjs|cjs)$/.test(normalized);
2064
+ }
2065
+ function detectCleanLayer(pathValue) {
2066
+ const normalized = asPosix5(pathValue);
2067
+ if (hasPath(normalized, "/src/domain/") || hasPath(normalized, "/src/core/domain/")) return "domain";
2068
+ if (hasPath(normalized, "/src/application/") || hasPath(normalized, "/src/core/application/")) return "application";
2069
+ if (hasPath(normalized, "/src/infrastructure/")) return "infrastructure";
2070
+ if (hasPath(normalized, "/src/interfaces/")) return "interface";
2071
+ return "unknown";
2072
+ }
2073
+ function detectHexLayer(pathValue) {
2074
+ const normalized = asPosix5(pathValue);
2075
+ if (hasPath(normalized, "/src/core/")) return "core";
2076
+ if (hasPath(normalized, "/src/adapters/inbound/")) return "adapter-inbound";
2077
+ if (hasPath(normalized, "/src/adapters/outbound/")) return "adapter-outbound";
2078
+ if (hasPath(normalized, "/src/adapters/")) return "adapter-other";
2079
+ return "unknown";
2080
+ }
2081
+ function activeArchitectures(config2, rules = []) {
2082
+ const names = /* @__PURE__ */ new Set();
2083
+ if (config2?.backendArchitecture) names.add(config2.backendArchitecture);
2084
+ if (config2?.frontendFramework) names.add(config2.frontendFramework);
2085
+ const text = rules.join("\n").toLowerCase();
2086
+ if (text.includes("modular monolith")) names.add("modular-monolith");
2087
+ if (text.includes("clean architecture")) names.add("clean-architecture");
2088
+ if (text.includes("hexagonal")) names.add("hexagonal");
2089
+ return names;
2090
+ }
2091
+ function pushUnique(target, incoming) {
2092
+ if (target.some(
2093
+ (entry) => entry.rule === incoming.rule && entry.file === incoming.file && entry.line === incoming.line && entry.issue === incoming.issue
2094
+ )) {
2095
+ return;
890
2096
  }
2097
+ target.push(incoming);
891
2098
  }
892
- function getChatProviderLabel() {
893
- const cfg = getChatConfig();
894
- if (cfg.provider === "ollama") return `ollama (${cfg.model})`;
895
- return `${cfg.provider} (${cfg.model})`;
2099
+ function evaluateFile(file, options) {
2100
+ const cwd = options.cwd ?? process.cwd();
2101
+ const rules = options.rules ?? [];
2102
+ const architectures = activeArchitectures(options.config, rules);
2103
+ const reasonLookup = options.reasonLookup ?? /* @__PURE__ */ new Map();
2104
+ const violations = [];
2105
+ const absFile = resolve3(cwd, file);
2106
+ const normalizedFile = asPosix5(file);
2107
+ const imports = collectResolvedImports(absFile, cwd);
2108
+ const fromCleanLayer = detectCleanLayer(normalizedFile);
2109
+ const fromHexLayer = detectHexLayer(normalizedFile);
2110
+ for (const imp of imports) {
2111
+ const target = imp.resolvedPath ? asPosix5(imp.resolvedPath) : void 0;
2112
+ if (isLegacyOrCompatibilitySpecifier(imp.specifier) || isLegacyOrCompatibilityPath(target)) {
2113
+ const rule = "Application code must not import compatibility or legacy adapter paths";
2114
+ pushUnique(violations, {
2115
+ rule,
2116
+ file,
2117
+ line: imp.line,
2118
+ issue: `Import references a removed migration path: ${imp.specifier}`,
2119
+ suggestion: "Import module services/ports from src/app, src/modules, src/shared/ports, or current infrastructure adapters.",
2120
+ reason: reasonLookup.get(rule)
2121
+ });
2122
+ }
2123
+ if (architectures.has("modular-monolith")) {
2124
+ const fromModule = moduleNameFromPath2(normalizedFile);
2125
+ const toModule = target ? moduleNameFromPath2(target) : void 0;
2126
+ if (fromModule && toModule && target && fromModule !== toModule && !isModulePublicPath(target)) {
2127
+ const rule = "Modules communicate only through public interfaces or events \u2014 never by importing internals";
2128
+ pushUnique(violations, {
2129
+ rule,
2130
+ file,
2131
+ line: imp.line,
2132
+ issue: `Cross-module import from "${fromModule}" to private path in module "${toModule}"`,
2133
+ suggestion: `Expose a public API from src/modules/${toModule}/index.ts (or public/) and import through it.`,
2134
+ reason: reasonLookup.get(rule)
2135
+ });
2136
+ }
2137
+ }
2138
+ if (architectures.has("clean-architecture")) {
2139
+ const toCleanLayer = target ? detectCleanLayer(target) : "unknown";
2140
+ if (fromCleanLayer === "domain" && ["application", "infrastructure", "interface"].includes(toCleanLayer)) {
2141
+ const rule = "Entities encapsulate core business logic and have no external dependencies";
2142
+ pushUnique(violations, {
2143
+ rule,
2144
+ file,
2145
+ line: imp.line,
2146
+ issue: `Domain layer imports ${toCleanLayer} layer: ${imp.specifier}`,
2147
+ suggestion: "Keep domain isolated and move orchestration concerns into application layer.",
2148
+ reason: reasonLookup.get(rule)
2149
+ });
2150
+ }
2151
+ if (fromCleanLayer === "application" && (toCleanLayer === "infrastructure" || toCleanLayer === "interface")) {
2152
+ const rule = "Infrastructure layer (DB, HTTP, queues) depends on application \u2014 never the reverse";
2153
+ pushUnique(violations, {
2154
+ rule,
2155
+ file,
2156
+ line: imp.line,
2157
+ issue: `Application layer imports ${toCleanLayer} layer: ${imp.specifier}`,
2158
+ suggestion: "Invert dependency via repository/port interface in application layer.",
2159
+ reason: reasonLookup.get(rule)
2160
+ });
2161
+ }
2162
+ if (fromCleanLayer === "interface" && toCleanLayer === "infrastructure") {
2163
+ const rule = "Controllers must only validate input and delegate to use cases";
2164
+ pushUnique(violations, {
2165
+ rule,
2166
+ file,
2167
+ line: imp.line,
2168
+ issue: `Interface/controller layer imports infrastructure directly: ${imp.specifier}`,
2169
+ suggestion: "Delegate to application use cases instead of calling infrastructure directly.",
2170
+ reason: reasonLookup.get(rule)
2171
+ });
2172
+ }
2173
+ if (fromCleanLayer === "domain" && imp.isExternal && isExternalFrameworkSpecifier(imp.specifier)) {
2174
+ const rule = "Domain layer must not import any framework or library code";
2175
+ pushUnique(violations, {
2176
+ rule,
2177
+ file,
2178
+ line: imp.line,
2179
+ issue: `Domain file imports framework package: ${imp.specifier}`,
2180
+ suggestion: "Keep domain pure. Move framework-specific logic to infrastructure/adapters.",
2181
+ reason: reasonLookup.get(rule)
2182
+ });
2183
+ }
2184
+ }
2185
+ if (architectures.has("hexagonal")) {
2186
+ const toHexLayer = target ? detectHexLayer(target) : "unknown";
2187
+ if (fromHexLayer === "core" && (toHexLayer === "adapter-inbound" || toHexLayer === "adapter-outbound" || toHexLayer === "adapter-other")) {
2188
+ const rule = "Direct imports of adapter code inside the core";
2189
+ pushUnique(violations, {
2190
+ rule,
2191
+ file,
2192
+ line: imp.line,
2193
+ issue: `Core imports adapter path directly: ${imp.specifier}`,
2194
+ suggestion: "Define a core port and resolve adapter at composition root.",
2195
+ reason: reasonLookup.get(rule)
2196
+ });
2197
+ }
2198
+ const crossAdapterBoundary = fromHexLayer === "adapter-inbound" && (toHexLayer === "adapter-outbound" || toHexLayer === "adapter-other") || fromHexLayer === "adapter-outbound" && (toHexLayer === "adapter-inbound" || toHexLayer === "adapter-other");
2199
+ if (crossAdapterBoundary) {
2200
+ const rule = "Adapters implement ports \u2014 one adapter per external system (DB, HTTP, queue, etc.)";
2201
+ pushUnique(violations, {
2202
+ rule,
2203
+ file,
2204
+ line: imp.line,
2205
+ issue: `Adapter imports another adapter layer directly: ${imp.specifier}`,
2206
+ suggestion: "Route adapter collaboration through core ports/use-cases, not direct adapter imports.",
2207
+ reason: reasonLookup.get(rule)
2208
+ });
2209
+ }
2210
+ }
2211
+ }
2212
+ return violations;
2213
+ }
2214
+ function findAstDeterministicViolationsForFile(file, options = {}) {
2215
+ return evaluateFile(file, options);
2216
+ }
2217
+ function findAstDeterministicViolationsForDiff(diff, options = {}) {
2218
+ const files = parseChangedFilesFromDiff(diff).filter((file) => /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file));
2219
+ const violations = [];
2220
+ for (const file of files) {
2221
+ for (const violation of evaluateFile(file, options)) {
2222
+ pushUnique(violations, violation);
2223
+ }
2224
+ }
2225
+ const architectures = activeArchitectures(options.config, options.rules ?? []);
2226
+ if (architectures.has("modular-monolith")) {
2227
+ const edges = buildModuleDependencyEdges(files, options.cwd ?? process.cwd());
2228
+ const cycles = detectModuleCycles(edges);
2229
+ for (const cycle of cycles) {
2230
+ const representative = edges.find((edge) => edge.fromModule === cycle[0] && edge.toModule === cycle[1]);
2231
+ if (!representative) continue;
2232
+ const rule = "No circular dependencies between modules";
2233
+ pushUnique(violations, {
2234
+ rule,
2235
+ file: representative.file,
2236
+ line: representative.line,
2237
+ issue: `Module dependency cycle detected: ${cycle.join(" -> ")}`,
2238
+ suggestion: "Break the cycle by introducing a public port/event or moving shared logic into src/shared.",
2239
+ reason: options.reasonLookup?.get(rule)
2240
+ });
2241
+ }
2242
+ }
2243
+ return violations;
896
2244
  }
897
2245
 
898
2246
  // src/watcher.ts
899
- import { watch } from "chokidar";
900
- import { spawnSync } from "child_process";
901
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
902
- import { join as join3, relative } from "path";
903
- import chalk from "chalk";
904
2247
  function getFileLines(filePath) {
905
2248
  try {
906
- return readFileSync3(filePath, "utf-8").split("\n");
2249
+ return readFileSync5(filePath, "utf-8").split("\n");
907
2250
  } catch {
908
2251
  return [];
909
2252
  }
@@ -937,16 +2280,16 @@ function formatCodeContext(filePath, line, contextLines = 2) {
937
2280
  return `${lineNum} ${marker} ${lines[current]}`;
938
2281
  }).join("\n");
939
2282
  }
940
- var SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
2283
+ var SOURCE_EXTENSIONS3 = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
941
2284
  var reasonMap = new Map(
942
2285
  seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
943
2286
  );
944
2287
  function recordViolations(violations) {
945
- const statsPath = join3(process.cwd(), ".memory-core-stats.json");
2288
+ const statsPath = join7(process.cwd(), ".memory-core-stats.json");
946
2289
  let stats = { rules: {}, files: {} };
947
- if (existsSync3(statsPath)) {
2290
+ if (existsSync6(statsPath)) {
948
2291
  try {
949
- stats = JSON.parse(readFileSync3(statsPath, "utf-8"));
2292
+ stats = JSON.parse(readFileSync5(statsPath, "utf-8"));
950
2293
  } catch {
951
2294
  stats = { rules: {}, files: {} };
952
2295
  }
@@ -960,29 +2303,29 @@ function recordViolations(violations) {
960
2303
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
961
2304
  const recent = violations.map((violation) => ({ ...violation, timestamp, source: "watch" }));
962
2305
  stats.recentViolations = [...recent, ...stats.recentViolations ?? []].slice(0, 50);
963
- writeFileSync2(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
2306
+ writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
964
2307
  }
965
2308
  function loadConfig(cwd) {
966
- const configPath = join3(cwd, ".memory-core.json");
967
- if (!existsSync3(configPath)) return null;
2309
+ const configPath = join7(cwd, ".memory-core.json");
2310
+ if (!existsSync6(configPath)) return null;
968
2311
  try {
969
- return JSON.parse(readFileSync3(configPath, "utf-8"));
2312
+ return JSON.parse(readFileSync5(configPath, "utf-8"));
970
2313
  } catch {
971
2314
  return null;
972
2315
  }
973
2316
  }
974
- function getProfileRules(config) {
2317
+ function getProfileRules(config2) {
975
2318
  const rules = [];
976
2319
  const avoids = [];
977
- if (config.backendArchitecture) {
978
- const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
2320
+ if (config2.backendArchitecture) {
2321
+ const profile = listProfiles("backend").find((p) => p.name === config2.backendArchitecture);
979
2322
  if (profile) {
980
2323
  rules.push(...profile.rules);
981
2324
  avoids.push(...profile.avoid);
982
2325
  }
983
2326
  }
984
- if (config.frontendFramework) {
985
- const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
2327
+ if (config2.frontendFramework) {
2328
+ const profile = listProfiles("frontend").find((p) => p.name === config2.frontendFramework);
986
2329
  if (profile) {
987
2330
  rules.push(...profile.rules);
988
2331
  avoids.push(...profile.avoid);
@@ -990,19 +2333,19 @@ function getProfileRules(config) {
990
2333
  }
991
2334
  return { rules, avoids };
992
2335
  }
993
- async function loadRelevantRules(config, rel, diff, fallbackRules) {
2336
+ async function loadRelevantRules(config2, rel, diff, fallbackRules) {
994
2337
  try {
995
2338
  const query = buildContextQuery([
996
2339
  rel,
997
2340
  diff.slice(0, 1200),
998
- config.backendArchitecture,
999
- config.frontendFramework,
1000
- config.language
2341
+ config2.backendArchitecture,
2342
+ config2.frontendFramework,
2343
+ config2.language
1001
2344
  ]);
1002
2345
  const memories = await retrieveContextualMemories({
1003
2346
  query,
1004
2347
  cwd: process.cwd(),
1005
- config,
2348
+ config: config2,
1006
2349
  limit: 15
1007
2350
  });
1008
2351
  const selected = memories.filter((memory) => ["rule", "pattern", "decision"].includes(memory.type)).map((memory) => memory.content);
@@ -1057,16 +2400,15 @@ ${JSON.stringify(violations, null, 2)}`;
1057
2400
  }
1058
2401
  async function loadIgnorePatterns() {
1059
2402
  try {
1060
- const { listMemories, closePool } = await import("./db-MF3VKVKH.js");
1061
- const ignores = await listMemories({ type: "ignore", limit: 1e3 });
1062
- await closePool();
2403
+ const app = getDefaultApplicationContainer();
2404
+ const ignores = await app.services.memoryEngine.list({ type: "ignore", limit: 1e3 });
1063
2405
  return ignores.map((ignore) => ignore.content);
1064
2406
  } catch {
1065
2407
  return [];
1066
2408
  }
1067
2409
  }
1068
- async function checkFile(filePath, cwd, config, verbose, debug) {
1069
- const rel = relative(cwd, filePath);
2410
+ async function checkFile(filePath, cwd, config2, verbose, debug) {
2411
+ const rel = relative2(cwd, filePath);
1070
2412
  let diff;
1071
2413
  const headResult = spawnSync("git", ["diff", "HEAD", "--", rel], { encoding: "utf-8", cwd });
1072
2414
  if (headResult.stdout?.trim()) {
@@ -1076,8 +2418,8 @@ async function checkFile(filePath, cwd, config, verbose, debug) {
1076
2418
  diff = noIndexResult.stdout ?? "";
1077
2419
  }
1078
2420
  if (!diff.trim()) return { type: "skipped", reason: "No changes compared with HEAD" };
1079
- const { rules: fallbackRules, avoids } = getProfileRules(config);
1080
- const rules = await loadRelevantRules(config, rel, diff, fallbackRules);
2421
+ const { rules: fallbackRules, avoids } = getProfileRules(config2);
2422
+ const rules = await loadRelevantRules(config2, rel, diff, fallbackRules);
1081
2423
  if (rules.length === 0) return { type: "skipped", reason: "No applicable architecture rules" };
1082
2424
  const MAX_DIFF = 6e3;
1083
2425
  const truncated = diff.length > MAX_DIFF;
@@ -1091,7 +2433,16 @@ async function checkFile(filePath, cwd, config, verbose, debug) {
1091
2433
  return why ? `${i + 1}. ${r}
1092
2434
  WHY: ${why}` : `${i + 1}. ${r}`;
1093
2435
  }).join("\n");
1094
- const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...await loadIgnorePatterns()])];
2436
+ const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config2), ...await loadIgnorePatterns()])];
2437
+ const astViolations = findAstDeterministicViolationsForFile(rel, {
2438
+ cwd,
2439
+ config: config2,
2440
+ rules,
2441
+ reasonLookup: reasonMap
2442
+ }).map((violation) => ({
2443
+ ...violation,
2444
+ severity: "error"
2445
+ }));
1095
2446
  const systemPrompt = `You are a strict code reviewer enforcing architecture rules.
1096
2447
  Analyze the file diff and identify ONLY clear, definite rule violations.
1097
2448
  Use the WHY for each rule to understand intent and judge edge cases.
@@ -1143,6 +2494,7 @@ ${diffToSend}` }
1143
2494
  violations = [];
1144
2495
  }
1145
2496
  violations = await verifyViolations(diff, violations, allowPatterns, debug);
2497
+ violations = [...astViolations, ...violations];
1146
2498
  violations = applyAllowPatterns(violations, allowPatterns);
1147
2499
  violations = violations.map((violation) => ({
1148
2500
  ...violation,
@@ -1163,7 +2515,7 @@ ${diffToSend}` }
1163
2515
  console.log(chalk.yellow(" Rule: ") + v.rule);
1164
2516
  const why = v.reason ?? reasonMap.get(v.rule);
1165
2517
  if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
1166
- if (v.line && existsSync3(filePath)) {
2518
+ if (v.line && existsSync6(filePath)) {
1167
2519
  printCodeContext(filePath, v.line, 1);
1168
2520
  }
1169
2521
  if (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
@@ -1175,20 +2527,47 @@ ${diffToSend}` }
1175
2527
  console.log();
1176
2528
  return { type: "checked", violations };
1177
2529
  } catch (err) {
1178
- if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
1179
- return { type: "error", message: `Check model unreachable for ${rel}: ${err.message}` };
1180
- }
1181
- if (verbose) {
1182
- console.log(chalk.yellow(` \u26A0 Check failed for ${rel}: ${err.message}`));
2530
+ const aiUnavailable = err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED");
2531
+ const message = aiUnavailable ? `Model unreachable for ${rel}; using deterministic checks only.` : `AI check failed for ${rel}; using deterministic checks only.`;
2532
+ console.log(chalk.yellow(` \u26A0 ${message}`));
2533
+ let violations = applyAllowPatterns(astViolations, allowPatterns);
2534
+ violations = violations.map((violation) => ({
2535
+ ...violation,
2536
+ code: violation.code ?? (violation.line ? formatCodeContext(filePath, violation.line, 1) : void 0)
2537
+ }));
2538
+ if (violations.length === 0) {
2539
+ console.log(chalk.green(` \u2713 ${rel}`) + chalk.dim(" \u2014 no deterministic violations"));
2540
+ return { type: "checked", violations: [] };
1183
2541
  }
1184
- return { type: "error", message: `Check failed for ${rel}: ${err.message}` };
2542
+ console.log(
2543
+ chalk.red.bold(`
2544
+ \u2717 ${violations.length} deterministic violation${violations.length > 1 ? "s" : ""} in ${rel}
2545
+ `)
2546
+ );
2547
+ violations.forEach((v, i) => {
2548
+ const loc = v.line ? `${v.file ?? rel}:${v.line}` : v.file ?? rel;
2549
+ console.log(chalk.bold(` [${i + 1}] ${loc}`));
2550
+ console.log(chalk.yellow(" Rule: ") + v.rule);
2551
+ const why = v.reason ?? reasonMap.get(v.rule);
2552
+ if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
2553
+ if (v.line && existsSync6(filePath)) {
2554
+ printCodeContext(filePath, v.line, 1);
2555
+ }
2556
+ if (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
2557
+ if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
2558
+ console.log();
2559
+ });
2560
+ recordViolations(violations);
2561
+ console.log(chalk.dim(' Fix violations or run: memory-core remember "<lesson>"'));
2562
+ console.log();
2563
+ return { type: "checked", violations };
1185
2564
  }
1186
2565
  }
1187
2566
  async function startWatch(options = {}) {
1188
2567
  const cwd = process.cwd();
1189
- const config = loadConfig(cwd);
2568
+ const config2 = loadConfig(cwd);
1190
2569
  const exitOnSetupFailure = options.exitOnSetupFailure ?? true;
1191
- if (!config) {
2570
+ if (!config2) {
1192
2571
  const message = "No .memory-core.json found. Run: memory-core init";
1193
2572
  console.error(chalk.red(`
1194
2573
  ${message}
@@ -1197,7 +2576,7 @@ async function startWatch(options = {}) {
1197
2576
  if (exitOnSetupFailure) process.exit(1);
1198
2577
  return;
1199
2578
  }
1200
- const { rules } = getProfileRules(config);
2579
+ const { rules } = getProfileRules(config2);
1201
2580
  if (rules.length === 0) {
1202
2581
  const message = "No architecture rules configured in .memory-core.json \u2014 nothing to watch.";
1203
2582
  console.log(chalk.yellow(`
@@ -1237,16 +2616,16 @@ async function startWatch(options = {}) {
1237
2616
  const keepAlive = setInterval(() => {
1238
2617
  }, 1 << 30);
1239
2618
  const handle = (filePath) => {
1240
- if (!SOURCE_EXTENSIONS.test(filePath)) return;
2619
+ if (!SOURCE_EXTENSIONS3.test(filePath)) return;
1241
2620
  if (pending.has(filePath)) clearTimeout(pending.get(filePath));
1242
2621
  const timer = setTimeout(async () => {
1243
2622
  pending.delete(filePath);
1244
- const rel = relative(cwd, filePath);
2623
+ const rel = relative2(cwd, filePath);
1245
2624
  const timestamp = /* @__PURE__ */ new Date();
1246
2625
  console.log(chalk.dim(`
1247
2626
  [${timestamp.toLocaleTimeString()}] saved: ${rel}`));
1248
2627
  options.onEvent?.({ type: "saved", timestamp: timestamp.toISOString(), file: rel });
1249
- const result = await checkFile(filePath, cwd, config, options.verbose ?? false, options.debug ?? false);
2628
+ const result = await checkFile(filePath, cwd, config2, options.verbose ?? false, options.debug ?? false);
1250
2629
  if (result.type === "skipped") {
1251
2630
  options.onEvent?.({ type: "skipped", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, reason: result.reason });
1252
2631
  return;
@@ -1282,8 +2661,23 @@ async function startWatch(options = {}) {
1282
2661
 
1283
2662
  export {
1284
2663
  detectProject,
1285
- retrieve,
1286
- dedupeMemories,
2664
+ Config,
2665
+ embed,
2666
+ callChatModel,
2667
+ getChatProviderLabel,
2668
+ getPool,
2669
+ runMigrations,
2670
+ saveMemory,
2671
+ listMemories,
2672
+ deleteMemory,
2673
+ updateMemory,
2674
+ closePool,
2675
+ migrateGraphSnapshots,
2676
+ probeGraphSnapshotStore,
2677
+ seeds,
2678
+ findAstDeterministicViolationsForDiff,
2679
+ startWatch,
2680
+ getDefaultApplicationContainer,
1287
2681
  inferProjectArchitectures,
1288
2682
  getAllowPatterns,
1289
2683
  buildContextQuery,
@@ -1292,9 +2686,5 @@ export {
1292
2686
  OUTPUT_FILES,
1293
2687
  AGENT_NAMES,
1294
2688
  listProfiles,
1295
- generate,
1296
- seeds,
1297
- callChatModel,
1298
- getChatProviderLabel,
1299
- startWatch
2689
+ generate
1300
2690
  };