@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.
- package/README.md +15 -0
- package/dist/{chunk-J7SPACA3.js → chunk-PDQXIKL7.js} +1847 -457
- package/dist/cli.js +268 -68
- package/dist/dashboard/assets/index-C612Qqha.js +2 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/{dashboard-server-QMNMSEPJ.js → dashboard-server-AUX4BQP6.js} +141 -37
- package/package.json +1 -1
- package/dist/chunk-HAGRPKR3.js +0 -30
- package/dist/chunk-KSLFLWB4.js +0 -32
- package/dist/chunk-WUL7HLAA.js +0 -264
- package/dist/dashboard/assets/index-BuVM8CHT.js +0 -2
- package/dist/db-MF3VKVKH.js +0 -30
- package/dist/embedding-PAYD2JYW.js +0 -8
|
@@ -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/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
138
|
+
const data = await res.json();
|
|
139
|
+
return data.message.content.trim();
|
|
131
140
|
}
|
|
132
|
-
function
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
229
|
+
pool = new Pool({ connectionString: Config.databaseUrl });
|
|
167
230
|
}
|
|
168
|
-
return
|
|
231
|
+
return pool;
|
|
169
232
|
}
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
184
|
-
|
|
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
|
|
187
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
234
|
-
|
|
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
|
|
237
|
-
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
452
|
+
async function closePool() {
|
|
453
|
+
if (pool) {
|
|
454
|
+
await pool.end();
|
|
455
|
+
pool = null;
|
|
456
|
+
migrationsRun = false;
|
|
457
|
+
}
|
|
243
458
|
}
|
|
244
459
|
|
|
245
|
-
// src/
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
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
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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/
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
|
|
822
|
-
return data.message.content.trim();
|
|
2035
|
+
return { written, skipped };
|
|
823
2036
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
|
|
842
|
-
const
|
|
843
|
-
const
|
|
844
|
-
|
|
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
|
-
|
|
863
|
-
const
|
|
864
|
-
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
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
|
|
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
|
|
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 =
|
|
2288
|
+
const statsPath = join7(process.cwd(), ".memory-core-stats.json");
|
|
946
2289
|
let stats = { rules: {}, files: {} };
|
|
947
|
-
if (
|
|
2290
|
+
if (existsSync6(statsPath)) {
|
|
948
2291
|
try {
|
|
949
|
-
stats = JSON.parse(
|
|
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
|
-
|
|
2306
|
+
writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
964
2307
|
}
|
|
965
2308
|
function loadConfig(cwd) {
|
|
966
|
-
const configPath =
|
|
967
|
-
if (!
|
|
2309
|
+
const configPath = join7(cwd, ".memory-core.json");
|
|
2310
|
+
if (!existsSync6(configPath)) return null;
|
|
968
2311
|
try {
|
|
969
|
-
return JSON.parse(
|
|
2312
|
+
return JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
970
2313
|
} catch {
|
|
971
2314
|
return null;
|
|
972
2315
|
}
|
|
973
2316
|
}
|
|
974
|
-
function getProfileRules(
|
|
2317
|
+
function getProfileRules(config2) {
|
|
975
2318
|
const rules = [];
|
|
976
2319
|
const avoids = [];
|
|
977
|
-
if (
|
|
978
|
-
const profile = listProfiles("backend").find((p) => p.name ===
|
|
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 (
|
|
985
|
-
const profile = listProfiles("frontend").find((p) => p.name ===
|
|
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(
|
|
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
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
|
1061
|
-
const ignores = await
|
|
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,
|
|
1069
|
-
const rel =
|
|
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(
|
|
1080
|
-
const rules = await loadRelevantRules(
|
|
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(
|
|
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 &&
|
|
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
|
-
|
|
1179
|
-
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
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
|
|
2568
|
+
const config2 = loadConfig(cwd);
|
|
1190
2569
|
const exitOnSetupFailure = options.exitOnSetupFailure ?? true;
|
|
1191
|
-
if (!
|
|
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(
|
|
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 (!
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
1286
|
-
|
|
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
|
};
|