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