@shahmilsaari/memory-core 1.0.2 → 1.0.4
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
CHANGED
|
@@ -356,11 +356,13 @@ Runs in the background and checks each file the moment you save it. You see viol
|
|
|
356
356
|
Options:
|
|
357
357
|
```bash
|
|
358
358
|
npx @shahmilsaari/memory-core watch --path src/ # watch a specific folder only
|
|
359
|
+
npx @shahmilsaari/memory-core watch --scan-on-start # snapshot-scan tracked files once, then keep watching
|
|
359
360
|
npx @shahmilsaari/memory-core watch --verbose # show extra details
|
|
360
361
|
npx @shahmilsaari/memory-core watch --debug # show prompt, diff, and raw model output
|
|
361
362
|
```
|
|
362
363
|
|
|
363
|
-
Only checks source files — ignores `node_modules`, `dist`, config files, JSON, etc.
|
|
364
|
+
Only checks source files — ignores `node_modules`, `dist`, config files, JSON, etc.
|
|
365
|
+
`--scan-on-start` is useful after a big refactor because it refreshes current live stats automatically before normal save-based watch mode begins.
|
|
364
366
|
|
|
365
367
|
---
|
|
366
368
|
|
|
@@ -369,7 +371,7 @@ Only checks source files — ignores `node_modules`, `dist`, config files, JSON,
|
|
|
369
371
|
```bash
|
|
370
372
|
npx @shahmilsaari/memory-core dashboard
|
|
371
373
|
npx @shahmilsaari/memory-core dashboard --port 5178
|
|
372
|
-
npx @shahmilsaari/memory-core dashboard --path
|
|
374
|
+
npx @shahmilsaari/memory-core dashboard --path /absolute/path/to/project
|
|
373
375
|
npx @shahmilsaari/memory-core dashboard --no-watch
|
|
374
376
|
```
|
|
375
377
|
|
|
@@ -384,6 +386,7 @@ Starts a local Svelte dashboard at `http://localhost:5178` by default. The dashb
|
|
|
384
386
|
- live reload for `.env`, `.memory-core.env`, `.memory-core.json`, and `.memory-core-stats.json`
|
|
385
387
|
|
|
386
388
|
The WebSocket endpoint is local: `ws://localhost:5178/ws`. Runtime config changes are reloaded and pushed to the browser without restarting the dashboard after the dashboard process is running.
|
|
389
|
+
When `--path` is provided, it must point to the project root (the directory containing `.memory-core.json`).
|
|
387
390
|
|
|
388
391
|
---
|
|
389
392
|
|
|
@@ -394,9 +397,13 @@ npx @shahmilsaari/memory-core check --staged # check staged files
|
|
|
394
397
|
npx @shahmilsaari/memory-core check --staged --verbose # with extra detail
|
|
395
398
|
npx @shahmilsaari/memory-core check --staged --debug # show prompt, diff, and raw model output
|
|
396
399
|
npx @shahmilsaari/memory-core check --ci # CI mode using memories.json
|
|
400
|
+
npx @shahmilsaari/memory-core check --all # scan all tracked source files (not just staged changes)
|
|
401
|
+
npx @shahmilsaari/memory-core check --all --path src/ # scan only tracked files under src/
|
|
397
402
|
```
|
|
398
403
|
|
|
399
|
-
`--staged` is the same path used by the pre-commit hook. `--ci` reads `memories.json` and uses a deterministic CI-friendly diff check, so pull requests can enforce rules without a local database or Ollama setup.
|
|
404
|
+
`--staged` is the same path used by the pre-commit hook. `--ci` reads `memories.json` and uses a deterministic CI-friendly diff check, so pull requests can enforce rules without a local database or Ollama setup.
|
|
405
|
+
`--all` runs a full tracked-file snapshot check and exits non-zero if violations are found.
|
|
406
|
+
`--all` and `--ci` are mutually exclusive in the same command.
|
|
400
407
|
|
|
401
408
|
---
|
|
402
409
|
|
|
@@ -585,9 +592,13 @@ Generates `.github/workflows/memory-core.yml`. Adds a PR check that runs `npx @s
|
|
|
585
592
|
|
|
586
593
|
```bash
|
|
587
594
|
npx @shahmilsaari/memory-core stats
|
|
595
|
+
npx @shahmilsaari/memory-core stats --reset
|
|
588
596
|
```
|
|
589
597
|
|
|
590
|
-
Shows which rules fire most often and which files have the most violations.
|
|
598
|
+
Shows which rules fire most often and which files have the most violations.
|
|
599
|
+
- If live watch state exists, `stats` shows current live counters.
|
|
600
|
+
- Otherwise it shows historical counters recorded over time.
|
|
601
|
+
- Use `--reset` to clear counters and recent violation history.
|
|
591
602
|
|
|
592
603
|
---
|
|
593
604
|
|
|
@@ -966,8 +966,8 @@ var seeds = [
|
|
|
966
966
|
// src/watcher.ts
|
|
967
967
|
import { watch } from "chokidar";
|
|
968
968
|
import { spawnSync } from "child_process";
|
|
969
|
-
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
970
|
-
import { join as join7, relative as relative2 } from "path";
|
|
969
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
970
|
+
import { join as join7, relative as relative2, resolve as resolve4, sep } from "path";
|
|
971
971
|
import chalk from "chalk";
|
|
972
972
|
|
|
973
973
|
// src/generator.ts
|
|
@@ -1195,6 +1195,14 @@ var ResilientGraphRepository = class {
|
|
|
1195
1195
|
var ChokidarWatchService = class {
|
|
1196
1196
|
async start(options = {}) {
|
|
1197
1197
|
await startWatch({
|
|
1198
|
+
path: options.path,
|
|
1199
|
+
verbose: options.verbose,
|
|
1200
|
+
debug: options.debug,
|
|
1201
|
+
scanOnStart: options.scanOnStart
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
async scan(options = {}) {
|
|
1205
|
+
return scanFiles({
|
|
1198
1206
|
path: options.path,
|
|
1199
1207
|
verbose: options.verbose,
|
|
1200
1208
|
debug: options.debug
|
|
@@ -1896,7 +1904,7 @@ var OUTPUT_FILES = [
|
|
|
1896
1904
|
var AGENT_NAMES = [...new Set(OUTPUT_FILES.map((f) => f.agent))];
|
|
1897
1905
|
Handlebars.registerHelper(
|
|
1898
1906
|
"join",
|
|
1899
|
-
(arr,
|
|
1907
|
+
(arr, sep2) => Array.isArray(arr) ? arr.join(sep2) : ""
|
|
1900
1908
|
);
|
|
1901
1909
|
Handlebars.registerHelper(
|
|
1902
1910
|
"bullet",
|
|
@@ -2284,25 +2292,62 @@ var SOURCE_EXTENSIONS3 = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|
|
|
|
2284
2292
|
var reasonMap = new Map(
|
|
2285
2293
|
seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
|
|
2286
2294
|
);
|
|
2287
|
-
function
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
+
function readStatsFile(statsPath) {
|
|
2296
|
+
if (!existsSync6(statsPath)) return { rules: {}, files: {} };
|
|
2297
|
+
try {
|
|
2298
|
+
return JSON.parse(readFileSync5(statsPath, "utf-8"));
|
|
2299
|
+
} catch {
|
|
2300
|
+
return { rules: {}, files: {} };
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
function rebuildLiveCounters(byFile) {
|
|
2304
|
+
const rules = {};
|
|
2305
|
+
const files = {};
|
|
2306
|
+
for (const [file, violations] of Object.entries(byFile)) {
|
|
2307
|
+
if (!Array.isArray(violations) || violations.length === 0) continue;
|
|
2308
|
+
files[file] = violations.length;
|
|
2309
|
+
for (const violation of violations) {
|
|
2310
|
+
rules[violation.rule] = (rules[violation.rule] ?? 0) + 1;
|
|
2295
2311
|
}
|
|
2296
2312
|
}
|
|
2313
|
+
return { rules, files };
|
|
2314
|
+
}
|
|
2315
|
+
function resetLiveStats(cwd) {
|
|
2316
|
+
const statsPath = join7(cwd, ".memory-core-stats.json");
|
|
2317
|
+
const stats = readStatsFile(statsPath);
|
|
2318
|
+
stats.rules ??= {};
|
|
2319
|
+
stats.files ??= {};
|
|
2320
|
+
stats.live = {
|
|
2321
|
+
rules: {},
|
|
2322
|
+
files: {},
|
|
2323
|
+
byFile: {}
|
|
2324
|
+
};
|
|
2325
|
+
writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
2326
|
+
}
|
|
2327
|
+
function recordWatchResult(cwd, file, violations) {
|
|
2328
|
+
const statsPath = join7(cwd, ".memory-core-stats.json");
|
|
2329
|
+
const stats = readStatsFile(statsPath);
|
|
2297
2330
|
stats.rules ??= {};
|
|
2298
2331
|
stats.files ??= {};
|
|
2332
|
+
stats.live ??= { rules: {}, files: {}, byFile: {} };
|
|
2333
|
+
stats.live.byFile ??= {};
|
|
2334
|
+
if (violations.length === 0) {
|
|
2335
|
+
delete stats.live.byFile[file];
|
|
2336
|
+
} else {
|
|
2337
|
+
stats.live.byFile[file] = violations;
|
|
2338
|
+
}
|
|
2339
|
+
const live = rebuildLiveCounters(stats.live.byFile);
|
|
2340
|
+
stats.live.rules = live.rules;
|
|
2341
|
+
stats.live.files = live.files;
|
|
2299
2342
|
for (const violation of violations) {
|
|
2300
2343
|
stats.rules[violation.rule] = (stats.rules[violation.rule] ?? 0) + 1;
|
|
2301
2344
|
if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
|
|
2302
2345
|
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2346
|
+
if (violations.length > 0) {
|
|
2347
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2348
|
+
const recent = violations.map((violation) => ({ ...violation, timestamp, source: "watch" }));
|
|
2349
|
+
stats.recentViolations = [...recent, ...stats.recentViolations ?? []].slice(0, 50);
|
|
2350
|
+
}
|
|
2306
2351
|
writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
2307
2352
|
}
|
|
2308
2353
|
function loadConfig(cwd) {
|
|
@@ -2333,7 +2378,7 @@ function getProfileRules(config2) {
|
|
|
2333
2378
|
}
|
|
2334
2379
|
return { rules, avoids };
|
|
2335
2380
|
}
|
|
2336
|
-
async function loadRelevantRules(config2, rel, diff, fallbackRules) {
|
|
2381
|
+
async function loadRelevantRules(cwd, config2, rel, diff, fallbackRules) {
|
|
2337
2382
|
try {
|
|
2338
2383
|
const query = buildContextQuery([
|
|
2339
2384
|
rel,
|
|
@@ -2344,7 +2389,7 @@ async function loadRelevantRules(config2, rel, diff, fallbackRules) {
|
|
|
2344
2389
|
]);
|
|
2345
2390
|
const memories = await retrieveContextualMemories({
|
|
2346
2391
|
query,
|
|
2347
|
-
cwd
|
|
2392
|
+
cwd,
|
|
2348
2393
|
config: config2,
|
|
2349
2394
|
limit: 15
|
|
2350
2395
|
});
|
|
@@ -2363,10 +2408,11 @@ ${violation.file}`.toLowerCase();
|
|
|
2363
2408
|
return !allowPatterns.some((pattern) => haystack.includes(pattern.toLowerCase()));
|
|
2364
2409
|
});
|
|
2365
2410
|
}
|
|
2366
|
-
async function verifyViolations(
|
|
2411
|
+
async function verifyViolations(inputText, violations, allowPatterns, debug, mode = "diff") {
|
|
2367
2412
|
if (violations.length === 0) return violations;
|
|
2413
|
+
const sourceLabel = mode === "snapshot" ? "file content" : "diff";
|
|
2368
2414
|
const systemPrompt = `You are verifying candidate architecture violations.
|
|
2369
|
-
Only keep violations that are directly supported by the
|
|
2415
|
+
Only keep violations that are directly supported by the ${sourceLabel}.
|
|
2370
2416
|
Reject speculative or weak matches.
|
|
2371
2417
|
Treat these allowlisted patterns as intentional and valid:
|
|
2372
2418
|
${allowPatterns.length ? allowPatterns.map((pattern, index) => `${index + 1}. ${pattern}`).join("\n") : "(none)"}
|
|
@@ -2374,8 +2420,8 @@ ${allowPatterns.length ? allowPatterns.map((pattern, index) => `${index + 1}. ${
|
|
|
2374
2420
|
Return strict JSON:
|
|
2375
2421
|
{"violations":[{"rule":"...","file":"...","line":1,"issue":"...","suggestion":"...","reason":"..."}]}
|
|
2376
2422
|
Do not include any text outside the JSON.`;
|
|
2377
|
-
const userPrompt =
|
|
2378
|
-
${
|
|
2423
|
+
const userPrompt = `${mode === "snapshot" ? "File content" : "Diff"}:
|
|
2424
|
+
${inputText.slice(0, 6e3)}
|
|
2379
2425
|
|
|
2380
2426
|
Candidate violations:
|
|
2381
2427
|
${JSON.stringify(violations, null, 2)}`;
|
|
@@ -2407,26 +2453,128 @@ async function loadIgnorePatterns() {
|
|
|
2407
2453
|
return [];
|
|
2408
2454
|
}
|
|
2409
2455
|
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
if (
|
|
2415
|
-
|
|
2456
|
+
function normalizeForGit(pathLike) {
|
|
2457
|
+
return pathLike.split(sep).join("/");
|
|
2458
|
+
}
|
|
2459
|
+
function listSourceFilesFromFilesystem(dir) {
|
|
2460
|
+
if (!existsSync6(dir)) return [];
|
|
2461
|
+
const files = [];
|
|
2462
|
+
const stack = [dir];
|
|
2463
|
+
while (stack.length > 0) {
|
|
2464
|
+
const current = stack.pop();
|
|
2465
|
+
let entries = [];
|
|
2466
|
+
try {
|
|
2467
|
+
entries = readdirSync3(current);
|
|
2468
|
+
} catch {
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2471
|
+
for (const entry of entries) {
|
|
2472
|
+
const absolute = join7(current, entry);
|
|
2473
|
+
let isDirectory = false;
|
|
2474
|
+
let isFile = false;
|
|
2475
|
+
try {
|
|
2476
|
+
const stats = statSync2(absolute);
|
|
2477
|
+
isDirectory = stats.isDirectory();
|
|
2478
|
+
isFile = stats.isFile();
|
|
2479
|
+
} catch {
|
|
2480
|
+
continue;
|
|
2481
|
+
}
|
|
2482
|
+
if (isDirectory) {
|
|
2483
|
+
if (entry === "node_modules" || entry === ".git" || entry === "dist" || entry === "build" || entry === "coverage") {
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2486
|
+
stack.push(absolute);
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
if (isFile && SOURCE_EXTENSIONS3.test(absolute)) files.push(absolute);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
return files;
|
|
2493
|
+
}
|
|
2494
|
+
function listTrackedSourceFiles(projectRoot, watchPath) {
|
|
2495
|
+
const relPrefix = normalizeForGit(relative2(projectRoot, watchPath));
|
|
2496
|
+
const inRoot = relPrefix === "" || relPrefix === ".";
|
|
2497
|
+
const prefixWithSlash = inRoot ? "" : `${relPrefix}/`;
|
|
2498
|
+
const listed = spawnSync("git", ["ls-files"], { encoding: "utf-8", cwd: projectRoot });
|
|
2499
|
+
if (listed.status !== 0) {
|
|
2500
|
+
return listSourceFilesFromFilesystem(watchPath).sort();
|
|
2501
|
+
}
|
|
2502
|
+
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));
|
|
2503
|
+
return [...new Set(files)].sort();
|
|
2504
|
+
}
|
|
2505
|
+
async function runSnapshotScan(projectRoot, watchPath, config2, verbose, debug, onEvent) {
|
|
2506
|
+
const files = listTrackedSourceFiles(projectRoot, watchPath);
|
|
2507
|
+
if (files.length === 0) {
|
|
2508
|
+
console.log(chalk.yellow("\n No tracked source files found for scan.\n"));
|
|
2509
|
+
return {
|
|
2510
|
+
filesChecked: 0,
|
|
2511
|
+
filesWithViolations: 0,
|
|
2512
|
+
violations: 0
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
console.log(chalk.dim(`
|
|
2516
|
+
scanning ${files.length} tracked source files...
|
|
2517
|
+
`));
|
|
2518
|
+
const summary = {
|
|
2519
|
+
filesChecked: 0,
|
|
2520
|
+
filesWithViolations: 0,
|
|
2521
|
+
violations: 0
|
|
2522
|
+
};
|
|
2523
|
+
for (const filePath of files) {
|
|
2524
|
+
const rel = normalizeForGit(relative2(projectRoot, filePath));
|
|
2525
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2526
|
+
onEvent?.({ type: "saved", timestamp, file: rel });
|
|
2527
|
+
const result = await checkFile(filePath, projectRoot, config2, verbose, debug, "snapshot");
|
|
2528
|
+
if (result.type !== "checked") {
|
|
2529
|
+
if (result.type === "skipped") {
|
|
2530
|
+
onEvent?.({ type: "skipped", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, reason: result.reason });
|
|
2531
|
+
} else {
|
|
2532
|
+
onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message: result.message });
|
|
2533
|
+
}
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2536
|
+
summary.filesChecked += 1;
|
|
2537
|
+
if (result.violations.length === 0) {
|
|
2538
|
+
onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
summary.filesWithViolations += 1;
|
|
2542
|
+
summary.violations += result.violations.length;
|
|
2543
|
+
onEvent?.({ type: "violations", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, violations: result.violations });
|
|
2544
|
+
}
|
|
2545
|
+
return summary;
|
|
2546
|
+
}
|
|
2547
|
+
async function checkFile(filePath, projectRoot, config2, verbose, debug, mode = "diff") {
|
|
2548
|
+
const rel = relative2(projectRoot, filePath).split(sep).join("/");
|
|
2549
|
+
if (rel.startsWith("..")) return { type: "skipped", reason: "File is outside project root" };
|
|
2550
|
+
let inputText;
|
|
2551
|
+
if (mode === "snapshot") {
|
|
2552
|
+
if (!existsSync6(filePath)) return { type: "skipped", reason: "File no longer exists" };
|
|
2553
|
+
inputText = readFileSync5(filePath, "utf-8");
|
|
2554
|
+
if (!inputText.trim()) return { type: "skipped", reason: "File is empty" };
|
|
2416
2555
|
} else {
|
|
2417
|
-
const
|
|
2418
|
-
|
|
2556
|
+
const headResult = spawnSync("git", ["diff", "HEAD", "--", rel], { encoding: "utf-8", cwd: projectRoot });
|
|
2557
|
+
if (headResult.stdout?.trim()) {
|
|
2558
|
+
inputText = headResult.stdout;
|
|
2559
|
+
} else {
|
|
2560
|
+
const noIndexResult = spawnSync("git", ["diff", "--no-index", "/dev/null", rel], {
|
|
2561
|
+
encoding: "utf-8",
|
|
2562
|
+
cwd: projectRoot
|
|
2563
|
+
});
|
|
2564
|
+
inputText = noIndexResult.stdout ?? "";
|
|
2565
|
+
}
|
|
2566
|
+
if (!inputText.trim()) return { type: "skipped", reason: "No changes compared with HEAD" };
|
|
2419
2567
|
}
|
|
2420
|
-
if (!diff.trim()) return { type: "skipped", reason: "No changes compared with HEAD" };
|
|
2421
2568
|
const { rules: fallbackRules, avoids } = getProfileRules(config2);
|
|
2422
|
-
const rules = await loadRelevantRules(config2, rel,
|
|
2569
|
+
const rules = await loadRelevantRules(projectRoot, config2, rel, inputText, fallbackRules);
|
|
2423
2570
|
if (rules.length === 0) return { type: "skipped", reason: "No applicable architecture rules" };
|
|
2424
|
-
const
|
|
2425
|
-
const truncated =
|
|
2426
|
-
const
|
|
2571
|
+
const MAX_INPUT = 6e3;
|
|
2572
|
+
const truncated = inputText.length > MAX_INPUT;
|
|
2573
|
+
const inputToSend = truncated ? inputText.slice(0, MAX_INPUT) + "\n\n[input truncated]" : inputText;
|
|
2427
2574
|
if (verbose || debug) {
|
|
2575
|
+
const label = mode === "snapshot" ? "snapshot" : `${inputText.length} chars`;
|
|
2428
2576
|
console.log(chalk.dim(`
|
|
2429
|
-
[watch] checking ${rel} (${
|
|
2577
|
+
[watch] checking ${rel} (${label})\u2026`));
|
|
2430
2578
|
}
|
|
2431
2579
|
const rulesWithReasons = rules.map((r, i) => {
|
|
2432
2580
|
const why = reasonMap.get(r);
|
|
@@ -2435,7 +2583,7 @@ async function checkFile(filePath, cwd, config2, verbose, debug) {
|
|
|
2435
2583
|
}).join("\n");
|
|
2436
2584
|
const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config2), ...await loadIgnorePatterns()])];
|
|
2437
2585
|
const astViolations = findAstDeterministicViolationsForFile(rel, {
|
|
2438
|
-
cwd,
|
|
2586
|
+
cwd: projectRoot,
|
|
2439
2587
|
config: config2,
|
|
2440
2588
|
rules,
|
|
2441
2589
|
reasonLookup: reasonMap
|
|
@@ -2443,8 +2591,9 @@ async function checkFile(filePath, cwd, config2, verbose, debug) {
|
|
|
2443
2591
|
...violation,
|
|
2444
2592
|
severity: "error"
|
|
2445
2593
|
}));
|
|
2594
|
+
const analysisTarget = mode === "snapshot" ? "file content" : "file diff";
|
|
2446
2595
|
const systemPrompt = `You are a strict code reviewer enforcing architecture rules.
|
|
2447
|
-
Analyze the
|
|
2596
|
+
Analyze the ${analysisTarget} and identify ONLY clear, definite rule violations.
|
|
2448
2597
|
Use the WHY for each rule to understand intent and judge edge cases.
|
|
2449
2598
|
|
|
2450
2599
|
Rules to enforce:
|
|
@@ -2464,16 +2613,19 @@ No text outside the JSON.`;
|
|
|
2464
2613
|
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"));
|
|
2465
2614
|
console.log(systemPrompt);
|
|
2466
2615
|
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"));
|
|
2467
|
-
console.log(chalk.gray(` [debug]
|
|
2468
|
-
console.log(chalk.dim(
|
|
2616
|
+
console.log(chalk.gray(` [debug] input length: ${inputText.length} chars`));
|
|
2617
|
+
console.log(chalk.dim(inputToSend));
|
|
2469
2618
|
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"));
|
|
2470
2619
|
}
|
|
2471
2620
|
try {
|
|
2621
|
+
const reviewPrompt = mode === "snapshot" ? `Review this file ${rel}:
|
|
2622
|
+
|
|
2623
|
+
${inputToSend}` : `Review this diff for ${rel}:
|
|
2624
|
+
|
|
2625
|
+
${inputToSend}`;
|
|
2472
2626
|
const raw = await callChatModel([
|
|
2473
2627
|
{ role: "system", content: systemPrompt },
|
|
2474
|
-
{ role: "user", content:
|
|
2475
|
-
|
|
2476
|
-
${diffToSend}` }
|
|
2628
|
+
{ role: "user", content: reviewPrompt }
|
|
2477
2629
|
]);
|
|
2478
2630
|
if (debug) {
|
|
2479
2631
|
console.log(chalk.gray(" [debug] raw response:"));
|
|
@@ -2493,7 +2645,7 @@ ${diffToSend}` }
|
|
|
2493
2645
|
} catch {
|
|
2494
2646
|
violations = [];
|
|
2495
2647
|
}
|
|
2496
|
-
violations = await verifyViolations(
|
|
2648
|
+
violations = await verifyViolations(inputText, violations, allowPatterns, debug, mode);
|
|
2497
2649
|
violations = [...astViolations, ...violations];
|
|
2498
2650
|
violations = applyAllowPatterns(violations, allowPatterns);
|
|
2499
2651
|
violations = violations.map((violation) => ({
|
|
@@ -2501,6 +2653,7 @@ ${diffToSend}` }
|
|
|
2501
2653
|
code: violation.code ?? (violation.line ? formatCodeContext(filePath, violation.line, 1) : void 0)
|
|
2502
2654
|
}));
|
|
2503
2655
|
if (violations.length === 0) {
|
|
2656
|
+
recordWatchResult(projectRoot, rel, []);
|
|
2504
2657
|
console.log(chalk.green(` \u2713 ${rel}`) + chalk.dim(" \u2014 no violations"));
|
|
2505
2658
|
return { type: "checked", violations: [] };
|
|
2506
2659
|
}
|
|
@@ -2522,7 +2675,7 @@ ${diffToSend}` }
|
|
|
2522
2675
|
if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
|
|
2523
2676
|
console.log();
|
|
2524
2677
|
});
|
|
2525
|
-
|
|
2678
|
+
recordWatchResult(projectRoot, rel, violations);
|
|
2526
2679
|
console.log(chalk.dim(' Fix violations or run: memory-core remember "<lesson>"'));
|
|
2527
2680
|
console.log();
|
|
2528
2681
|
return { type: "checked", violations };
|
|
@@ -2536,6 +2689,7 @@ ${diffToSend}` }
|
|
|
2536
2689
|
code: violation.code ?? (violation.line ? formatCodeContext(filePath, violation.line, 1) : void 0)
|
|
2537
2690
|
}));
|
|
2538
2691
|
if (violations.length === 0) {
|
|
2692
|
+
recordWatchResult(projectRoot, rel, []);
|
|
2539
2693
|
console.log(chalk.green(` \u2713 ${rel}`) + chalk.dim(" \u2014 no deterministic violations"));
|
|
2540
2694
|
return { type: "checked", violations: [] };
|
|
2541
2695
|
}
|
|
@@ -2557,15 +2711,56 @@ ${diffToSend}` }
|
|
|
2557
2711
|
if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
|
|
2558
2712
|
console.log();
|
|
2559
2713
|
});
|
|
2560
|
-
|
|
2714
|
+
recordWatchResult(projectRoot, rel, violations);
|
|
2561
2715
|
console.log(chalk.dim(' Fix violations or run: memory-core remember "<lesson>"'));
|
|
2562
2716
|
console.log();
|
|
2563
2717
|
return { type: "checked", violations };
|
|
2564
2718
|
}
|
|
2565
2719
|
}
|
|
2720
|
+
async function scanFiles(options = {}) {
|
|
2721
|
+
const projectRoot = resolve4(process.cwd());
|
|
2722
|
+
const watchPath = resolve4(projectRoot, options.path ?? ".");
|
|
2723
|
+
const config2 = loadConfig(projectRoot);
|
|
2724
|
+
if (!config2) {
|
|
2725
|
+
throw new Error("No .memory-core.json found. Run: memory-core init");
|
|
2726
|
+
}
|
|
2727
|
+
const { rules } = getProfileRules(config2);
|
|
2728
|
+
if (rules.length === 0) {
|
|
2729
|
+
console.log(chalk.yellow("\n No architecture rules configured in .memory-core.json \u2014 nothing to scan.\n"));
|
|
2730
|
+
return {
|
|
2731
|
+
filesChecked: 0,
|
|
2732
|
+
filesWithViolations: 0,
|
|
2733
|
+
violations: 0
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
resetLiveStats(projectRoot);
|
|
2737
|
+
console.log(chalk.cyan("\n archmind scan \u2014 checking tracked source files\n"));
|
|
2738
|
+
console.log(chalk.dim(` project: ${projectRoot}`));
|
|
2739
|
+
console.log(chalk.dim(` path: ${watchPath}`));
|
|
2740
|
+
console.log(chalk.dim(` model: ${getChatProviderLabel()}`));
|
|
2741
|
+
console.log(chalk.dim(` rules: ${rules.length}
|
|
2742
|
+
`));
|
|
2743
|
+
const summary = await runSnapshotScan(
|
|
2744
|
+
projectRoot,
|
|
2745
|
+
watchPath,
|
|
2746
|
+
config2,
|
|
2747
|
+
options.verbose ?? false,
|
|
2748
|
+
options.debug ?? false,
|
|
2749
|
+
options.onEvent
|
|
2750
|
+
);
|
|
2751
|
+
const cleanFiles = summary.filesChecked - summary.filesWithViolations;
|
|
2752
|
+
console.log(chalk.bold("\n scan summary\n"));
|
|
2753
|
+
console.log(chalk.dim(` files checked: ${summary.filesChecked}`));
|
|
2754
|
+
console.log(chalk.dim(` files clean: ${cleanFiles}`));
|
|
2755
|
+
console.log(chalk.dim(` files with violations: ${summary.filesWithViolations}`));
|
|
2756
|
+
console.log(chalk.dim(` total violations: ${summary.violations}
|
|
2757
|
+
`));
|
|
2758
|
+
return summary;
|
|
2759
|
+
}
|
|
2566
2760
|
async function startWatch(options = {}) {
|
|
2567
|
-
const
|
|
2568
|
-
const
|
|
2761
|
+
const projectRoot = resolve4(process.cwd());
|
|
2762
|
+
const watchPath = resolve4(projectRoot, options.path ?? ".");
|
|
2763
|
+
const config2 = loadConfig(projectRoot);
|
|
2569
2764
|
const exitOnSetupFailure = options.exitOnSetupFailure ?? true;
|
|
2570
2765
|
if (!config2) {
|
|
2571
2766
|
const message = "No .memory-core.json found. Run: memory-core init";
|
|
@@ -2586,8 +2781,9 @@ async function startWatch(options = {}) {
|
|
|
2586
2781
|
if (exitOnSetupFailure) process.exit(0);
|
|
2587
2782
|
return;
|
|
2588
2783
|
}
|
|
2589
|
-
|
|
2784
|
+
resetLiveStats(projectRoot);
|
|
2590
2785
|
console.log(chalk.cyan("\n archmind watch \u2014 real-time rule enforcement\n"));
|
|
2786
|
+
console.log(chalk.dim(` project: ${projectRoot}`));
|
|
2591
2787
|
console.log(chalk.dim(` watching: ${watchPath}`));
|
|
2592
2788
|
console.log(chalk.dim(` model: ${getChatProviderLabel()}`));
|
|
2593
2789
|
console.log(chalk.dim(` rules: ${rules.length}`));
|
|
@@ -2599,6 +2795,18 @@ async function startWatch(options = {}) {
|
|
|
2599
2795
|
model: getChatProviderLabel(),
|
|
2600
2796
|
rules: rules.length
|
|
2601
2797
|
});
|
|
2798
|
+
if (options.scanOnStart) {
|
|
2799
|
+
console.log(chalk.dim(" running initial snapshot scan before watch events..."));
|
|
2800
|
+
await runSnapshotScan(
|
|
2801
|
+
projectRoot,
|
|
2802
|
+
watchPath,
|
|
2803
|
+
config2,
|
|
2804
|
+
options.verbose ?? false,
|
|
2805
|
+
options.debug ?? false,
|
|
2806
|
+
options.onEvent
|
|
2807
|
+
);
|
|
2808
|
+
console.log(chalk.dim(" initial scan complete.\n"));
|
|
2809
|
+
}
|
|
2602
2810
|
const pending = /* @__PURE__ */ new Map();
|
|
2603
2811
|
const watcher = watch(watchPath, {
|
|
2604
2812
|
ignored: [
|
|
@@ -2620,13 +2828,26 @@ async function startWatch(options = {}) {
|
|
|
2620
2828
|
if (pending.has(filePath)) clearTimeout(pending.get(filePath));
|
|
2621
2829
|
const timer = setTimeout(async () => {
|
|
2622
2830
|
pending.delete(filePath);
|
|
2623
|
-
const rel = relative2(
|
|
2831
|
+
const rel = normalizeForGit(relative2(projectRoot, filePath));
|
|
2832
|
+
if (rel.startsWith("..")) return;
|
|
2624
2833
|
const timestamp = /* @__PURE__ */ new Date();
|
|
2625
2834
|
console.log(chalk.dim(`
|
|
2626
2835
|
[${timestamp.toLocaleTimeString()}] saved: ${rel}`));
|
|
2627
2836
|
options.onEvent?.({ type: "saved", timestamp: timestamp.toISOString(), file: rel });
|
|
2628
|
-
const result = await checkFile(
|
|
2837
|
+
const result = await checkFile(
|
|
2838
|
+
filePath,
|
|
2839
|
+
projectRoot,
|
|
2840
|
+
config2,
|
|
2841
|
+
options.verbose ?? false,
|
|
2842
|
+
options.debug ?? false,
|
|
2843
|
+
"diff"
|
|
2844
|
+
);
|
|
2629
2845
|
if (result.type === "skipped") {
|
|
2846
|
+
if (result.reason === "No changes compared with HEAD") {
|
|
2847
|
+
recordWatchResult(projectRoot, rel, []);
|
|
2848
|
+
options.onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2630
2851
|
options.onEvent?.({ type: "skipped", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, reason: result.reason });
|
|
2631
2852
|
return;
|
|
2632
2853
|
}
|
|
@@ -2645,6 +2866,12 @@ async function startWatch(options = {}) {
|
|
|
2645
2866
|
};
|
|
2646
2867
|
watcher.on("add", handle);
|
|
2647
2868
|
watcher.on("change", handle);
|
|
2869
|
+
watcher.on("unlink", (filePath) => {
|
|
2870
|
+
const rel = normalizeForGit(relative2(projectRoot, filePath));
|
|
2871
|
+
if (rel.startsWith("..")) return;
|
|
2872
|
+
recordWatchResult(projectRoot, rel, []);
|
|
2873
|
+
options.onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
|
|
2874
|
+
});
|
|
2648
2875
|
watcher.on("error", (err) => {
|
|
2649
2876
|
const message = err instanceof Error ? err.message : String(err);
|
|
2650
2877
|
console.error(chalk.red(` watcher error: ${message}`));
|
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
retrieveMemorySelection,
|
|
21
21
|
runMigrations,
|
|
22
22
|
seeds
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-WJG77BPO.js";
|
|
24
24
|
|
|
25
25
|
// src/cli.ts
|
|
26
26
|
import { Command } from "commander";
|
|
@@ -122,10 +122,18 @@ var reasonMap = new Map(
|
|
|
122
122
|
);
|
|
123
123
|
var HOOK_PATH = join2(".git", "hooks", "pre-commit");
|
|
124
124
|
var HOOK_MARKER = "# archmind-memory-core";
|
|
125
|
-
function
|
|
125
|
+
function buildHookBody(advisory) {
|
|
126
126
|
const suffix = advisory ? " || true" : "";
|
|
127
|
-
return
|
|
128
|
-
|
|
127
|
+
return `${HOOK_MARKER}${advisory ? " advisory" : ""}
|
|
128
|
+
if [ "\${MEMORY_CORE_SKIP_HOOK:-}" = "1" ] || [ "\${ARCHMIND_SKIP_HOOK:-}" = "1" ] || [ "\${HUSKY:-}" = "0" ] || [ "\${HUSKY_SKIP_HOOKS:-}" = "1" ]; then
|
|
129
|
+
exit 0
|
|
130
|
+
fi
|
|
131
|
+
if [ -n "\${SKIP:-}" ] && echo ",$SKIP," | grep -qiE ',(memory-core|archmind),'; then
|
|
132
|
+
exit 0
|
|
133
|
+
fi
|
|
134
|
+
if [ -n "\${SKIP_HOOKS:-}" ]; then
|
|
135
|
+
exit 0
|
|
136
|
+
fi
|
|
129
137
|
if command -v memory-core >/dev/null 2>&1; then
|
|
130
138
|
memory-core check --staged${suffix}
|
|
131
139
|
elif [ -f "./node_modules/.bin/memory-core" ]; then
|
|
@@ -137,6 +145,26 @@ else
|
|
|
137
145
|
fi
|
|
138
146
|
`;
|
|
139
147
|
}
|
|
148
|
+
function buildHookScript(advisory) {
|
|
149
|
+
return `#!/bin/sh
|
|
150
|
+
|
|
151
|
+
${buildHookBody(advisory)}`;
|
|
152
|
+
}
|
|
153
|
+
function normalizeHookPreamble(content) {
|
|
154
|
+
const lines = content.split("\n");
|
|
155
|
+
const normalized = [];
|
|
156
|
+
let shebangSeen = false;
|
|
157
|
+
for (const line of lines) {
|
|
158
|
+
if (/^\s*#!\/bin\/sh\s*$/.test(line)) {
|
|
159
|
+
if (shebangSeen) continue;
|
|
160
|
+
shebangSeen = true;
|
|
161
|
+
normalized.push("#!/bin/sh");
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
normalized.push(line);
|
|
165
|
+
}
|
|
166
|
+
return normalized.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
167
|
+
}
|
|
140
168
|
function recordViolations(violations, source = "hook") {
|
|
141
169
|
const statsPath = join2(process.cwd(), ".memory-core-stats.json");
|
|
142
170
|
let stats = { rules: {}, files: {} };
|
|
@@ -403,18 +431,26 @@ function installHook(advisory = true) {
|
|
|
403
431
|
process.exit(1);
|
|
404
432
|
}
|
|
405
433
|
const script = buildHookScript(advisory);
|
|
434
|
+
const body = buildHookBody(advisory).trimEnd();
|
|
406
435
|
if (existsSync2(HOOK_PATH)) {
|
|
407
436
|
const existing = readFileSync2(HOOK_PATH, "utf-8");
|
|
408
437
|
if (existing.includes(HOOK_MARKER)) {
|
|
409
438
|
const markerIndex = existing.indexOf(HOOK_MARKER);
|
|
410
|
-
const
|
|
411
|
-
|
|
439
|
+
const beforeRaw = markerIndex > 0 ? existing.slice(0, markerIndex) : "";
|
|
440
|
+
const normalizedBefore = normalizeHookPreamble(beforeRaw);
|
|
441
|
+
const preamble = normalizedBefore.length > 0 ? normalizedBefore : "#!/bin/sh";
|
|
442
|
+
const preambleWithShebang = preamble.startsWith("#!/bin/sh") ? preamble : `#!/bin/sh
|
|
443
|
+
${preamble}`;
|
|
444
|
+
writeFileSync2(HOOK_PATH, `${preambleWithShebang}
|
|
445
|
+
|
|
446
|
+
${body}
|
|
447
|
+
`);
|
|
412
448
|
chmodSync(HOOK_PATH, 493);
|
|
413
449
|
const modeLabel2 = advisory ? chalk.cyan("advisory") : chalk.yellow("strict");
|
|
414
450
|
console.log(chalk.green("\n \u2713 Pre-commit hook updated") + chalk.dim(` (${modeLabel2} mode)`));
|
|
415
451
|
return;
|
|
416
452
|
}
|
|
417
|
-
writeFileSync2(HOOK_PATH, existing.trimEnd() + "\n\n" +
|
|
453
|
+
writeFileSync2(HOOK_PATH, existing.trimEnd() + "\n\n" + body + "\n");
|
|
418
454
|
} else {
|
|
419
455
|
writeFileSync2(HOOK_PATH, script);
|
|
420
456
|
}
|
|
@@ -435,7 +471,7 @@ function uninstallHook() {
|
|
|
435
471
|
return;
|
|
436
472
|
}
|
|
437
473
|
const markerIndex = content.indexOf(HOOK_MARKER);
|
|
438
|
-
const before = markerIndex > 1 ? content.slice(0, markerIndex)
|
|
474
|
+
const before = markerIndex > 1 ? normalizeHookPreamble(content.slice(0, markerIndex)) : "";
|
|
439
475
|
if (before && before !== "#!/bin/sh") {
|
|
440
476
|
writeFileSync2(HOOK_PATH, `${before}
|
|
441
477
|
`);
|
|
@@ -575,6 +611,7 @@ ${diffToSend}` }
|
|
|
575
611
|
});
|
|
576
612
|
console.log(chalk.dim(" Fix the violations above, then commit again."));
|
|
577
613
|
console.log(chalk.dim(" To bypass (not recommended): git commit --no-verify"));
|
|
614
|
+
console.log(chalk.dim(" Env bypass: MEMORY_CORE_SKIP_HOOK=1 git commit"));
|
|
578
615
|
console.log(chalk.dim(' To save as memory: memory-core remember "<lesson>"'));
|
|
579
616
|
console.log();
|
|
580
617
|
recordViolations(violations);
|
|
@@ -2036,8 +2073,23 @@ program.command("uninstall").description("Remove memory-core from the current pr
|
|
|
2036
2073
|
console.log(chalk2.gray(" \u2713 cleaned .gitignore memory-core block"));
|
|
2037
2074
|
}
|
|
2038
2075
|
});
|
|
2039
|
-
program.command("stats").description("Show violation counters recorded by check and watch").action(() => {
|
|
2076
|
+
program.command("stats").description("Show violation counters recorded by check and watch").option("--reset", "Reset violation counters and recent history").action((opts) => {
|
|
2040
2077
|
const statsPath = join3(process.cwd(), ".memory-core-stats.json");
|
|
2078
|
+
if (opts.reset) {
|
|
2079
|
+
const emptyStats = {
|
|
2080
|
+
rules: {},
|
|
2081
|
+
files: {},
|
|
2082
|
+
live: {
|
|
2083
|
+
rules: {},
|
|
2084
|
+
files: {},
|
|
2085
|
+
byFile: {}
|
|
2086
|
+
},
|
|
2087
|
+
recentViolations: []
|
|
2088
|
+
};
|
|
2089
|
+
writeFileSync3(statsPath, JSON.stringify(emptyStats, null, 2) + "\n", "utf-8");
|
|
2090
|
+
console.log(chalk2.green("\n Violation stats reset.\n"));
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2041
2093
|
if (!existsSync3(statsPath)) {
|
|
2042
2094
|
console.log(chalk2.yellow("\n No violation stats recorded yet.\n"));
|
|
2043
2095
|
return;
|
|
@@ -2051,12 +2103,31 @@ program.command("stats").description("Show violation counters recorded by check
|
|
|
2051
2103
|
console.log(` ${index + 1}. ${truncate(name, 44).padEnd(46)} ${count}`);
|
|
2052
2104
|
});
|
|
2053
2105
|
};
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2106
|
+
const liveRules = stats.live?.rules ?? {};
|
|
2107
|
+
const liveFiles = stats.live?.files ?? {};
|
|
2108
|
+
const hasLiveState = !!stats.live;
|
|
2109
|
+
const hasLiveViolations = Object.keys(liveRules).length > 0 || Object.keys(liveFiles).length > 0;
|
|
2110
|
+
printTop(
|
|
2111
|
+
hasLiveState ? "Top rules (current watch state)" : "Top rules",
|
|
2112
|
+
hasLiveState ? liveRules : stats.rules
|
|
2113
|
+
);
|
|
2114
|
+
printTop(
|
|
2115
|
+
hasLiveState ? "Top files (current watch state)" : "Top files",
|
|
2116
|
+
hasLiveState ? liveFiles : stats.files
|
|
2117
|
+
);
|
|
2118
|
+
if (!hasLiveState) {
|
|
2119
|
+
console.log(chalk2.dim("\n Note: these counters are historical events, not live current code state."));
|
|
2120
|
+
console.log(chalk2.dim(" Start watch for live counters, or reset with: memory-core stats --reset\n"));
|
|
2121
|
+
} else {
|
|
2122
|
+
if (hasLiveViolations) {
|
|
2123
|
+
console.log(chalk2.dim("\n Live counters auto-refresh while watch is running.\n"));
|
|
2124
|
+
} else {
|
|
2125
|
+
console.log(chalk2.dim("\n Current live state has no violations.\n"));
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2057
2128
|
});
|
|
2058
2129
|
program.command("dashboard").description("Start the live Svelte dashboard with WebSocket watch events").option("-p, --port <port>", "Dashboard port", "5178").option("--path <dir>", "Directory to watch (default: current directory)").option("--no-watch", "Serve the dashboard without starting file watch").action(async (opts) => {
|
|
2059
|
-
const { startDashboard } = await import("./dashboard-server-
|
|
2130
|
+
const { startDashboard } = await import("./dashboard-server-OEDFMSFB.js");
|
|
2060
2131
|
await startDashboard({
|
|
2061
2132
|
port: parseInt(opts.port, 10),
|
|
2062
2133
|
path: opts.path,
|
|
@@ -2460,16 +2531,30 @@ hook.command("install").description("Install pre-commit hook (advisory mode by d
|
|
|
2460
2531
|
hook.command("uninstall").description("Remove the pre-commit hook").action(() => {
|
|
2461
2532
|
uninstallHook();
|
|
2462
2533
|
});
|
|
2463
|
-
program.command("check").description("Check staged changes against architecture rules (used by pre-commit hook)").option("--staged", "Check git staged diff (default behaviour)").option("--ci", `Check CI diff using ${MEMORY_FILE}`).option("--verbose", "Show model and diff details").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
|
|
2534
|
+
program.command("check").description("Check staged changes against architecture rules (used by pre-commit hook)").option("--staged", "Check git staged diff (default behaviour)").option("--ci", `Check CI diff using ${MEMORY_FILE}`).option("--all", "Check all tracked source files, including already-committed files").option("--path <dir>", "Directory to check for --all mode (default: current directory)").option("--verbose", "Show model and diff details").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
|
|
2535
|
+
if (opts.ci && opts.all) {
|
|
2536
|
+
console.error(chalk2.red("\n Choose one mode: --ci or --all.\n"));
|
|
2537
|
+
process.exit(1);
|
|
2538
|
+
}
|
|
2464
2539
|
if (opts.ci) {
|
|
2465
2540
|
await checkCi({ verbose: opts.verbose ?? false, debug: opts.debug ?? false });
|
|
2466
2541
|
return;
|
|
2467
2542
|
}
|
|
2543
|
+
if (opts.all) {
|
|
2544
|
+
const summary = await phase1.providers.watchService.scan({
|
|
2545
|
+
path: opts.path,
|
|
2546
|
+
verbose: opts.verbose,
|
|
2547
|
+
debug: opts.debug
|
|
2548
|
+
});
|
|
2549
|
+
if (summary.violations > 0) process.exit(1);
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2468
2552
|
await checkStaged({ verbose: opts.verbose ?? false, debug: opts.debug ?? false });
|
|
2469
2553
|
});
|
|
2470
|
-
program.command("watch").description("Watch source files and check violations in real-time on every save").option("--path <dir>", "Directory to watch (default: current directory)").option("--verbose", "Show diff size and model details per file").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
|
|
2554
|
+
program.command("watch").description("Watch source files and check violations in real-time on every save").option("--path <dir>", "Directory to watch (default: current directory)").option("--scan-on-start", "Run an initial full snapshot scan before watching file changes").option("--verbose", "Show diff size and model details per file").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
|
|
2471
2555
|
await phase1.providers.watchService.start({
|
|
2472
2556
|
path: opts.path,
|
|
2557
|
+
scanOnStart: opts.scanOnStart,
|
|
2473
2558
|
verbose: opts.verbose,
|
|
2474
2559
|
debug: opts.debug
|
|
2475
2560
|
});
|
|
@@ -12,13 +12,13 @@ import {
|
|
|
12
12
|
saveMemory,
|
|
13
13
|
startWatch,
|
|
14
14
|
updateMemory
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-WJG77BPO.js";
|
|
16
16
|
|
|
17
17
|
// src/dashboard-server.ts
|
|
18
18
|
import { createHash } from "crypto";
|
|
19
19
|
import { createReadStream, existsSync, readFileSync, watch } from "fs";
|
|
20
20
|
import { createServer } from "http";
|
|
21
|
-
import { extname, join, normalize, relative } from "path";
|
|
21
|
+
import { extname, join, normalize, relative, resolve } from "path";
|
|
22
22
|
import { fileURLToPath } from "url";
|
|
23
23
|
import chalk from "chalk";
|
|
24
24
|
var clients = /* @__PURE__ */ new Set();
|
|
@@ -46,6 +46,7 @@ var snapshotBroadcastTimer;
|
|
|
46
46
|
var snapshotBroadcastInFlight = false;
|
|
47
47
|
var snapshotBroadcastQueued = false;
|
|
48
48
|
var snapshotBroadcastForceRefresh = false;
|
|
49
|
+
var projectRoot = process.cwd();
|
|
49
50
|
function readJsonFile(path, fallback) {
|
|
50
51
|
if (!existsSync(path)) return fallback;
|
|
51
52
|
try {
|
|
@@ -55,7 +56,7 @@ function readJsonFile(path, fallback) {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
function readProjectConfig() {
|
|
58
|
-
return readJsonFile(join(
|
|
59
|
+
return readJsonFile(join(projectRoot, ".memory-core.json"), null);
|
|
59
60
|
}
|
|
60
61
|
function parseEnvFile(raw) {
|
|
61
62
|
const values = {};
|
|
@@ -71,8 +72,8 @@ function parseEnvFile(raw) {
|
|
|
71
72
|
return values;
|
|
72
73
|
}
|
|
73
74
|
function getRuntimeEnvPath() {
|
|
74
|
-
const memoryEnv = join(
|
|
75
|
-
return existsSync(memoryEnv) ? memoryEnv : join(
|
|
75
|
+
const memoryEnv = join(projectRoot, ".memory-core.env");
|
|
76
|
+
return existsSync(memoryEnv) ? memoryEnv : join(projectRoot, ".env");
|
|
76
77
|
}
|
|
77
78
|
function reloadRuntimeEnv() {
|
|
78
79
|
const envPath = getRuntimeEnvPath();
|
|
@@ -91,18 +92,20 @@ function invalidateSnapshotBase() {
|
|
|
91
92
|
snapshotBaseCache = void 0;
|
|
92
93
|
}
|
|
93
94
|
function readStats() {
|
|
94
|
-
return readJsonFile(join(
|
|
95
|
+
return readJsonFile(join(projectRoot, ".memory-core-stats.json"), { rules: {}, files: {} });
|
|
95
96
|
}
|
|
96
97
|
function topEntries(values = {}, limit = 8) {
|
|
97
98
|
return Object.entries(values).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([name, count]) => ({ name, count }));
|
|
98
99
|
}
|
|
99
100
|
function buildStatsPayload() {
|
|
100
101
|
const stats = readStats();
|
|
102
|
+
const rules = stats.live?.rules ?? stats.rules ?? {};
|
|
103
|
+
const files = stats.live?.files ?? stats.files ?? {};
|
|
101
104
|
return {
|
|
102
|
-
rules
|
|
103
|
-
files
|
|
104
|
-
topRules: topEntries(
|
|
105
|
-
topFiles: topEntries(
|
|
105
|
+
rules,
|
|
106
|
+
files,
|
|
107
|
+
topRules: topEntries(rules),
|
|
108
|
+
topFiles: topEntries(files),
|
|
106
109
|
recentViolations: stats.recentViolations ?? []
|
|
107
110
|
};
|
|
108
111
|
}
|
|
@@ -206,12 +209,12 @@ async function getModelStatus(forceRefresh = false) {
|
|
|
206
209
|
return status;
|
|
207
210
|
}
|
|
208
211
|
async function getRuntimeStatus(config) {
|
|
209
|
-
const detected = detectProject(
|
|
212
|
+
const detected = detectProject(projectRoot);
|
|
210
213
|
const declaredArchitectures = [
|
|
211
214
|
config?.backendArchitecture,
|
|
212
215
|
config?.frontendFramework
|
|
213
216
|
].filter((value) => typeof value === "string" && value.length > 0);
|
|
214
|
-
const activeArchitectures = inferProjectArchitectures(
|
|
217
|
+
const activeArchitectures = inferProjectArchitectures(projectRoot, config);
|
|
215
218
|
const database = parseDatabaseUrl(Config.databaseUrl);
|
|
216
219
|
let postgres = {
|
|
217
220
|
...database,
|
|
@@ -242,7 +245,7 @@ async function getRuntimeStatus(config) {
|
|
|
242
245
|
const model = await modelPromise;
|
|
243
246
|
return {
|
|
244
247
|
project: {
|
|
245
|
-
name: config?.projectName ??
|
|
248
|
+
name: config?.projectName ?? projectRoot.split("/").pop() ?? "project",
|
|
246
249
|
type: config?.projectType ?? "unknown",
|
|
247
250
|
language: config?.language ?? detected.language,
|
|
248
251
|
initialized: config !== null,
|
|
@@ -376,7 +379,7 @@ async function handleApi(req, res, url) {
|
|
|
376
379
|
return;
|
|
377
380
|
}
|
|
378
381
|
const config = readProjectConfig();
|
|
379
|
-
const activeArchitectures = inferProjectArchitectures(
|
|
382
|
+
const activeArchitectures = inferProjectArchitectures(projectRoot, config);
|
|
380
383
|
await saveMemory({
|
|
381
384
|
type: typeof body.type === "string" ? body.type : "rule",
|
|
382
385
|
scope: typeof body.scope === "string" ? body.scope : "project",
|
|
@@ -635,14 +638,14 @@ function startConfigWatch() {
|
|
|
635
638
|
}, 150);
|
|
636
639
|
};
|
|
637
640
|
for (const file of watchedFiles) {
|
|
638
|
-
const filePath = join(
|
|
641
|
+
const filePath = join(projectRoot, file);
|
|
639
642
|
if (!existsSync(filePath)) continue;
|
|
640
643
|
watchedPaths.add(filePath);
|
|
641
644
|
watchers.push(watch(filePath, () => reload(filePath)));
|
|
642
645
|
}
|
|
643
|
-
watchers.push(watch(
|
|
646
|
+
watchers.push(watch(projectRoot, (_eventType, filename) => {
|
|
644
647
|
if (typeof filename !== "string" || !watchedFiles.includes(filename)) return;
|
|
645
|
-
const filePath = join(
|
|
648
|
+
const filePath = join(projectRoot, filename);
|
|
646
649
|
if (!existsSync(filePath)) return;
|
|
647
650
|
if (!watchedPaths.has(filePath)) {
|
|
648
651
|
watchedPaths.add(filePath);
|
|
@@ -656,6 +659,7 @@ function startConfigWatch() {
|
|
|
656
659
|
};
|
|
657
660
|
}
|
|
658
661
|
async function startDashboard(options = {}) {
|
|
662
|
+
projectRoot = resolve(options.path ?? process.cwd());
|
|
659
663
|
reloadRuntimeEnv();
|
|
660
664
|
const port = options.port ?? 5178;
|
|
661
665
|
const stopConfigWatch = startConfigWatch();
|
|
@@ -683,8 +687,8 @@ async function startDashboard(options = {}) {
|
|
|
683
687
|
}
|
|
684
688
|
void closePool();
|
|
685
689
|
});
|
|
686
|
-
await new Promise((
|
|
687
|
-
server.listen(port,
|
|
690
|
+
await new Promise((resolve2) => {
|
|
691
|
+
server.listen(port, resolve2);
|
|
688
692
|
});
|
|
689
693
|
console.log(chalk.green(`
|
|
690
694
|
Dashboard: http://localhost:${port}
|
|
@@ -692,7 +696,7 @@ async function startDashboard(options = {}) {
|
|
|
692
696
|
if (options.watch ?? true) {
|
|
693
697
|
watcherStatus.enabled = true;
|
|
694
698
|
void startWatch({
|
|
695
|
-
path:
|
|
699
|
+
path: projectRoot,
|
|
696
700
|
onEvent: handleWatchEvent,
|
|
697
701
|
exitOnSetupFailure: false
|
|
698
702
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shahmilsaari/memory-core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Universal AI memory core — generate AI context files from architecture profiles with RAG support",
|
|
5
5
|
"homepage": "https://memory-core.shahmilsaari.my/",
|
|
6
6
|
"type": "module",
|