@shahmilsaari/memory-core 1.0.25 → 1.0.27
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 +75 -659
- package/dist/approval-queue-YBYRGBHP.js +7 -0
- package/dist/ast-analyzer-JM4CIOFY.js +44 -0
- package/dist/check-cache-6NWRTZJD.js +52 -0
- package/dist/check-logger-5HYSWA3S.js +21 -0
- package/dist/{chunk-35ZWQFTO.js → chunk-3XTHE74V.js} +239 -830
- package/dist/chunk-M7NKSXFS.js +301 -0
- package/dist/chunk-PQBWHAZN.js +156 -0
- package/dist/chunk-W6WEAV3S.js +69 -0
- package/dist/chunk-ZZBQEXEO.js +183 -0
- package/dist/classifier-MZ65R7FK.js +60 -0
- package/dist/cli.js +415 -14
- package/dist/confidence-gate-ZQDAOS6P.js +64 -0
- package/dist/dashboard/assets/index-Cd_SCzKw.js +2 -0
- package/dist/dashboard/assets/index-DAEzcad9.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/{dashboard-server-SSYZLQKB.js → dashboard-server-TWLZQE2J.js} +129 -17
- package/dist/db-PRDHI2CN.js +29 -0
- package/dist/deepseek-critique-MALVIYGF.js +82 -0
- package/dist/deterministic-validator-PP56B46I.js +18 -0
- package/dist/evidence-HVMSONTT.js +65 -0
- package/dist/graph-TFNTB5OK.js +98 -0
- package/dist/incident-capture-RVPZULS7.js +20 -0
- package/dist/ollama-judge-D2LFK5PB.js +137 -0
- package/dist/rate-limiter-SLIPCXRF.js +41 -0
- package/dist/rules-V3QMN3AR.js +95 -0
- package/dist/watch-errors-B3FA26N4.js +99 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-B7gd4JQc.js +0 -2
- package/dist/dashboard/assets/index-CHgjllWU.css +0 -1
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
callChatModel,
|
|
4
|
+
getChatProviderLabel
|
|
5
|
+
} from "./chunk-PQBWHAZN.js";
|
|
6
|
+
import {
|
|
7
|
+
Config,
|
|
8
|
+
deleteMemories,
|
|
9
|
+
deleteMemory,
|
|
10
|
+
getMemory,
|
|
11
|
+
getPool,
|
|
12
|
+
listMemories,
|
|
13
|
+
saveMemory,
|
|
14
|
+
searchMemories,
|
|
15
|
+
updateMemory,
|
|
16
|
+
upsertMemory
|
|
17
|
+
} from "./chunk-M7NKSXFS.js";
|
|
18
|
+
import {
|
|
19
|
+
buildModuleDependencyEdges,
|
|
20
|
+
collectResolvedImports,
|
|
21
|
+
detectModuleCycles,
|
|
22
|
+
isExternalFrameworkSpecifier,
|
|
23
|
+
parseChangedFilesFromDiff
|
|
24
|
+
} from "./chunk-ZZBQEXEO.js";
|
|
2
25
|
|
|
3
26
|
// src/project-detector.ts
|
|
4
27
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -62,33 +85,6 @@ function detectProject(cwd = process.cwd()) {
|
|
|
62
85
|
return { language: "Unknown", framework: "Unknown" };
|
|
63
86
|
}
|
|
64
87
|
|
|
65
|
-
// src/config.ts
|
|
66
|
-
import { config } from "dotenv";
|
|
67
|
-
import { existsSync as existsSync2 } from "fs";
|
|
68
|
-
import { join as join2 } from "path";
|
|
69
|
-
var localEnv = join2(process.cwd(), ".memory-core.env");
|
|
70
|
-
config({ path: existsSync2(localEnv) ? localEnv : join2(process.cwd(), ".env") });
|
|
71
|
-
var Config = {
|
|
72
|
-
get databaseUrl() {
|
|
73
|
-
return process.env.DATABASE_URL ?? "";
|
|
74
|
-
},
|
|
75
|
-
get ollamaUrl() {
|
|
76
|
-
return process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
77
|
-
},
|
|
78
|
-
get ollamaModel() {
|
|
79
|
-
return process.env.OLLAMA_MODEL ?? "nomic-embed-text";
|
|
80
|
-
},
|
|
81
|
-
get chatModel() {
|
|
82
|
-
return process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
|
|
83
|
-
},
|
|
84
|
-
get chatProvider() {
|
|
85
|
-
return process.env.CHAT_PROVIDER ?? "ollama";
|
|
86
|
-
},
|
|
87
|
-
get chatApiKey() {
|
|
88
|
-
return process.env.CHAT_API_KEY ?? "";
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
88
|
// src/embedding.ts
|
|
93
89
|
function getEmbeddingTimeoutMs() {
|
|
94
90
|
const raw = Number(process.env.EMBEDDING_TIMEOUT_MS ?? process.env.MEMORY_CORE_RETRIEVAL_TIMEOUT_MS ?? 5e3);
|
|
@@ -116,409 +112,6 @@ async function embed(text) {
|
|
|
116
112
|
return data.embedding;
|
|
117
113
|
}
|
|
118
114
|
|
|
119
|
-
// src/chat.ts
|
|
120
|
-
function getChatConfig() {
|
|
121
|
-
const provider = process.env.CHAT_PROVIDER ?? "ollama";
|
|
122
|
-
const model = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
|
|
123
|
-
return {
|
|
124
|
-
provider,
|
|
125
|
-
model,
|
|
126
|
-
ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
|
|
127
|
-
apiKey: process.env.CHAT_API_KEY ?? "",
|
|
128
|
-
baseUrl: process.env.CHAT_BASE_URL ?? ""
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
function getDefaultTimeoutMs() {
|
|
132
|
-
const raw = Number(process.env.CHAT_TIMEOUT_MS ?? 6e4);
|
|
133
|
-
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 6e4;
|
|
134
|
-
}
|
|
135
|
-
function timeoutSignal(timeoutMs) {
|
|
136
|
-
return AbortSignal.timeout(timeoutMs ?? getDefaultTimeoutMs());
|
|
137
|
-
}
|
|
138
|
-
function normalizeChatError(err, timeoutMs) {
|
|
139
|
-
const ms = timeoutMs ?? getDefaultTimeoutMs();
|
|
140
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
141
|
-
return new Error(`TIMEOUT:${ms}`);
|
|
142
|
-
}
|
|
143
|
-
return err instanceof Error ? err : new Error(String(err));
|
|
144
|
-
}
|
|
145
|
-
async function callOllama(cfg, messages, options = {}) {
|
|
146
|
-
const res = await fetch(`${cfg.ollamaUrl}/api/chat`, {
|
|
147
|
-
method: "POST",
|
|
148
|
-
headers: { "Content-Type": "application/json" },
|
|
149
|
-
signal: timeoutSignal(options.timeoutMs),
|
|
150
|
-
body: JSON.stringify({ model: cfg.model, messages, stream: false, format: "json" })
|
|
151
|
-
});
|
|
152
|
-
if (!res.ok) {
|
|
153
|
-
const body = await res.text();
|
|
154
|
-
if (body.includes("not found") || body.includes("model")) {
|
|
155
|
-
throw new Error(`MODEL_NOT_FOUND:${cfg.model}`);
|
|
156
|
-
}
|
|
157
|
-
throw new Error(body);
|
|
158
|
-
}
|
|
159
|
-
const data = await res.json();
|
|
160
|
-
const inputTokens = data.prompt_eval_count ?? 0;
|
|
161
|
-
const outputTokens = data.eval_count ?? 0;
|
|
162
|
-
return {
|
|
163
|
-
content: data.message.content.trim(),
|
|
164
|
-
usage: inputTokens || outputTokens ? { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens } : void 0
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
async function callOpenAICompat(cfg, messages, options = {}) {
|
|
168
|
-
const base = (cfg.baseUrl ?? "").replace(/\/$/, "") || "https://api.openai.com/v1";
|
|
169
|
-
const res = await fetch(`${base}/chat/completions`, {
|
|
170
|
-
method: "POST",
|
|
171
|
-
headers: {
|
|
172
|
-
"Content-Type": "application/json",
|
|
173
|
-
"Authorization": `Bearer ${cfg.apiKey}`
|
|
174
|
-
},
|
|
175
|
-
signal: timeoutSignal(options.timeoutMs),
|
|
176
|
-
body: JSON.stringify({
|
|
177
|
-
model: cfg.model,
|
|
178
|
-
messages,
|
|
179
|
-
response_format: { type: "json_object" }
|
|
180
|
-
})
|
|
181
|
-
});
|
|
182
|
-
if (!res.ok) throw new Error(`OpenAI API error ${res.status}: ${await res.text()}`);
|
|
183
|
-
const data = await res.json();
|
|
184
|
-
const u = data.usage;
|
|
185
|
-
return {
|
|
186
|
-
content: data.choices[0].message.content.trim(),
|
|
187
|
-
usage: u ? { inputTokens: u.prompt_tokens, outputTokens: u.completion_tokens, totalTokens: u.total_tokens } : void 0
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
async function callAnthropic(cfg, messages, options = {}) {
|
|
191
|
-
const system = messages.find((m) => m.role === "system")?.content ?? "";
|
|
192
|
-
const userMessages = messages.filter((m) => m.role !== "system");
|
|
193
|
-
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
194
|
-
method: "POST",
|
|
195
|
-
headers: {
|
|
196
|
-
"Content-Type": "application/json",
|
|
197
|
-
"x-api-key": cfg.apiKey,
|
|
198
|
-
"anthropic-version": "2023-06-01"
|
|
199
|
-
},
|
|
200
|
-
signal: timeoutSignal(options.timeoutMs),
|
|
201
|
-
body: JSON.stringify({
|
|
202
|
-
model: cfg.model,
|
|
203
|
-
max_tokens: 4096,
|
|
204
|
-
system,
|
|
205
|
-
messages: userMessages
|
|
206
|
-
})
|
|
207
|
-
});
|
|
208
|
-
if (!res.ok) throw new Error(`Anthropic API error ${res.status}: ${await res.text()}`);
|
|
209
|
-
const data = await res.json();
|
|
210
|
-
const u = data.usage;
|
|
211
|
-
return {
|
|
212
|
-
content: data.content[0].text.trim(),
|
|
213
|
-
usage: u ? { inputTokens: u.input_tokens, outputTokens: u.output_tokens, totalTokens: u.input_tokens + u.output_tokens } : void 0
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
async function callMiniMax(cfg, messages, options = {}) {
|
|
217
|
-
const res = await fetch("https://api.minimax.io/v1/chat/completions", {
|
|
218
|
-
method: "POST",
|
|
219
|
-
headers: {
|
|
220
|
-
"Content-Type": "application/json",
|
|
221
|
-
"Authorization": `Bearer ${cfg.apiKey}`
|
|
222
|
-
},
|
|
223
|
-
signal: timeoutSignal(options.timeoutMs),
|
|
224
|
-
body: JSON.stringify({
|
|
225
|
-
model: cfg.model,
|
|
226
|
-
messages,
|
|
227
|
-
response_format: { type: "json_object" }
|
|
228
|
-
})
|
|
229
|
-
});
|
|
230
|
-
if (!res.ok) throw new Error(`MiniMax API error ${res.status}: ${await res.text()}`);
|
|
231
|
-
const data = await res.json();
|
|
232
|
-
const u = data.usage;
|
|
233
|
-
return {
|
|
234
|
-
content: data.choices[0].message.content.trim(),
|
|
235
|
-
usage: u ? { inputTokens: u.prompt_tokens, outputTokens: u.completion_tokens, totalTokens: u.total_tokens } : void 0
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
async function callChatModel(messages, options = {}) {
|
|
239
|
-
const cfg = getChatConfig();
|
|
240
|
-
try {
|
|
241
|
-
switch (cfg.provider) {
|
|
242
|
-
case "openai":
|
|
243
|
-
case "openai-compatible":
|
|
244
|
-
return await callOpenAICompat(cfg, messages, options);
|
|
245
|
-
case "anthropic":
|
|
246
|
-
return await callAnthropic(cfg, messages, options);
|
|
247
|
-
case "minimax":
|
|
248
|
-
return await callMiniMax(cfg, messages, options);
|
|
249
|
-
default:
|
|
250
|
-
return await callOllama(cfg, messages, options);
|
|
251
|
-
}
|
|
252
|
-
} catch (err) {
|
|
253
|
-
throw normalizeChatError(err, options.timeoutMs);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
function getChatProviderLabel() {
|
|
257
|
-
const cfg = getChatConfig();
|
|
258
|
-
if (cfg.provider === "ollama") return `ollama (${cfg.model})`;
|
|
259
|
-
if (cfg.provider === "openai-compatible") {
|
|
260
|
-
const host = cfg.baseUrl ? new URL(cfg.baseUrl).hostname : "custom";
|
|
261
|
-
return `openai-compat/${host} (${cfg.model})`;
|
|
262
|
-
}
|
|
263
|
-
return `${cfg.provider} (${cfg.model})`;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// src/db.ts
|
|
267
|
-
import pg from "pg";
|
|
268
|
-
import { createHash } from "crypto";
|
|
269
|
-
var { Pool } = pg;
|
|
270
|
-
var pool = null;
|
|
271
|
-
var migrationsRun = false;
|
|
272
|
-
function readPositiveIntEnv(name, fallback) {
|
|
273
|
-
const raw = Number(process.env[name]);
|
|
274
|
-
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
|
|
275
|
-
}
|
|
276
|
-
function hashMemoryContent(content) {
|
|
277
|
-
return createHash("md5").update(content.trim()).digest("hex");
|
|
278
|
-
}
|
|
279
|
-
function getPool() {
|
|
280
|
-
if (!pool) {
|
|
281
|
-
if (!Config.databaseUrl) {
|
|
282
|
-
throw new Error("DATABASE_URL is not set. Add it to your .env or .memory-core.env file.");
|
|
283
|
-
}
|
|
284
|
-
const timeoutMs = readPositiveIntEnv("DATABASE_TIMEOUT_MS", 5e3);
|
|
285
|
-
pool = new Pool({
|
|
286
|
-
connectionString: Config.databaseUrl,
|
|
287
|
-
connectionTimeoutMillis: timeoutMs,
|
|
288
|
-
query_timeout: timeoutMs,
|
|
289
|
-
statement_timeout: timeoutMs
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
return pool;
|
|
293
|
-
}
|
|
294
|
-
async function runMigrations() {
|
|
295
|
-
if (migrationsRun) return;
|
|
296
|
-
const client = await getPool().connect();
|
|
297
|
-
try {
|
|
298
|
-
await client.query("BEGIN");
|
|
299
|
-
await client.query(`ALTER TABLE memories ALTER COLUMN scope SET DEFAULT 'project'`);
|
|
300
|
-
await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS reason TEXT`);
|
|
301
|
-
await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS content_hash TEXT`);
|
|
302
|
-
await client.query(`ALTER TABLE memories ADD COLUMN IF NOT EXISTS context JSONB NOT NULL DEFAULT '{}'::jsonb`);
|
|
303
|
-
await client.query(
|
|
304
|
-
`UPDATE memories
|
|
305
|
-
SET content_hash = md5(trim(content))
|
|
306
|
-
WHERE content_hash IS NULL`
|
|
307
|
-
);
|
|
308
|
-
await client.query(`CREATE INDEX IF NOT EXISTS memories_content_hash_idx ON memories (content_hash)`);
|
|
309
|
-
await client.query("COMMIT");
|
|
310
|
-
migrationsRun = true;
|
|
311
|
-
} catch (err) {
|
|
312
|
-
await client.query("ROLLBACK");
|
|
313
|
-
throw err;
|
|
314
|
-
} finally {
|
|
315
|
-
client.release();
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
async function saveMemory(memory) {
|
|
319
|
-
await runMigrations();
|
|
320
|
-
const { type, scope, architecture, projectName, title, content, reason, context, tags, embedding } = memory;
|
|
321
|
-
const contentHash = hashMemoryContent(content);
|
|
322
|
-
await getPool().query(
|
|
323
|
-
`INSERT INTO memories (type, scope, architecture, project_name, title, content, reason, context, tags, embedding, content_hash)
|
|
324
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11)`,
|
|
325
|
-
[
|
|
326
|
-
type,
|
|
327
|
-
scope,
|
|
328
|
-
architecture ?? null,
|
|
329
|
-
projectName ?? null,
|
|
330
|
-
title ?? null,
|
|
331
|
-
content,
|
|
332
|
-
reason ?? null,
|
|
333
|
-
JSON.stringify(context ?? {}),
|
|
334
|
-
tags ?? [],
|
|
335
|
-
`[${embedding.join(",")}]`,
|
|
336
|
-
contentHash
|
|
337
|
-
]
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
async function upsertMemory(memory) {
|
|
341
|
-
await runMigrations();
|
|
342
|
-
const contentHash = hashMemoryContent(memory.content);
|
|
343
|
-
const existing = await getPool().query(
|
|
344
|
-
`SELECT id FROM memories
|
|
345
|
-
WHERE content_hash = $1
|
|
346
|
-
AND COALESCE(architecture, '') = COALESCE($2, '')
|
|
347
|
-
AND scope = $3
|
|
348
|
-
AND type = $4
|
|
349
|
-
LIMIT 1`,
|
|
350
|
-
[contentHash, memory.architecture ?? null, memory.scope, memory.type]
|
|
351
|
-
);
|
|
352
|
-
if (existing.rowCount) return "skipped";
|
|
353
|
-
await saveMemory(memory);
|
|
354
|
-
return "inserted";
|
|
355
|
-
}
|
|
356
|
-
async function listMemories(filters = {}) {
|
|
357
|
-
await runMigrations();
|
|
358
|
-
const where = [];
|
|
359
|
-
const params = [];
|
|
360
|
-
if (filters.type) {
|
|
361
|
-
params.push(filters.type);
|
|
362
|
-
where.push(`type = $${params.length}`);
|
|
363
|
-
}
|
|
364
|
-
if (filters.scope) {
|
|
365
|
-
params.push(filters.scope);
|
|
366
|
-
where.push(`scope = $${params.length}`);
|
|
367
|
-
}
|
|
368
|
-
if (filters.architecture) {
|
|
369
|
-
if (Array.isArray(filters.architecture)) {
|
|
370
|
-
params.push(filters.architecture);
|
|
371
|
-
where.push(filters.includeGlobal ? `(architecture IS NULL OR architecture = ANY($${params.length}))` : `architecture = ANY($${params.length})`);
|
|
372
|
-
} else {
|
|
373
|
-
params.push(filters.architecture);
|
|
374
|
-
where.push(filters.includeGlobal ? `(architecture IS NULL OR architecture = $${params.length})` : `architecture = $${params.length}`);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
if (filters.projectName) {
|
|
378
|
-
params.push(filters.projectName);
|
|
379
|
-
where.push(filters.includeGlobal ? `(project_name IS NULL OR project_name = $${params.length})` : `project_name = $${params.length}`);
|
|
380
|
-
}
|
|
381
|
-
if (filters.tags?.length) {
|
|
382
|
-
params.push(filters.tags);
|
|
383
|
-
where.push(`tags && $${params.length}::text[]`);
|
|
384
|
-
}
|
|
385
|
-
const limit = filters.limit ?? 200;
|
|
386
|
-
params.push(limit);
|
|
387
|
-
const result = await getPool().query(
|
|
388
|
-
`SELECT id, type, scope, architecture, project_name, title, content, reason, context, tags, content_hash
|
|
389
|
-
FROM memories
|
|
390
|
-
${where.length ? `WHERE ${where.join(" AND ")}` : ""}
|
|
391
|
-
ORDER BY id ASC
|
|
392
|
-
LIMIT $${params.length}`,
|
|
393
|
-
params
|
|
394
|
-
);
|
|
395
|
-
return result.rows;
|
|
396
|
-
}
|
|
397
|
-
async function getMemory(id) {
|
|
398
|
-
await runMigrations();
|
|
399
|
-
const result = await getPool().query(
|
|
400
|
-
`SELECT id, type, scope, architecture, project_name, title, content, reason, context, tags, content_hash
|
|
401
|
-
FROM memories
|
|
402
|
-
WHERE id = $1`,
|
|
403
|
-
[id]
|
|
404
|
-
);
|
|
405
|
-
return result.rows[0] ?? null;
|
|
406
|
-
}
|
|
407
|
-
async function deleteMemory(id) {
|
|
408
|
-
await runMigrations();
|
|
409
|
-
const result = await getPool().query(`DELETE FROM memories WHERE id = $1`, [id]);
|
|
410
|
-
return (result.rowCount ?? 0) > 0;
|
|
411
|
-
}
|
|
412
|
-
async function deleteMemories(filters) {
|
|
413
|
-
await runMigrations();
|
|
414
|
-
const where = [];
|
|
415
|
-
const params = [];
|
|
416
|
-
if (filters.type) {
|
|
417
|
-
params.push(filters.type);
|
|
418
|
-
where.push(`type = $${params.length}`);
|
|
419
|
-
}
|
|
420
|
-
if (filters.scope) {
|
|
421
|
-
params.push(filters.scope);
|
|
422
|
-
where.push(`scope = $${params.length}`);
|
|
423
|
-
}
|
|
424
|
-
if (filters.architecture) {
|
|
425
|
-
if (Array.isArray(filters.architecture)) {
|
|
426
|
-
params.push(filters.architecture);
|
|
427
|
-
where.push(`architecture = ANY($${params.length})`);
|
|
428
|
-
} else {
|
|
429
|
-
params.push(filters.architecture);
|
|
430
|
-
where.push(`architecture = $${params.length}`);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
if (filters.tag) {
|
|
434
|
-
params.push(filters.tag);
|
|
435
|
-
where.push(`$${params.length} = ANY(tags)`);
|
|
436
|
-
}
|
|
437
|
-
if (where.length === 0) {
|
|
438
|
-
throw new Error("Refusing to bulk-delete without filters");
|
|
439
|
-
}
|
|
440
|
-
const result = await getPool().query(
|
|
441
|
-
`DELETE FROM memories WHERE ${where.join(" AND ")}`,
|
|
442
|
-
params
|
|
443
|
-
);
|
|
444
|
-
return result.rowCount ?? 0;
|
|
445
|
-
}
|
|
446
|
-
async function updateMemory(id, patch) {
|
|
447
|
-
await runMigrations();
|
|
448
|
-
const current = await getMemory(id);
|
|
449
|
-
if (!current) return null;
|
|
450
|
-
const content = patch.content ?? current.content;
|
|
451
|
-
const contentHash = hashMemoryContent(content);
|
|
452
|
-
const embedding = patch.embedding ? `[${patch.embedding.join(",")}]` : null;
|
|
453
|
-
const result = await getPool().query(
|
|
454
|
-
`UPDATE memories
|
|
455
|
-
SET type = $2,
|
|
456
|
-
scope = $3,
|
|
457
|
-
title = $4,
|
|
458
|
-
content = $5,
|
|
459
|
-
reason = $6,
|
|
460
|
-
context = $7::jsonb,
|
|
461
|
-
tags = $8,
|
|
462
|
-
content_hash = $9,
|
|
463
|
-
embedding = COALESCE($10::vector, embedding)
|
|
464
|
-
WHERE id = $1
|
|
465
|
-
RETURNING id, type, scope, architecture, project_name, title, content, reason, context, tags, content_hash`,
|
|
466
|
-
[
|
|
467
|
-
id,
|
|
468
|
-
patch.type ?? current.type,
|
|
469
|
-
patch.scope ?? current.scope,
|
|
470
|
-
patch.title ?? current.title ?? null,
|
|
471
|
-
content,
|
|
472
|
-
patch.reason ?? current.reason ?? null,
|
|
473
|
-
JSON.stringify(patch.context ?? current.context ?? {}),
|
|
474
|
-
patch.tags ?? current.tags ?? [],
|
|
475
|
-
contentHash,
|
|
476
|
-
embedding
|
|
477
|
-
]
|
|
478
|
-
);
|
|
479
|
-
return result.rows[0] ?? null;
|
|
480
|
-
}
|
|
481
|
-
async function searchMemories(embedding, architectures, limit = 10) {
|
|
482
|
-
await runMigrations();
|
|
483
|
-
const vector = `[${embedding.join(",")}]`;
|
|
484
|
-
const params = [vector];
|
|
485
|
-
let whereClause = "";
|
|
486
|
-
const selectedArchitectures = architectures ? (Array.isArray(architectures) ? architectures : [architectures]).filter(Boolean) : [];
|
|
487
|
-
if (selectedArchitectures.length > 0) {
|
|
488
|
-
whereClause = `WHERE (
|
|
489
|
-
architecture = ANY($2)
|
|
490
|
-
OR architecture IS NULL
|
|
491
|
-
OR architecture = 'global'
|
|
492
|
-
)`;
|
|
493
|
-
params.push(selectedArchitectures);
|
|
494
|
-
}
|
|
495
|
-
const client = await getPool().connect();
|
|
496
|
-
try {
|
|
497
|
-
await client.query("BEGIN");
|
|
498
|
-
await client.query("SET LOCAL ivfflat.probes = 10");
|
|
499
|
-
const result = await client.query(
|
|
500
|
-
`SELECT id, type, scope, architecture, project_name, title, content, reason, context, tags,
|
|
501
|
-
1 - (embedding <=> $1) AS similarity
|
|
502
|
-
FROM memories
|
|
503
|
-
${whereClause}
|
|
504
|
-
ORDER BY embedding <=> $1
|
|
505
|
-
LIMIT $${params.length + 1}`,
|
|
506
|
-
[...params, limit]
|
|
507
|
-
);
|
|
508
|
-
await client.query("COMMIT");
|
|
509
|
-
return result.rows;
|
|
510
|
-
} finally {
|
|
511
|
-
client.release();
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
async function closePool() {
|
|
515
|
-
if (pool) {
|
|
516
|
-
await pool.end();
|
|
517
|
-
pool = null;
|
|
518
|
-
migrationsRun = false;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
115
|
// src/infrastructure/persistence/postgres/postgres-graph-repository.ts
|
|
523
116
|
var graphMigrationsRun = false;
|
|
524
117
|
function asPosix(value) {
|
|
@@ -1026,8 +619,8 @@ var seeds = [
|
|
|
1026
619
|
];
|
|
1027
620
|
|
|
1028
621
|
// src/memory-file.ts
|
|
1029
|
-
import { existsSync as
|
|
1030
|
-
import { join as
|
|
622
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
623
|
+
import { join as join2 } from "path";
|
|
1031
624
|
var MEMORY_FILE = "memories.json";
|
|
1032
625
|
function toPortableMemory(memory) {
|
|
1033
626
|
return {
|
|
@@ -1061,13 +654,13 @@ function parseContext(value) {
|
|
|
1061
654
|
return Object.keys(context).length ? context : void 0;
|
|
1062
655
|
}
|
|
1063
656
|
function writeMemoryFile(memories, cwd = process.cwd()) {
|
|
1064
|
-
const path =
|
|
657
|
+
const path = join2(cwd, MEMORY_FILE);
|
|
1065
658
|
writeFileSync(path, JSON.stringify(memories, null, 2) + "\n", "utf-8");
|
|
1066
659
|
return path;
|
|
1067
660
|
}
|
|
1068
661
|
function readMemoryFile(cwd = process.cwd()) {
|
|
1069
|
-
const path =
|
|
1070
|
-
if (!
|
|
662
|
+
const path = join2(cwd, MEMORY_FILE);
|
|
663
|
+
if (!existsSync2(path)) {
|
|
1071
664
|
throw new Error(`${MEMORY_FILE} not found. Run: memory-core export`);
|
|
1072
665
|
}
|
|
1073
666
|
return parseMemoryFile(readFileSync2(path, "utf-8"));
|
|
@@ -1105,8 +698,8 @@ function parseMemoryFile(raw) {
|
|
|
1105
698
|
}
|
|
1106
699
|
|
|
1107
700
|
// src/modules/rule-engine/infrastructure/schema-violations.ts
|
|
1108
|
-
import { existsSync as
|
|
1109
|
-
import { join as
|
|
701
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
702
|
+
import { join as join3 } from "path";
|
|
1110
703
|
function parseSchemaRule(content) {
|
|
1111
704
|
try {
|
|
1112
705
|
const parsed = JSON.parse(content);
|
|
@@ -1157,9 +750,9 @@ async function findSchemaViolations(opts) {
|
|
|
1157
750
|
for (const memory of schemaMemories) {
|
|
1158
751
|
const rule = parseSchemaRule(memory.content);
|
|
1159
752
|
if (!rule) continue;
|
|
1160
|
-
const tsPath =
|
|
1161
|
-
const goPath =
|
|
1162
|
-
if (!
|
|
753
|
+
const tsPath = join3(opts.cwd, rule.tsFile);
|
|
754
|
+
const goPath = join3(opts.cwd, rule.goFile);
|
|
755
|
+
if (!existsSync3(tsPath) || !existsSync3(goPath)) continue;
|
|
1163
756
|
const tsSource = readFileSync3(tsPath, "utf-8");
|
|
1164
757
|
const goSource = readFileSync3(goPath, "utf-8");
|
|
1165
758
|
const tsFields = new Set(extractTsFields(tsSource));
|
|
@@ -1192,13 +785,13 @@ async function findSchemaViolations(opts) {
|
|
|
1192
785
|
|
|
1193
786
|
// src/hook.ts
|
|
1194
787
|
import { execSync, spawnSync as spawnSync2 } from "child_process";
|
|
1195
|
-
import { writeFileSync as writeFileSync5, existsSync as
|
|
1196
|
-
import { join as
|
|
788
|
+
import { writeFileSync as writeFileSync5, existsSync as existsSync7, unlinkSync, readFileSync as readFileSync7, chmodSync, statSync as statSync3 } from "fs";
|
|
789
|
+
import { join as join8 } from "path";
|
|
1197
790
|
import chalk2 from "chalk";
|
|
1198
791
|
|
|
1199
792
|
// src/generator.ts
|
|
1200
|
-
import { readFileSync as
|
|
1201
|
-
import { join as
|
|
793
|
+
import { readFileSync as readFileSync6, readdirSync as readdirSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync6 } from "fs";
|
|
794
|
+
import { join as join7, dirname as dirname3, basename } from "path";
|
|
1202
795
|
import { fileURLToPath } from "url";
|
|
1203
796
|
import Handlebars from "handlebars";
|
|
1204
797
|
import yaml from "js-yaml";
|
|
@@ -1307,9 +900,9 @@ var PostgresMemoryRepository = class {
|
|
|
1307
900
|
};
|
|
1308
901
|
|
|
1309
902
|
// src/infrastructure/persistence/filesystem/file-graph-repository.ts
|
|
1310
|
-
import { existsSync as
|
|
1311
|
-
import { dirname, join as
|
|
1312
|
-
var DEFAULT_FILE =
|
|
903
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
904
|
+
import { dirname, join as join4 } from "path";
|
|
905
|
+
var DEFAULT_FILE = join4(".memory-core", "graph-snapshots.json");
|
|
1313
906
|
function asPosix2(value) {
|
|
1314
907
|
return value.replace(/\\/g, "/");
|
|
1315
908
|
}
|
|
@@ -1348,7 +941,7 @@ var FileGraphRepository = class {
|
|
|
1348
941
|
}
|
|
1349
942
|
readData() {
|
|
1350
943
|
const filePath = this.absolutePath();
|
|
1351
|
-
if (!
|
|
944
|
+
if (!existsSync4(filePath)) {
|
|
1352
945
|
return { version: 1, snapshots: [] };
|
|
1353
946
|
}
|
|
1354
947
|
try {
|
|
@@ -1373,7 +966,7 @@ var FileGraphRepository = class {
|
|
|
1373
966
|
`, "utf-8");
|
|
1374
967
|
}
|
|
1375
968
|
absolutePath() {
|
|
1376
|
-
return
|
|
969
|
+
return join4(this.rootPath, this.relativeFilePath);
|
|
1377
970
|
}
|
|
1378
971
|
};
|
|
1379
972
|
|
|
@@ -1421,214 +1014,39 @@ var ResilientGraphRepository = class {
|
|
|
1421
1014
|
// src/watcher.ts
|
|
1422
1015
|
import { watch } from "chokidar";
|
|
1423
1016
|
import { spawnSync } from "child_process";
|
|
1424
|
-
import { existsSync as
|
|
1425
|
-
import { dirname as
|
|
1017
|
+
import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1018
|
+
import { dirname as dirname2, join as join5, relative, resolve as resolve2, sep } from "path";
|
|
1426
1019
|
import chalk from "chalk";
|
|
1427
1020
|
|
|
1428
1021
|
// src/modules/rule-engine/infrastructure/ast-deterministic-violations.ts
|
|
1429
|
-
import { resolve
|
|
1430
|
-
|
|
1431
|
-
// src/infrastructure/ast/import-analysis.ts
|
|
1432
|
-
import { builtinModules } from "module";
|
|
1433
|
-
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
1434
|
-
import { dirname as dirname2, extname, isAbsolute, join as join6, normalize, resolve } from "path";
|
|
1435
|
-
var SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
1436
|
-
var NODE_BUILTINS = /* @__PURE__ */ new Set([...builtinModules, ...builtinModules.map((entry) => `node:${entry}`)]);
|
|
1437
|
-
function countNewlines(value) {
|
|
1438
|
-
let count = 0;
|
|
1439
|
-
for (const char of value) {
|
|
1440
|
-
if (char === "\n") count += 1;
|
|
1441
|
-
}
|
|
1442
|
-
return count;
|
|
1443
|
-
}
|
|
1444
|
-
function parseImports(source) {
|
|
1445
|
-
const imports = [];
|
|
1446
|
-
const patterns = [
|
|
1447
|
-
{ kind: "import", regex: /(^|\n)\s*import\s+(?:type\s+)?(?:[^'"\n]+?\s+from\s+)?['"]([^'"\n]+)['"]/g },
|
|
1448
|
-
{ kind: "export-from", regex: /(^|\n)\s*export\s+(?:type\s+)?(?:[^'"\n]+?\s+from\s+)['"]([^'"\n]+)['"]/g },
|
|
1449
|
-
{ kind: "require", regex: /(^|\n)[^\n]*?\brequire\(\s*['"]([^'"\n]+)['"]\s*\)/g },
|
|
1450
|
-
{ kind: "dynamic-import", regex: /(^|\n)[^\n]*?\bimport\(\s*['"]([^'"\n]+)['"]\s*\)/g }
|
|
1451
|
-
];
|
|
1452
|
-
for (const { kind, regex } of patterns) {
|
|
1453
|
-
for (const match of source.matchAll(regex)) {
|
|
1454
|
-
const prefix = source.slice(0, match.index ?? 0);
|
|
1455
|
-
imports.push({
|
|
1456
|
-
kind,
|
|
1457
|
-
specifier: match[2],
|
|
1458
|
-
line: countNewlines(prefix) + 1
|
|
1459
|
-
});
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
return imports;
|
|
1463
|
-
}
|
|
1464
|
-
function tryResolveFilePath(candidate) {
|
|
1465
|
-
if (existsSync6(candidate)) return normalize(candidate);
|
|
1466
|
-
if (!extname(candidate)) {
|
|
1467
|
-
for (const ext of SOURCE_EXTENSIONS) {
|
|
1468
|
-
const withExt = `${candidate}${ext}`;
|
|
1469
|
-
if (existsSync6(withExt)) return normalize(withExt);
|
|
1470
|
-
}
|
|
1471
|
-
for (const ext of SOURCE_EXTENSIONS) {
|
|
1472
|
-
const indexFile = join6(candidate, `index${ext}`);
|
|
1473
|
-
if (existsSync6(indexFile)) return normalize(indexFile);
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
return void 0;
|
|
1477
|
-
}
|
|
1478
|
-
function looksLikeExternal(specifier) {
|
|
1479
|
-
return !specifier.startsWith(".") && !specifier.startsWith("/") && !specifier.startsWith("@/");
|
|
1480
|
-
}
|
|
1481
|
-
function resolveImportPath(fromFile, specifier, cwd = process.cwd()) {
|
|
1482
|
-
if (specifier.startsWith(".")) {
|
|
1483
|
-
return tryResolveFilePath(resolve(dirname2(fromFile), specifier));
|
|
1484
|
-
}
|
|
1485
|
-
if (specifier.startsWith("/")) {
|
|
1486
|
-
return tryResolveFilePath(resolve(cwd, `.${specifier}`));
|
|
1487
|
-
}
|
|
1488
|
-
if (specifier.startsWith("@/")) {
|
|
1489
|
-
return tryResolveFilePath(resolve(cwd, "src", specifier.slice(2)));
|
|
1490
|
-
}
|
|
1491
|
-
if (isAbsolute(specifier)) {
|
|
1492
|
-
return tryResolveFilePath(specifier);
|
|
1493
|
-
}
|
|
1494
|
-
return void 0;
|
|
1495
|
-
}
|
|
1496
|
-
function collectResolvedImports(filePath, cwd = process.cwd()) {
|
|
1497
|
-
if (!existsSync6(filePath)) return [];
|
|
1498
|
-
const source = readFileSync5(filePath, "utf-8");
|
|
1499
|
-
const imports = parseImports(source);
|
|
1500
|
-
return imports.map((entry) => {
|
|
1501
|
-
if (looksLikeExternal(entry.specifier) || NODE_BUILTINS.has(entry.specifier)) {
|
|
1502
|
-
return { ...entry, isExternal: true };
|
|
1503
|
-
}
|
|
1504
|
-
return {
|
|
1505
|
-
...entry,
|
|
1506
|
-
isExternal: false,
|
|
1507
|
-
resolvedPath: resolveImportPath(filePath, entry.specifier, cwd)
|
|
1508
|
-
};
|
|
1509
|
-
});
|
|
1510
|
-
}
|
|
1022
|
+
import { resolve } from "path";
|
|
1511
1023
|
function asPosix3(value) {
|
|
1512
1024
|
return value.replace(/\\/g, "/");
|
|
1513
1025
|
}
|
|
1514
|
-
function moduleNameFromPath(absPath, cwd = process.cwd()) {
|
|
1515
|
-
const rel = asPosix3(normalize(absPath)).replace(asPosix3(normalize(cwd)) + "/", "");
|
|
1516
|
-
const match = rel.match(/^src\/modules\/([^/]+)\//);
|
|
1517
|
-
return match?.[1];
|
|
1518
|
-
}
|
|
1519
|
-
function buildModuleDependencyEdges(files, cwd = process.cwd()) {
|
|
1520
|
-
const edges = [];
|
|
1521
|
-
for (const file of files) {
|
|
1522
|
-
const absoluteFile = resolve(cwd, file);
|
|
1523
|
-
const fromModule = moduleNameFromPath(absoluteFile, cwd);
|
|
1524
|
-
if (!fromModule) continue;
|
|
1525
|
-
const imports = collectResolvedImports(absoluteFile, cwd);
|
|
1526
|
-
for (const imp of imports) {
|
|
1527
|
-
if (!imp.resolvedPath) continue;
|
|
1528
|
-
const toModule = moduleNameFromPath(imp.resolvedPath, cwd);
|
|
1529
|
-
if (!toModule || toModule === fromModule) continue;
|
|
1530
|
-
edges.push({
|
|
1531
|
-
fromModule,
|
|
1532
|
-
toModule,
|
|
1533
|
-
file,
|
|
1534
|
-
line: imp.line
|
|
1535
|
-
});
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
return edges;
|
|
1539
|
-
}
|
|
1540
|
-
function parseChangedFilesFromDiff(diff) {
|
|
1541
|
-
const files = /* @__PURE__ */ new Set();
|
|
1542
|
-
for (const line of diff.split("\n")) {
|
|
1543
|
-
if (!line.startsWith("+++ b/")) continue;
|
|
1544
|
-
const file = line.slice("+++ b/".length).trim();
|
|
1545
|
-
if (!file || file === "/dev/null") continue;
|
|
1546
|
-
files.add(file);
|
|
1547
|
-
}
|
|
1548
|
-
return [...files];
|
|
1549
|
-
}
|
|
1550
|
-
function detectModuleCycles(edges) {
|
|
1551
|
-
const graph = /* @__PURE__ */ new Map();
|
|
1552
|
-
for (const edge of edges) {
|
|
1553
|
-
const next = graph.get(edge.fromModule) ?? /* @__PURE__ */ new Set();
|
|
1554
|
-
next.add(edge.toModule);
|
|
1555
|
-
graph.set(edge.fromModule, next);
|
|
1556
|
-
}
|
|
1557
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1558
|
-
const stack = /* @__PURE__ */ new Set();
|
|
1559
|
-
const cycles = /* @__PURE__ */ new Set();
|
|
1560
|
-
const visit = (node, path) => {
|
|
1561
|
-
visited.add(node);
|
|
1562
|
-
stack.add(node);
|
|
1563
|
-
const next = graph.get(node) ?? /* @__PURE__ */ new Set();
|
|
1564
|
-
for (const target of next) {
|
|
1565
|
-
if (!visited.has(target)) {
|
|
1566
|
-
visit(target, [...path, target]);
|
|
1567
|
-
continue;
|
|
1568
|
-
}
|
|
1569
|
-
if (stack.has(target)) {
|
|
1570
|
-
const start = path.indexOf(target);
|
|
1571
|
-
const cycle = start >= 0 ? path.slice(start).concat(target) : [node, target, node];
|
|
1572
|
-
cycles.add(cycle.join("->"));
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
stack.delete(node);
|
|
1576
|
-
};
|
|
1577
|
-
for (const node of graph.keys()) {
|
|
1578
|
-
if (!visited.has(node)) {
|
|
1579
|
-
visit(node, [node]);
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
return [...cycles].map((value) => value.split("->"));
|
|
1583
|
-
}
|
|
1584
|
-
function isExternalFrameworkSpecifier(specifier) {
|
|
1585
|
-
const frameworkPrefixes = [
|
|
1586
|
-
"express",
|
|
1587
|
-
"fastify",
|
|
1588
|
-
"@nestjs/",
|
|
1589
|
-
"react",
|
|
1590
|
-
"vue",
|
|
1591
|
-
"svelte",
|
|
1592
|
-
"@angular/",
|
|
1593
|
-
"next/",
|
|
1594
|
-
"nuxt",
|
|
1595
|
-
"typeorm",
|
|
1596
|
-
"@prisma/",
|
|
1597
|
-
"mongoose",
|
|
1598
|
-
"sequelize",
|
|
1599
|
-
"axios"
|
|
1600
|
-
];
|
|
1601
|
-
return frameworkPrefixes.some((prefix) => specifier === prefix || specifier.startsWith(`${prefix}/`));
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
// src/modules/rule-engine/infrastructure/ast-deterministic-violations.ts
|
|
1605
|
-
function asPosix4(value) {
|
|
1606
|
-
return value.replace(/\\/g, "/");
|
|
1607
|
-
}
|
|
1608
1026
|
function hasPath(value, pathSegment) {
|
|
1609
|
-
const normalized =
|
|
1027
|
+
const normalized = asPosix3(value);
|
|
1610
1028
|
const trimmed = pathSegment.startsWith("/") ? pathSegment.slice(1) : pathSegment;
|
|
1611
1029
|
return normalized.includes(pathSegment) || normalized.includes(trimmed);
|
|
1612
1030
|
}
|
|
1613
1031
|
function isLegacyOrCompatibilitySpecifier(specifier) {
|
|
1614
|
-
const normalized =
|
|
1032
|
+
const normalized = asPosix3(specifier);
|
|
1615
1033
|
return normalized.includes("/compatibility/") || normalized.includes("compatibility/") || normalized.includes("legacy-");
|
|
1616
1034
|
}
|
|
1617
1035
|
function isLegacyOrCompatibilityPath(pathValue) {
|
|
1618
1036
|
if (!pathValue) return false;
|
|
1619
|
-
const normalized =
|
|
1037
|
+
const normalized = asPosix3(pathValue);
|
|
1620
1038
|
return normalized.includes("/src/compatibility/") || normalized.includes("/legacy-");
|
|
1621
1039
|
}
|
|
1622
|
-
function
|
|
1623
|
-
const match =
|
|
1040
|
+
function moduleNameFromPath(pathValue) {
|
|
1041
|
+
const match = asPosix3(pathValue).match(/src\/modules\/([^/]+)\//);
|
|
1624
1042
|
return match?.[1];
|
|
1625
1043
|
}
|
|
1626
1044
|
function isModulePublicPath(pathValue) {
|
|
1627
|
-
const normalized =
|
|
1045
|
+
const normalized = asPosix3(pathValue);
|
|
1628
1046
|
return /src\/modules\/[^/]+\/(public|api)\//.test(normalized) || /src\/modules\/[^/]+\/index\.(ts|tsx|js|jsx|mjs|cjs)$/.test(normalized);
|
|
1629
1047
|
}
|
|
1630
1048
|
function detectCleanLayer(pathValue) {
|
|
1631
|
-
const normalized =
|
|
1049
|
+
const normalized = asPosix3(pathValue);
|
|
1632
1050
|
if (hasPath(normalized, "/src/domain/") || hasPath(normalized, "/src/core/domain/")) return "domain";
|
|
1633
1051
|
if (hasPath(normalized, "/src/application/") || hasPath(normalized, "/src/core/application/")) return "application";
|
|
1634
1052
|
if (hasPath(normalized, "/src/infrastructure/")) return "infrastructure";
|
|
@@ -1636,17 +1054,17 @@ function detectCleanLayer(pathValue) {
|
|
|
1636
1054
|
return "unknown";
|
|
1637
1055
|
}
|
|
1638
1056
|
function detectHexLayer(pathValue) {
|
|
1639
|
-
const normalized =
|
|
1057
|
+
const normalized = asPosix3(pathValue);
|
|
1640
1058
|
if (hasPath(normalized, "/src/core/")) return "core";
|
|
1641
1059
|
if (hasPath(normalized, "/src/adapters/inbound/")) return "adapter-inbound";
|
|
1642
1060
|
if (hasPath(normalized, "/src/adapters/outbound/")) return "adapter-outbound";
|
|
1643
1061
|
if (hasPath(normalized, "/src/adapters/")) return "adapter-other";
|
|
1644
1062
|
return "unknown";
|
|
1645
1063
|
}
|
|
1646
|
-
function activeArchitectures(
|
|
1064
|
+
function activeArchitectures(config, rules = []) {
|
|
1647
1065
|
const names = /* @__PURE__ */ new Set();
|
|
1648
|
-
if (
|
|
1649
|
-
if (
|
|
1066
|
+
if (config?.backendArchitecture) names.add(config.backendArchitecture);
|
|
1067
|
+
if (config?.frontendFramework) names.add(config.frontendFramework);
|
|
1650
1068
|
const text = rules.join("\n").toLowerCase();
|
|
1651
1069
|
if (text.includes("modular monolith")) names.add("modular-monolith");
|
|
1652
1070
|
if (text.includes("clean architecture")) names.add("clean-architecture");
|
|
@@ -1667,13 +1085,13 @@ function evaluateFile(file, options) {
|
|
|
1667
1085
|
const architectures = activeArchitectures(options.config, rules);
|
|
1668
1086
|
const reasonLookup = options.reasonLookup ?? /* @__PURE__ */ new Map();
|
|
1669
1087
|
const violations = [];
|
|
1670
|
-
const absFile =
|
|
1671
|
-
const normalizedFile =
|
|
1088
|
+
const absFile = resolve(cwd, file);
|
|
1089
|
+
const normalizedFile = asPosix3(file);
|
|
1672
1090
|
const imports = collectResolvedImports(absFile, cwd);
|
|
1673
1091
|
const fromCleanLayer = detectCleanLayer(normalizedFile);
|
|
1674
1092
|
const fromHexLayer = detectHexLayer(normalizedFile);
|
|
1675
1093
|
for (const imp of imports) {
|
|
1676
|
-
const target = imp.resolvedPath ?
|
|
1094
|
+
const target = imp.resolvedPath ? asPosix3(imp.resolvedPath) : void 0;
|
|
1677
1095
|
if (isLegacyOrCompatibilitySpecifier(imp.specifier) || isLegacyOrCompatibilityPath(target)) {
|
|
1678
1096
|
const rule = "Application code must not import compatibility or legacy adapter paths";
|
|
1679
1097
|
pushUnique(violations, {
|
|
@@ -1686,8 +1104,8 @@ function evaluateFile(file, options) {
|
|
|
1686
1104
|
});
|
|
1687
1105
|
}
|
|
1688
1106
|
if (architectures.has("modular-monolith")) {
|
|
1689
|
-
const fromModule =
|
|
1690
|
-
const toModule = target ?
|
|
1107
|
+
const fromModule = moduleNameFromPath(normalizedFile);
|
|
1108
|
+
const toModule = target ? moduleNameFromPath(target) : void 0;
|
|
1691
1109
|
if (fromModule && toModule && target && fromModule !== toModule && !isModulePublicPath(target)) {
|
|
1692
1110
|
const rule = "Modules communicate only through public interfaces or events \u2014 never by importing internals";
|
|
1693
1111
|
pushUnique(violations, {
|
|
@@ -1811,7 +1229,7 @@ function findAstDeterministicViolationsForDiff(diff, options = {}) {
|
|
|
1811
1229
|
// src/watcher.ts
|
|
1812
1230
|
function getFileLines(filePath) {
|
|
1813
1231
|
try {
|
|
1814
|
-
return
|
|
1232
|
+
return readFileSync5(filePath, "utf-8").split("\n");
|
|
1815
1233
|
} catch {
|
|
1816
1234
|
return [];
|
|
1817
1235
|
}
|
|
@@ -1845,36 +1263,36 @@ function formatCodeContext(filePath, line, contextLines = 2) {
|
|
|
1845
1263
|
return `${lineNum} ${marker} ${lines[current]}`;
|
|
1846
1264
|
}).join("\n");
|
|
1847
1265
|
}
|
|
1848
|
-
var
|
|
1266
|
+
var SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
|
|
1849
1267
|
var reasonMap = new Map(
|
|
1850
1268
|
seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
|
|
1851
1269
|
);
|
|
1852
1270
|
function findProjectRoot(startPath) {
|
|
1853
|
-
let current =
|
|
1271
|
+
let current = resolve2(startPath);
|
|
1854
1272
|
while (true) {
|
|
1855
|
-
if (
|
|
1856
|
-
const parent =
|
|
1273
|
+
if (existsSync5(join5(current, ".memory-core.json"))) return current;
|
|
1274
|
+
const parent = dirname2(current);
|
|
1857
1275
|
if (parent === current) return null;
|
|
1858
1276
|
current = parent;
|
|
1859
1277
|
}
|
|
1860
1278
|
}
|
|
1861
1279
|
function resolveWatchPaths(pathOption, projectRootOption) {
|
|
1862
1280
|
if (projectRootOption) {
|
|
1863
|
-
const projectRoot2 =
|
|
1281
|
+
const projectRoot2 = resolve2(projectRootOption);
|
|
1864
1282
|
return {
|
|
1865
1283
|
projectRoot: projectRoot2,
|
|
1866
|
-
watchPath:
|
|
1284
|
+
watchPath: resolve2(projectRoot2, pathOption ?? ".")
|
|
1867
1285
|
};
|
|
1868
1286
|
}
|
|
1869
|
-
const cwdRoot =
|
|
1870
|
-
const watchPath =
|
|
1287
|
+
const cwdRoot = resolve2(process.cwd());
|
|
1288
|
+
const watchPath = resolve2(cwdRoot, pathOption ?? ".");
|
|
1871
1289
|
const projectRoot = findProjectRoot(watchPath) ?? findProjectRoot(cwdRoot) ?? cwdRoot;
|
|
1872
1290
|
return { projectRoot, watchPath };
|
|
1873
1291
|
}
|
|
1874
1292
|
function readStatsFile(statsPath) {
|
|
1875
|
-
if (!
|
|
1293
|
+
if (!existsSync5(statsPath)) return { rules: {}, files: {} };
|
|
1876
1294
|
try {
|
|
1877
|
-
return JSON.parse(
|
|
1295
|
+
return JSON.parse(readFileSync5(statsPath, "utf-8"));
|
|
1878
1296
|
} catch {
|
|
1879
1297
|
return { rules: {}, files: {} };
|
|
1880
1298
|
}
|
|
@@ -1892,7 +1310,7 @@ function rebuildLiveCounters(byFile) {
|
|
|
1892
1310
|
return { rules, files };
|
|
1893
1311
|
}
|
|
1894
1312
|
function resetLiveStats(cwd) {
|
|
1895
|
-
const statsPath =
|
|
1313
|
+
const statsPath = join5(cwd, ".memory-core-stats.json");
|
|
1896
1314
|
const stats = readStatsFile(statsPath);
|
|
1897
1315
|
stats.rules ??= {};
|
|
1898
1316
|
stats.files ??= {};
|
|
@@ -1904,7 +1322,7 @@ function resetLiveStats(cwd) {
|
|
|
1904
1322
|
writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
1905
1323
|
}
|
|
1906
1324
|
function recordWatchResult(cwd, file, violations) {
|
|
1907
|
-
const statsPath =
|
|
1325
|
+
const statsPath = join5(cwd, ".memory-core-stats.json");
|
|
1908
1326
|
const stats = readStatsFile(statsPath);
|
|
1909
1327
|
stats.rules ??= {};
|
|
1910
1328
|
stats.files ??= {};
|
|
@@ -1930,26 +1348,26 @@ function recordWatchResult(cwd, file, violations) {
|
|
|
1930
1348
|
writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
1931
1349
|
}
|
|
1932
1350
|
function loadConfig(cwd) {
|
|
1933
|
-
const configPath =
|
|
1934
|
-
if (!
|
|
1351
|
+
const configPath = join5(cwd, ".memory-core.json");
|
|
1352
|
+
if (!existsSync5(configPath)) return null;
|
|
1935
1353
|
try {
|
|
1936
|
-
return JSON.parse(
|
|
1354
|
+
return JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
1937
1355
|
} catch {
|
|
1938
1356
|
return null;
|
|
1939
1357
|
}
|
|
1940
1358
|
}
|
|
1941
|
-
function getProfileRules(
|
|
1359
|
+
function getProfileRules(config) {
|
|
1942
1360
|
const rules = [];
|
|
1943
1361
|
const avoids = [];
|
|
1944
|
-
if (
|
|
1945
|
-
const profile = listProfiles("backend").find((p) => p.name ===
|
|
1362
|
+
if (config.backendArchitecture) {
|
|
1363
|
+
const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
|
|
1946
1364
|
if (profile) {
|
|
1947
1365
|
rules.push(...profile.rules);
|
|
1948
1366
|
avoids.push(...profile.avoid);
|
|
1949
1367
|
}
|
|
1950
1368
|
}
|
|
1951
|
-
if (
|
|
1952
|
-
const profile = listProfiles("frontend").find((p) => p.name ===
|
|
1369
|
+
if (config.frontendFramework) {
|
|
1370
|
+
const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
|
|
1953
1371
|
if (profile) {
|
|
1954
1372
|
rules.push(...profile.rules);
|
|
1955
1373
|
avoids.push(...profile.avoid);
|
|
@@ -1957,19 +1375,19 @@ function getProfileRules(config2) {
|
|
|
1957
1375
|
}
|
|
1958
1376
|
return { rules, avoids };
|
|
1959
1377
|
}
|
|
1960
|
-
async function loadRelevantRules(cwd,
|
|
1378
|
+
async function loadRelevantRules(cwd, config, rel, diff, fallbackRules) {
|
|
1961
1379
|
try {
|
|
1962
1380
|
const query = buildContextQuery([
|
|
1963
1381
|
rel,
|
|
1964
1382
|
diff.slice(0, 1200),
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1383
|
+
config.backendArchitecture,
|
|
1384
|
+
config.frontendFramework,
|
|
1385
|
+
config.language
|
|
1968
1386
|
]);
|
|
1969
1387
|
const memories = await retrieveContextualMemories({
|
|
1970
1388
|
query,
|
|
1971
1389
|
cwd,
|
|
1972
|
-
config
|
|
1390
|
+
config,
|
|
1973
1391
|
limit: 15
|
|
1974
1392
|
});
|
|
1975
1393
|
const selected = memories.filter((memory) => ["rule", "pattern", "decision"].includes(memory.type)).map((memory) => memory.content);
|
|
@@ -2037,7 +1455,7 @@ function normalizeForGit(pathLike) {
|
|
|
2037
1455
|
return pathLike.split(sep).join("/");
|
|
2038
1456
|
}
|
|
2039
1457
|
function listSourceFilesFromFilesystem(dir) {
|
|
2040
|
-
if (!
|
|
1458
|
+
if (!existsSync5(dir)) return [];
|
|
2041
1459
|
const files = [];
|
|
2042
1460
|
const stack = [dir];
|
|
2043
1461
|
while (stack.length > 0) {
|
|
@@ -2049,7 +1467,7 @@ function listSourceFilesFromFilesystem(dir) {
|
|
|
2049
1467
|
continue;
|
|
2050
1468
|
}
|
|
2051
1469
|
for (const entry of entries) {
|
|
2052
|
-
const absolute =
|
|
1470
|
+
const absolute = join5(current, entry);
|
|
2053
1471
|
let isDirectory = false;
|
|
2054
1472
|
let isFile = false;
|
|
2055
1473
|
try {
|
|
@@ -2066,7 +1484,7 @@ function listSourceFilesFromFilesystem(dir) {
|
|
|
2066
1484
|
stack.push(absolute);
|
|
2067
1485
|
continue;
|
|
2068
1486
|
}
|
|
2069
|
-
if (isFile &&
|
|
1487
|
+
if (isFile && SOURCE_EXTENSIONS.test(absolute)) files.push(absolute);
|
|
2070
1488
|
}
|
|
2071
1489
|
}
|
|
2072
1490
|
return files;
|
|
@@ -2079,10 +1497,10 @@ function listTrackedSourceFiles(projectRoot, watchPath) {
|
|
|
2079
1497
|
if (listed.status !== 0) {
|
|
2080
1498
|
return listSourceFilesFromFilesystem(watchPath).sort();
|
|
2081
1499
|
}
|
|
2082
|
-
const files = (listed.stdout ?? "").split("\n").filter((file) => file.length > 0).filter((file) =>
|
|
1500
|
+
const files = (listed.stdout ?? "").split("\n").filter((file) => file.length > 0).filter((file) => SOURCE_EXTENSIONS.test(file)).filter((file) => inRoot || file.startsWith(prefixWithSlash)).map((file) => join5(projectRoot, file)).filter((file) => existsSync5(file));
|
|
2083
1501
|
return [...new Set(files)].sort();
|
|
2084
1502
|
}
|
|
2085
|
-
async function runSnapshotScan(projectRoot, watchPath,
|
|
1503
|
+
async function runSnapshotScan(projectRoot, watchPath, config, verbose, debug, onEvent) {
|
|
2086
1504
|
const files = listTrackedSourceFiles(projectRoot, watchPath);
|
|
2087
1505
|
if (files.length === 0) {
|
|
2088
1506
|
console.log(chalk.yellow("\n No tracked source files found for scan.\n"));
|
|
@@ -2104,7 +1522,7 @@ async function runSnapshotScan(projectRoot, watchPath, config2, verbose, debug,
|
|
|
2104
1522
|
const rel = normalizeForGit(relative(projectRoot, filePath));
|
|
2105
1523
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2106
1524
|
onEvent?.({ type: "saved", timestamp, file: rel });
|
|
2107
|
-
const result = await checkFile(filePath, projectRoot,
|
|
1525
|
+
const result = await checkFile(filePath, projectRoot, config, verbose, debug, "snapshot", onEvent);
|
|
2108
1526
|
if (result.type !== "checked") {
|
|
2109
1527
|
if (result.type === "skipped") {
|
|
2110
1528
|
onEvent?.({ type: "skipped", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, reason: result.reason });
|
|
@@ -2125,8 +1543,8 @@ async function runSnapshotScan(projectRoot, watchPath, config2, verbose, debug,
|
|
|
2125
1543
|
return summary;
|
|
2126
1544
|
}
|
|
2127
1545
|
async function autoFixFile(filePath, projectRoot, violations, rules, avoids, debug) {
|
|
2128
|
-
if (!
|
|
2129
|
-
const content =
|
|
1546
|
+
if (!existsSync5(filePath)) return false;
|
|
1547
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
2130
1548
|
const rel = relative(projectRoot, filePath).split(sep).join("/");
|
|
2131
1549
|
const violationSummary = violations.map(
|
|
2132
1550
|
(v, i) => `${i + 1}. Rule: "${v.rule}"
|
|
@@ -2182,13 +1600,13 @@ ${content}`;
|
|
|
2182
1600
|
return false;
|
|
2183
1601
|
}
|
|
2184
1602
|
}
|
|
2185
|
-
async function checkFile(filePath, projectRoot,
|
|
1603
|
+
async function checkFile(filePath, projectRoot, config, verbose, debug, mode = "diff", onEvent) {
|
|
2186
1604
|
const rel = relative(projectRoot, filePath).split(sep).join("/");
|
|
2187
1605
|
if (rel.startsWith("..")) return { type: "skipped", reason: "File is outside project root" };
|
|
2188
1606
|
let inputText;
|
|
2189
1607
|
if (mode === "snapshot") {
|
|
2190
|
-
if (!
|
|
2191
|
-
inputText =
|
|
1608
|
+
if (!existsSync5(filePath)) return { type: "skipped", reason: "File no longer exists" };
|
|
1609
|
+
inputText = readFileSync5(filePath, "utf-8");
|
|
2192
1610
|
if (!inputText.trim()) return { type: "skipped", reason: "File is empty" };
|
|
2193
1611
|
} else {
|
|
2194
1612
|
const headResult = spawnSync("git", ["diff", "HEAD", "--", rel], { encoding: "utf-8", cwd: projectRoot });
|
|
@@ -2203,8 +1621,8 @@ async function checkFile(filePath, projectRoot, config2, verbose, debug, mode =
|
|
|
2203
1621
|
}
|
|
2204
1622
|
if (!inputText.trim()) return { type: "skipped", reason: "No changes compared with HEAD" };
|
|
2205
1623
|
}
|
|
2206
|
-
const { rules: fallbackRules, avoids } = getProfileRules(
|
|
2207
|
-
const rules = await loadRelevantRules(projectRoot,
|
|
1624
|
+
const { rules: fallbackRules, avoids } = getProfileRules(config);
|
|
1625
|
+
const rules = await loadRelevantRules(projectRoot, config, rel, inputText, fallbackRules);
|
|
2208
1626
|
if (rules.length === 0) return { type: "skipped", reason: "No applicable architecture rules" };
|
|
2209
1627
|
const MAX_INPUT = 6e3;
|
|
2210
1628
|
const truncated = inputText.length > MAX_INPUT;
|
|
@@ -2219,10 +1637,10 @@ async function checkFile(filePath, projectRoot, config2, verbose, debug, mode =
|
|
|
2219
1637
|
return why ? `${i + 1}. ${r}
|
|
2220
1638
|
WHY: ${why}` : `${i + 1}. ${r}`;
|
|
2221
1639
|
}).join("\n");
|
|
2222
|
-
const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(
|
|
1640
|
+
const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...await loadIgnorePatterns()])];
|
|
2223
1641
|
const astViolations = findAstDeterministicViolationsForFile(rel, {
|
|
2224
1642
|
cwd: projectRoot,
|
|
2225
|
-
config
|
|
1643
|
+
config,
|
|
2226
1644
|
rules,
|
|
2227
1645
|
reasonLookup: reasonMap
|
|
2228
1646
|
}).map((violation) => ({
|
|
@@ -2309,7 +1727,7 @@ ${inputToSend}`;
|
|
|
2309
1727
|
console.log(chalk.yellow(" Rule: ") + v.rule);
|
|
2310
1728
|
const why = v.reason ?? reasonMap.get(v.rule);
|
|
2311
1729
|
if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
|
|
2312
|
-
if (v.line &&
|
|
1730
|
+
if (v.line && existsSync5(filePath)) {
|
|
2313
1731
|
printCodeContext(filePath, v.line, 1);
|
|
2314
1732
|
}
|
|
2315
1733
|
if (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
|
|
@@ -2345,7 +1763,7 @@ ${inputToSend}`;
|
|
|
2345
1763
|
console.log(chalk.yellow(" Rule: ") + v.rule);
|
|
2346
1764
|
const why = v.reason ?? reasonMap.get(v.rule);
|
|
2347
1765
|
if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
|
|
2348
|
-
if (v.line &&
|
|
1766
|
+
if (v.line && existsSync5(filePath)) {
|
|
2349
1767
|
printCodeContext(filePath, v.line, 1);
|
|
2350
1768
|
}
|
|
2351
1769
|
if (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
|
|
@@ -2360,11 +1778,11 @@ ${inputToSend}`;
|
|
|
2360
1778
|
}
|
|
2361
1779
|
async function scanFiles(options = {}) {
|
|
2362
1780
|
const { projectRoot, watchPath } = resolveWatchPaths(options.path, options.projectRoot);
|
|
2363
|
-
const
|
|
2364
|
-
if (!
|
|
1781
|
+
const config = loadConfig(projectRoot);
|
|
1782
|
+
if (!config) {
|
|
2365
1783
|
throw new Error("No .memory-core.json found. Run: memory-core init");
|
|
2366
1784
|
}
|
|
2367
|
-
const { rules } = getProfileRules(
|
|
1785
|
+
const { rules } = getProfileRules(config);
|
|
2368
1786
|
if (rules.length === 0) {
|
|
2369
1787
|
console.log(chalk.yellow("\n No architecture rules configured in .memory-core.json \u2014 nothing to scan.\n"));
|
|
2370
1788
|
return {
|
|
@@ -2383,7 +1801,7 @@ async function scanFiles(options = {}) {
|
|
|
2383
1801
|
const summary = await runSnapshotScan(
|
|
2384
1802
|
projectRoot,
|
|
2385
1803
|
watchPath,
|
|
2386
|
-
|
|
1804
|
+
config,
|
|
2387
1805
|
options.verbose ?? false,
|
|
2388
1806
|
options.debug ?? false,
|
|
2389
1807
|
options.onEvent
|
|
@@ -2399,9 +1817,9 @@ async function scanFiles(options = {}) {
|
|
|
2399
1817
|
}
|
|
2400
1818
|
async function startWatch(options = {}) {
|
|
2401
1819
|
const { projectRoot, watchPath } = resolveWatchPaths(options.path, options.projectRoot);
|
|
2402
|
-
const
|
|
1820
|
+
const config = loadConfig(projectRoot);
|
|
2403
1821
|
const exitOnSetupFailure = options.exitOnSetupFailure ?? true;
|
|
2404
|
-
if (!
|
|
1822
|
+
if (!config) {
|
|
2405
1823
|
const message = "No .memory-core.json found. Run: memory-core init";
|
|
2406
1824
|
console.error(chalk.red(`
|
|
2407
1825
|
${message}
|
|
@@ -2410,7 +1828,7 @@ async function startWatch(options = {}) {
|
|
|
2410
1828
|
if (exitOnSetupFailure) process.exit(1);
|
|
2411
1829
|
return;
|
|
2412
1830
|
}
|
|
2413
|
-
const { rules, avoids } = getProfileRules(
|
|
1831
|
+
const { rules, avoids } = getProfileRules(config);
|
|
2414
1832
|
if (rules.length === 0) {
|
|
2415
1833
|
const message = "No architecture rules configured in .memory-core.json \u2014 nothing to watch.";
|
|
2416
1834
|
console.log(chalk.yellow(`
|
|
@@ -2440,7 +1858,7 @@ async function startWatch(options = {}) {
|
|
|
2440
1858
|
await runSnapshotScan(
|
|
2441
1859
|
projectRoot,
|
|
2442
1860
|
watchPath,
|
|
2443
|
-
|
|
1861
|
+
config,
|
|
2444
1862
|
options.verbose ?? false,
|
|
2445
1863
|
options.debug ?? false,
|
|
2446
1864
|
options.onEvent
|
|
@@ -2464,7 +1882,7 @@ async function startWatch(options = {}) {
|
|
|
2464
1882
|
const keepAlive = setInterval(() => {
|
|
2465
1883
|
}, 1 << 30);
|
|
2466
1884
|
const handle = (filePath) => {
|
|
2467
|
-
if (!
|
|
1885
|
+
if (!SOURCE_EXTENSIONS.test(filePath)) return;
|
|
2468
1886
|
if (pending.has(filePath)) clearTimeout(pending.get(filePath));
|
|
2469
1887
|
const timer = setTimeout(async () => {
|
|
2470
1888
|
pending.delete(filePath);
|
|
@@ -2477,7 +1895,7 @@ async function startWatch(options = {}) {
|
|
|
2477
1895
|
const result = await checkFile(
|
|
2478
1896
|
filePath,
|
|
2479
1897
|
projectRoot,
|
|
2480
|
-
|
|
1898
|
+
config,
|
|
2481
1899
|
options.verbose ?? false,
|
|
2482
1900
|
options.debug ?? false,
|
|
2483
1901
|
"diff",
|
|
@@ -2699,18 +2117,18 @@ var RuleEngineService = class {
|
|
|
2699
2117
|
|
|
2700
2118
|
// src/modules/graph-engine/application/graph-engine-service.ts
|
|
2701
2119
|
import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
2702
|
-
import { join as
|
|
2703
|
-
var
|
|
2120
|
+
import { join as join6, relative as relative2, resolve as resolve3 } from "path";
|
|
2121
|
+
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
2704
2122
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", ".memory-core"]);
|
|
2705
2123
|
function isSourceFile(pathValue) {
|
|
2706
2124
|
const extension = pathValue.slice(pathValue.lastIndexOf("."));
|
|
2707
|
-
return
|
|
2125
|
+
return SOURCE_EXTENSIONS2.has(extension);
|
|
2708
2126
|
}
|
|
2709
|
-
function
|
|
2127
|
+
function asPosix4(value) {
|
|
2710
2128
|
return value.replace(/\\/g, "/");
|
|
2711
2129
|
}
|
|
2712
2130
|
function normalizeNode(pathValue) {
|
|
2713
|
-
return
|
|
2131
|
+
return asPosix4(pathValue);
|
|
2714
2132
|
}
|
|
2715
2133
|
function edgeKey(edge) {
|
|
2716
2134
|
return `${edge.from}\0${edge.to}\0${edge.kind}`;
|
|
@@ -2734,19 +2152,19 @@ var GraphEngineService = class {
|
|
|
2734
2152
|
}
|
|
2735
2153
|
graphRepository;
|
|
2736
2154
|
async buildGraph(options = {}) {
|
|
2737
|
-
const cwd =
|
|
2155
|
+
const cwd = resolve3(options.cwd ?? process.cwd());
|
|
2738
2156
|
const files = this.collectSourceFiles(cwd);
|
|
2739
2157
|
const nodes = /* @__PURE__ */ new Set();
|
|
2740
2158
|
const edges = /* @__PURE__ */ new Map();
|
|
2741
2159
|
for (const relativeFile of files) {
|
|
2742
|
-
const absoluteFile =
|
|
2160
|
+
const absoluteFile = resolve3(cwd, relativeFile);
|
|
2743
2161
|
const fromNode = normalizeNode(relativeFile);
|
|
2744
2162
|
nodes.add(fromNode);
|
|
2745
2163
|
const imports = collectResolvedImports(absoluteFile, cwd);
|
|
2746
2164
|
for (const imp of imports) {
|
|
2747
2165
|
let toNode;
|
|
2748
2166
|
if (imp.resolvedPath) {
|
|
2749
|
-
toNode = normalizeNode(
|
|
2167
|
+
toNode = normalizeNode(asPosix4(relative2(cwd, imp.resolvedPath)));
|
|
2750
2168
|
} else if (imp.isExternal) {
|
|
2751
2169
|
toNode = `pkg:${imp.specifier}`;
|
|
2752
2170
|
}
|
|
@@ -2817,13 +2235,13 @@ var GraphEngineService = class {
|
|
|
2817
2235
|
const walk = (dir) => {
|
|
2818
2236
|
for (const entry of readdirSync2(dir)) {
|
|
2819
2237
|
if (IGNORED_DIRS.has(entry)) continue;
|
|
2820
|
-
const absolutePath =
|
|
2238
|
+
const absolutePath = join6(dir, entry);
|
|
2821
2239
|
const stat = statSync2(absolutePath);
|
|
2822
2240
|
if (stat.isDirectory()) {
|
|
2823
2241
|
walk(absolutePath);
|
|
2824
2242
|
continue;
|
|
2825
2243
|
}
|
|
2826
|
-
const rel =
|
|
2244
|
+
const rel = asPosix4(relative2(cwd, absolutePath));
|
|
2827
2245
|
if (isSourceFile(rel)) files.push(rel);
|
|
2828
2246
|
}
|
|
2829
2247
|
};
|
|
@@ -2965,11 +2383,11 @@ function getStackReason(memory, activeArchitectures2) {
|
|
|
2965
2383
|
reason: `excluded: tagged for ${architectureKeys.join(", ")}; active stack is ${active}`
|
|
2966
2384
|
};
|
|
2967
2385
|
}
|
|
2968
|
-
function inferProjectArchitectures(cwd = process.cwd(),
|
|
2386
|
+
function inferProjectArchitectures(cwd = process.cwd(), config) {
|
|
2969
2387
|
const inferred = /* @__PURE__ */ new Set();
|
|
2970
|
-
if (
|
|
2971
|
-
if (
|
|
2972
|
-
if (
|
|
2388
|
+
if (config?.backendArchitecture && config.backendArchitecture !== "custom") inferred.add(config.backendArchitecture);
|
|
2389
|
+
if (config?.frontendFramework && config.frontendFramework !== "custom") inferred.add(config.frontendFramework);
|
|
2390
|
+
if (config?.projectType === "backend" && !config.backendArchitecture) {
|
|
2973
2391
|
inferred.add("clean-architecture");
|
|
2974
2392
|
}
|
|
2975
2393
|
const detected = detectProject(cwd);
|
|
@@ -2978,14 +2396,14 @@ function inferProjectArchitectures(cwd = process.cwd(), config2) {
|
|
|
2978
2396
|
}
|
|
2979
2397
|
return [...inferred];
|
|
2980
2398
|
}
|
|
2981
|
-
function getAllowPatterns(
|
|
2982
|
-
return [...new Set(
|
|
2399
|
+
function getAllowPatterns(config) {
|
|
2400
|
+
return [...new Set(config?.allowPatterns?.filter(Boolean) ?? [])];
|
|
2983
2401
|
}
|
|
2984
|
-
function filterRelevantMemories(memories,
|
|
2985
|
-
return explainMemorySelection(memories,
|
|
2402
|
+
function filterRelevantMemories(memories, config, cwd = process.cwd()) {
|
|
2403
|
+
return explainMemorySelection(memories, config, cwd).included;
|
|
2986
2404
|
}
|
|
2987
|
-
function explainMemorySelection(memories,
|
|
2988
|
-
const activeArchitectures2 = inferProjectArchitectures(cwd,
|
|
2405
|
+
function explainMemorySelection(memories, config, cwd = process.cwd(), threshold = 0.8) {
|
|
2406
|
+
const activeArchitectures2 = inferProjectArchitectures(cwd, config);
|
|
2989
2407
|
const activeSet = new Set(activeArchitectures2);
|
|
2990
2408
|
const included = [];
|
|
2991
2409
|
const decisions = [];
|
|
@@ -3061,8 +2479,8 @@ async function retrieveMemorySelection(options) {
|
|
|
3061
2479
|
|
|
3062
2480
|
// src/generator.ts
|
|
3063
2481
|
var __filename = fileURLToPath(import.meta.url);
|
|
3064
|
-
var __dirname =
|
|
3065
|
-
var PKG_ROOT =
|
|
2482
|
+
var __dirname = dirname3(__filename);
|
|
2483
|
+
var PKG_ROOT = join7(__dirname, "..");
|
|
3066
2484
|
function stringifyProfileScalar(value) {
|
|
3067
2485
|
if (typeof value === "string") {
|
|
3068
2486
|
const trimmed = value.trim();
|
|
@@ -3153,15 +2571,15 @@ function loadProfile(name) {
|
|
|
3153
2571
|
if (name === "custom") {
|
|
3154
2572
|
return { name: "custom", displayName: "Custom", layer: "backend", description: "Custom architecture \u2014 rules added via memory-core remember", rules: [], folders: [], avoid: [] };
|
|
3155
2573
|
}
|
|
3156
|
-
const profilePath =
|
|
3157
|
-
if (!
|
|
3158
|
-
return normalizeArchitectureProfile(yaml.load(
|
|
2574
|
+
const profilePath = join7(PKG_ROOT, "profiles", `${name}.yml`);
|
|
2575
|
+
if (!existsSync6(profilePath)) throw new Error(`Profile not found: ${name}`);
|
|
2576
|
+
return normalizeArchitectureProfile(yaml.load(readFileSync6(profilePath, "utf-8")), name);
|
|
3159
2577
|
}
|
|
3160
2578
|
function listProfiles(layer) {
|
|
3161
|
-
const files = readdirSync3(
|
|
2579
|
+
const files = readdirSync3(join7(PKG_ROOT, "profiles")).filter((f) => f.endsWith(".yml"));
|
|
3162
2580
|
const all = files.map(
|
|
3163
2581
|
(f) => normalizeArchitectureProfile(
|
|
3164
|
-
yaml.load(
|
|
2582
|
+
yaml.load(readFileSync6(join7(PKG_ROOT, "profiles", f), "utf-8")),
|
|
3165
2583
|
basename(f, ".yml")
|
|
3166
2584
|
)
|
|
3167
2585
|
);
|
|
@@ -3231,15 +2649,15 @@ function buildTemplateData(options, cwd = process.cwd()) {
|
|
|
3231
2649
|
};
|
|
3232
2650
|
}
|
|
3233
2651
|
function renderTemplate(templateName, data) {
|
|
3234
|
-
const templatePath =
|
|
3235
|
-
if (!
|
|
3236
|
-
return Handlebars.compile(
|
|
2652
|
+
const templatePath = join7(PKG_ROOT, "templates", templateName);
|
|
2653
|
+
if (!existsSync6(templatePath)) throw new Error(`Template not found: ${templateName}`);
|
|
2654
|
+
return Handlebars.compile(readFileSync6(templatePath, "utf-8"))(data);
|
|
3237
2655
|
}
|
|
3238
2656
|
function writeFile(filePath, content) {
|
|
3239
|
-
const dir =
|
|
3240
|
-
if (!
|
|
3241
|
-
if (
|
|
3242
|
-
const existing =
|
|
2657
|
+
const dir = dirname3(filePath);
|
|
2658
|
+
if (!existsSync6(dir)) mkdirSync2(dir, { recursive: true });
|
|
2659
|
+
if (existsSync6(filePath)) {
|
|
2660
|
+
const existing = readFileSync6(filePath, "utf-8");
|
|
3243
2661
|
if (existing === content) return "skipped";
|
|
3244
2662
|
}
|
|
3245
2663
|
writeFileSync4(filePath, content, "utf-8");
|
|
@@ -3251,8 +2669,8 @@ async function generate(options, cwd = process.cwd(), onlyAgents) {
|
|
|
3251
2669
|
const skipped = [];
|
|
3252
2670
|
const files = onlyAgents ? OUTPUT_FILES.filter((f) => onlyAgents.includes(f.agent)) : OUTPUT_FILES;
|
|
3253
2671
|
for (const output of files) {
|
|
3254
|
-
const targetPath =
|
|
3255
|
-
if (output.skipIfExists &&
|
|
2672
|
+
const targetPath = join7(cwd, output.path);
|
|
2673
|
+
if (output.skipIfExists && existsSync6(targetPath)) {
|
|
3256
2674
|
skipped.push(output.path);
|
|
3257
2675
|
continue;
|
|
3258
2676
|
}
|
|
@@ -3272,9 +2690,9 @@ async function generate(options, cwd = process.cwd(), onlyAgents) {
|
|
|
3272
2690
|
var reasonMap2 = new Map(
|
|
3273
2691
|
seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
|
|
3274
2692
|
);
|
|
3275
|
-
var HOOK_PATH =
|
|
2693
|
+
var HOOK_PATH = join8(".git", "hooks", "pre-commit");
|
|
3276
2694
|
var HOOK_MARKER = "# archmind-memory-core";
|
|
3277
|
-
var COMMIT_MSG_HOOK_PATH =
|
|
2695
|
+
var COMMIT_MSG_HOOK_PATH = join8(".git", "hooks", "commit-msg");
|
|
3278
2696
|
var COMMIT_MSG_HOOK_MARKER = "# archmind-memory-core commit-msg";
|
|
3279
2697
|
var RULE_CACHE_FILE = ".memory-core-rules-cache.json";
|
|
3280
2698
|
var DB_VERSION_FILE = ".memory-core-db-version";
|
|
@@ -3334,7 +2752,7 @@ function toRuleStatEntry(raw) {
|
|
|
3334
2752
|
if (typeof raw === "number") return { count: raw, falsePositives: 0 };
|
|
3335
2753
|
return raw;
|
|
3336
2754
|
}
|
|
3337
|
-
function
|
|
2755
|
+
function readPositiveIntEnv(name, fallback) {
|
|
3338
2756
|
const raw = Number(process.env[name]);
|
|
3339
2757
|
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
|
|
3340
2758
|
}
|
|
@@ -3346,8 +2764,8 @@ async function withTimeout(promise, timeoutMs, fallback) {
|
|
|
3346
2764
|
try {
|
|
3347
2765
|
return await Promise.race([
|
|
3348
2766
|
promise,
|
|
3349
|
-
new Promise((
|
|
3350
|
-
timer = setTimeout(() =>
|
|
2767
|
+
new Promise((resolve4) => {
|
|
2768
|
+
timer = setTimeout(() => resolve4(fallback), timeoutMs);
|
|
3351
2769
|
})
|
|
3352
2770
|
]);
|
|
3353
2771
|
} finally {
|
|
@@ -3355,11 +2773,11 @@ async function withTimeout(promise, timeoutMs, fallback) {
|
|
|
3355
2773
|
}
|
|
3356
2774
|
}
|
|
3357
2775
|
function recordViolations(violations, source = "hook") {
|
|
3358
|
-
const statsPath =
|
|
2776
|
+
const statsPath = join8(process.cwd(), ".memory-core-stats.json");
|
|
3359
2777
|
let stats = { rules: {}, files: {} };
|
|
3360
|
-
if (
|
|
2778
|
+
if (existsSync7(statsPath)) {
|
|
3361
2779
|
try {
|
|
3362
|
-
stats = JSON.parse(
|
|
2780
|
+
stats = JSON.parse(readFileSync7(statsPath, "utf-8"));
|
|
3363
2781
|
} catch {
|
|
3364
2782
|
stats = { rules: {}, files: {} };
|
|
3365
2783
|
}
|
|
@@ -3377,11 +2795,11 @@ function recordViolations(violations, source = "hook") {
|
|
|
3377
2795
|
writeFileSync5(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
3378
2796
|
}
|
|
3379
2797
|
function resetViolationStats(cwd = process.cwd()) {
|
|
3380
|
-
const statsPath =
|
|
3381
|
-
if (!
|
|
2798
|
+
const statsPath = join8(cwd, ".memory-core-stats.json");
|
|
2799
|
+
if (!existsSync7(statsPath)) return;
|
|
3382
2800
|
let stats = {};
|
|
3383
2801
|
try {
|
|
3384
|
-
const parsed = JSON.parse(
|
|
2802
|
+
const parsed = JSON.parse(readFileSync7(statsPath, "utf-8"));
|
|
3385
2803
|
if (parsed && typeof parsed === "object") {
|
|
3386
2804
|
stats = parsed;
|
|
3387
2805
|
}
|
|
@@ -3394,11 +2812,11 @@ function resetViolationStats(cwd = process.cwd()) {
|
|
|
3394
2812
|
writeFileSync5(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
3395
2813
|
}
|
|
3396
2814
|
function recordBypass(hadReason, cwd = process.cwd()) {
|
|
3397
|
-
const statsPath =
|
|
2815
|
+
const statsPath = join8(cwd, ".memory-core-stats.json");
|
|
3398
2816
|
let stats = { rules: {}, files: {} };
|
|
3399
|
-
if (
|
|
2817
|
+
if (existsSync7(statsPath)) {
|
|
3400
2818
|
try {
|
|
3401
|
-
stats = JSON.parse(
|
|
2819
|
+
stats = JSON.parse(readFileSync7(statsPath, "utf-8"));
|
|
3402
2820
|
} catch {
|
|
3403
2821
|
}
|
|
3404
2822
|
}
|
|
@@ -3412,10 +2830,10 @@ function recordBypass(hadReason, cwd = process.cwd()) {
|
|
|
3412
2830
|
return stats.bypasses;
|
|
3413
2831
|
}
|
|
3414
2832
|
function readBypassStats(cwd = process.cwd()) {
|
|
3415
|
-
const statsPath =
|
|
3416
|
-
if (!
|
|
2833
|
+
const statsPath = join8(cwd, ".memory-core-stats.json");
|
|
2834
|
+
if (!existsSync7(statsPath)) return { total: 0, withReason: 0, withoutReason: 0 };
|
|
3417
2835
|
try {
|
|
3418
|
-
const stats = JSON.parse(
|
|
2836
|
+
const stats = JSON.parse(readFileSync7(statsPath, "utf-8"));
|
|
3419
2837
|
return stats.bypasses ?? { total: 0, withReason: 0, withoutReason: 0 };
|
|
3420
2838
|
} catch {
|
|
3421
2839
|
return { total: 0, withReason: 0, withoutReason: 0 };
|
|
@@ -3423,11 +2841,11 @@ function readBypassStats(cwd = process.cwd()) {
|
|
|
3423
2841
|
}
|
|
3424
2842
|
function accumulateTokenUsage(usage, cwd = process.cwd()) {
|
|
3425
2843
|
if (!usage) return;
|
|
3426
|
-
const statsPath =
|
|
2844
|
+
const statsPath = join8(cwd, ".memory-core-stats.json");
|
|
3427
2845
|
let stats = { rules: {}, files: {} };
|
|
3428
|
-
if (
|
|
2846
|
+
if (existsSync7(statsPath)) {
|
|
3429
2847
|
try {
|
|
3430
|
-
stats = JSON.parse(
|
|
2848
|
+
stats = JSON.parse(readFileSync7(statsPath, "utf-8"));
|
|
3431
2849
|
} catch {
|
|
3432
2850
|
}
|
|
3433
2851
|
}
|
|
@@ -3471,17 +2889,17 @@ async function promptToSaveViolations(violations) {
|
|
|
3471
2889
|
}
|
|
3472
2890
|
}
|
|
3473
2891
|
function readRuleCache(cwd) {
|
|
3474
|
-
const cachePath =
|
|
3475
|
-
const configPath =
|
|
3476
|
-
if (!
|
|
2892
|
+
const cachePath = join8(cwd, RULE_CACHE_FILE);
|
|
2893
|
+
const configPath = join8(cwd, ".memory-core.json");
|
|
2894
|
+
if (!existsSync7(cachePath) || !existsSync7(configPath)) return null;
|
|
3477
2895
|
try {
|
|
3478
|
-
const entry = JSON.parse(
|
|
2896
|
+
const entry = JSON.parse(readFileSync7(cachePath, "utf-8"));
|
|
3479
2897
|
const now = Date.now();
|
|
3480
2898
|
if (now - entry.timestamp > RULE_CACHE_TTL_MS) return null;
|
|
3481
2899
|
const configMtime = statSync3(configPath).mtimeMs;
|
|
3482
2900
|
if (configMtime !== entry.configMtime) return null;
|
|
3483
|
-
const dbVersionPath =
|
|
3484
|
-
const dbVersionMtime =
|
|
2901
|
+
const dbVersionPath = join8(cwd, DB_VERSION_FILE);
|
|
2902
|
+
const dbVersionMtime = existsSync7(dbVersionPath) ? statSync3(dbVersionPath).mtimeMs : 0;
|
|
3485
2903
|
if (dbVersionMtime !== entry.dbVersionMtime) return null;
|
|
3486
2904
|
return entry;
|
|
3487
2905
|
} catch {
|
|
@@ -3489,18 +2907,18 @@ function readRuleCache(cwd) {
|
|
|
3489
2907
|
}
|
|
3490
2908
|
}
|
|
3491
2909
|
function saveRuleCache(cwd, data) {
|
|
3492
|
-
const configPath =
|
|
2910
|
+
const configPath = join8(cwd, ".memory-core.json");
|
|
3493
2911
|
try {
|
|
3494
2912
|
const configMtime = statSync3(configPath).mtimeMs;
|
|
3495
|
-
const dbVersionPath =
|
|
3496
|
-
const dbVersionMtime =
|
|
2913
|
+
const dbVersionPath = join8(cwd, DB_VERSION_FILE);
|
|
2914
|
+
const dbVersionMtime = existsSync7(dbVersionPath) ? statSync3(dbVersionPath).mtimeMs : 0;
|
|
3497
2915
|
const entry = {
|
|
3498
2916
|
timestamp: Date.now(),
|
|
3499
2917
|
configMtime,
|
|
3500
2918
|
dbVersionMtime,
|
|
3501
2919
|
...data
|
|
3502
2920
|
};
|
|
3503
|
-
writeFileSync5(
|
|
2921
|
+
writeFileSync5(join8(cwd, RULE_CACHE_FILE), JSON.stringify(entry, null, 2) + "\n", "utf-8");
|
|
3504
2922
|
} catch {
|
|
3505
2923
|
}
|
|
3506
2924
|
}
|
|
@@ -3513,18 +2931,18 @@ async function loadIgnorePatterns2() {
|
|
|
3513
2931
|
return [];
|
|
3514
2932
|
}
|
|
3515
2933
|
}
|
|
3516
|
-
function getProfileRules2(
|
|
2934
|
+
function getProfileRules2(config) {
|
|
3517
2935
|
const rules = [];
|
|
3518
2936
|
const avoids = [];
|
|
3519
|
-
if (
|
|
3520
|
-
const profile = listProfiles("backend").find((p) => p.name ===
|
|
2937
|
+
if (config.backendArchitecture) {
|
|
2938
|
+
const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
|
|
3521
2939
|
if (profile) {
|
|
3522
2940
|
rules.push(...profile.rules);
|
|
3523
2941
|
avoids.push(...profile.avoid);
|
|
3524
2942
|
}
|
|
3525
2943
|
}
|
|
3526
|
-
if (
|
|
3527
|
-
const profile = listProfiles("frontend").find((p) => p.name ===
|
|
2944
|
+
if (config.frontendFramework) {
|
|
2945
|
+
const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
|
|
3528
2946
|
if (profile) {
|
|
3529
2947
|
rules.push(...profile.rules);
|
|
3530
2948
|
avoids.push(...profile.avoid);
|
|
@@ -3532,19 +2950,19 @@ function getProfileRules2(config2) {
|
|
|
3532
2950
|
}
|
|
3533
2951
|
return { rules, avoids };
|
|
3534
2952
|
}
|
|
3535
|
-
async function loadRelevantRules2(
|
|
2953
|
+
async function loadRelevantRules2(config, diff, stagedFiles, fallbackRules) {
|
|
3536
2954
|
try {
|
|
3537
2955
|
const query = buildContextQuery([
|
|
3538
2956
|
stagedFiles.join("\n"),
|
|
3539
2957
|
diff.slice(0, 1200),
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
2958
|
+
config.backendArchitecture,
|
|
2959
|
+
config.frontendFramework,
|
|
2960
|
+
config.language
|
|
3543
2961
|
]);
|
|
3544
2962
|
const memories = await retrieveContextualMemories({
|
|
3545
2963
|
query,
|
|
3546
2964
|
cwd: process.cwd(),
|
|
3547
|
-
config
|
|
2965
|
+
config,
|
|
3548
2966
|
limit: 15
|
|
3549
2967
|
});
|
|
3550
2968
|
const selected = memories.filter((memory) => ["rule", "pattern", "decision"].includes(memory.type)).map((memory) => memory.content);
|
|
@@ -3725,10 +3143,10 @@ function parseFalsePositiveDecisions(raw) {
|
|
|
3725
3143
|
return { valid: false, decisions: [] };
|
|
3726
3144
|
}
|
|
3727
3145
|
function loadRecentViolationsFromStats(cwd = process.cwd()) {
|
|
3728
|
-
const statsPath =
|
|
3729
|
-
if (!
|
|
3146
|
+
const statsPath = join8(cwd, ".memory-core-stats.json");
|
|
3147
|
+
if (!existsSync7(statsPath)) return [];
|
|
3730
3148
|
try {
|
|
3731
|
-
const parsed = JSON.parse(
|
|
3149
|
+
const parsed = JSON.parse(readFileSync7(statsPath, "utf-8"));
|
|
3732
3150
|
if (!Array.isArray(parsed.recentViolations)) return [];
|
|
3733
3151
|
return parsed.recentViolations.filter(
|
|
3734
3152
|
(entry) => Boolean(entry) && typeof entry.rule === "string" && typeof entry.issue === "string" && typeof entry.file === "string" && typeof entry.timestamp === "string"
|
|
@@ -3739,11 +3157,11 @@ function loadRecentViolationsFromStats(cwd = process.cwd()) {
|
|
|
3739
3157
|
}
|
|
3740
3158
|
function incrementFalsePositivesForPatterns(learnedPatterns, violations, cwd = process.cwd()) {
|
|
3741
3159
|
if (learnedPatterns.length === 0 || violations.length === 0) return;
|
|
3742
|
-
const statsPath =
|
|
3743
|
-
if (!
|
|
3160
|
+
const statsPath = join8(cwd, ".memory-core-stats.json");
|
|
3161
|
+
if (!existsSync7(statsPath)) return;
|
|
3744
3162
|
let stats;
|
|
3745
3163
|
try {
|
|
3746
|
-
stats = JSON.parse(
|
|
3164
|
+
stats = JSON.parse(readFileSync7(statsPath, "utf-8"));
|
|
3747
3165
|
} catch {
|
|
3748
3166
|
return;
|
|
3749
3167
|
}
|
|
@@ -3787,7 +3205,7 @@ ${JSON.stringify(options.allowPatterns, null, 2)}`;
|
|
|
3787
3205
|
console.log(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
3788
3206
|
}
|
|
3789
3207
|
try {
|
|
3790
|
-
const recheckTimeoutMs =
|
|
3208
|
+
const recheckTimeoutMs = readPositiveIntEnv("MEMORY_CORE_FALSE_POSITIVE_TIMEOUT_MS", 6e3);
|
|
3791
3209
|
const { content: raw, usage: recheckUsage } = await callChatModel([
|
|
3792
3210
|
{ role: "system", content: systemPrompt },
|
|
3793
3211
|
{ role: "user", content: userPrompt }
|
|
@@ -3963,14 +3381,14 @@ function filterModelViolationsByStagedDiff(violations, stagedFiles, diff) {
|
|
|
3963
3381
|
return filtered;
|
|
3964
3382
|
}
|
|
3965
3383
|
function installHook(advisory = true, fast = false) {
|
|
3966
|
-
if (!
|
|
3384
|
+
if (!existsSync7(".git")) {
|
|
3967
3385
|
console.error(chalk2.red("\n Not a git repository. Run from project root.\n"));
|
|
3968
3386
|
process.exit(1);
|
|
3969
3387
|
}
|
|
3970
3388
|
const script = buildHookScript(advisory, fast);
|
|
3971
3389
|
const body = buildHookBody(advisory, fast).trimEnd();
|
|
3972
|
-
if (
|
|
3973
|
-
const existing =
|
|
3390
|
+
if (existsSync7(HOOK_PATH)) {
|
|
3391
|
+
const existing = readFileSync7(HOOK_PATH, "utf-8");
|
|
3974
3392
|
if (existing.includes(HOOK_MARKER)) {
|
|
3975
3393
|
const markerIndex = existing.indexOf(HOOK_MARKER);
|
|
3976
3394
|
const beforeRaw = markerIndex > 0 ? existing.slice(0, markerIndex) : "";
|
|
@@ -4002,11 +3420,11 @@ ${body}
|
|
|
4002
3420
|
console.log(chalk2.gray(" To uninstall: memory-core hook uninstall\n"));
|
|
4003
3421
|
}
|
|
4004
3422
|
function uninstallHook() {
|
|
4005
|
-
if (!
|
|
3423
|
+
if (!existsSync7(HOOK_PATH)) {
|
|
4006
3424
|
console.log(chalk2.yellow("\n No pre-commit hook found.\n"));
|
|
4007
3425
|
return;
|
|
4008
3426
|
}
|
|
4009
|
-
const content =
|
|
3427
|
+
const content = readFileSync7(HOOK_PATH, "utf-8");
|
|
4010
3428
|
if (!content.includes(HOOK_MARKER)) {
|
|
4011
3429
|
console.log(chalk2.yellow("\n ArchMind hook not found in pre-commit \u2014 nothing to remove.\n"));
|
|
4012
3430
|
return;
|
|
@@ -4051,8 +3469,8 @@ function installCommitMsgHook(advisory = true) {
|
|
|
4051
3469
|
|
|
4052
3470
|
${body}
|
|
4053
3471
|
`;
|
|
4054
|
-
if (
|
|
4055
|
-
const existing =
|
|
3472
|
+
if (existsSync7(COMMIT_MSG_HOOK_PATH)) {
|
|
3473
|
+
const existing = readFileSync7(COMMIT_MSG_HOOK_PATH, "utf-8");
|
|
4056
3474
|
if (existing.includes(COMMIT_MSG_HOOK_MARKER)) {
|
|
4057
3475
|
const markerIndex = existing.indexOf(COMMIT_MSG_HOOK_MARKER);
|
|
4058
3476
|
const beforeRaw = markerIndex > 0 ? existing.slice(0, markerIndex) : "";
|
|
@@ -4073,8 +3491,8 @@ ${body}
|
|
|
4073
3491
|
chmodSync(COMMIT_MSG_HOOK_PATH, 493);
|
|
4074
3492
|
}
|
|
4075
3493
|
function uninstallCommitMsgHook() {
|
|
4076
|
-
if (!
|
|
4077
|
-
const content =
|
|
3494
|
+
if (!existsSync7(COMMIT_MSG_HOOK_PATH)) return;
|
|
3495
|
+
const content = readFileSync7(COMMIT_MSG_HOOK_PATH, "utf-8");
|
|
4078
3496
|
if (!content.includes(COMMIT_MSG_HOOK_MARKER)) return;
|
|
4079
3497
|
const markerIndex = content.indexOf(COMMIT_MSG_HOOK_MARKER);
|
|
4080
3498
|
const before = markerIndex > 1 ? normalizeHookPreamble(content.slice(0, markerIndex)) : "";
|
|
@@ -4086,20 +3504,20 @@ function uninstallCommitMsgHook() {
|
|
|
4086
3504
|
}
|
|
4087
3505
|
}
|
|
4088
3506
|
async function checkCommitMsg(msgFile, options = {}) {
|
|
4089
|
-
if (!
|
|
3507
|
+
if (!existsSync7(msgFile)) {
|
|
4090
3508
|
if (options.verbose) console.log(chalk2.gray(" No commit message file \u2014 skipping."));
|
|
4091
3509
|
return;
|
|
4092
3510
|
}
|
|
4093
|
-
const raw =
|
|
3511
|
+
const raw = readFileSync7(msgFile, "utf-8");
|
|
4094
3512
|
const cleanMsg = raw.split("\n").filter((l) => !l.startsWith("#")).join("\n").trim();
|
|
4095
3513
|
if (!cleanMsg) {
|
|
4096
3514
|
if (options.verbose) console.log(chalk2.gray(" Empty commit message \u2014 skipping."));
|
|
4097
3515
|
return;
|
|
4098
3516
|
}
|
|
4099
|
-
const configPath =
|
|
4100
|
-
if (!
|
|
4101
|
-
const
|
|
4102
|
-
const rules = (
|
|
3517
|
+
const configPath = join8(process.cwd(), ".memory-core.json");
|
|
3518
|
+
if (!existsSync7(configPath)) return;
|
|
3519
|
+
const config = JSON.parse(readFileSync7(configPath, "utf-8"));
|
|
3520
|
+
const rules = (config.commitRules ?? []).filter(Boolean);
|
|
4103
3521
|
if (rules.length === 0) return;
|
|
4104
3522
|
console.log(chalk2.cyan("\n archmind \u2014 checking commit message\u2026"));
|
|
4105
3523
|
const violations = [];
|
|
@@ -4132,11 +3550,11 @@ async function checkCommitMsg(msgFile, options = {}) {
|
|
|
4132
3550
|
process.exit(1);
|
|
4133
3551
|
}
|
|
4134
3552
|
async function checkStaged(options = {}) {
|
|
4135
|
-
const
|
|
3553
|
+
const SOURCE_EXTENSIONS3 = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
|
|
4136
3554
|
let diff;
|
|
4137
3555
|
let stagedFiles = [];
|
|
4138
3556
|
try {
|
|
4139
|
-
stagedFiles = execSync("git diff --cached --name-only --diff-filter=ACMRT", { encoding: "utf-8" }).split("\n").filter((f) => f &&
|
|
3557
|
+
stagedFiles = execSync("git diff --cached --name-only --diff-filter=ACMRT", { encoding: "utf-8" }).split("\n").filter((f) => f && SOURCE_EXTENSIONS3.test(f)).map((f) => normalizePath(f));
|
|
4140
3558
|
if (stagedFiles.length === 0) {
|
|
4141
3559
|
if (options.verbose) console.log(chalk2.gray(" No source files staged \u2014 skipping rule check."));
|
|
4142
3560
|
return;
|
|
@@ -4155,20 +3573,20 @@ async function checkStaged(options = {}) {
|
|
|
4155
3573
|
if (options.verbose) console.log(chalk2.gray(" No staged changes to check."));
|
|
4156
3574
|
return;
|
|
4157
3575
|
}
|
|
4158
|
-
const configPath =
|
|
4159
|
-
if (!
|
|
4160
|
-
const
|
|
4161
|
-
const { rules: fallbackRules, avoids } = getProfileRules2(
|
|
3576
|
+
const configPath = join8(process.cwd(), ".memory-core.json");
|
|
3577
|
+
if (!existsSync7(configPath)) return;
|
|
3578
|
+
const config = JSON.parse(readFileSync7(configPath, "utf-8"));
|
|
3579
|
+
const { rules: fallbackRules, avoids } = getProfileRules2(config);
|
|
4162
3580
|
const fast = isFastCheck(options);
|
|
4163
|
-
const ruleLoadTimeoutMs =
|
|
4164
|
-
const ignoreLoadTimeoutMs =
|
|
3581
|
+
const ruleLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_RULE_LOAD_TIMEOUT_MS", 2e3);
|
|
3582
|
+
const ignoreLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_IGNORE_LOAD_TIMEOUT_MS", 1500);
|
|
4165
3583
|
let rules;
|
|
4166
3584
|
let ignores;
|
|
4167
3585
|
let allowPatterns;
|
|
4168
3586
|
if (fast) {
|
|
4169
3587
|
rules = fallbackRules;
|
|
4170
3588
|
ignores = [];
|
|
4171
|
-
allowPatterns = [...new Set(getAllowPatterns(
|
|
3589
|
+
allowPatterns = [...new Set(getAllowPatterns(config))];
|
|
4172
3590
|
} else {
|
|
4173
3591
|
const cwd = process.cwd();
|
|
4174
3592
|
const cached = readRuleCache(cwd);
|
|
@@ -4181,17 +3599,17 @@ async function checkStaged(options = {}) {
|
|
|
4181
3599
|
}
|
|
4182
3600
|
} else {
|
|
4183
3601
|
const [loadedRules, loadedIgnores] = await Promise.all([
|
|
4184
|
-
withTimeout(loadRelevantRules2(
|
|
3602
|
+
withTimeout(loadRelevantRules2(config, diff, stagedFiles, fallbackRules), ruleLoadTimeoutMs, fallbackRules),
|
|
4185
3603
|
withTimeout(loadIgnorePatterns2(), ignoreLoadTimeoutMs, [])
|
|
4186
3604
|
]);
|
|
4187
3605
|
rules = loadedRules;
|
|
4188
3606
|
ignores = loadedIgnores;
|
|
4189
|
-
allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(
|
|
3607
|
+
allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...loadedIgnores])];
|
|
4190
3608
|
saveRuleCache(cwd, { rules, ignores, allowPatterns });
|
|
4191
3609
|
}
|
|
4192
3610
|
}
|
|
4193
3611
|
if (rules.length === 0) return;
|
|
4194
|
-
const modelInputMaxChars =
|
|
3612
|
+
const modelInputMaxChars = readPositiveIntEnv("MEMORY_CORE_MODEL_INPUT_MAX_CHARS", 8e3);
|
|
4195
3613
|
const modelInput = buildModelInputFromDiff(diff, modelInputMaxChars);
|
|
4196
3614
|
console.log(chalk2.cyan("\n archmind \u2014 checking staged changes against rules\u2026"));
|
|
4197
3615
|
if (options.verbose || options.debug) {
|
|
@@ -4235,7 +3653,7 @@ Do not include any text outside the JSON object.`;
|
|
|
4235
3653
|
const deterministicViolations = findDeterministicViolations(diff, rules, avoids, allowPatterns);
|
|
4236
3654
|
const astViolations = findAstDeterministicViolationsForDiff(diff, {
|
|
4237
3655
|
cwd: process.cwd(),
|
|
4238
|
-
config
|
|
3656
|
+
config,
|
|
4239
3657
|
rules,
|
|
4240
3658
|
reasonLookup: reasonMap2
|
|
4241
3659
|
});
|
|
@@ -4251,7 +3669,7 @@ Do not include any text outside the JSON object.`;
|
|
|
4251
3669
|
console.log(chalk2.gray(" AI check skipped; running deterministic checks only."));
|
|
4252
3670
|
}
|
|
4253
3671
|
} else try {
|
|
4254
|
-
const checkTimeoutMs =
|
|
3672
|
+
const checkTimeoutMs = readPositiveIntEnv("MEMORY_CORE_CHECK_TIMEOUT_MS", readPositiveIntEnv("CHAT_TIMEOUT_MS", 2e4));
|
|
4255
3673
|
const { content: raw, usage: checkUsage } = await callChatModel([
|
|
4256
3674
|
{ role: "system", content: systemPrompt },
|
|
4257
3675
|
{ role: "user", content: `Review these staged changes:
|
|
@@ -4350,10 +3768,10 @@ ${modelInput.text}` }
|
|
|
4350
3768
|
}
|
|
4351
3769
|
let ruleStatsSnapshot = {};
|
|
4352
3770
|
{
|
|
4353
|
-
const statsPath =
|
|
4354
|
-
if (
|
|
3771
|
+
const statsPath = join8(process.cwd(), ".memory-core-stats.json");
|
|
3772
|
+
if (existsSync7(statsPath)) {
|
|
4355
3773
|
try {
|
|
4356
|
-
const parsed = JSON.parse(
|
|
3774
|
+
const parsed = JSON.parse(readFileSync7(statsPath, "utf-8"));
|
|
4357
3775
|
ruleStatsSnapshot = parsed.rules ?? {};
|
|
4358
3776
|
} catch {
|
|
4359
3777
|
}
|
|
@@ -4467,7 +3885,7 @@ function getCiDiff() {
|
|
|
4467
3885
|
}
|
|
4468
3886
|
async function checkFile2(filePath, options = {}) {
|
|
4469
3887
|
const { readFileSync: readFile, existsSync: fileExists } = await import("fs");
|
|
4470
|
-
const resolvedPath = filePath.startsWith("/") ? filePath :
|
|
3888
|
+
const resolvedPath = filePath.startsWith("/") ? filePath : join8(process.cwd(), filePath);
|
|
4471
3889
|
if (!fileExists(resolvedPath)) {
|
|
4472
3890
|
console.error(chalk2.red(`
|
|
4473
3891
|
File not found: ${filePath}
|
|
@@ -4482,14 +3900,14 @@ async function checkFile2(filePath, options = {}) {
|
|
|
4482
3900
|
`@@ -0,0 +1,${lines.length} @@`,
|
|
4483
3901
|
...lines.map((l) => `+${l}`)
|
|
4484
3902
|
].join("\n");
|
|
4485
|
-
const configPath =
|
|
4486
|
-
if (!
|
|
3903
|
+
const configPath = join8(process.cwd(), ".memory-core.json");
|
|
3904
|
+
if (!existsSync7(configPath)) {
|
|
4487
3905
|
console.error(chalk2.red("\n No .memory-core.json found. Run: memory-core init\n"));
|
|
4488
3906
|
process.exit(1);
|
|
4489
3907
|
}
|
|
4490
|
-
const
|
|
4491
|
-
const { rules: fallbackRules, avoids } = getProfileRules2(
|
|
4492
|
-
const allowPatterns = [...new Set(getAllowPatterns(
|
|
3908
|
+
const config = JSON.parse(readFileSync7(configPath, "utf-8"));
|
|
3909
|
+
const { rules: fallbackRules, avoids } = getProfileRules2(config);
|
|
3910
|
+
const allowPatterns = [...new Set(getAllowPatterns(config))];
|
|
4493
3911
|
const fast = isFastCheck(options);
|
|
4494
3912
|
let rules;
|
|
4495
3913
|
if (fast) {
|
|
@@ -4499,9 +3917,9 @@ async function checkFile2(filePath, options = {}) {
|
|
|
4499
3917
|
if (cached) {
|
|
4500
3918
|
rules = cached.rules;
|
|
4501
3919
|
} else {
|
|
4502
|
-
const ruleLoadTimeoutMs =
|
|
3920
|
+
const ruleLoadTimeoutMs = readPositiveIntEnv("MEMORY_CORE_RULE_LOAD_TIMEOUT_MS", 2e3);
|
|
4503
3921
|
rules = await withTimeout(
|
|
4504
|
-
loadRelevantRules2(
|
|
3922
|
+
loadRelevantRules2(config, pseudoDiff, [filePath], fallbackRules),
|
|
4505
3923
|
ruleLoadTimeoutMs,
|
|
4506
3924
|
fallbackRules
|
|
4507
3925
|
);
|
|
@@ -4516,7 +3934,7 @@ async function checkFile2(filePath, options = {}) {
|
|
|
4516
3934
|
const deterministicViolations = findDeterministicViolations(pseudoDiff, rules, avoids, allowPatterns);
|
|
4517
3935
|
const astViolations = findAstDeterministicViolationsForDiff(pseudoDiff, {
|
|
4518
3936
|
cwd: process.cwd(),
|
|
4519
|
-
config
|
|
3937
|
+
config,
|
|
4520
3938
|
rules,
|
|
4521
3939
|
reasonLookup: reasonMap2
|
|
4522
3940
|
});
|
|
@@ -4606,22 +4024,13 @@ function printModelMissing(model) {
|
|
|
4606
4024
|
console.log(chalk2.yellow(`
|
|
4607
4025
|
\u26A0 Chat model "${model}" not found in Ollama.`));
|
|
4608
4026
|
console.log(chalk2.gray(` Pull a model: ollama pull ${model}`));
|
|
4609
|
-
console.log(chalk2.gray(" Or set OLLAMA_CHAT_MODEL=<model> in .env"));
|
|
4027
|
+
console.log(chalk2.gray(" Or set OLLAMA_CHAT_MODEL=<model> in .memory-core.env"));
|
|
4610
4028
|
console.log(chalk2.gray(" Recommended: llama3.2 | qwen2.5-coder:3b | mistral\n"));
|
|
4611
4029
|
}
|
|
4612
4030
|
|
|
4613
4031
|
export {
|
|
4614
4032
|
detectProject,
|
|
4615
|
-
Config,
|
|
4616
4033
|
embed,
|
|
4617
|
-
getChatProviderLabel,
|
|
4618
|
-
getPool,
|
|
4619
|
-
runMigrations,
|
|
4620
|
-
saveMemory,
|
|
4621
|
-
listMemories,
|
|
4622
|
-
deleteMemory,
|
|
4623
|
-
updateMemory,
|
|
4624
|
-
closePool,
|
|
4625
4034
|
migrateGraphSnapshots,
|
|
4626
4035
|
probeGraphSnapshotStore,
|
|
4627
4036
|
seeds,
|