@shahmilsaari/memory-core 1.0.25 → 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 -659
- package/dist/approval-queue-YBYRGBHP.js +7 -0
- package/dist/ast-analyzer-JM4CIOFY.js +44 -0
- package/dist/check-cache-6NWRTZJD.js +52 -0
- package/dist/check-logger-5HYSWA3S.js +21 -0
- package/dist/{chunk-35ZWQFTO.js → chunk-3XTHE74V.js} +239 -830
- package/dist/chunk-M7NKSXFS.js +301 -0
- package/dist/chunk-PQBWHAZN.js +156 -0
- package/dist/chunk-W6WEAV3S.js +69 -0
- package/dist/chunk-ZZBQEXEO.js +183 -0
- package/dist/classifier-MZ65R7FK.js +60 -0
- package/dist/cli.js +404 -10
- package/dist/confidence-gate-ZQDAOS6P.js +64 -0
- package/dist/dashboard/assets/index-CE3AMEOD.js +2 -0
- package/dist/dashboard/assets/{index-CHgjllWU.css → index-CNc2vvZF.css} +1 -1
- package/dist/dashboard/index.html +2 -2
- package/dist/{dashboard-server-SSYZLQKB.js → dashboard-server-EEFNE6NX.js} +95 -11
- package/dist/db-PRDHI2CN.js +29 -0
- package/dist/deepseek-critique-MALVIYGF.js +82 -0
- package/dist/deterministic-validator-PP56B46I.js +18 -0
- package/dist/evidence-HVMSONTT.js +65 -0
- package/dist/graph-TFNTB5OK.js +98 -0
- package/dist/incident-capture-RVPZULS7.js +20 -0
- package/dist/ollama-judge-D2LFK5PB.js +137 -0
- package/dist/rate-limiter-SLIPCXRF.js +41 -0
- package/dist/rules-V3QMN3AR.js +95 -0
- package/dist/watch-errors-B3FA26N4.js +99 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-B7gd4JQc.js +0 -2
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/core/classifier.ts
|
|
4
|
+
import { existsSync, readFileSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
function globToRegex(pattern) {
|
|
7
|
+
const escaped = pattern.replace(/\./g, "\\.").replace(/\*\*\//g, "(?:.+/)?").replace(/\*\*/g, ".+").replace(/\*/g, "[^/]+");
|
|
8
|
+
return new RegExp(`^${escaped}`);
|
|
9
|
+
}
|
|
10
|
+
function matchesGlob(filepath, pattern) {
|
|
11
|
+
return globToRegex(pattern).test(filepath);
|
|
12
|
+
}
|
|
13
|
+
var FileClassifier = class _FileClassifier {
|
|
14
|
+
constructor(layers) {
|
|
15
|
+
this.layers = layers;
|
|
16
|
+
}
|
|
17
|
+
layers;
|
|
18
|
+
classifyFile(filepath) {
|
|
19
|
+
const normalized = filepath.replace(/\\/g, "/");
|
|
20
|
+
for (const layer of this.layers) {
|
|
21
|
+
if (layer.paths.some((p) => matchesGlob(normalized, p))) {
|
|
22
|
+
return { file: filepath, layer: layer.name, isNew: false, isModified: false };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
classifyDiff(diff) {
|
|
28
|
+
const result = [];
|
|
29
|
+
for (const file of diff.added) {
|
|
30
|
+
const c = this.classifyFile(file);
|
|
31
|
+
if (c) result.push({ ...c, isNew: true });
|
|
32
|
+
}
|
|
33
|
+
for (const file of diff.modified) {
|
|
34
|
+
const c = this.classifyFile(file);
|
|
35
|
+
if (c) result.push({ ...c, isModified: true });
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
static loadFromConfig(configDir) {
|
|
40
|
+
const layersPath = join(configDir, "layers.json");
|
|
41
|
+
if (!existsSync(layersPath)) {
|
|
42
|
+
return new _FileClassifier(defaultLayers());
|
|
43
|
+
}
|
|
44
|
+
const config = JSON.parse(readFileSync(layersPath, "utf-8"));
|
|
45
|
+
return new _FileClassifier(config.layers);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
function defaultLayers() {
|
|
49
|
+
return [
|
|
50
|
+
{ name: "domain", paths: ["src/domain/**", "src/core/domain/**"], description: "Business logic, entities, use cases" },
|
|
51
|
+
{ name: "application", paths: ["src/application/**", "src/core/application/**"], description: "Use cases and orchestration" },
|
|
52
|
+
{ name: "infrastructure", paths: ["src/infrastructure/**"], description: "Database, external APIs, low-level I/O" },
|
|
53
|
+
{ name: "api", paths: ["src/api/**", "src/interfaces/**", "src/controllers/**"], description: "HTTP handlers, request/response" },
|
|
54
|
+
{ name: "shared", paths: ["src/shared/**", "src/utils/**", "src/helpers/**"], description: "Utilities, helpers, constants" }
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
FileClassifier,
|
|
59
|
+
defaultLayers
|
|
60
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -7,13 +7,11 @@ import {
|
|
|
7
7
|
checkCommitMsg,
|
|
8
8
|
checkFile,
|
|
9
9
|
checkStaged,
|
|
10
|
-
closePool,
|
|
11
10
|
detectProject,
|
|
12
11
|
findSchemaViolations,
|
|
13
12
|
generate,
|
|
14
13
|
getAllowPatterns,
|
|
15
14
|
getDefaultApplicationContainer,
|
|
16
|
-
getPool,
|
|
17
15
|
inferProjectArchitectures,
|
|
18
16
|
installHook,
|
|
19
17
|
listProfiles,
|
|
@@ -25,12 +23,18 @@ import {
|
|
|
25
23
|
readMemoryFileFromUrl,
|
|
26
24
|
recordBypass,
|
|
27
25
|
retrieveMemorySelection,
|
|
28
|
-
runMigrations,
|
|
29
26
|
seeds,
|
|
30
27
|
toPortableMemory,
|
|
31
28
|
uninstallHook,
|
|
32
29
|
writeMemoryFile
|
|
33
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-3XTHE74V.js";
|
|
31
|
+
import "./chunk-PQBWHAZN.js";
|
|
32
|
+
import {
|
|
33
|
+
closePool,
|
|
34
|
+
getPool,
|
|
35
|
+
runMigrations
|
|
36
|
+
} from "./chunk-M7NKSXFS.js";
|
|
37
|
+
import "./chunk-ZZBQEXEO.js";
|
|
34
38
|
|
|
35
39
|
// src/cli.ts
|
|
36
40
|
import { Command } from "commander";
|
|
@@ -40,6 +44,7 @@ import ora from "ora";
|
|
|
40
44
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, rmSync } from "fs";
|
|
41
45
|
import { join, dirname, resolve } from "path";
|
|
42
46
|
import { homedir } from "os";
|
|
47
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
43
48
|
|
|
44
49
|
// src/remote-install.ts
|
|
45
50
|
import { spawnSync } from "child_process";
|
|
@@ -134,10 +139,7 @@ var DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
|
|
|
134
139
|
var DEFAULT_CHAT_MODEL = "llama3.2";
|
|
135
140
|
var phase1 = getDefaultApplicationContainer();
|
|
136
141
|
function getEnvPath() {
|
|
137
|
-
|
|
138
|
-
if (existsSync(memoryEnv)) return memoryEnv;
|
|
139
|
-
const dotEnv = join(process.cwd(), ".env");
|
|
140
|
-
return existsSync(dotEnv) ? dotEnv : memoryEnv;
|
|
142
|
+
return join(process.cwd(), ".memory-core.env");
|
|
141
143
|
}
|
|
142
144
|
function getWriteEnvPath() {
|
|
143
145
|
return join(process.cwd(), ".memory-core.env");
|
|
@@ -1052,6 +1054,34 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1052
1054
|
);
|
|
1053
1055
|
writeProjectConfig(config);
|
|
1054
1056
|
spinner.succeed(`Generated ${written.written.length} files`);
|
|
1057
|
+
try {
|
|
1058
|
+
const archmindDir = join(process.cwd(), ".archmind");
|
|
1059
|
+
mkdirSync(archmindDir, { recursive: true });
|
|
1060
|
+
const layersPath = join(archmindDir, "layers.json");
|
|
1061
|
+
const rulesPath = join(archmindDir, "rules.json");
|
|
1062
|
+
const archmindGitignorePath = join(archmindDir, ".gitignore");
|
|
1063
|
+
if (!existsSync(layersPath)) {
|
|
1064
|
+
const arch2 = backendArchitecture ?? frontendFramework ?? "custom";
|
|
1065
|
+
const layersTemplate = buildLayersTemplate(arch2);
|
|
1066
|
+
writeFileSync(layersPath, JSON.stringify(layersTemplate, null, 2) + "\n", "utf-8");
|
|
1067
|
+
console.log(chalk.green(" \u2713 Created .archmind/layers.json"));
|
|
1068
|
+
}
|
|
1069
|
+
if (!existsSync(rulesPath)) {
|
|
1070
|
+
writeFileSync(rulesPath, JSON.stringify(DEFAULT_ARCH_RULES, null, 2) + "\n", "utf-8");
|
|
1071
|
+
console.log(chalk.green(" \u2713 Created .archmind/rules.json"));
|
|
1072
|
+
}
|
|
1073
|
+
if (!existsSync(archmindGitignorePath)) {
|
|
1074
|
+
writeFileSync(archmindGitignorePath, "approval-queue.json\nwatch-errors.json\n", "utf-8");
|
|
1075
|
+
}
|
|
1076
|
+
} catch {
|
|
1077
|
+
}
|
|
1078
|
+
try {
|
|
1079
|
+
const graphSpinner = ora(" Building dependency graph snapshot\u2026").start();
|
|
1080
|
+
const { snapshot } = await phase1.services.graphEngine.buildAndStoreSnapshot({ cwd: process.cwd() });
|
|
1081
|
+
graphSpinner.succeed(` Graph snapshot ready (${snapshot.nodes.length} nodes, ${snapshot.edges.length} edges)`);
|
|
1082
|
+
} catch {
|
|
1083
|
+
console.log(chalk.dim(" (graph build skipped \u2014 run `memory-core graph build` once manually)"));
|
|
1084
|
+
}
|
|
1055
1085
|
const gitignoreEntries = [...written.written, ...LOCAL_GENERATED_FILES];
|
|
1056
1086
|
if (gitignoreEntries.length > 0) {
|
|
1057
1087
|
const added = appendMissingGitignoreEntries(gitignoreEntries, GITIGNORE_HEADING);
|
|
@@ -1715,7 +1745,7 @@ program.command("dashboard").description("Start the live Svelte dashboard with W
|
|
|
1715
1745
|
}
|
|
1716
1746
|
return void 0;
|
|
1717
1747
|
};
|
|
1718
|
-
const { startDashboard } = await import("./dashboard-server-
|
|
1748
|
+
const { startDashboard } = await import("./dashboard-server-EEFNE6NX.js");
|
|
1719
1749
|
await startDashboard({
|
|
1720
1750
|
port: parseInt(opts.port, 10),
|
|
1721
1751
|
path: resolveDashboardPath(),
|
|
@@ -2182,7 +2212,241 @@ hook.command("bypass-prompt").description("Prompt developer for a bypass reason
|
|
|
2182
2212
|
}
|
|
2183
2213
|
await closePool();
|
|
2184
2214
|
});
|
|
2185
|
-
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("--file <path>", "Check a specific file retroactively against all rules").option("--commit-msg [file]", "Check commit message (defaults to .git/COMMIT_EDITMSG)").option("--verbose", "Show model and diff details").option("--debug", "Show prompt, diff, and raw model response").option("--fast", "Skip AI and memory retrieval; run deterministic checks only").option("--dry-run", "Show what would be flagged without blocking the commit").action(async (opts) => {
|
|
2215
|
+
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("--file <path>", "Check a specific file retroactively against all rules").option("--commit-msg [file]", "Check commit message (defaults to .git/COMMIT_EDITMSG)").option("--verbose", "Show model and diff details").option("--debug", "Show prompt, diff, and raw model response").option("--fast", "Skip AI and memory retrieval; run deterministic checks only").option("--dry-run", "Show what would be flagged without blocking the commit").option("--diff <ref>", "Check a git diff for architecture violations (evidence-based)").option("--second-opinion", "Run a second critique pass for uncertain decisions (use with --diff)").option("--json", "Output full JSON result (use with --diff)").action(async (opts) => {
|
|
2216
|
+
if (opts.diff !== void 0) {
|
|
2217
|
+
const { values: envValues } = readRuntimeEnv();
|
|
2218
|
+
applyRuntimeEnv(envValues);
|
|
2219
|
+
const startMs = Date.now();
|
|
2220
|
+
const diffRef = opts.diff || "HEAD~1";
|
|
2221
|
+
const diag = opts.json ? (msg) => process.stderr.write(msg + "\n") : (msg) => console.log(msg);
|
|
2222
|
+
const diagSpinner = (text) => ora({ text, stream: opts.json ? process.stderr : process.stdout });
|
|
2223
|
+
const { CheckRateLimiter } = await import("./rate-limiter-SLIPCXRF.js");
|
|
2224
|
+
const release = await new CheckRateLimiter(process.cwd()).acquire();
|
|
2225
|
+
process.on("exit", release);
|
|
2226
|
+
const { FileClassifier } = await import("./classifier-MZ65R7FK.js");
|
|
2227
|
+
const { ASTAnalyzer } = await import("./ast-analyzer-JM4CIOFY.js");
|
|
2228
|
+
const { GraphBuilder } = await import("./graph-TFNTB5OK.js");
|
|
2229
|
+
const { RuleMatcher } = await import("./rules-V3QMN3AR.js");
|
|
2230
|
+
const { EvidencePacketBuilder } = await import("./evidence-HVMSONTT.js");
|
|
2231
|
+
const { DeterministicValidator } = await import("./deterministic-validator-PP56B46I.js");
|
|
2232
|
+
const { OllamaJudge } = await import("./ollama-judge-D2LFK5PB.js");
|
|
2233
|
+
const { DeepSeekCritique } = await import("./deepseek-critique-MALVIYGF.js");
|
|
2234
|
+
const { ConfidenceGate, defaultGateConfig } = await import("./confidence-gate-ZQDAOS6P.js");
|
|
2235
|
+
const { CheckCache } = await import("./check-cache-6NWRTZJD.js");
|
|
2236
|
+
const { CheckLogger } = await import("./check-logger-5HYSWA3S.js");
|
|
2237
|
+
const configDir = getArchmindDir();
|
|
2238
|
+
const classifier = FileClassifier.loadFromConfig(configDir);
|
|
2239
|
+
const ruleMatcher = RuleMatcher.loadFromConfig(configDir);
|
|
2240
|
+
const astAnalyzer = new ASTAnalyzer(process.cwd());
|
|
2241
|
+
const graphBuilder = new GraphBuilder(astAnalyzer, classifier, process.cwd());
|
|
2242
|
+
const logger = new CheckLogger(process.cwd());
|
|
2243
|
+
let diffOutput = "";
|
|
2244
|
+
try {
|
|
2245
|
+
const result = spawnSync2("git", ["diff", "--name-status", diffRef], { encoding: "utf-8" });
|
|
2246
|
+
if (result.status !== 0) throw new Error(result.stderr);
|
|
2247
|
+
diffOutput = result.stdout;
|
|
2248
|
+
} catch {
|
|
2249
|
+
console.error("Could not run git diff against " + diffRef);
|
|
2250
|
+
process.exit(1);
|
|
2251
|
+
}
|
|
2252
|
+
const added = [];
|
|
2253
|
+
const modified = [];
|
|
2254
|
+
const deleted = [];
|
|
2255
|
+
for (const line of diffOutput.split("\n").filter(Boolean)) {
|
|
2256
|
+
const [status, ...rest] = line.split(" ");
|
|
2257
|
+
const file = rest.join(" ").trim();
|
|
2258
|
+
if (!file) continue;
|
|
2259
|
+
if (status.startsWith("A")) added.push(file);
|
|
2260
|
+
else if (status.startsWith("D")) deleted.push(file);
|
|
2261
|
+
else modified.push(file);
|
|
2262
|
+
}
|
|
2263
|
+
const allChanged = [...added, ...modified].filter((f) => /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(f));
|
|
2264
|
+
const graph2 = await graphBuilder.buildFromFiles(allChanged);
|
|
2265
|
+
const builder = new EvidencePacketBuilder(process.cwd());
|
|
2266
|
+
const packet = await builder.build({ added, modified, deleted }, astAnalyzer, classifier, ruleMatcher, graph2);
|
|
2267
|
+
try {
|
|
2268
|
+
const fullGraph = await phase1.services.graphEngine.latest(process.cwd());
|
|
2269
|
+
if (fullGraph) {
|
|
2270
|
+
const snapshotAge = fullGraph.createdAt ? Date.now() - new Date(fullGraph.createdAt).getTime() : null;
|
|
2271
|
+
const STALE_MS = 24 * 60 * 60 * 1e3;
|
|
2272
|
+
if (snapshotAge !== null && snapshotAge > STALE_MS) {
|
|
2273
|
+
const hours = Math.round(snapshotAge / 36e5);
|
|
2274
|
+
diag(chalk.yellow(` \u26A0 Graph snapshot is ${hours}h old \u2014 run \`memory-core graph build\` to refresh`));
|
|
2275
|
+
}
|
|
2276
|
+
const changedSet = /* @__PURE__ */ new Set([...added, ...modified]);
|
|
2277
|
+
for (const edge of fullGraph.edges) {
|
|
2278
|
+
if (!changedSet.has(edge.from) || edge.to.startsWith("pkg:")) continue;
|
|
2279
|
+
const fromClass = classifier.classifyFile(edge.from);
|
|
2280
|
+
const toClass = classifier.classifyFile(edge.to);
|
|
2281
|
+
if (!fromClass || !toClass) continue;
|
|
2282
|
+
const violating = ruleMatcher.findViolatingRules(fromClass.layer, toClass.layer);
|
|
2283
|
+
for (const rule of violating) {
|
|
2284
|
+
const alreadyReported = packet.graphViolations.some(
|
|
2285
|
+
(v) => v.from === edge.from && v.to === edge.to && v.ruleName === rule.name
|
|
2286
|
+
);
|
|
2287
|
+
if (!alreadyReported) {
|
|
2288
|
+
packet.graphViolations.push({
|
|
2289
|
+
from: edge.from,
|
|
2290
|
+
fromLayer: fromClass.layer,
|
|
2291
|
+
to: edge.to,
|
|
2292
|
+
toLayer: toClass.layer,
|
|
2293
|
+
ruleName: rule.name,
|
|
2294
|
+
path: [edge.from, edge.to]
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
} else {
|
|
2300
|
+
diag(chalk.yellow(" \u26A0 No graph snapshot \u2014 building now for full codebase enforcement\u2026"));
|
|
2301
|
+
const autoSpinner = diagSpinner(" Building dependency graph\u2026").start();
|
|
2302
|
+
try {
|
|
2303
|
+
const buildResult = await Promise.race([
|
|
2304
|
+
phase1.services.graphEngine.buildAndStoreSnapshot({ cwd: process.cwd() }),
|
|
2305
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("graph build timeout")), 15e3))
|
|
2306
|
+
]);
|
|
2307
|
+
autoSpinner.succeed(` Graph snapshot ready (${buildResult.snapshot.nodes.length} nodes, ${buildResult.snapshot.edges.length} edges)`);
|
|
2308
|
+
const freshGraph = await phase1.services.graphEngine.latest(process.cwd());
|
|
2309
|
+
if (freshGraph) {
|
|
2310
|
+
const changedSet2 = /* @__PURE__ */ new Set([...added, ...modified]);
|
|
2311
|
+
for (const edge of freshGraph.edges) {
|
|
2312
|
+
if (!changedSet2.has(edge.from) || edge.to.startsWith("pkg:")) continue;
|
|
2313
|
+
const fromClass = classifier.classifyFile(edge.from);
|
|
2314
|
+
const toClass = classifier.classifyFile(edge.to);
|
|
2315
|
+
if (!fromClass || !toClass) continue;
|
|
2316
|
+
const violating = ruleMatcher.findViolatingRules(fromClass.layer, toClass.layer);
|
|
2317
|
+
for (const rule of violating) {
|
|
2318
|
+
const alreadyReported = packet.graphViolations.some(
|
|
2319
|
+
(v) => v.from === edge.from && v.to === edge.to && v.ruleName === rule.name
|
|
2320
|
+
);
|
|
2321
|
+
if (!alreadyReported) {
|
|
2322
|
+
packet.graphViolations.push({
|
|
2323
|
+
from: edge.from,
|
|
2324
|
+
fromLayer: fromClass.layer,
|
|
2325
|
+
to: edge.to,
|
|
2326
|
+
toLayer: toClass.layer,
|
|
2327
|
+
ruleName: rule.name,
|
|
2328
|
+
path: [edge.from, edge.to]
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
} catch (e) {
|
|
2335
|
+
autoSpinner.stop();
|
|
2336
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2337
|
+
diag(chalk.dim(` (auto-build skipped: ${msg} \u2014 run \`memory-core graph build\` manually)`));
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
} catch {
|
|
2341
|
+
}
|
|
2342
|
+
const det = new DeterministicValidator();
|
|
2343
|
+
if (det.validate(packet) === "block") {
|
|
2344
|
+
const out = {
|
|
2345
|
+
decision: "block",
|
|
2346
|
+
source: "deterministic",
|
|
2347
|
+
confidence: 1,
|
|
2348
|
+
violations: packet.violations.map((v) => ({ rule: v.rule.name, from: v.fromFile, to: v.toFile })),
|
|
2349
|
+
graphViolations: packet.graphViolations,
|
|
2350
|
+
...opts.json ? { evidence: packet } : {}
|
|
2351
|
+
};
|
|
2352
|
+
console.log(JSON.stringify(out, null, 2));
|
|
2353
|
+
logger.append({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), ref: diffRef, decision: "block", source: "deterministic", confidence: 1, violationCount: packet.violations.length, graphViolationCount: packet.graphViolations.length, changedFiles: allChanged.length, durationMs: Date.now() - startMs, fast: opts.fast });
|
|
2354
|
+
process.exit(1);
|
|
2355
|
+
}
|
|
2356
|
+
if (opts.fast) {
|
|
2357
|
+
const out = { decision: "allow", source: "deterministic", confidence: 1, violations: [], graphViolations: packet.graphViolations };
|
|
2358
|
+
if (opts.json) {
|
|
2359
|
+
console.log(JSON.stringify(out, null, 2));
|
|
2360
|
+
} else {
|
|
2361
|
+
diag(chalk.green("\n Decision: ALLOW") + chalk.dim(" (source: deterministic+fast, confidence: 1.00)"));
|
|
2362
|
+
}
|
|
2363
|
+
logger.append({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), ref: diffRef, decision: "allow", source: "deterministic", confidence: 1, violationCount: 0, graphViolationCount: packet.graphViolations.length, changedFiles: allChanged.length, durationMs: Date.now() - startMs, fast: true });
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
let storedMemories = [];
|
|
2367
|
+
try {
|
|
2368
|
+
const { listMemories } = await import("./db-PRDHI2CN.js");
|
|
2369
|
+
const allMemories = await listMemories({ limit: 1e4 });
|
|
2370
|
+
const ranked = allMemories.filter((m) => ["rule", "pattern", "decision"].includes(m.type));
|
|
2371
|
+
const changedLayers = packet.layersAffected;
|
|
2372
|
+
const scored = ranked.map((m) => {
|
|
2373
|
+
const text = m.content.toLowerCase();
|
|
2374
|
+
const score = changedLayers.reduce((s, l) => s + (text.includes(l) ? 2 : 0), 0);
|
|
2375
|
+
return { m, score };
|
|
2376
|
+
});
|
|
2377
|
+
scored.sort((a, b) => b.score - a.score);
|
|
2378
|
+
storedMemories = scored.slice(0, 30).map(({ m }) => ({
|
|
2379
|
+
type: m.type,
|
|
2380
|
+
content: m.content,
|
|
2381
|
+
reason: m.reason ?? void 0
|
|
2382
|
+
}));
|
|
2383
|
+
} catch {
|
|
2384
|
+
}
|
|
2385
|
+
const cache = new CheckCache(process.cwd());
|
|
2386
|
+
const mHash = cache.memoriesHash(storedMemories);
|
|
2387
|
+
const rHash = cache.rulesHash(configDir);
|
|
2388
|
+
const cacheKey = cache.key(diffOutput, mHash, rHash);
|
|
2389
|
+
const cached = !opts.secondOpinion && cache.get(cacheKey);
|
|
2390
|
+
if (cached) {
|
|
2391
|
+
diag(chalk.dim(" (cached result)"));
|
|
2392
|
+
const output2 = {
|
|
2393
|
+
decision: cached.decision,
|
|
2394
|
+
source: cached.source + "+cache",
|
|
2395
|
+
violations: cached.violations,
|
|
2396
|
+
suggestedFix: cached.suggestedFix,
|
|
2397
|
+
reasoning: cached.reasoning,
|
|
2398
|
+
confidence: cached.confidence
|
|
2399
|
+
};
|
|
2400
|
+
if (opts.json) {
|
|
2401
|
+
console.log(JSON.stringify(output2, null, 2));
|
|
2402
|
+
} else {
|
|
2403
|
+
const color = cached.decision === "block" ? chalk.red : cached.decision === "warn" ? chalk.yellow : chalk.green;
|
|
2404
|
+
diag(color(`
|
|
2405
|
+
Decision: ${cached.decision.toUpperCase()}`) + chalk.dim(` (source: ${cached.source}+cache, confidence: ${cached.confidence.toFixed(2)})`));
|
|
2406
|
+
}
|
|
2407
|
+
logger.append({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), ref: diffRef, decision: cached.decision, source: cached.source + "+cache", confidence: cached.confidence, violationCount: cached.violations.length, graphViolationCount: packet.graphViolations.length, changedFiles: allChanged.length, durationMs: Date.now() - startMs, cached: true });
|
|
2408
|
+
if (cached.decision === "block") process.exit(1);
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
const chatModel = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "unknown";
|
|
2412
|
+
const provider2 = process.env.CHAT_PROVIDER ?? "ollama";
|
|
2413
|
+
const spinner = diagSpinner(` Analyzing with ${provider2}/${chatModel}\u2026`).start();
|
|
2414
|
+
const primaryDecision = await new OllamaJudge().judge(packet, storedMemories);
|
|
2415
|
+
spinner.stop();
|
|
2416
|
+
const critiqueEnabled = opts.secondOpinion ?? false;
|
|
2417
|
+
const critiqueFn = critiqueEnabled ? async (p) => new DeepSeekCritique().critique(p, primaryDecision) : async () => ({ decision: primaryDecision.decision, confidence: primaryDecision.confidence, reasoning: "skipped", override: false });
|
|
2418
|
+
const gateDecision = await new ConfidenceGate(defaultGateConfig(critiqueEnabled)).decide(packet, primaryDecision, critiqueFn);
|
|
2419
|
+
cache.set(cacheKey, {
|
|
2420
|
+
decision: gateDecision.final,
|
|
2421
|
+
source: gateDecision.source,
|
|
2422
|
+
confidence: primaryDecision.confidence,
|
|
2423
|
+
violations: primaryDecision.violations,
|
|
2424
|
+
reasoning: primaryDecision.reasoning,
|
|
2425
|
+
suggestedFix: primaryDecision.suggestedFix
|
|
2426
|
+
});
|
|
2427
|
+
const output = {
|
|
2428
|
+
decision: gateDecision.final,
|
|
2429
|
+
source: gateDecision.source,
|
|
2430
|
+
violations: primaryDecision.violations,
|
|
2431
|
+
suggestedFix: primaryDecision.suggestedFix,
|
|
2432
|
+
reasoning: primaryDecision.reasoning,
|
|
2433
|
+
confidence: primaryDecision.confidence,
|
|
2434
|
+
...opts.json ? { judge: primaryDecision, critique: gateDecision.deepseek, selfReview: gateDecision.selfReview, evidence: packet } : {}
|
|
2435
|
+
};
|
|
2436
|
+
if (opts.json) {
|
|
2437
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2438
|
+
} else {
|
|
2439
|
+
const color = gateDecision.final === "block" ? chalk.red : gateDecision.final === "warn" ? chalk.yellow : chalk.green;
|
|
2440
|
+
diag(color(`
|
|
2441
|
+
Decision: ${gateDecision.final.toUpperCase()}`) + chalk.dim(` (source: ${gateDecision.source}, confidence: ${primaryDecision.confidence.toFixed(2)})`));
|
|
2442
|
+
if (primaryDecision.violations.length > 0) diag(chalk.dim(` Violations: ${primaryDecision.violations.join(", ")}`));
|
|
2443
|
+
if (primaryDecision.suggestedFix) diag(chalk.dim(` Fix: ${primaryDecision.suggestedFix}`));
|
|
2444
|
+
diag("");
|
|
2445
|
+
}
|
|
2446
|
+
logger.append({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), ref: diffRef, decision: gateDecision.final, source: gateDecision.source, confidence: primaryDecision.confidence, violationCount: primaryDecision.violations.length, graphViolationCount: packet.graphViolations.length, changedFiles: allChanged.length, durationMs: Date.now() - startMs });
|
|
2447
|
+
if (gateDecision.final === "block") process.exit(1);
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2186
2450
|
if (opts.file) {
|
|
2187
2451
|
await checkFile(opts.file, { verbose: opts.verbose ?? false, debug: opts.debug ?? false, fast: opts.fast ?? false, dryRun: opts.dryRun ?? false });
|
|
2188
2452
|
await closePool();
|
|
@@ -2289,4 +2553,134 @@ program.command("watch").description("Watch source files and check violations in
|
|
|
2289
2553
|
debug: opts.debug
|
|
2290
2554
|
});
|
|
2291
2555
|
});
|
|
2556
|
+
var ARCHMIND_DIR = join(process.cwd(), ".archmind");
|
|
2557
|
+
var DEFAULT_ARCH_RULES = {
|
|
2558
|
+
rules: [
|
|
2559
|
+
{ id: "domain-no-infra", name: "Domain must not import infrastructure", fromLayer: "domain", toLayer: "infrastructure", allowed: false, severity: "critical", enforcement: "block" },
|
|
2560
|
+
{ id: "domain-no-api", name: "Domain must not import API layer", fromLayer: "domain", toLayer: "api", allowed: false, severity: "critical", enforcement: "block" },
|
|
2561
|
+
{ id: "api-no-infra-direct", name: "API should not import infrastructure directly", fromLayer: "api", toLayer: "infrastructure", allowed: false, severity: "medium", enforcement: "warn" },
|
|
2562
|
+
{ id: "api-depends-on-domain", name: "API depends on domain", fromLayer: "api", toLayer: "domain", allowed: true, severity: "low", enforcement: "suggest" },
|
|
2563
|
+
{ id: "no-circular", name: "No circular dependencies", fromLayer: "*", toLayer: "*", allowed: false, severity: "critical", enforcement: "block" },
|
|
2564
|
+
{ id: "application-no-api", name: "Application must not import API layer", fromLayer: "application", toLayer: "api", allowed: false, severity: "critical", enforcement: "block" },
|
|
2565
|
+
{ id: "application-uses-domain", name: "Application depends on domain", fromLayer: "application", toLayer: "domain", allowed: true, severity: "low", enforcement: "suggest" },
|
|
2566
|
+
{ id: "shared-no-domain", name: "Shared must not import domain", fromLayer: "shared", toLayer: "domain", allowed: false, severity: "medium", enforcement: "warn" }
|
|
2567
|
+
]
|
|
2568
|
+
};
|
|
2569
|
+
function buildLayersTemplate(arch2) {
|
|
2570
|
+
const mvc = ["nestjs", "mvc", "laravel", "go-api"].includes(arch2);
|
|
2571
|
+
const frontend = ["react", "vue", "angular", "svelte", "nuxt", "react-native"].includes(arch2);
|
|
2572
|
+
if (frontend) {
|
|
2573
|
+
return {
|
|
2574
|
+
layers: [
|
|
2575
|
+
{ name: "domain", paths: ["src/domain/**", "src/store/**", "src/state/**"], description: "State, models, business logic" },
|
|
2576
|
+
{ name: "application", paths: ["src/services/**", "src/hooks/**", "src/composables/**"], description: "App services and hooks" },
|
|
2577
|
+
{ name: "infrastructure", paths: ["src/api/**", "src/lib/**", "src/clients/**"], description: "External API clients, adapters" },
|
|
2578
|
+
{ name: "shared", paths: ["src/utils/**", "src/helpers/**", "src/constants/**"], description: "Shared utilities" }
|
|
2579
|
+
]
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
if (mvc) {
|
|
2583
|
+
return {
|
|
2584
|
+
layers: [
|
|
2585
|
+
{ name: "domain", paths: ["src/models/**", "src/entities/**"], description: "Data models and entities" },
|
|
2586
|
+
{ name: "application", paths: ["src/services/**"], description: "Business logic services" },
|
|
2587
|
+
{ name: "infrastructure", paths: ["src/repositories/**", "src/database/**", "src/infrastructure/**"], description: "DB, external APIs" },
|
|
2588
|
+
{ name: "api", paths: ["src/controllers/**", "src/routes/**", "src/handlers/**"], description: "HTTP handlers" },
|
|
2589
|
+
{ name: "shared", paths: ["src/utils/**", "src/helpers/**", "src/shared/**"], description: "Shared utilities" }
|
|
2590
|
+
]
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
return {
|
|
2594
|
+
layers: [
|
|
2595
|
+
{ name: "domain", paths: ["src/domain/**", "src/core/domain/**", "src/modules/*/domain/**"], description: "Business logic, entities, use cases" },
|
|
2596
|
+
{ name: "application", paths: ["src/application/**", "src/core/application/**", "src/modules/*/application/**"], description: "Use cases and orchestration" },
|
|
2597
|
+
{ name: "infrastructure", paths: ["src/infrastructure/**", "src/modules/*/infrastructure/**"], description: "Database, external APIs, low-level I/O" },
|
|
2598
|
+
{ name: "api", paths: ["src/api/**", "src/interfaces/**", "src/controllers/**"], description: "HTTP handlers, request/response" },
|
|
2599
|
+
{ name: "shared", paths: ["src/shared/**", "src/utils/**", "src/helpers/**"], description: "Utilities, helpers, constants" }
|
|
2600
|
+
]
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
function getArchmindDir() {
|
|
2604
|
+
return ARCHMIND_DIR;
|
|
2605
|
+
}
|
|
2606
|
+
var arch = program.command("arch").description("Architecture enforcement: check, review captured rules, watch for errors, log incidents");
|
|
2607
|
+
arch.command("review").description("Review and approve/reject auto-captured architecture rules").option("--approve-all", "Approve everything without prompting").option("--reject-all", "Reject everything without prompting").action(async (opts) => {
|
|
2608
|
+
const { ApprovalQueue } = await import("./approval-queue-YBYRGBHP.js");
|
|
2609
|
+
const { join: join2 } = await import("path");
|
|
2610
|
+
const configDir = getArchmindDir();
|
|
2611
|
+
const queue = new ApprovalQueue(configDir);
|
|
2612
|
+
const pending = queue.pending();
|
|
2613
|
+
if (pending.length === 0) {
|
|
2614
|
+
console.log(chalk.dim(" No pending rules."));
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
console.log(chalk.bold(`
|
|
2618
|
+
Pending rules (${pending.length}):
|
|
2619
|
+
`));
|
|
2620
|
+
if (opts.approveAll) {
|
|
2621
|
+
for (const item of pending) queue.approve(item.id);
|
|
2622
|
+
const count2 = queue.persistApproved(join2(configDir, "rules.json"));
|
|
2623
|
+
console.log(chalk.green(` \u2713 Approved and saved ${count2} rules`));
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
if (opts.rejectAll) {
|
|
2627
|
+
for (const item of pending) queue.reject(item.id);
|
|
2628
|
+
console.log(chalk.yellow(` \u2717 Rejected ${pending.length} rules`));
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
for (const item of pending) {
|
|
2632
|
+
console.log(chalk.cyan(` [${item.source.toUpperCase()}]`) + ` ${item.rule.name}`);
|
|
2633
|
+
console.log(chalk.dim(` ${item.rule.description}`));
|
|
2634
|
+
console.log(chalk.dim(` captured: ${item.capturedAt} confidence: ${item.confidence}`));
|
|
2635
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
2636
|
+
const accept = await confirm2({ message: " Keep this rule?" });
|
|
2637
|
+
accept ? queue.approve(item.id) : queue.reject(item.id);
|
|
2638
|
+
console.log(accept ? chalk.green(" \u2713 Kept") : chalk.yellow(" \u2717 Dropped"));
|
|
2639
|
+
console.log();
|
|
2640
|
+
}
|
|
2641
|
+
const count = queue.persistApproved(join2(configDir, "rules.json"));
|
|
2642
|
+
if (count > 0) console.log(chalk.green(` \u2713 Saved ${count} rules to .archmind/rules.json`));
|
|
2643
|
+
});
|
|
2644
|
+
arch.command("watch").description("Watch your dev processes and auto-draft rules from errors").action(async () => {
|
|
2645
|
+
const { WatchErrors, loadWatchConfig } = await import("./watch-errors-B3FA26N4.js");
|
|
2646
|
+
const configDir = getArchmindDir();
|
|
2647
|
+
const config = loadWatchConfig(configDir);
|
|
2648
|
+
console.log(chalk.cyan("\n Watching:"));
|
|
2649
|
+
for (const cmd of config.commands) console.log(chalk.dim(` \u2022 ${cmd}`));
|
|
2650
|
+
console.log(chalk.dim("\n Press Ctrl+C to stop.\n"));
|
|
2651
|
+
const watcher = new WatchErrors(config, configDir);
|
|
2652
|
+
watcher.start((rule) => {
|
|
2653
|
+
console.log(chalk.yellow(` [DRAFT] ${rule.name}`));
|
|
2654
|
+
console.log(chalk.dim(` ${rule.description.slice(0, 100)}`));
|
|
2655
|
+
console.log(chalk.dim(' Run "memory-core arch review" to approve.\n'));
|
|
2656
|
+
});
|
|
2657
|
+
process.on("SIGINT", () => {
|
|
2658
|
+
watcher.stop();
|
|
2659
|
+
console.log(chalk.dim("\n Stopped."));
|
|
2660
|
+
process.exit(0);
|
|
2661
|
+
});
|
|
2662
|
+
await new Promise(() => {
|
|
2663
|
+
});
|
|
2664
|
+
});
|
|
2665
|
+
arch.command("incident").description("Log a production incident and capture it as an architecture rule").action(async () => {
|
|
2666
|
+
const { input: input2, confirm: confirm2 } = await import("@inquirer/prompts");
|
|
2667
|
+
const { IncidentCaptureService } = await import("./incident-capture-RVPZULS7.js");
|
|
2668
|
+
const { ApprovalQueue } = await import("./approval-queue-YBYRGBHP.js");
|
|
2669
|
+
console.log(chalk.cyan("\n Incident capture\n"));
|
|
2670
|
+
const what = await input2({ message: "What broke?" });
|
|
2671
|
+
const why = await input2({ message: "Root cause?" });
|
|
2672
|
+
const where = await input2({ message: "Which file / layer / pattern?" });
|
|
2673
|
+
const rule = new IncidentCaptureService().draftRule({ what, why, where, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2674
|
+
console.log(chalk.dim(`
|
|
2675
|
+
Rule: ${rule.name}`));
|
|
2676
|
+
console.log(chalk.dim(` Description: ${rule.description}
|
|
2677
|
+
`));
|
|
2678
|
+
const save = await confirm2({ message: "Save to approval queue?" });
|
|
2679
|
+
if (save) {
|
|
2680
|
+
new ApprovalQueue(getArchmindDir()).add(rule, "incident", "high");
|
|
2681
|
+
console.log(chalk.green(' \u2713 Saved. Run "memory-core arch review" to approve.\n'));
|
|
2682
|
+
} else {
|
|
2683
|
+
console.log(chalk.dim(" Discarded.\n"));
|
|
2684
|
+
}
|
|
2685
|
+
});
|
|
2292
2686
|
program.parseAsync(process.argv);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/models/self-review.ts
|
|
4
|
+
var SelfReviewValidator = class {
|
|
5
|
+
validate(packet, decision) {
|
|
6
|
+
const issues = [];
|
|
7
|
+
const hasBlockViolations = packet.violations.some((v) => v.rule.enforcement === "block");
|
|
8
|
+
const hasGraphViolations = packet.graphViolations.length > 0;
|
|
9
|
+
if (decision === "allow" && hasBlockViolations) {
|
|
10
|
+
issues.push('Decision is "allow" but critical violations are present');
|
|
11
|
+
}
|
|
12
|
+
if (decision === "block" && !hasBlockViolations && !hasGraphViolations) {
|
|
13
|
+
issues.push("Block decision but no deterministic violations found \u2014 possible false positive");
|
|
14
|
+
}
|
|
15
|
+
if (decision === "allow" && hasGraphViolations) {
|
|
16
|
+
issues.push('Decision is "allow" but graph violations are present');
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
valid: issues.length === 0,
|
|
20
|
+
reason: issues.length === 0 ? "Decision is supported by evidence" : `${issues.length} issue(s) found`,
|
|
21
|
+
issues
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/models/confidence-gate.ts
|
|
27
|
+
var ConfidenceGate = class {
|
|
28
|
+
constructor(config) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
}
|
|
31
|
+
config;
|
|
32
|
+
selfReview = new SelfReviewValidator();
|
|
33
|
+
async decide(packet, ollama, deepseek) {
|
|
34
|
+
if (packet.violations.some((v) => v.rule.enforcement === "block")) {
|
|
35
|
+
return { final: "block", source: "deterministic", ollama, selfReview: { valid: true, reason: "AST violation", issues: [] } };
|
|
36
|
+
}
|
|
37
|
+
if (packet.graphViolations.length > 0) {
|
|
38
|
+
return { final: "block", source: "deterministic", ollama, selfReview: { valid: true, reason: "Graph violation", issues: [] } };
|
|
39
|
+
}
|
|
40
|
+
if (ollama.decision === "block" && ollama.confidence >= this.config.blockConfidenceThreshold) {
|
|
41
|
+
const sr = this.selfReview.validate(packet, "block");
|
|
42
|
+
if (sr.valid) {
|
|
43
|
+
return { final: "block", source: "ollama", ollama, selfReview: sr };
|
|
44
|
+
}
|
|
45
|
+
return { final: "warn", source: "self-review", ollama, selfReview: sr };
|
|
46
|
+
}
|
|
47
|
+
if (this.config.escalateToDeepSeek && (ollama.confidence < this.config.warnConfidenceThreshold || ollama.decision === "warn")) {
|
|
48
|
+
const ds = await deepseek(packet);
|
|
49
|
+
return { final: ds.decision, source: "deepseek", ollama, deepseek: ds };
|
|
50
|
+
}
|
|
51
|
+
return { final: ollama.decision === "block" ? "warn" : ollama.decision, source: "ollama", ollama };
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function defaultGateConfig(useDeepSeek = false) {
|
|
55
|
+
return {
|
|
56
|
+
blockConfidenceThreshold: 0.75,
|
|
57
|
+
warnConfidenceThreshold: 0.6,
|
|
58
|
+
escalateToDeepSeek: useDeepSeek
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
ConfidenceGate,
|
|
63
|
+
defaultGateConfig
|
|
64
|
+
};
|