@shahmilsaari/memory-core 0.2.16 → 0.2.17
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 +79 -5
- package/dist/chunk-T4WJR6L6.js +1287 -0
- package/dist/cli.js +341 -1463
- package/dist/dashboard/assets/index-BCu-gBna.js +2 -0
- package/dist/dashboard/assets/index-BxS_xPdw.css +1 -0
- package/dist/dashboard/index.html +13 -0
- package/dist/dashboard-server-EVN4FL4L.js +547 -0
- package/package.json +13 -3
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AGENT_NAMES,
|
|
4
|
+
OUTPUT_FILES,
|
|
5
|
+
buildContextQuery,
|
|
6
|
+
callChatModel,
|
|
7
|
+
dedupeMemories,
|
|
8
|
+
detectProject,
|
|
9
|
+
generate,
|
|
10
|
+
getAllowPatterns,
|
|
11
|
+
getChatProviderLabel,
|
|
12
|
+
inferProjectArchitectures,
|
|
13
|
+
listProfiles,
|
|
14
|
+
retrieve,
|
|
15
|
+
retrieveContextualMemories,
|
|
16
|
+
retrieveMemorySelection,
|
|
17
|
+
seeds,
|
|
18
|
+
startWatch
|
|
19
|
+
} from "./chunk-T4WJR6L6.js";
|
|
2
20
|
import {
|
|
3
21
|
embed
|
|
4
22
|
} from "./chunk-HAGRPKR3.js";
|
|
@@ -11,7 +29,6 @@ import {
|
|
|
11
29
|
listMemories,
|
|
12
30
|
runMigrations,
|
|
13
31
|
saveMemory,
|
|
14
|
-
searchMemories,
|
|
15
32
|
updateMemory,
|
|
16
33
|
upsertMemory
|
|
17
34
|
} from "./chunk-WUL7HLAA.js";
|
|
@@ -20,912 +37,21 @@ import "./chunk-KSLFLWB4.js";
|
|
|
20
37
|
// src/cli.ts
|
|
21
38
|
import { Command } from "commander";
|
|
22
39
|
import { input, select, confirm } from "@inquirer/prompts";
|
|
23
|
-
import
|
|
40
|
+
import chalk2 from "chalk";
|
|
24
41
|
import ora from "ora";
|
|
25
|
-
import { readFileSync as
|
|
26
|
-
import { join as
|
|
42
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync, appendFileSync, rmSync } from "fs";
|
|
43
|
+
import { join as join3, dirname } from "path";
|
|
27
44
|
import { homedir } from "os";
|
|
28
45
|
|
|
29
|
-
// src/generator.ts
|
|
30
|
-
import { readFileSync as readFileSync2, readdirSync, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
|
|
31
|
-
import { join as join2, dirname } from "path";
|
|
32
|
-
import { fileURLToPath } from "url";
|
|
33
|
-
import Handlebars from "handlebars";
|
|
34
|
-
import yaml from "js-yaml";
|
|
35
|
-
|
|
36
|
-
// src/project-detector.ts
|
|
37
|
-
import { existsSync, readFileSync } from "fs";
|
|
38
|
-
import { join } from "path";
|
|
39
|
-
function detectProject(cwd = process.cwd()) {
|
|
40
|
-
const has = (file) => existsSync(join(cwd, file));
|
|
41
|
-
const readJson = (file) => {
|
|
42
|
-
try {
|
|
43
|
-
return JSON.parse(readFileSync(join(cwd, file), "utf-8"));
|
|
44
|
-
} catch {
|
|
45
|
-
return {};
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
if (has("next.config.js") || has("next.config.ts") || has("next.config.mjs")) {
|
|
49
|
-
return { language: "TypeScript", framework: "Next.js" };
|
|
50
|
-
}
|
|
51
|
-
if (has("artisan") && has("composer.json")) {
|
|
52
|
-
return { language: "PHP", framework: "Laravel" };
|
|
53
|
-
}
|
|
54
|
-
if (has("nuxt.config.ts") || has("nuxt.config.js")) {
|
|
55
|
-
return { language: "TypeScript", framework: "Nuxt.js" };
|
|
56
|
-
}
|
|
57
|
-
if (has("manage.py")) {
|
|
58
|
-
if (has("requirements.txt")) {
|
|
59
|
-
const req = readFileSync(join(cwd, "requirements.txt"), "utf-8");
|
|
60
|
-
if (req.includes("djangorestframework")) {
|
|
61
|
-
return { language: "Python", framework: "Django REST Framework" };
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return { language: "Python", framework: "Django" };
|
|
65
|
-
}
|
|
66
|
-
if (has("go.mod")) {
|
|
67
|
-
return { language: "Go", framework: "Go" };
|
|
68
|
-
}
|
|
69
|
-
if (has("Cargo.toml")) {
|
|
70
|
-
return { language: "Rust", framework: "Rust" };
|
|
71
|
-
}
|
|
72
|
-
if (has("pubspec.yaml")) {
|
|
73
|
-
return { language: "Dart", framework: "Flutter" };
|
|
74
|
-
}
|
|
75
|
-
if (has("pom.xml")) {
|
|
76
|
-
return { language: "Java", framework: "Spring Boot" };
|
|
77
|
-
}
|
|
78
|
-
if (has("build.gradle") || has("build.gradle.kts")) {
|
|
79
|
-
return { language: "Kotlin", framework: "Kotlin/JVM" };
|
|
80
|
-
}
|
|
81
|
-
if (has("package.json")) {
|
|
82
|
-
const pkg = readJson("package.json");
|
|
83
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
84
|
-
if (deps["@nestjs/core"]) return { language: "TypeScript", framework: "NestJS" };
|
|
85
|
-
if (deps["express"]) return { language: "TypeScript", framework: "Express.js" };
|
|
86
|
-
if (deps["fastify"]) return { language: "TypeScript", framework: "Fastify" };
|
|
87
|
-
if (deps["react"]) return { language: "TypeScript", framework: "React" };
|
|
88
|
-
if (deps["vue"]) return { language: "TypeScript", framework: "Vue.js" };
|
|
89
|
-
if (deps["svelte"]) return { language: "TypeScript", framework: "Svelte" };
|
|
90
|
-
return { language: "TypeScript/JavaScript", framework: "Node.js" };
|
|
91
|
-
}
|
|
92
|
-
if (has("requirements.txt") || has("pyproject.toml")) {
|
|
93
|
-
return { language: "Python", framework: "Python" };
|
|
94
|
-
}
|
|
95
|
-
return { language: "Unknown", framework: "Unknown" };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// src/retriever.ts
|
|
99
|
-
async function retrieve(query, architecture, limit = 10) {
|
|
100
|
-
const embedding = await embed(query);
|
|
101
|
-
return searchMemories(embedding, architecture, limit);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// src/memory-selection.ts
|
|
105
|
-
var FRAMEWORK_ARCHITECTURE_MAP = {
|
|
106
|
-
Laravel: ["laravel-service-repository"],
|
|
107
|
-
"Next.js": ["nextjs"],
|
|
108
|
-
"Nuxt.js": ["nuxt"],
|
|
109
|
-
Go: ["go-api"],
|
|
110
|
-
NestJS: ["nestjs"],
|
|
111
|
-
React: ["react"],
|
|
112
|
-
"Vue.js": ["vue"],
|
|
113
|
-
Svelte: ["svelte"]
|
|
114
|
-
};
|
|
115
|
-
var KNOWN_ARCHITECTURE_KEYS = /* @__PURE__ */ new Set([
|
|
116
|
-
...Object.values(FRAMEWORK_ARCHITECTURE_MAP).flat(),
|
|
117
|
-
"angular",
|
|
118
|
-
"clean-architecture",
|
|
119
|
-
"express",
|
|
120
|
-
"fastify",
|
|
121
|
-
"hexagonal",
|
|
122
|
-
"modular-monolith",
|
|
123
|
-
"mvc",
|
|
124
|
-
"react-native"
|
|
125
|
-
]);
|
|
126
|
-
function normalizeText(value) {
|
|
127
|
-
return value.toLowerCase().replace(/[`"'()[\]{}.,:;!?/\\<>|=*+-]/g, " ").replace(/\s+/g, " ").trim();
|
|
128
|
-
}
|
|
129
|
-
function tokenSet(value) {
|
|
130
|
-
return new Set(
|
|
131
|
-
normalizeText(value).split(" ").filter((token) => token.length > 2)
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
function similarityScore(a, b) {
|
|
135
|
-
const left = tokenSet(a);
|
|
136
|
-
const right = tokenSet(b);
|
|
137
|
-
if (left.size === 0 || right.size === 0) return 0;
|
|
138
|
-
let intersection = 0;
|
|
139
|
-
for (const token of left) {
|
|
140
|
-
if (right.has(token)) intersection++;
|
|
141
|
-
}
|
|
142
|
-
return 2 * intersection / (left.size + right.size);
|
|
143
|
-
}
|
|
144
|
-
function mergeMemory(primary, secondary) {
|
|
145
|
-
const mergedTags = [.../* @__PURE__ */ new Set([...primary.tags ?? [], ...secondary.tags ?? []])];
|
|
146
|
-
const reason = [primary.reason, secondary.reason].filter(Boolean).join(" | ") || void 0;
|
|
147
|
-
return {
|
|
148
|
-
...primary,
|
|
149
|
-
tags: mergedTags,
|
|
150
|
-
reason
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
function memoryArchitectureKeys(memory) {
|
|
154
|
-
if (memory.architecture && memory.architecture !== "global") {
|
|
155
|
-
return [memory.architecture];
|
|
156
|
-
}
|
|
157
|
-
return (memory.tags ?? []).filter((tag) => KNOWN_ARCHITECTURE_KEYS.has(tag));
|
|
158
|
-
}
|
|
159
|
-
function getStackReason(memory, activeArchitectures) {
|
|
160
|
-
const architectureKeys = memoryArchitectureKeys(memory);
|
|
161
|
-
if (architectureKeys.length === 0) {
|
|
162
|
-
return {
|
|
163
|
-
included: true,
|
|
164
|
-
reason: "global memory: no architecture-specific tag"
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
const matched = architectureKeys.filter((architecture) => activeArchitectures.has(architecture));
|
|
168
|
-
if (matched.length > 0) {
|
|
169
|
-
return {
|
|
170
|
-
included: true,
|
|
171
|
-
reason: `matched active architecture: ${matched.join(", ")}`
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
const active = [...activeArchitectures].join(", ") || "none detected";
|
|
175
|
-
return {
|
|
176
|
-
included: false,
|
|
177
|
-
reason: `excluded: tagged for ${architectureKeys.join(", ")}; active stack is ${active}`
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
function dedupeMemories(memories, threshold = 0.8) {
|
|
181
|
-
const deduped = [];
|
|
182
|
-
for (const memory of memories) {
|
|
183
|
-
const existingIndex = deduped.findIndex((candidate) => {
|
|
184
|
-
if (candidate.content_hash && memory.content_hash && candidate.content_hash === memory.content_hash) {
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
|
-
return similarityScore(candidate.content, memory.content) >= threshold;
|
|
188
|
-
});
|
|
189
|
-
if (existingIndex === -1) {
|
|
190
|
-
deduped.push(memory);
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
deduped[existingIndex] = mergeMemory(deduped[existingIndex], memory);
|
|
194
|
-
}
|
|
195
|
-
return deduped;
|
|
196
|
-
}
|
|
197
|
-
function inferProjectArchitectures(cwd = process.cwd(), config) {
|
|
198
|
-
const inferred = /* @__PURE__ */ new Set();
|
|
199
|
-
if (config?.backendArchitecture) inferred.add(config.backendArchitecture);
|
|
200
|
-
if (config?.frontendFramework) inferred.add(config.frontendFramework);
|
|
201
|
-
if (config?.projectType === "backend" && !config.backendArchitecture) {
|
|
202
|
-
inferred.add("clean-architecture");
|
|
203
|
-
}
|
|
204
|
-
const detected = detectProject(cwd);
|
|
205
|
-
for (const architecture of FRAMEWORK_ARCHITECTURE_MAP[detected.framework] ?? []) {
|
|
206
|
-
inferred.add(architecture);
|
|
207
|
-
}
|
|
208
|
-
return [...inferred];
|
|
209
|
-
}
|
|
210
|
-
function getAllowPatterns(config) {
|
|
211
|
-
return [...new Set(config?.allowPatterns?.filter(Boolean) ?? [])];
|
|
212
|
-
}
|
|
213
|
-
function filterRelevantMemories(memories, config, cwd = process.cwd()) {
|
|
214
|
-
return explainMemorySelection(memories, config, cwd).included;
|
|
215
|
-
}
|
|
216
|
-
function explainMemorySelection(memories, config, cwd = process.cwd(), threshold = 0.8) {
|
|
217
|
-
const activeArchitectures = inferProjectArchitectures(cwd, config);
|
|
218
|
-
const activeSet = new Set(activeArchitectures);
|
|
219
|
-
const included = [];
|
|
220
|
-
const decisions = [];
|
|
221
|
-
for (const memory of memories) {
|
|
222
|
-
const stackDecision = getStackReason(memory, activeSet);
|
|
223
|
-
if (!stackDecision.included) {
|
|
224
|
-
decisions.push({
|
|
225
|
-
memory,
|
|
226
|
-
status: "excluded",
|
|
227
|
-
reason: stackDecision.reason
|
|
228
|
-
});
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
const existingIndex = included.findIndex((candidate) => {
|
|
232
|
-
if (candidate.content_hash && memory.content_hash && candidate.content_hash === memory.content_hash) {
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
return similarityScore(candidate.content, memory.content) >= threshold;
|
|
236
|
-
});
|
|
237
|
-
if (existingIndex === -1) {
|
|
238
|
-
included.push(memory);
|
|
239
|
-
decisions.push({
|
|
240
|
-
memory,
|
|
241
|
-
status: "included",
|
|
242
|
-
reason: stackDecision.reason
|
|
243
|
-
});
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
included[existingIndex] = mergeMemory(included[existingIndex], memory);
|
|
247
|
-
decisions.push({
|
|
248
|
-
memory,
|
|
249
|
-
status: "excluded",
|
|
250
|
-
reason: `duplicate or near-duplicate of memory #${included[existingIndex].id}`
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
return {
|
|
254
|
-
included,
|
|
255
|
-
excluded: decisions.filter((decision) => decision.status === "excluded"),
|
|
256
|
-
decisions,
|
|
257
|
-
activeArchitectures
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
function buildContextQuery(parts, maxLength = 1200) {
|
|
261
|
-
return parts.filter(Boolean).join("\n").slice(0, maxLength);
|
|
262
|
-
}
|
|
263
|
-
async function retrieveContextualMemories(options) {
|
|
264
|
-
return (await retrieveMemorySelection(options)).included;
|
|
265
|
-
}
|
|
266
|
-
async function retrieveMemorySelection(options) {
|
|
267
|
-
const architectures = inferProjectArchitectures(options.cwd, options.config);
|
|
268
|
-
const memories = await retrieve(options.query, architectures, options.limit ?? 15);
|
|
269
|
-
return explainMemorySelection(memories, options.config, options.cwd);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// src/generator.ts
|
|
273
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
274
|
-
var __dirname = dirname(__filename);
|
|
275
|
-
var PKG_ROOT = join2(__dirname, "..");
|
|
276
|
-
var OUTPUT_FILES = [
|
|
277
|
-
{ template: "CLAUDE.md.hbs", path: "CLAUDE.md", agent: "Claude Code" },
|
|
278
|
-
{ template: "copilot-instructions.md.hbs", path: ".github/copilot-instructions.md", agent: "GitHub Copilot" },
|
|
279
|
-
{ template: "cursorrules.hbs", path: ".cursorrules", agent: "Cursor" },
|
|
280
|
-
{ template: "cursor-rule.mdc.hbs", path: ".cursor/rules/memory-core.mdc", agent: "Cursor" },
|
|
281
|
-
{ template: "windsurfrules.hbs", path: ".windsurfrules", agent: "Windsurf" },
|
|
282
|
-
{ template: "clinerules.hbs", path: ".clinerules", agent: "Cline" },
|
|
283
|
-
{ template: "roo-rule.md.hbs", path: ".roo/rules/memory-core.md", agent: "Roo Code" },
|
|
284
|
-
{ template: "aider.conf.yml.hbs", path: ".aider.conf.yml", agent: "Aider" },
|
|
285
|
-
{ template: "continue-config.json.hbs", path: ".continue/config.json", agent: "Continue.dev", skipIfExists: true },
|
|
286
|
-
{ template: "DEVIN.md.hbs", path: "DEVIN.md", agent: "Devin" },
|
|
287
|
-
{ template: "amazonq-guidelines.md.hbs", path: ".amazonq/dev/guidelines.md", agent: "Amazon Q" },
|
|
288
|
-
{ template: "gemini-styleguide.md.hbs", path: ".gemini/styleguide.md", agent: "Gemini Code Assist" },
|
|
289
|
-
{ template: "zed-settings.json.hbs", path: ".zed/settings.json", agent: "Zed AI", skipIfExists: true },
|
|
290
|
-
{ template: "jetbrains-ai.md.hbs", path: ".idea/ai-instructions.md", agent: "JetBrains AI" },
|
|
291
|
-
{ template: "AGENTS.md.hbs", path: "AGENTS.md", agent: "OpenHands" },
|
|
292
|
-
{ template: "AI_RULES.md.hbs", path: "AI_RULES.md", agent: "Shared" },
|
|
293
|
-
{ template: "ARCHITECTURE.md.hbs", path: "ARCHITECTURE.md", agent: "Shared" },
|
|
294
|
-
{ template: "PROJECT_MEMORY.md.hbs", path: "PROJECT_MEMORY.md", agent: "Shared" }
|
|
295
|
-
];
|
|
296
|
-
var AGENT_NAMES = [...new Set(OUTPUT_FILES.map((f) => f.agent))];
|
|
297
|
-
Handlebars.registerHelper(
|
|
298
|
-
"join",
|
|
299
|
-
(arr, sep) => Array.isArray(arr) ? arr.join(sep) : ""
|
|
300
|
-
);
|
|
301
|
-
Handlebars.registerHelper(
|
|
302
|
-
"bullet",
|
|
303
|
-
(arr) => Array.isArray(arr) ? arr.map((i) => `- ${i}`).join("\n") : ""
|
|
304
|
-
);
|
|
305
|
-
Handlebars.registerHelper(
|
|
306
|
-
"numbered",
|
|
307
|
-
(arr) => Array.isArray(arr) ? arr.map((i, idx) => `${idx + 1}. ${i}`).join("\n") : ""
|
|
308
|
-
);
|
|
309
|
-
Handlebars.registerHelper("json", (val) => JSON.stringify(val, null, 2));
|
|
310
|
-
Handlebars.registerHelper("memoryBlock", (memory) => {
|
|
311
|
-
const meta = [memory.type, memory.architecture].filter(Boolean).join(" \xB7 ");
|
|
312
|
-
const label = memory.title ? `${memory.title}: ${memory.content}` : memory.content;
|
|
313
|
-
const lines = [`- [${meta || "memory"}] ${label}`];
|
|
314
|
-
if (memory.reason) lines.push(` Why: ${memory.reason}`);
|
|
315
|
-
if (memory.context?.appliesTo?.length) lines.push(` Use when: ${memory.context.appliesTo.join("; ")}`);
|
|
316
|
-
if (memory.context?.avoidWhen?.length) lines.push(` Avoid when: ${memory.context.avoidWhen.join("; ")}`);
|
|
317
|
-
if (memory.context?.examples?.length) {
|
|
318
|
-
lines.push(" Examples:");
|
|
319
|
-
for (const example of memory.context.examples) lines.push(` - ${example}`);
|
|
320
|
-
}
|
|
321
|
-
if (memory.tags?.length) lines.push(` Tags: ${memory.tags.join(", ")}`);
|
|
322
|
-
if (memory.project_name || memory.context?.source) {
|
|
323
|
-
lines.push(` Source: ${memory.context?.source ?? memory.project_name}`);
|
|
324
|
-
}
|
|
325
|
-
return new Handlebars.SafeString(lines.join("\n"));
|
|
326
|
-
});
|
|
327
|
-
function loadProfile(name) {
|
|
328
|
-
const profilePath = join2(PKG_ROOT, "profiles", `${name}.yml`);
|
|
329
|
-
if (!existsSync2(profilePath)) throw new Error(`Profile not found: ${name}`);
|
|
330
|
-
return yaml.load(readFileSync2(profilePath, "utf-8"));
|
|
331
|
-
}
|
|
332
|
-
function listProfiles(layer) {
|
|
333
|
-
const files = readdirSync(join2(PKG_ROOT, "profiles")).filter((f) => f.endsWith(".yml"));
|
|
334
|
-
const all = files.map((f) => yaml.load(readFileSync2(join2(PKG_ROOT, "profiles", f), "utf-8")));
|
|
335
|
-
if (!layer) return all;
|
|
336
|
-
if (layer === "backend") return all.filter((p) => p.layer === "backend" || p.layer === "fullstack");
|
|
337
|
-
if (layer === "frontend") return all.filter((p) => p.layer === "frontend" || p.layer === "fullstack");
|
|
338
|
-
return all;
|
|
339
|
-
}
|
|
340
|
-
function buildTemplateData(options, cwd = process.cwd()) {
|
|
341
|
-
const backend = options.backendArchitecture ? loadProfile(options.backendArchitecture) : null;
|
|
342
|
-
const frontend = options.frontendFramework ? loadProfile(options.frontendFramework) : null;
|
|
343
|
-
const dedupedMemories = filterRelevantMemories(options.memories, {
|
|
344
|
-
projectType: options.projectType,
|
|
345
|
-
backendArchitecture: options.backendArchitecture,
|
|
346
|
-
frontendFramework: options.frontendFramework,
|
|
347
|
-
language: options.language
|
|
348
|
-
}, cwd);
|
|
349
|
-
const allRules = [
|
|
350
|
-
...backend?.rules ?? [],
|
|
351
|
-
...frontend?.rules ?? []
|
|
352
|
-
];
|
|
353
|
-
const allFolders = [
|
|
354
|
-
...backend?.folders ?? [],
|
|
355
|
-
...frontend?.folders ?? []
|
|
356
|
-
];
|
|
357
|
-
const allAvoid = [
|
|
358
|
-
...backend?.avoid ?? [],
|
|
359
|
-
...frontend?.avoid ?? []
|
|
360
|
-
];
|
|
361
|
-
const archLabel = [
|
|
362
|
-
backend ? `Backend: ${backend.displayName}` : null,
|
|
363
|
-
frontend ? `Frontend: ${frontend.displayName}` : null
|
|
364
|
-
].filter(Boolean).join(" \xB7 ");
|
|
365
|
-
return {
|
|
366
|
-
projectName: options.projectName,
|
|
367
|
-
projectType: options.projectType,
|
|
368
|
-
isBackend: options.projectType === "backend" || options.projectType === "fullstack",
|
|
369
|
-
isFrontend: options.projectType === "frontend" || options.projectType === "fullstack",
|
|
370
|
-
isFullstack: options.projectType === "fullstack",
|
|
371
|
-
// backend
|
|
372
|
-
hasBackend: !!backend,
|
|
373
|
-
backendArchitecture: backend?.displayName,
|
|
374
|
-
backendDescription: backend?.description,
|
|
375
|
-
backendRules: backend?.rules ?? [],
|
|
376
|
-
backendFolders: backend?.folders ?? [],
|
|
377
|
-
backendAvoid: backend?.avoid ?? [],
|
|
378
|
-
// frontend
|
|
379
|
-
hasFrontend: !!frontend,
|
|
380
|
-
frontendFramework: frontend?.displayName,
|
|
381
|
-
frontendDescription: frontend?.description,
|
|
382
|
-
frontendRules: frontend?.rules ?? [],
|
|
383
|
-
frontendFolders: frontend?.folders ?? [],
|
|
384
|
-
frontendAvoid: frontend?.avoid ?? [],
|
|
385
|
-
// combined — used by simple templates
|
|
386
|
-
architecture: archLabel,
|
|
387
|
-
rules: allRules,
|
|
388
|
-
folders: allFolders,
|
|
389
|
-
avoid: allAvoid,
|
|
390
|
-
description: [backend?.description, frontend?.description].filter(Boolean).join(" | "),
|
|
391
|
-
// memories
|
|
392
|
-
memories: dedupedMemories,
|
|
393
|
-
hasMemories: dedupedMemories.length > 0,
|
|
394
|
-
// misc
|
|
395
|
-
language: options.language,
|
|
396
|
-
caveman: options.caveman,
|
|
397
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
function renderTemplate(templateName, data) {
|
|
401
|
-
const templatePath = join2(PKG_ROOT, "templates", templateName);
|
|
402
|
-
if (!existsSync2(templatePath)) throw new Error(`Template not found: ${templateName}`);
|
|
403
|
-
return Handlebars.compile(readFileSync2(templatePath, "utf-8"))(data);
|
|
404
|
-
}
|
|
405
|
-
function writeFile(filePath, content) {
|
|
406
|
-
const dir = dirname(filePath);
|
|
407
|
-
if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
|
|
408
|
-
if (existsSync2(filePath)) {
|
|
409
|
-
const existing = readFileSync2(filePath, "utf-8");
|
|
410
|
-
if (existing === content) return "skipped";
|
|
411
|
-
}
|
|
412
|
-
writeFileSync(filePath, content, "utf-8");
|
|
413
|
-
return "written";
|
|
414
|
-
}
|
|
415
|
-
async function generate(options, cwd = process.cwd(), onlyAgents) {
|
|
416
|
-
const data = buildTemplateData(options, cwd);
|
|
417
|
-
const written = [];
|
|
418
|
-
const skipped = [];
|
|
419
|
-
const files = onlyAgents ? OUTPUT_FILES.filter((f) => onlyAgents.includes(f.agent)) : OUTPUT_FILES;
|
|
420
|
-
for (const output of files) {
|
|
421
|
-
const targetPath = join2(cwd, output.path);
|
|
422
|
-
if (output.skipIfExists && existsSync2(targetPath)) {
|
|
423
|
-
skipped.push(output.path);
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
try {
|
|
427
|
-
const content = renderTemplate(output.template, data);
|
|
428
|
-
const result = writeFile(targetPath, content);
|
|
429
|
-
if (result === "written") written.push(output.path);
|
|
430
|
-
else skipped.push(output.path);
|
|
431
|
-
} catch (err) {
|
|
432
|
-
if (!(err instanceof Error && err.message.includes("Template not found"))) throw err;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
return { written, skipped };
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// src/seeds.ts
|
|
439
|
-
var seeds = [
|
|
440
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
441
|
-
// GLOBAL — applies to every architecture
|
|
442
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
443
|
-
// ── API Design ───────────────────────────────────────────────────────────
|
|
444
|
-
{ type: "rule", scope: "global", architecture: "global", title: "DTOs for all API responses", content: "Use DTOs for all API responses. Never expose raw database models or ORM entities to the client.", reason: "Raw DB entities leak your internal schema, expose sensitive fields (password hashes, internal IDs), and couple clients to your DB structure \u2014 any DB refactor becomes a breaking API change.", tags: ["api", "dto", "response"] },
|
|
445
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Consistent error response shape", content: "All error responses must follow one shape: { error: { code, message, details? } }. Never return raw exception messages to clients.", reason: "Inconsistent error shapes force every client to implement custom parsing per endpoint. Raw exception messages expose stack traces, file paths, and internal logic \u2014 a security and debugging nightmare.", tags: ["api", "errors", "response"] },
|
|
446
|
-
{ type: "rule", scope: "global", architecture: "global", title: "HTTP status codes correctly", content: "Use correct HTTP status codes: 200 OK, 201 Created, 204 No Content, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable, 429 Rate Limited, 500 Internal Error.", reason: "Wrong status codes break HTTP clients, caches, and monitoring tools that rely on them to distinguish success from failure. Returning 200 with an error body is the most common and painful mistake.", tags: ["api", "http", "status-codes"] },
|
|
447
|
-
{ type: "rule", scope: "global", architecture: "global", title: "API versioning", content: "Version all public APIs from day one via URL prefix (/v1/) or header. Never make breaking changes to existing versions.", reason: "Without versioning, any API change is a potential breaking change for every consumer. Adding versioning retroactively requires coordinating all clients simultaneously \u2014 impossible at scale.", tags: ["api", "versioning"] },
|
|
448
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Pagination on all list endpoints", content: "All list endpoints must support pagination. Use cursor-based pagination for large datasets, offset for smaller ones. Never return unbounded collections.", reason: "Unbounded list endpoints will eventually return millions of rows, crashing the DB, exhausting memory, and timing out. Retrofitting pagination later requires changing the API contract and all consumers.", tags: ["api", "pagination", "performance"] },
|
|
449
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Idempotent mutations", content: "POST/PUT endpoints must be idempotent where possible. Use idempotency keys for financial and critical mutations.", reason: "Network retries happen. Without idempotency, a retry on a payment or order endpoint creates duplicate charges or records. Idempotency keys let clients safely retry without side effects.", tags: ["api", "idempotency"] },
|
|
450
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Request/response logging", content: "Log all inbound requests and outbound responses at INFO level. Include method, path, status, duration, and correlation ID. Never log request bodies in production.", reason: "Without request logs you cannot debug production issues, measure latency, or audit access. Logging bodies in production violates GDPR and leaks passwords and tokens into log storage.", tags: ["logging", "api", "observability"] },
|
|
451
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Correlation IDs", content: "Attach a unique correlation/trace ID to every request. Propagate it through all downstream calls. Include it in logs and error responses.", reason: "In distributed systems, a single user action triggers calls across multiple services. Without a shared ID, tracing a bug through logs across services is impossible \u2014 you are searching for a needle in multiple haystacks.", tags: ["observability", "tracing", "api"] },
|
|
452
|
-
// ── Validation & Input ───────────────────────────────────────────────────
|
|
453
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Validate at the boundary", content: "Validate all input at the system boundary (HTTP, queue, CLI). Never re-validate inside domain or service layers. Trust data that has already crossed the boundary.", reason: "Re-validating deep inside the stack means your validation logic is scattered and inconsistent. Validate once at entry, then trust it \u2014 duplicate validation is noise that masks where the real check lives.", tags: ["validation", "boundary"] },
|
|
454
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Allowlist not blocklist", content: "Validate inputs using allowlists (permitted values, formats, ranges). Never rely on blocklists to filter malicious input.", reason: "Attackers constantly find new attack strings blocklists don't cover. An allowlist defines exactly what is permitted \u2014 anything else is rejected, including unknown future attack vectors.", tags: ["validation", "security"] },
|
|
455
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Sanitize user-generated content", content: "Sanitize all user-generated content before rendering. Escape HTML, strip scripts, and use CSP headers to prevent XSS.", reason: "Unsanitized user content lets attackers inject scripts that run in other users' browsers, steal session tokens, redirect to phishing pages, and perform actions as the victim.", tags: ["security", "xss", "sanitization"] },
|
|
456
|
-
// ── Security ─────────────────────────────────────────────────────────────
|
|
457
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Auth via middleware", content: "Authentication and authorization are enforced in middleware/guards before reaching any handler. Never duplicate auth logic in individual controllers.", reason: "Duplicated auth logic means one forgotten check opens a hole. Centralising it in middleware ensures no endpoint is ever accidentally unprotected and makes auditing trivial.", tags: ["auth", "middleware", "security"] },
|
|
458
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Never trust client input", content: "Treat all client-supplied data as untrusted. Validate types, lengths, formats, and ranges on every input before processing.", reason: "Clients can send any data regardless of your UI constraints. An attacker bypasses the UI entirely \u2014 server-side validation is the only real defence.", tags: ["security", "validation"] },
|
|
459
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Secrets via environment only", content: "All secrets (API keys, DB passwords, signing keys) come from environment variables or a secrets manager. Never hardcode secrets or commit them to git.", reason: "Secrets in code are permanently exposed once committed \u2014 even if removed later they remain in git history. Leaked API keys cause data breaches, unexpected bills, and loss of customer trust.", tags: ["security", "secrets", "config"] },
|
|
460
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Rate limiting on all public routes", content: "Apply rate limiting to all public-facing endpoints. Use stricter limits on auth endpoints (login, register, password reset).", reason: "Without rate limiting, auth endpoints are trivially brute-forced. Unrestricted endpoints can be hammered to cause DoS, spike infrastructure costs, or harvest data through enumeration.", tags: ["security", "rate-limiting", "api"] },
|
|
461
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Parameterized queries always", content: "Always use parameterized queries or ORM query builders. Never concatenate user input into SQL, shell commands, or OS paths.", reason: "SQL injection is still the #1 cause of data breaches. Concatenating user input into queries lets attackers read, modify, or delete your entire database with a single crafted string.", tags: ["security", "sql-injection", "database"] },
|
|
462
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Least privilege", content: "Database users, service accounts, and API keys must have the minimum permissions required. Never use superuser credentials in application code.", reason: "If application credentials are compromised, least privilege limits the blast radius. A DB user that can only SELECT cannot be used to DROP TABLE or exfiltrate the entire schema.", tags: ["security", "permissions"] },
|
|
463
|
-
{ type: "rule", scope: "global", architecture: "global", title: "CORS explicitly configured", content: "Configure CORS with an explicit allowlist of origins. Never use wildcard (*) for authenticated APIs.", reason: "Wildcard CORS on authenticated APIs lets any malicious website make credentialed requests on behalf of your logged-in users, enabling CSRF-style attacks even with modern browsers.", tags: ["security", "cors", "api"] },
|
|
464
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Hash passwords with bcrypt/argon2", content: "Always hash passwords with bcrypt or argon2 before storing. Never store plain text, MD5, or SHA1 passwords.", reason: "Plain or weakly-hashed passwords in a DB dump let attackers crack and reuse credentials across every service your users share passwords with. bcrypt/argon2 are deliberately slow \u2014 cracking is computationally infeasible.", tags: ["security", "auth", "passwords"] },
|
|
465
|
-
{ type: "rule", scope: "global", architecture: "global", title: "JWT expiry and refresh tokens", content: "Access tokens must expire in 15\u201360 minutes. Use refresh tokens for long-lived sessions. Store refresh tokens in httpOnly cookies, not localStorage.", reason: "Long-lived JWTs cannot be revoked \u2014 a stolen token grants access until expiry. localStorage is readable by any JS on the page (XSS). httpOnly cookies are inaccessible to scripts.", tags: ["security", "jwt", "auth"] },
|
|
466
|
-
// ── Error Handling ───────────────────────────────────────────────────────
|
|
467
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Typed custom error classes", content: "Define typed custom error classes (NotFoundError, ValidationError, UnauthorizedError). Never throw generic Error objects with only string messages.", reason: "Generic errors force callers to parse string messages to determine what went wrong \u2014 brittle and breaks on message changes. Typed errors let you catch and handle specific failure modes explicitly and safely.", tags: ["errors", "exceptions"] },
|
|
468
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Global error handler", content: "Register one global error handler that maps domain errors to HTTP responses. Individual handlers must not format error responses.", reason: "Scattered error formatting leads to inconsistent responses across endpoints. One handler guarantees a uniform contract, makes logging centralised, and prevents accidental leakage of stack traces.", tags: ["errors", "middleware"] },
|
|
469
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Never swallow errors", content: "Never catch an error and do nothing. Either handle it, re-throw it, or log it with full context. Silent catch blocks hide real failures.", reason: "Silent errors turn recoverable bugs into permanent mysteries. A failure that is caught and ignored looks like success \u2014 the bug stays hidden until data corruption or a user complaint reveals it.", tags: ["errors", "reliability"] },
|
|
470
|
-
// ── Database ─────────────────────────────────────────────────────────────
|
|
471
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Migrations for all schema changes", content: "All database schema changes go through versioned migration files. Never alter tables directly in production. Migrations must be reversible.", reason: "Manual production changes are untracked, unrepeatable, and irreversible. When a deployment goes wrong and you need to roll back, you need to know exactly what was changed and how to undo it.", tags: ["database", "migrations"] },
|
|
472
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Index foreign keys and filters", content: "Add indexes on all foreign key columns and any column used in WHERE, ORDER BY, or JOIN clauses. Missing indexes cause full table scans.", reason: "A query without an index scans every row in the table. At 1000 rows it's fine. At 1M rows it takes seconds and locks resources. Indexes are the single highest-leverage DB performance tool.", tags: ["database", "performance", "indexes"] },
|
|
473
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Avoid N+1 queries", content: "Use eager loading (includes/joins) for related data. Always audit query counts in development. N+1 is the most common performance bug.", reason: "Fetching a list of 100 orders then querying the customer for each one fires 101 queries. At scale this collapses response times and saturates DB connections. One join is always faster than N round-trips.", tags: ["database", "performance", "n+1"] },
|
|
474
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Transactions for multi-step writes", content: "Wrap operations that modify multiple tables in a database transaction. Partial writes that leave data inconsistent are worse than failures.", reason: "Without a transaction, a crash halfway through a multi-step write leaves the DB in a corrupt half-state. Debugging and manually fixing inconsistent data in production is extremely difficult and risky.", tags: ["database", "transactions", "consistency"] },
|
|
475
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Soft deletes for user data", content: "Use soft deletes (deleted_at timestamp) for all user-owned data. Hard delete only for explicit GDPR/PII erasure requests.", reason: 'Hard-deleting user data makes accidental deletions and "undo" features impossible, breaks audit trails, and may violate data retention requirements. Soft deletes preserve history while keeping records logically removed.', tags: ["database", "soft-delete"] },
|
|
476
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Connection pooling", content: "Use a connection pool (pg-pool, HikariCP, etc). Never open a new DB connection per request. Configure pool size based on expected concurrency.", reason: "Opening a new DB connection per request adds 50\u2013200ms of latency and exhausts DB connection limits under load. PostgreSQL supports ~100 connections by default \u2014 100 concurrent requests with no pooling saturates it instantly.", tags: ["database", "performance", "pooling"] },
|
|
477
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Never SELECT *", content: "Always select only the columns you need. SELECT * loads unnecessary data, breaks column-order assumptions, and leaks sensitive fields.", reason: "SELECT * fetches data you don't need (wasted I/O), breaks if columns are reordered, and can accidentally expose password hashes or PII that you'd never intentionally return.", tags: ["database", "performance", "sql"] },
|
|
478
|
-
// ── Caching ──────────────────────────────────────────────────────────────
|
|
479
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Cache at the right layer", content: "Cache computed/expensive results at the service or repository layer, not in controllers. Cache keys must include all parameters that affect the result.", tags: ["caching", "performance"] },
|
|
480
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Cache invalidation strategy", content: "Define cache invalidation at write time. Use TTLs as a safety net, not primary strategy. Stale cache is worse than no cache for critical data.", tags: ["caching", "consistency"] },
|
|
481
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Never cache sensitive data", content: "Never cache personally identifiable information, tokens, or credentials. Cache only non-sensitive, shareable data.", tags: ["caching", "security"] },
|
|
482
|
-
// ── Logging & Observability ───────────────────────────────────────────────
|
|
483
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Structured logging only", content: "Use a structured logger (winston, pino, monolog) that outputs JSON. Never use console.log or print in production code.", tags: ["logging", "observability"] },
|
|
484
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Log levels correctly", content: "ERROR: unexpected failures. WARN: recoverable issues. INFO: significant business events. DEBUG: development detail. Never log DEBUG in production.", tags: ["logging"] },
|
|
485
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Health check endpoint", content: "Expose GET /health that returns 200 when the service is ready (DB connected, dependencies reachable). Used by load balancers and orchestrators.", tags: ["observability", "devops", "health"] },
|
|
486
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Metrics on critical paths", content: "Instrument request duration, error rate, and queue depth on all critical paths. Use Prometheus-compatible metrics or APM tooling.", tags: ["observability", "metrics", "performance"] },
|
|
487
|
-
// ── Testing ──────────────────────────────────────────────────────────────
|
|
488
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Test behavior not implementation", content: "Tests assert observable outputs and side effects. Never test private methods, internal state, or implementation details. Tests should survive refactors.", tags: ["testing"] },
|
|
489
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Arrange-Act-Assert structure", content: "Every test follows Arrange (setup) \u2192 Act (call) \u2192 Assert (verify). One concept per test. One assertion block per test.", tags: ["testing", "structure"] },
|
|
490
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Test naming convention", content: 'Name tests as: "should [expected behavior] when [condition]". Never use test1, checkSomething, or vague names.', tags: ["testing", "naming"] },
|
|
491
|
-
{ type: "rule", scope: "global", architecture: "global", title: "No test interdependence", content: "Tests must be fully isolated. Each test sets up its own state and cleans up after. Never rely on execution order or shared mutable state between tests.", tags: ["testing", "isolation"] },
|
|
492
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Integration tests hit real DB", content: "Integration tests use a real database in a test transaction that rolls back after each test. Never mock the database in integration tests.", tags: ["testing", "integration", "database"] },
|
|
493
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Mock only external systems", content: "Mock only third-party APIs, email providers, payment gateways \u2014 things you do not own. Never mock your own classes in unit tests; use real implementations.", tags: ["testing", "mocks"] },
|
|
494
|
-
// ── Code Quality ─────────────────────────────────────────────────────────
|
|
495
|
-
{ type: "rule", scope: "global", architecture: "global", title: "No magic strings or numbers", content: "Replace magic strings and numbers with named constants or enums. Code should communicate intent, not memorized values.", tags: ["code-quality", "constants"] },
|
|
496
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Single responsibility always", content: 'Every function, class, and module does one thing. If you need "and" to describe it, split it into two.', tags: ["solid", "srp", "code-quality"] },
|
|
497
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Functions under 30 lines", content: "Functions that exceed 30 lines are doing too much. Extract meaningful sub-functions with descriptive names.", tags: ["code-quality", "functions"] },
|
|
498
|
-
{ type: "rule", scope: "global", architecture: "global", title: "No circular dependencies", content: "Modules must not import from each other circularly. Use dependency injection or events to break cycles.", tags: ["architecture", "dependencies"] },
|
|
499
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Async consistency", content: "Be consistent with async/await throughout. Never mix .then()/.catch() chains with async/await in the same function.", tags: ["async", "code-quality"] },
|
|
500
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Immutability by default", content: "Prefer immutable data structures. Use const over let. Never mutate function arguments. Return new objects instead of modifying inputs.", tags: ["code-quality", "immutability"] },
|
|
501
|
-
// ── DevOps & Reliability ─────────────────────────────────────────────────
|
|
502
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Graceful shutdown", content: "Handle SIGTERM and SIGINT signals. Finish in-flight requests, close DB connections, and drain queues before exiting. Never kill the process abruptly.", tags: ["devops", "reliability"] },
|
|
503
|
-
{ type: "rule", scope: "global", architecture: "global", title: "12-factor config", content: "All environment-specific config comes from environment variables. No config files checked into git. Follow 12-factor app methodology.", tags: ["devops", "config", "12-factor"] },
|
|
504
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Idempotent deployments", content: "Deployments and migrations must be idempotent. Running them twice must produce the same result. No manual steps required.", tags: ["devops", "deployments"] },
|
|
505
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Queue heavy background work", content: "Offload any operation exceeding 200ms (emails, file processing, external APIs, reports) to a background job queue. Never block HTTP responses.", tags: ["performance", "queues", "reliability"] },
|
|
506
|
-
// ── Universal Coding Practices ────────────────────────────────────────────
|
|
507
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Filtering and pagination always on the backend", content: "All filtering, sorting, searching, and pagination is done on the server \u2014 never on the client. The API returns already-filtered, paginated results. The client never receives a full list to filter itself.", reason: "Sending all records to the client and filtering in the browser leaks data the user should not see, kills performance on large datasets, and breaks as soon as the table grows past a few hundred rows. Backend filtering is the only approach that is secure and scalable.", tags: ["api", "performance", "security", "pagination"] },
|
|
508
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Table and data-grid filtering is always server-side", content: "Data tables and grids must send filter params (column filters, search query, sort field, sort direction, page, page size) to the API as query parameters. The server applies all filters and returns only the matching rows. Never load all rows then filter in memory on the client.", reason: "A table that loads all rows to filter in the browser will work fine at 200 rows and collapse at 20,000. Server-side filtering keeps the payload small regardless of dataset size, keeps sensitive rows hidden from unauthorized users, and means the DB index does the work \u2014 not the browser CPU.", tags: ["api", "tables", "performance", "security", "pagination"] },
|
|
509
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Remove all unused imports", content: "Delete unused imports immediately. Never leave dead imports in any file. Enable linting rules (no-unused-vars, @typescript-eslint/no-unused-vars) to enforce this automatically.", reason: "Unused imports increase bundle size, slow down IDE indexing, mislead readers into thinking a dependency is actively used, and accumulate into noise that makes real imports harder to find. They are a sign of code that was partially cleaned up.", tags: ["code-quality", "cleanup"] },
|
|
510
|
-
{ type: "rule", scope: "global", architecture: "global", title: "No console.log in production code", content: "Remove all console.log, console.debug, and console.info from production code. Use a structured logger (Winston, Pino, etc.) with log levels. Lint rules should flag any console.* call.", reason: "console.log blocks the event loop on busy servers, cannot be controlled by log level, leaks sensitive data to server logs read by ops teams, and has no structure \u2014 it is impossible to query, alert on, or correlate with other events.", tags: ["code-quality", "logging", "security"] },
|
|
511
|
-
{ type: "rule", scope: "global", architecture: "global", title: "No commented-out code", content: "Delete code that is no longer needed. Never comment it out and leave it. Git history is the record of what existed before \u2014 it can always be recovered from there.", reason: "Commented-out code is noise that accumulates over time. Readers must decide whether it is important, temporary, or forgotten. It never gets uncommented \u2014 it just stays there forever confusing people. Delete it. Git has it.", tags: ["code-quality", "cleanup"] },
|
|
512
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Meaningful variable and function names", content: "Name variables and functions for what they represent or do, not how they are implemented. Avoid abbreviations, single-letter names (except loop counters), and vague names like data, info, manager, helper.", reason: 'Code is read far more often than it is written. A variable named "d" or "res" forces every reader to trace backward to understand its meaning. A name like "activeUserCount" is self-documenting \u2014 no comment needed, no mental overhead.', tags: ["code-quality", "naming"] },
|
|
513
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Functions do one thing", content: "Each function has a single, clear responsibility. If a function needs more than one paragraph to describe what it does, split it. Functions that take more than 3 parameters should accept an options object.", reason: "A function that does multiple things cannot be named accurately, cannot be tested in isolation, and cannot be reused for one of its sub-tasks. Single-responsibility functions compose cleanly and each can be understood, tested, and replaced independently.", tags: ["code-quality", "srp"] },
|
|
514
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Never use magic numbers or strings", content: "Replace magic values with named constants: MAX_RETRY_COUNT = 3, not just 3. Group related constants in a constants file or enum. No raw strings for status values \u2014 use enums or const maps.", reason: "A raw number like 3 or 86400 in the middle of logic is unreadable \u2014 readers must guess what it means and why it was chosen. A named constant explains the value's purpose. When the value changes, there is one place to update, not a grep across the whole codebase.", tags: ["code-quality", "constants"] },
|
|
515
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Explicit return types on all public functions", content: "Every public function and method has an explicit return type annotation. Inferred return types are acceptable only for private helpers and simple one-liners.", reason: "An explicit return type is a contract \u2014 it tells the caller what to expect and the compiler what to enforce. Without it, an accidental change to what the function returns silently breaks callers. Public APIs especially need explicit types because they cross module boundaries.", tags: ["typescript", "code-quality"] },
|
|
516
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Never use any type", content: "Never use the any type in TypeScript. Use unknown for values of truly unknown shape and narrow with type guards. Use generics for reusable functions. Configure strict: true in tsconfig.", reason: "any disables TypeScript's type checker for that value and everything derived from it \u2014 the entire benefit of TypeScript is lost. unknown forces you to prove the shape before using the value, which is exactly what type safety means.", tags: ["typescript", "code-quality"] },
|
|
517
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Handle all promise rejections", content: "Every Promise and async function must have error handling \u2014 either a try/catch or a .catch() handler. Never let a rejected promise go unhandled. Use a global unhandledRejection handler as a last resort.", reason: "An unhandled promise rejection silently swallows the error in older Node.js versions and crashes the process in newer ones. Either outcome is bad. Explicit error handling forces you to decide what to do when something fails \u2014 ignore, retry, or propagate.", tags: ["error-handling", "async", "code-quality"] },
|
|
518
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Dates always in UTC, ISO 8601 format", content: "Store all dates in UTC. Return all dates from APIs in ISO 8601 format (2024-01-15T10:30:00Z). Convert to local timezone only at the display layer on the client.", reason: "Storing local time in the database causes DST ambiguity \u2014 the same clock time can mean two different instants. UTC is unambiguous. ISO 8601 is universally parseable. Mixing formats across APIs forces every consumer to write custom date parsing.", tags: ["data", "dates", "api"] },
|
|
519
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Validate file uploads strictly", content: "Validate uploaded files by MIME type (read magic bytes, not just extension), maximum file size, and allowed file types. Store uploads outside the web root. Scan for malware in high-risk contexts.", reason: "An extension check is trivially bypassed \u2014 rename malware.exe to malware.jpg. Magic byte validation reads the actual file header. Storing uploads in the web root allows direct execution of uploaded scripts. File upload is the most common path for RCE vulnerabilities.", tags: ["security", "file-upload"] },
|
|
520
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Never expose internal IDs in public APIs", content: "Use UUIDs or slugs as public identifiers in API URLs and responses. Never expose sequential database integer IDs to clients.", reason: "Sequential IDs leak your table size (user #4 means you have ~4 users), enable enumeration attacks (fetch /users/1, /users/2 ...), and make it trivial to guess other users' resource IDs. UUIDs are opaque and non-enumerable.", tags: ["security", "api", "privacy"] },
|
|
521
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Audit log for sensitive operations", content: "Log who did what and when for every sensitive operation: login, permission change, data export, deletion, payment. Audit logs are append-only and stored separately from application logs.", reason: `Without an audit trail you cannot answer "who deleted this record?" or "when did this user's role change?". Audit logs are required for compliance (GDPR, SOC2, HIPAA) and essential for debugging security incidents after the fact.`, tags: ["security", "audit", "compliance"] },
|
|
522
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Never log sensitive data", content: "Never log passwords, tokens, API keys, credit card numbers, SSNs, or any PII. Log the user ID and action, not the sensitive value. Scrub request bodies before logging them.", reason: "Application logs are often stored in plaintext, shipped to third-party log services, and accessible to ops teams who should not see user passwords. One accidental log line can cause a credential exposure incident and a compliance violation.", tags: ["security", "logging", "privacy"] },
|
|
523
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Dependency updates on a schedule", content: "Review and update dependencies at least monthly. Use automated tools (Dependabot, Renovate) to get PRs for new versions. Never ignore security advisories.", reason: "Outdated dependencies are the most common source of vulnerabilities in production systems. Most supply-chain attacks exploit known CVEs in packages that were never updated. Automated PRs make updates low-friction \u2014 they can be reviewed and merged in minutes.", tags: ["security", "dependencies", "devops"] },
|
|
524
|
-
{ type: "rule", scope: "global", architecture: "global", title: "Feature flags for risky changes", content: "Deploy risky changes behind a feature flag. Enable for internal users first, then a percentage rollout, then full release. Roll back by toggling the flag, not redeploying.", reason: "A traditional deployment is all-or-nothing \u2014 if it breaks, you redeploy. A feature flag separates deployment from release. A broken flag is turned off in seconds with no deployment needed. This makes releases low-risk and rollbacks instant.", tags: ["devops", "deployment", "reliability"] },
|
|
525
|
-
{ type: "rule", scope: "global", architecture: "global", title: "API contracts documented before implementation", content: "Define the API contract (request shape, response shape, status codes, error codes) before writing implementation code. Use OpenAPI spec or a shared DTO. Consumers can mock against the contract immediately.", reason: "Discovering API shape mismatches during integration costs 10x more than discovering them before writing code. An upfront contract lets frontend and backend work in parallel against the same spec. It also forces you to think about the API from the consumer's perspective first.", tags: ["api", "documentation", "design"] },
|
|
526
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
527
|
-
// CLEAN ARCHITECTURE
|
|
528
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
529
|
-
// ── Core Rules ───────────────────────────────────────────────────────────
|
|
530
|
-
{ type: "rule", scope: "global", architecture: "clean-architecture", title: "Dependency rule is absolute", content: "Dependencies flow inward only: Interfaces \u2192 Controllers \u2192 Use Cases \u2192 Domain. No layer may import from a layer outside it.", reason: "The moment an inner layer imports from an outer layer, your architecture collapses \u2014 you can no longer swap infrastructure, test in isolation, or reason about which layer owns which concern. The rule must be machine-enforced.", tags: ["dependency-rule", "clean-architecture"] },
|
|
531
|
-
{ type: "rule", scope: "global", architecture: "clean-architecture", title: "Thin controllers", content: "Controllers parse HTTP input, call one use case, and format the response. Zero logic. No conditionals, no calculations.", reason: "Logic in controllers cannot be reused by other entry points (CLI, queues, scheduled jobs). Thin controllers mean your business logic is always in use cases \u2014 usable, testable, and transport-agnostic.", tags: ["controller", "clean-architecture"] },
|
|
532
|
-
{ type: "rule", scope: "global", architecture: "clean-architecture", title: "Use cases are single-purpose", content: "Each use case handles one workflow. Name it as an action verb: CreateUser, PublishPost, ProcessRefund. One public execute() method.", reason: "Multi-purpose use cases grow into god objects with conditional branches for different callers. Single-purpose use cases are easy to test, find, understand, and replace \u2014 one use case per user story.", tags: ["use-case", "clean-architecture"] },
|
|
533
|
-
{ type: "rule", scope: "global", architecture: "clean-architecture", title: "Domain has zero external imports", content: "Entities and value objects must import nothing from Node.js stdlib, frameworks, or ORMs. Pure language types only.", reason: "Any external import in the domain layer creates a hard dependency on that library. Swapping the DB, the HTTP framework, or upgrading a library then requires touching business logic \u2014 the most sensitive, hardest-to-test code.", tags: ["domain", "clean-architecture"] },
|
|
534
|
-
{ type: "rule", scope: "global", architecture: "clean-architecture", title: "Repository interfaces in domain", content: "IUserRepository is defined inside domain/application. PostgresUserRepository lives in infrastructure. Domain depends on the interface, never the concrete.", reason: "Without the interface in domain, your domain layer must import the concrete repository \u2014 tying business logic to PostgreSQL. Swapping to MongoDB or testing with an in-memory store requires changing domain code.", tags: ["repository", "clean-architecture"] },
|
|
535
|
-
{ type: "rule", scope: "global", architecture: "clean-architecture", title: "No ORM entities in use cases", content: "Use cases receive and return plain domain objects or DTOs. Never accept Prisma/TypeORM entities as parameters or return them.", reason: "ORM entities carry lazy-loading behaviour, active-record patterns, and framework magic. Passing them into use cases couples business logic to the ORM \u2014 change Prisma to TypeORM and every use case breaks.", tags: ["use-case", "orm", "clean-architecture"] },
|
|
536
|
-
// ── Domain ───────────────────────────────────────────────────────────────
|
|
537
|
-
{ type: "pattern", scope: "global", architecture: "clean-architecture", title: "Value objects for domain concepts", content: "Wrap primitives in value objects: Email, Money, UserId, PhoneNumber. Value objects validate themselves in the constructor and are immutable.", reason: 'A raw string "email" can contain anything. A value object Email("invalid") throws immediately. You cannot accidentally pass a UserId where a ProductId is expected \u2014 type safety encodes business rules.', tags: ["value-object", "domain", "clean-architecture"] },
|
|
538
|
-
{ type: "pattern", scope: "global", architecture: "clean-architecture", title: "Aggregate roots control their data", content: "All mutations to an aggregate go through its root. External code never mutates child entities directly. The root maintains invariants.", reason: "Direct mutation of child entities bypasses the aggregate's invariant checks. The root exists to ensure business rules are always enforced \u2014 circumventing it allows the domain to reach invalid states.", tags: ["aggregate", "domain", "clean-architecture"] },
|
|
539
|
-
{ type: "pattern", scope: "global", architecture: "clean-architecture", title: "Domain services for cross-entity logic", content: "When logic involves multiple entities but belongs to no single one, put it in a domain service (e.g. TransferService for moving money between accounts).", reason: "Forcing cross-entity logic into one entity creates inappropriate dependencies between aggregates. A domain service is stateless and coordinates without either entity knowing about the other.", tags: ["domain-service", "clean-architecture"] },
|
|
540
|
-
{ type: "pattern", scope: "global", architecture: "clean-architecture", title: "Domain events for side effects", content: "Entities raise domain events (UserRegistered, OrderPlaced). Application layer subscribes and triggers side effects (emails, notifications, audit logs).", reason: "Side effects (emails, webhooks, cache invalidation) in the domain layer couple business logic to infrastructure. Domain events keep the domain pure \u2014 the application layer decides what to do after something happens.", tags: ["domain-events", "clean-architecture"] },
|
|
541
|
-
{ type: "pattern", scope: "global", architecture: "clean-architecture", title: "Factory for complex entity creation", content: "Use factory methods or factory classes to create entities with complex invariants. Constructors must never fail silently.", reason: "Complex construction logic scattered across multiple callers leads to inconsistent entity state. A factory centralises creation rules, ensures all invariants are enforced, and is the only place you need to change when rules evolve.", tags: ["factory", "domain", "clean-architecture"] },
|
|
542
|
-
{ type: "rule", scope: "global", architecture: "clean-architecture", title: "Rich domain model not anemic", content: "Entities must contain behavior (methods) that enforce business rules, not just getters and setters. An entity with only properties is an anemic model.", reason: "An anemic model pushes business logic into services, which become procedural scripts. The domain becomes a passive data holder \u2014 you lose encapsulation, invariant enforcement, and the ability to reason about entity behaviour.", tags: ["domain", "clean-architecture", "anemic"] },
|
|
543
|
-
// ── Application ──────────────────────────────────────────────────────────
|
|
544
|
-
{ type: "pattern", scope: "global", architecture: "clean-architecture", title: "Input and output DTOs per use case", content: "Each use case has a dedicated InputDTO and OutputDTO. This decouples the HTTP layer from domain models and makes use cases testable in isolation.", reason: "Without DTOs, use cases accept HTTP request objects or return domain entities \u2014 making them impossible to test without an HTTP context, and coupling your transport layer to your application layer.", tags: ["dto", "use-case", "clean-architecture"] },
|
|
545
|
-
{ type: "pattern", scope: "global", architecture: "clean-architecture", title: "CQRS separates reads and writes", content: "Commands mutate state (CreateOrderCommand). Queries read state (GetOrderQuery). Keep them in separate classes. Queries can bypass domain for performance.", reason: "Mixed read/write use cases grow complex quickly. Separating them lets reads be optimised independently (direct DB views, caching) without risking mutation logic. Scale reads and writes independently.", tags: ["cqrs", "clean-architecture"] },
|
|
546
|
-
{ type: "pattern", scope: "global", architecture: "clean-architecture", title: "Unit of work for transactions", content: "Wrap multi-repository operations in a Unit of Work. The use case commits or rolls back atomically without knowing the DB implementation.", reason: "Without unit of work, a use case calling two repositories in sequence cannot atomically commit or roll back \u2014 partial writes leave your data inconsistent with no clean recovery path.", tags: ["unit-of-work", "transactions", "clean-architecture"] },
|
|
547
|
-
{ type: "pattern", scope: "global", architecture: "clean-architecture", title: "Presenter for response formatting", content: "Use a Presenter class to transform use case output into HTTP response shape. Controllers pass output to presenters \u2014 no inline transformation.", reason: "Inline response formatting in controllers cannot be reused across different response formats (JSON, XML, GraphQL). Presenters make formatting a separate concern that can be swapped or extended without touching business logic.", tags: ["presenter", "clean-architecture"] },
|
|
548
|
-
// ── Infrastructure ────────────────────────────────────────────────────────
|
|
549
|
-
{ type: "rule", scope: "global", architecture: "clean-architecture", title: "Mapper between domain and ORM", content: "Write explicit mappers between domain entities and ORM models. Never let ORM entity shapes leak into domain objects.", reason: "When ORM entities leak into domain, a DB schema change forces domain code changes. Mappers are the firewall \u2014 your domain model can evolve independently of your DB schema.", tags: ["mapper", "infrastructure", "clean-architecture"] },
|
|
550
|
-
{ type: "rule", scope: "global", architecture: "clean-architecture", title: "Infrastructure errors mapped", content: "Infrastructure adapters catch low-level errors (DB errors, HTTP errors) and rethrow them as domain errors (NotFoundError, ConflictError).", reason: "Without mapping, DB-specific errors (unique constraint violations, connection timeouts) bubble up into use cases and controllers \u2014 coupling business logic to PostgreSQL error codes and making error handling brittle.", tags: ["errors", "infrastructure", "clean-architecture"] },
|
|
551
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
552
|
-
// MODULAR MONOLITH
|
|
553
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
554
|
-
{ type: "rule", scope: "global", architecture: "modular-monolith", title: "One public index per module", content: "Each module exposes exactly one public barrel file (index.ts). All external imports must come from the barrel. Never import from a module's internal files.", tags: ["module", "encapsulation", "modular-monolith"] },
|
|
555
|
-
{ type: "rule", scope: "global", architecture: "modular-monolith", title: "No cross-module DB joins", content: "Modules must not join across each other's tables in a single query. Compose data in the service layer after separate queries.", tags: ["database", "module", "modular-monolith"] },
|
|
556
|
-
{ type: "rule", scope: "global", architecture: "modular-monolith", title: "Events for cross-module communication", content: "Cross-module calls use an in-process event bus or message broker. Never call another module's private service directly.", tags: ["events", "decoupling", "modular-monolith"] },
|
|
557
|
-
{ type: "rule", scope: "global", architecture: "modular-monolith", title: "Shared kernel stays minimal", content: "The shared/common module holds only: base types, utility functions, base errors, and interfaces. No business logic, no DB access.", tags: ["shared-kernel", "modular-monolith"] },
|
|
558
|
-
{ type: "rule", scope: "global", architecture: "modular-monolith", title: "Module owns its migrations", content: "Each module maintains its own DB migration files. Never have one module's migration alter another module's tables.", tags: ["database", "migrations", "modular-monolith"] },
|
|
559
|
-
{ type: "rule", scope: "global", architecture: "modular-monolith", title: "Table namespace per module", content: "Prefix DB table names with the module name (users_profiles, orders_items, payments_invoices). Prevents naming collisions and clarifies ownership.", tags: ["database", "naming", "modular-monolith"] },
|
|
560
|
-
{ type: "rule", scope: "global", architecture: "modular-monolith", title: "No circular module dependencies", content: "Module dependency graph must be acyclic. If A depends on B and B depends on A, extract the shared concern into a third module or shared kernel.", tags: ["dependencies", "modular-monolith"] },
|
|
561
|
-
{ type: "pattern", scope: "global", architecture: "modular-monolith", title: "Facade exposes module to outsiders", content: "Expose module functionality through a facade class (UserModuleFacade). Consumers call the facade. Internals remain private and refactorable.", tags: ["facade", "modular-monolith"] },
|
|
562
|
-
{ type: "pattern", scope: "global", architecture: "modular-monolith", title: "Eventual consistency between modules", content: "Cross-module data consistency is eventual via events. Do not attempt distributed transactions. Accept brief inconsistency windows.", tags: ["consistency", "events", "modular-monolith"] },
|
|
563
|
-
{ type: "pattern", scope: "global", architecture: "modular-monolith", title: "Test each module in isolation", content: "Unit test each module by mocking its event bus and repository interfaces. Integration tests mount only the module under test plus its dependencies.", tags: ["testing", "isolation", "modular-monolith"] },
|
|
564
|
-
{ type: "decision", scope: "global", architecture: "modular-monolith", title: "Design for extraction", content: "Every module must be extractable to a microservice with only infrastructure changes (DB connection, event bus transport). Domain and application layers stay unchanged.", tags: ["microservices", "modular-monolith"] },
|
|
565
|
-
{ type: "rule", scope: "global", architecture: "modular-monolith", title: "Module-level feature flags", content: "Enable or disable entire modules via feature flags at startup. Never conditionally import module code at runtime.", tags: ["feature-flags", "modular-monolith"] },
|
|
566
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
567
|
-
// MVC
|
|
568
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
569
|
-
{ type: "rule", scope: "global", architecture: "mvc", title: "Fat service, thin controller", content: "All business logic lives in services. Controllers handle only HTTP: parse request, call service, return response. No if/else logic in controllers.", tags: ["controller", "service", "mvc"] },
|
|
570
|
-
{ type: "rule", scope: "global", architecture: "mvc", title: "Models are data containers only", content: "Models define schema, relationships, casts, and scopes. No business methods. Move orchestration logic to services, domain logic to domain objects.", tags: ["model", "mvc"] },
|
|
571
|
-
{ type: "rule", scope: "global", architecture: "mvc", title: "One service per domain resource", content: "Create UserService, OrderService, PaymentService. One service per resource. Avoid god services that span multiple unrelated domain concepts.", tags: ["service", "srp", "mvc"] },
|
|
572
|
-
{ type: "rule", scope: "global", architecture: "mvc", title: "Services are framework-agnostic", content: "Service classes must work without an HTTP context. No request objects, no response objects inside services. Makes them fully testable without a server.", tags: ["service", "testability", "mvc"] },
|
|
573
|
-
{ type: "rule", scope: "global", architecture: "mvc", title: "Group files by feature, not type", content: "Organize by feature: /users, /orders, /payments. Not by type: /controllers, /models, /services. Feature grouping makes a module self-contained.", tags: ["structure", "mvc"] },
|
|
574
|
-
{ type: "rule", scope: "global", architecture: "mvc", title: "Middleware for cross-cutting concerns", content: "Auth, logging, rate limiting, CORS, and error handling are implemented as middleware. Never inline these in individual controllers.", tags: ["middleware", "mvc"] },
|
|
575
|
-
{ type: "rule", scope: "global", architecture: "mvc", title: "Error middleware is last", content: "Register the global error handling middleware last in the middleware chain. Express/Koa error handlers must have the (err, req, res, next) signature.", tags: ["errors", "middleware", "mvc"] },
|
|
576
|
-
{ type: "pattern", scope: "global", architecture: "mvc", title: "Reuse via service layer, not inheritance", content: "If two controllers share logic, it belongs in a service. Never solve DRY problems by inheriting from a base controller.", tags: ["service", "dry", "mvc"] },
|
|
577
|
-
{ type: "pattern", scope: "global", architecture: "mvc", title: "Request-specific data via middleware context", content: "Pass authenticated user, locale, and tenant ID through request context (res.locals or ctx.state). Never thread these as function arguments through service calls.", tags: ["middleware", "context", "mvc"] },
|
|
578
|
-
{ type: "decision", scope: "global", architecture: "mvc", title: "Repository pattern for data access", content: "Introduce a repository layer below services for all DB queries. Services never write raw queries. Repositories never contain business logic.", tags: ["repository", "database", "mvc"] },
|
|
579
|
-
{ type: "rule", scope: "global", architecture: "mvc", title: "Route files only for routing", content: "Route files declare paths, methods, middleware, and point to controllers. No business logic, no inline handlers in route files.", tags: ["routes", "mvc"] },
|
|
580
|
-
{ type: "rule", scope: "global", architecture: "mvc", title: "Controller tests via HTTP layer", content: "Test controllers by making real HTTP requests to a test server. Unit tests belong at the service layer, not controller level.", tags: ["testing", "controller", "mvc"] },
|
|
581
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
582
|
-
// HEXAGONAL ARCHITECTURE
|
|
583
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
584
|
-
{ type: "rule", scope: "global", architecture: "hexagonal", title: "Core is framework-free", content: "The application core (domain + use cases) must not import Express, NestJS, Fastify, Prisma, TypeORM, or any framework library. Pure language code only.", tags: ["core", "framework-free", "hexagonal"] },
|
|
585
|
-
{ type: "rule", scope: "global", architecture: "hexagonal", title: "Ports defined inside core", content: "Port interfaces (IUserRepository, IEmailSender, IPaymentGateway) are defined inside the core. Adapters implement them outside.", tags: ["ports", "hexagonal"] },
|
|
586
|
-
{ type: "rule", scope: "global", architecture: "hexagonal", title: "One adapter per external system", content: "PostgresUserRepository, RedisCache, StripePaymentGateway \u2014 one adapter per external dependency. Never combine two external systems in one adapter.", tags: ["adapters", "hexagonal"] },
|
|
587
|
-
{ type: "rule", scope: "global", architecture: "hexagonal", title: "DI wires adapters at startup", content: "The DI container wires concrete adapters to port interfaces at application startup. The core never instantiates adapters directly.", tags: ["dependency-injection", "hexagonal"] },
|
|
588
|
-
{ type: "rule", scope: "global", architecture: "hexagonal", title: "Primary ports for inbound", content: "Primary (driving) ports are interfaces called by the outside world: ICreateUserUseCase, IGetOrderQuery. HTTP controllers call primary ports.", tags: ["primary-ports", "hexagonal"] },
|
|
589
|
-
{ type: "rule", scope: "global", architecture: "hexagonal", title: "Secondary ports for outbound", content: "Secondary (driven) ports are interfaces the core calls: IUserRepository, IEmailPort. Infrastructure adapters implement these.", tags: ["secondary-ports", "hexagonal"] },
|
|
590
|
-
{ type: "pattern", scope: "global", architecture: "hexagonal", title: "In-memory adapters for unit tests", content: "Implement InMemoryUserRepository, InMemoryEmailSender for all secondary ports. Use them in unit tests. No database, no network required.", tags: ["testing", "adapters", "hexagonal"] },
|
|
591
|
-
{ type: "pattern", scope: "global", architecture: "hexagonal", title: "Adapter error mapping", content: "Each adapter catches infrastructure exceptions and translates them to domain errors (EntityNotFoundError, ConflictError). Core only sees domain errors.", tags: ["errors", "adapters", "hexagonal"] },
|
|
592
|
-
{ type: "pattern", scope: "global", architecture: "hexagonal", title: "Multiple adapters per port", content: "One port can have multiple adapters (PostgresUserRepo + MongoUserRepo). Switch adapters via DI config without touching core code.", tags: ["adapters", "flexibility", "hexagonal"] },
|
|
593
|
-
{ type: "pattern", scope: "global", architecture: "hexagonal", title: "Configuration as inbound adapter", content: "Treat environment config as an inbound adapter that provides values to the core via a port. Core never reads process.env directly.", tags: ["config", "adapters", "hexagonal"] },
|
|
594
|
-
{ type: "rule", scope: "global", architecture: "hexagonal", title: "Port naming convention", content: "Name ports as I + [Action] + [Resource]: ICreateUser, IFindOrderById, ISendEmail. Adapters use the technology name: PostgresCreateUser, StripeCharge.", tags: ["naming", "ports", "hexagonal"] },
|
|
595
|
-
{ type: "decision", scope: "global", architecture: "hexagonal", title: "Test core without infrastructure", content: "Core unit tests run in milliseconds with no DB or network. Integration tests add adapters one at a time. E2E tests use real infrastructure.", tags: ["testing", "hexagonal"] },
|
|
596
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
597
|
-
// GO REST API
|
|
598
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
599
|
-
// ── Package Structure ────────────────────────────────────────────────────
|
|
600
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "cmd/internal/pkg layout", content: "Organize code into cmd/ (main packages), internal/ (private app code), pkg/ (reusable public packages). cmd/api/main.go is the only entry point. Never put business logic in main.go.", tags: ["structure", "packages", "go"] },
|
|
601
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Thin HTTP handlers", content: "Handlers parse the request, call a service method, and write the response. No business logic, no DB calls, no conditional branching beyond input validation in handlers.", tags: ["handler", "structure", "go"] },
|
|
602
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Service layer owns business logic", content: "Services accept and return domain types, not http.Request or http.ResponseWriter. Services are fully testable without starting an HTTP server.", tags: ["service", "business-logic", "go"] },
|
|
603
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Repository layer for data access", content: "All database calls live in a repository layer. Services depend on repository interfaces, never on database drivers (database/sql, pgx, gorm) directly.", tags: ["repository", "database", "go"] },
|
|
604
|
-
// ── Error Handling ───────────────────────────────────────────────────────
|
|
605
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Always return errors explicitly", content: "Return errors as the last return value. Never use panic in library, service, or handler code. Reserve panic only for unrecoverable failures at startup (e.g., missing config).", tags: ["error-handling", "go"] },
|
|
606
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Wrap errors with context", content: 'Wrap errors at each layer boundary: fmt.Errorf("createUser: %w", err). This preserves the original error for errors.Is/errors.As and adds context to stack traces.', tags: ["error-handling", "wrapping", "go"] },
|
|
607
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Never ignore returned errors", content: "Every returned error must be handled or explicitly discarded with a comment. Silently assigning to _ hides bugs. Lint with errcheck to enforce this.", tags: ["error-handling", "go"] },
|
|
608
|
-
{ type: "pattern", scope: "global", architecture: "go-api", title: "Sentinel errors for known cases", content: 'Define sentinel errors (var ErrNotFound = errors.New("not found")) for expected failure cases. Callers use errors.Is() to check. Never compare error strings.', tags: ["error-handling", "sentinel", "go"] },
|
|
609
|
-
// ── Interfaces & Types ───────────────────────────────────────────────────
|
|
610
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Small interfaces at the point of use", content: "Define interfaces where they are consumed, not where implementations live. Prefer single-method interfaces. A 10-method interface is a sign the consumer needs too much.", tags: ["interfaces", "design", "go"] },
|
|
611
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Request/response structs for all handlers", content: "Define explicit request and response structs for every handler. Never bind directly to domain models. This decouples API shape from internal representation.", tags: ["dto", "handler", "go"] },
|
|
612
|
-
// ── Context & Concurrency ────────────────────────────────────────────────
|
|
613
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "context.Context as first parameter", content: "Every function that may block, make a network call, or query a database accepts context.Context as its first parameter. Never store Context in a struct.", tags: ["context", "concurrency", "go"] },
|
|
614
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Graceful shutdown", content: "Catch SIGINT and SIGTERM via signal.NotifyContext. Call server.Shutdown(ctx) to drain in-flight requests before exiting. Never call os.Exit(1) directly in the server loop.", tags: ["shutdown", "reliability", "go"] },
|
|
615
|
-
// ── Configuration & Logging ──────────────────────────────────────────────
|
|
616
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Config struct loaded at startup", content: "Read all configuration from environment variables into a validated Config struct in main.go before starting the server. Fail fast on missing required values. Never read env vars inside handlers or services.", tags: ["config", "go"] },
|
|
617
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Structured logging only", content: "Use slog (stdlib) or zerolog for all logging. Log with key-value fields, not format strings. Never use fmt.Println or log.Printf in production paths.", tags: ["logging", "observability", "go"] },
|
|
618
|
-
// ── Testing ──────────────────────────────────────────────────────────────
|
|
619
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Table-driven tests", content: "Write unit tests as table-driven tests using t.Run(). Each test case has a name, input, and expected output. This keeps tests readable and easy to extend.", tags: ["testing", "go"] },
|
|
620
|
-
{ type: "pattern", scope: "global", architecture: "go-api", title: "Test handlers with httptest", content: "Test HTTP handlers using httptest.NewRecorder() and httptest.NewRequest(). Never spin up a real server in unit tests.", tags: ["testing", "handler", "go"] },
|
|
621
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Mock with interfaces, not libraries", content: "Inject mock implementations via interfaces rather than using reflection-based mock libraries. Write mocks by hand or use mockery \u2014 keep them simple.", tags: ["testing", "mocking", "go"] },
|
|
622
|
-
// ── Middleware & Security ────────────────────────────────────────────────
|
|
623
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Middleware registered at router level", content: "Register all middleware (auth, logging, CORS, recovery) at the router, not inside individual handlers. Middleware wraps the handler chain \u2014 it should never be called manually.", tags: ["middleware", "structure", "go"] },
|
|
624
|
-
{ type: "rule", scope: "global", architecture: "go-api", title: "Validate all incoming data", content: "Validate and sanitize all request inputs before passing to the service layer. Return 400 Bad Request with a structured error body for invalid input. Never trust client data.", tags: ["validation", "security", "go"] },
|
|
625
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
626
|
-
// LARAVEL SERVICE REPOSITORY
|
|
627
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
628
|
-
// ── Core Rules ───────────────────────────────────────────────────────────
|
|
629
|
-
{ type: "rule", scope: "global", architecture: "laravel-service-repository", title: "Form Requests for all validation", content: "All HTTP input validation lives in Form Request classes. Never validate in controllers, services, or models.", tags: ["validation", "form-request", "laravel"] },
|
|
630
|
-
{ type: "rule", scope: "global", architecture: "laravel-service-repository", title: "API Resources for all responses", content: "All API responses are transformed through API Resource or Resource Collection classes. Never return Eloquent models or plain arrays from controllers.", tags: ["api-resource", "response", "laravel"] },
|
|
631
|
-
{ type: "rule", scope: "global", architecture: "laravel-service-repository", title: "Services contain all business logic", content: "Services orchestrate business rules and call repositories for data. No logic in controllers, models, or repositories beyond queries.", tags: ["service", "business-logic", "laravel"] },
|
|
632
|
-
{ type: "rule", scope: "global", architecture: "laravel-service-repository", title: "Repositories abstract all queries", content: "All Eloquent queries live in repository classes. Services never call Eloquent model static methods directly (User::where(), User::find()).", tags: ["repository", "database", "laravel"] },
|
|
633
|
-
{ type: "rule", scope: "global", architecture: "laravel-service-repository", title: "Interface binding in service provider", content: "Bind IUserRepository \u2192 EloquentUserRepository in a service provider. Inject IUserRepository into services. Enables swapping implementations without changing service code.", tags: ["repository", "interface", "di", "laravel"] },
|
|
634
|
-
{ type: "rule", scope: "global", architecture: "laravel-service-repository", title: "Eloquent models are thin", content: "Models contain: fillable, casts, relationships, local scopes, and accessors/mutators. No business methods, no service calls, no external API calls.", tags: ["model", "eloquent", "laravel"] },
|
|
635
|
-
// ── Patterns ─────────────────────────────────────────────────────────────
|
|
636
|
-
{ type: "pattern", scope: "global", architecture: "laravel-service-repository", title: "Policies for authorization", content: "All authorization logic lives in Policy classes registered in AuthServiceProvider. Use $this->authorize() in controllers. Never inline auth checks.", tags: ["auth", "policies", "laravel"] },
|
|
637
|
-
{ type: "pattern", scope: "global", architecture: "laravel-service-repository", title: "Observers for model lifecycle events", content: "Use Observers to react to model events (created, updated, deleted). Keep them focused on one concern (cache invalidation, audit logging, indexing).", tags: ["observers", "events", "laravel"] },
|
|
638
|
-
{ type: "pattern", scope: "global", architecture: "laravel-service-repository", title: "Events and Listeners for side effects", content: "Dispatch Laravel events for domain happenings (UserRegistered, OrderPlaced). Listeners handle emails, notifications, and third-party syncs.", tags: ["events", "listeners", "side-effects", "laravel"] },
|
|
639
|
-
{ type: "pattern", scope: "global", architecture: "laravel-service-repository", title: "Repository base class for CRUD", content: "Create BaseRepository with generic find, create, update, delete methods. Entity repositories extend BaseRepository and add domain-specific query methods.", tags: ["repository", "base-class", "laravel"] },
|
|
640
|
-
{ type: "pattern", scope: "global", architecture: "laravel-service-repository", title: "Query scopes for reusable filters", content: "Define local scopes on models for commonly reused query conditions: scopeActive(), scopePublished(), scopeByUser(). Chain scopes in repositories.", tags: ["scopes", "eloquent", "database", "laravel"] },
|
|
641
|
-
{ type: "pattern", scope: "global", architecture: "laravel-service-repository", title: "Queued jobs for heavy operations", content: "Dispatch queued jobs for operations over 200ms: file processing, report generation, bulk emails, external API calls. Never block the HTTP response.", tags: ["queue", "jobs", "performance", "laravel"] },
|
|
642
|
-
// ── Advanced ─────────────────────────────────────────────────────────────
|
|
643
|
-
{ type: "rule", scope: "global", architecture: "laravel-service-repository", title: "API versioning via route prefix", content: 'Version APIs with route prefixes: Route::prefix("v1")->group(...). Maintain separate controllers per version. Never break existing API consumers.', tags: ["api", "versioning", "laravel"] },
|
|
644
|
-
{ type: "rule", scope: "global", architecture: "laravel-service-repository", title: "Eager load to prevent N+1", content: "Always use with() to eager load relationships in repositories. Never lazy load inside a loop. Use Laravel Debugbar in development to detect N+1 issues.", tags: ["performance", "n+1", "eloquent", "laravel"] },
|
|
645
|
-
{ type: "rule", scope: "global", architecture: "laravel-service-repository", title: "Database transactions in services", content: "Wrap multi-step write operations in DB::transaction() in the service layer. Repositories do not manage transactions \u2014 services do.", tags: ["transactions", "database", "laravel"] },
|
|
646
|
-
{ type: "rule", scope: "global", architecture: "laravel-service-repository", title: "Cache at service layer", content: "Apply caching in services or repositories using Cache::remember(). Never cache in controllers. Tag caches to enable targeted invalidation.", tags: ["caching", "performance", "laravel"] },
|
|
647
|
-
{ type: "rule", scope: "global", architecture: "laravel-service-repository", title: "Rate limiting on API routes", content: "Apply throttle middleware to all API route groups. Use custom rate limiters in RouteServiceProvider for different limits per user tier.", tags: ["rate-limiting", "security", "laravel"] },
|
|
648
|
-
{ type: "decision", scope: "global", architecture: "laravel-service-repository", title: "Sanctum for SPA auth, Passport for OAuth", content: "Use Laravel Sanctum for SPA and mobile token auth. Use Laravel Passport only when you need full OAuth2 server capabilities for third-party clients.", tags: ["auth", "sanctum", "passport", "laravel"] },
|
|
649
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
650
|
-
// REACT
|
|
651
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
652
|
-
{ type: "rule", scope: "global", architecture: "react", title: "Functional components only", content: "Use functional components exclusively. Never write class components. Hooks replace all lifecycle methods.", reason: "Class components require understanding of this binding, lifecycle method order, and cannot use hooks. Functional components are simpler, smaller, and composable \u2014 the entire React ecosystem now assumes them.", tags: ["components", "react"] },
|
|
653
|
-
{ type: "rule", scope: "global", architecture: "react", title: "Custom hooks for shared logic", content: "Extract all shared stateful logic into custom hooks with use* prefix. Components should only compose hooks and render JSX.", reason: "Logic inside components cannot be shared or tested in isolation. Custom hooks make logic reusable across components and independently unit-testable without mounting a component.", tags: ["hooks", "react"] },
|
|
654
|
-
{ type: "rule", scope: "global", architecture: "react", title: "React Query for server state", content: "Use React Query (TanStack Query) for all server data fetching, caching, and synchronisation. Never use useEffect for data loading.", reason: "useEffect for fetching creates race conditions, missing loading/error states, duplicate requests, and no caching. React Query gives you caching, deduplication, background refetch, and optimistic updates for free.", tags: ["data-fetching", "react-query", "react"] },
|
|
655
|
-
{ type: "rule", scope: "global", architecture: "react", title: "Zustand for global client state", content: "Use Zustand for global client state. Context is only for static values (theme, auth). Never store server state in Zustand \u2014 use React Query.", reason: "React Context re-renders every consumer on every change \u2014 using it for frequently-updated state kills performance. Storing server state in Zustand duplicates it and causes sync bugs with the actual server state.", tags: ["state", "zustand", "react"] },
|
|
656
|
-
{ type: "rule", scope: "global", architecture: "react", title: "Co-locate state close to usage", content: "Keep state as local as possible. Only lift state when siblings need it. Never hoist state to the top just because it might be needed later.", reason: "State hoisted too high causes unnecessary re-renders across the component tree. Local state is easier to reason about, delete, and refactor \u2014 lift only when you have a concrete reason, not a hypothetical one.", tags: ["state", "react"] },
|
|
657
|
-
{ type: "rule", scope: "global", architecture: "react", title: "Lazy load route components", content: "All route-level components are lazy loaded with React.lazy() + Suspense. This reduces initial bundle size.", reason: "Eagerly loading all routes means users download code for pages they never visit. Lazy loading cuts initial bundle size dramatically \u2014 a settings page loaded by 5% of users should not be in the main bundle.", tags: ["performance", "lazy-loading", "react"] },
|
|
658
|
-
{ type: "rule", scope: "global", architecture: "react", title: "Error boundaries on major sections", content: "Wrap each major UI section in an Error Boundary. Errors in one section must never crash the whole app.", reason: "Without Error Boundaries, a JavaScript error in a single widget unmounts the entire React tree \u2014 users see a blank screen. Boundaries contain failures to the affected section so the rest of the app stays usable.", tags: ["error-handling", "react"] },
|
|
659
|
-
{ type: "rule", scope: "global", architecture: "react", title: "Never mutate state directly", content: "Always return new objects and arrays when updating state. Never push() to an array or assign properties on a state object in place.", reason: "React detects changes by reference equality. Mutating state in place means the reference doesn't change, so React never re-renders. Bugs from direct mutation are silent and extremely hard to trace.", tags: ["state", "immutability", "react"] },
|
|
660
|
-
{ type: "rule", scope: "global", architecture: "react", title: "Memoize only after profiling", content: "Add React.memo, useMemo, and useCallback only after React DevTools Profiler shows a measurable render issue. Premature memoization adds complexity with no gain.", reason: "useMemo and useCallback have their own cost \u2014 they run on every render to check dependencies. Added speculatively they slow things down rather than speeding them up. Profile first, optimise second.", tags: ["performance", "memoization", "react"] },
|
|
661
|
-
{ type: "pattern", scope: "global", architecture: "react", title: "Component file structure", content: "Each component lives in its own folder: ComponentName/index.tsx, ComponentName/ComponentName.test.tsx, ComponentName/ComponentName.module.css.", reason: "Flat file structures become unnavigable as components grow. Collocating a component with its tests and styles makes it self-contained \u2014 move or delete the folder and everything related moves with it.", tags: ["structure", "react"] },
|
|
662
|
-
{ type: "pattern", scope: "global", architecture: "react", title: "Compound component pattern", content: "Use compound components (Modal, Modal.Header, Modal.Body) for complex UI with shared state. Avoid passing many props to control sub-parts.", reason: "Passing 10 boolean props to control sub-parts of a complex component makes the API unreadable and forces consumers to know internal implementation details. Compound components expose a clean composable API.", tags: ["patterns", "components", "react"] },
|
|
663
|
-
{ type: "rule", scope: "global", architecture: "react", title: "Never use index as list key", content: "Always use a stable unique ID as the key prop in dynamic lists. Index keys cause incorrect reconciliation when items are reordered or removed.", reason: 'When items are reordered, index keys tell React the item at position 0 is still "the same" \u2014 so it reuses the wrong DOM node. This causes input values, animations, and state to bleed between items in visually broken ways.', tags: ["keys", "performance", "react"] },
|
|
664
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
665
|
-
// VUE 3
|
|
666
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
667
|
-
{ type: "rule", scope: "global", architecture: "vue", title: "Composition API with script setup", content: "Use <script setup> with Composition API for all new components. Never use Options API. defineProps and defineEmits must have TypeScript types.", reason: "Options API scatters related logic across data, methods, computed, and watch \u2014 you must mentally assemble the pieces. Composition API keeps related logic together, makes TypeScript inference work properly, and enables composables.", tags: ["composition-api", "vue"] },
|
|
668
|
-
{ type: "rule", scope: "global", architecture: "vue", title: "Composables for reusable logic", content: "Extract all reusable logic into composable functions (use* prefix) in src/composables/. Components stay thin and focused on rendering.", reason: "Logic inside components cannot be shared or unit-tested without mounting a component. Composables are plain functions \u2014 testable with no DOM, shareable across any component, and independently refactorable.", tags: ["composables", "vue"] },
|
|
669
|
-
{ type: "rule", scope: "global", architecture: "vue", title: "Pinia for global state", content: "Use Pinia with one store per domain (useUserStore, useCartStore). Never use Vuex. Stores should be thin \u2014 complex logic belongs in composables or services.", reason: "Vuex requires boilerplate mutations that exist only to satisfy the API. Pinia is the official successor \u2014 simpler, fully typed, and devtools-supported. One store per domain prevents monolithic state that nobody owns.", tags: ["pinia", "state", "vue"] },
|
|
670
|
-
{ type: "rule", scope: "global", architecture: "vue", title: "Props down events up strictly", content: "Never mutate props directly. Always emit events to notify the parent of changes. Use defineModel() for clean two-way binding in form components.", reason: "Mutating props creates hidden two-way coupling between parent and child \u2014 the parent cannot trust its own state. Props are a contract from parent to child; breaking it makes data flow impossible to trace.", tags: ["props", "events", "vue"] },
|
|
671
|
-
{ type: "rule", scope: "global", architecture: "vue", title: "Lazy load all routes", content: 'All route components use dynamic imports: component: () => import("./views/Home.vue"). This is mandatory for all routes.', reason: "Eagerly loading all routes bundles code the user may never need. Dynamic imports split the bundle so each route's code is only fetched when that route is visited \u2014 critical for large apps with many views.", tags: ["performance", "routing", "vue"] },
|
|
672
|
-
{ type: "rule", scope: "global", architecture: "vue", title: "No logic in templates", content: "Templates contain only rendering logic. Move conditionals, data transformations, and computed values into computed properties or composables.", reason: "Logic in templates is not testable, not reusable, and hard to read. Computed properties are cached, named, and unit-testable \u2014 the template becomes a pure mapping from state to UI.", tags: ["templates", "code-quality", "vue"] },
|
|
673
|
-
{ type: "pattern", scope: "global", architecture: "vue", title: "VueUse for common utilities", content: "Use VueUse composables for browser APIs (useFetch, useLocalStorage, useDark, useIntersectionObserver). Never re-implement what VueUse already provides.", reason: "Browser API integration (resize observer, intersection observer, local storage) is full of edge cases around cleanup and SSR. VueUse handles all of them. Re-implementing risks memory leaks and SSR hydration errors.", tags: ["vueuse", "composables", "vue"] },
|
|
674
|
-
{ type: "rule", scope: "global", architecture: "vue", title: "Provide/inject sparingly", content: "Use provide/inject only for deeply nested component trees where prop drilling is genuinely painful. Document every provide/inject pair.", reason: "Provide/inject is invisible \u2014 there is no clear indication in a component where its injected values come from. Overuse creates hidden dependencies that break when the provider is removed or renamed.", tags: ["dependency-injection", "vue"] },
|
|
675
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
676
|
-
// ANGULAR
|
|
677
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
678
|
-
{ type: "rule", scope: "global", architecture: "angular", title: "Standalone components always", content: "Use standalone components for all new features. No NgModules except AppModule for bootstrapping. Import dependencies directly in each component.", reason: "NgModules create indirection \u2014 you declare a component in one file, import its module in another, and Angular resolves it at runtime. Standalone components are self-contained, explicit, and tree-shakable by default.", tags: ["standalone", "angular"] },
|
|
679
|
-
{ type: "rule", scope: "global", architecture: "angular", title: "OnPush change detection", content: "Set changeDetection: ChangeDetectionStrategy.OnPush on every component. Default change detection re-renders on every event \u2014 this kills performance at scale.", reason: "Default change detection checks every component on every browser event \u2014 click, scroll, keypress. In a large app with hundreds of components this becomes a visible performance problem. OnPush only checks when inputs change.", tags: ["performance", "change-detection", "angular"] },
|
|
680
|
-
{ type: "rule", scope: "global", architecture: "angular", title: "Signals for reactive state", content: "Use Angular Signals (signal(), computed(), effect()) for component state. Use RxJS only for complex async streams (WebSockets, multicasting, debounce).", reason: "RxJS for simple component state requires managing subscriptions and is overkill. Signals are synchronous, automatically tracked, and integrate with OnPush change detection \u2014 no manual subscribe/unsubscribe needed.", tags: ["signals", "state", "angular"] },
|
|
681
|
-
{ type: "rule", scope: "global", architecture: "angular", title: "Smart and dumb components", content: "Smart components (containers) fetch data and manage state. Dumb components (presentational) receive inputs and emit events only. Never mix concerns.", reason: "Components that both fetch data and render it cannot be reused in different contexts. Dumb components are pure UI \u2014 pass different data and get different output, making them trivially testable and reusable.", tags: ["components", "architecture", "angular"] },
|
|
682
|
-
{ type: "rule", scope: "global", architecture: "angular", title: "HTTP only in services", content: "All HttpClient calls live in service classes. Never call HttpClient inside a component. Components call service methods and handle the returned observable/promise.", reason: "HTTP calls in components cannot be reused, cached, or tested without mocking HttpClient at the component level. Services centralise API calls \u2014 swap the endpoint or add caching in one place.", tags: ["http", "services", "angular"] },
|
|
683
|
-
{ type: "rule", scope: "global", architecture: "angular", title: "Unsubscribe with takeUntilDestroyed", content: "Use takeUntilDestroyed() operator or async pipe for all subscriptions. Never subscribe() without a corresponding unsubscribe. Memory leaks from orphaned subscriptions are common.", reason: "A subscription that outlives its component continues to run, process events, and hold references long after the component is destroyed. These leaks accumulate silently and cause hard-to-reproduce bugs.", tags: ["subscriptions", "memory-leaks", "angular"] },
|
|
684
|
-
{ type: "rule", scope: "global", architecture: "angular", title: "Lazy load feature routes", content: "Every feature is a lazy-loaded route. Use loadComponent or loadChildren in the router. The root bundle must only contain the shell.", reason: "Eagerly loading all features in the root bundle means every user downloads code for every feature on first load. Lazy routes load only when needed \u2014 mandatory for apps with more than 5 features.", tags: ["performance", "lazy-loading", "angular"] },
|
|
685
|
-
{ type: "rule", scope: "global", architecture: "angular", title: "Reactive forms for complex forms", content: "Use FormBuilder and reactive forms for all non-trivial forms. Template-driven forms are only acceptable for single-field forms.", reason: "Template-driven forms make validation logic invisible inside the template and untestable without a DOM. Reactive forms define validation in TypeScript \u2014 fully typed, unit-testable, and programmatically controlled.", tags: ["forms", "angular"] },
|
|
686
|
-
{ type: "pattern", scope: "global", architecture: "angular", title: "inject() over constructor DI", content: "Use inject() function at the top of components and services instead of constructor injection. It works with standalone components and is more tree-shakable.", reason: "Constructor injection cannot be used outside class constructors (e.g. in factory functions). inject() works anywhere in the injection context, is less verbose, and tree-shakes unused dependencies properly.", tags: ["dependency-injection", "angular"] },
|
|
687
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
688
|
-
// SVELTE / SVELTEKIT
|
|
689
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
690
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Svelte 5 runes for reactivity", content: "Use $state, $derived, and $effect runes for all reactivity. Never use legacy $: reactive declarations in new Svelte 5 code.", reason: "$: reactive declarations in Svelte 4 are implicit and surprising \u2014 any variable referenced in the block triggers it. Svelte 5 runes are explicit, fine-grained, and predictable. Mixing them causes confusing double-reactivity.", tags: ["runes", "reactivity", "svelte"] },
|
|
691
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Load functions for data fetching", content: "Fetch data in SvelteKit load functions (load, +page.server.ts). Never fetch in onMount for SSR pages \u2014 this breaks server-side rendering.", reason: "onMount only runs in the browser \u2014 SSR renders the page without data, causing a flash of empty content and no SEO value. Load functions run on the server and pass data to the page before it renders.", tags: ["data-fetching", "sveltekit", "svelte"] },
|
|
692
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Form actions for mutations", content: "Use SvelteKit form actions for all data mutations. Never use manual fetch() for form submissions \u2014 form actions work without JavaScript.", reason: "Manual fetch() for forms requires JavaScript to be loaded. Form actions use progressive enhancement \u2014 they work as plain HTML form submissions with no JS, then enhance with JS when available. More resilient by default.", tags: ["form-actions", "sveltekit", "svelte"] },
|
|
693
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Scoped styles in components", content: "Write styles inside each component's <style> block. They are scoped automatically. No global styles inside components.", reason: "Svelte automatically scopes component styles with a generated attribute \u2014 no class name collisions, no specificity wars. Global styles inside components defeat this and silently affect elements outside the component.", tags: ["styles", "svelte"] },
|
|
694
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Stores for shared state", content: "Use Svelte writable, readable, and derived stores for shared state. Keep stores in src/lib/stores/. One store file per domain.", reason: "Props cannot pass data sideways between components. Stores are the official Svelte mechanism for shared reactive state \u2014 any component can subscribe, and all subscribers update atomically when the store changes.", tags: ["stores", "state", "svelte"] },
|
|
695
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "TypeScript in all files", content: "Use TypeScript in all .svelte, .ts, and .server.ts files. Type all props with $props(), all store values, and all load function returns.", reason: "Untyped Svelte props and load function returns make the data flow invisible \u2014 you cannot tell what shape data is as it flows from server to page to component. Types catch shape mismatches at compile time not at runtime in prod.", tags: ["typescript", "svelte"] },
|
|
696
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
697
|
-
// NUXT 3
|
|
698
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
699
|
-
{ type: "rule", scope: "global", architecture: "nuxt", title: "useFetch for all data fetching", content: "Use useFetch or useAsyncData for all data fetching in pages and components. Never use axios or raw fetch() directly in setup(). useFetch is SSR-aware.", reason: "Raw fetch() in setup() runs on both server and client, causing double-fetching and hydration mismatches. useFetch is SSR-aware \u2014 it fetches on the server, serialises the result, and rehydrates on the client without a second request.", tags: ["data-fetching", "nuxt"] },
|
|
700
|
-
{ type: "rule", scope: "global", architecture: "nuxt", title: "Server routes for backend logic", content: "Put all backend logic in server/api/ routes. Components never call external APIs directly \u2014 they call your own server routes.", reason: "Calling external APIs directly from components exposes your API keys in the browser and removes your ability to add caching, auth, or transformation. Server routes are your API gateway \u2014 controlled, cacheable, and secret-safe.", tags: ["server-routes", "nuxt"] },
|
|
701
|
-
{ type: "rule", scope: "global", architecture: "nuxt", title: "useRuntimeConfig for env vars", content: "Access environment variables via useRuntimeConfig(). Public vars go in runtimeConfig.public. Never access process.env in components or composables.", reason: "process.env does not exist in the browser. Accessing it in a component crashes client-side rendering. useRuntimeConfig provides the correct value in both environments and prevents accidentally exposing server-only secrets publicly.", tags: ["config", "env", "nuxt"] },
|
|
702
|
-
{ type: "rule", scope: "global", architecture: "nuxt", title: "Auto-imports \u2014 no manual imports", content: "Rely on Nuxt auto-imports for composables, utils, and Vue APIs. Never manually import ref, computed, or your own composables \u2014 Nuxt handles it.", reason: "Manual imports in Nuxt duplicate what the auto-import system does and can cause conflicts. Auto-imports are tree-shaken, typed automatically, and consistent \u2014 manually importing the same thing twice causes duplicate module instances.", tags: ["auto-imports", "nuxt"] },
|
|
703
|
-
{ type: "rule", scope: "global", architecture: "nuxt", title: "Pinia for global state", content: "Use Pinia for global client state with the @pinia/nuxt module. useState is only for simple SSR-safe values that do not need actions.", reason: "useState is SSR-safe but has no actions, getters, or devtools integration. Pinia provides the full state management experience with SSR support when used with @pinia/nuxt \u2014 use the right tool for the right scope.", tags: ["pinia", "state", "nuxt"] },
|
|
704
|
-
{ type: "rule", scope: "global", architecture: "nuxt", title: "definePageMeta for page config", content: "Use definePageMeta() to configure middleware, layout, and head for each page. Never configure these outside the page component.", reason: "Page configuration scattered across router config and component options cannot be read in one place. definePageMeta co-locates middleware, layout, and SEO config with the page \u2014 Nuxt reads it at build time, not runtime.", tags: ["pages", "nuxt"] },
|
|
705
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
706
|
-
// REACT NATIVE
|
|
707
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
708
|
-
{ type: "rule", scope: "global", architecture: "react-native", title: "StyleSheet.create for all styles", content: "Define all styles with StyleSheet.create(). Never use inline style objects in JSX \u2014 they create new object references on every render.", reason: 'Inline style objects ({color: "red"}) create a new JS object on every render, increasing garbage collection pressure. StyleSheet.create validates styles at development time and creates a reference that stays stable across renders.', tags: ["styles", "performance", "react-native"] },
|
|
709
|
-
{ type: "rule", scope: "global", architecture: "react-native", title: "FlatList for all dynamic lists", content: "Always use FlatList (or FlashList for large datasets) for scrollable lists. Never use map() inside ScrollView for more than 10 items.", reason: "ScrollView renders all children at once, no matter how many. 500 items = 500 native views in memory simultaneously. FlatList virtualises the list \u2014 only visible items are mounted, keeping memory and frame rate stable.", tags: ["performance", "lists", "react-native"] },
|
|
710
|
-
{ type: "rule", scope: "global", architecture: "react-native", title: "React Query for server state", content: "Use React Query (TanStack Query) for all API calls, caching, and background sync. Never use useEffect for data fetching.", reason: "useEffect for fetching has no caching, no request deduplication, and is full of race condition edge cases. On a mobile network, background refetch, stale-while-revalidate, and retry logic are not optional extras \u2014 they are essential.", tags: ["data-fetching", "react-query", "react-native"] },
|
|
711
|
-
{ type: "rule", scope: "global", architecture: "react-native", title: "Secure storage for tokens", content: "Store auth tokens and sensitive data in expo-secure-store or react-native-keychain. Never use AsyncStorage for sensitive values.", reason: "AsyncStorage is unencrypted plaintext on disk. A rooted Android device or iTunes backup exposes every AsyncStorage value. Secure storage uses the device keychain/keystore \u2014 hardware-backed encryption for sensitive data.", tags: ["security", "auth", "react-native"] },
|
|
712
|
-
{ type: "rule", scope: "global", architecture: "react-native", title: "React Navigation for routing", content: "Use React Navigation with typed route params. Define all routes in a central navigation types file. Never navigate by string without types.", reason: "Untyped route strings let you navigate to routes that don't exist or pass the wrong params \u2014 crashes discovered at runtime not compile time. Typed routes catch navigation errors during development when they are cheap to fix.", tags: ["navigation", "typescript", "react-native"] },
|
|
713
|
-
{ type: "rule", scope: "global", architecture: "react-native", title: "Handle all loading and error states", content: "Every screen must handle loading, error, and empty states explicitly. Never render a blank screen while data is loading.", reason: "Mobile users frequently hit slow and interrupted connections. A screen that silently shows nothing while loading, or crashes on a fetch error, feels broken. Explicit states make the app feel polished and reliable on real networks.", tags: ["ux", "error-handling", "react-native"] },
|
|
714
|
-
{ type: "rule", scope: "global", architecture: "react-native", title: "Zustand for global state", content: "Use Zustand for global client state. Keep stores minimal \u2014 server state stays in React Query, not Zustand.", reason: "Storing server state in Zustand duplicates it and creates a sync problem \u2014 the Zustand copy and the React Query cache diverge, and you spend time writing sync code instead of features. Keep server state in React Query only.", tags: ["state", "zustand", "react-native"] },
|
|
715
|
-
{ type: "pattern", scope: "global", architecture: "react-native", title: "Optimise FlatList rendering", content: "Always provide keyExtractor, getItemLayout (if item height is fixed), maxToRenderPerBatch, and windowSize on FlatList for smooth scrolling.", reason: "Without these props, FlatList cannot calculate item positions without measuring each one, disabling scroll-to-index and causing layout jank. These optimisations are necessary for smooth 60fps scrolling on mid-range devices.", tags: ["performance", "flatlist", "react-native"] },
|
|
716
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
717
|
-
// NESTJS
|
|
718
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
719
|
-
// ── Modules ──
|
|
720
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Feature modules are self-contained", content: "Each feature lives in its own module folder: module, controller, service, DTOs, entities, and repository. Nothing leaks out except what is exported from the module class.", reason: "A feature module that bleeds into other folders is a module in name only. Self-containment means you can move, extract, or delete a feature by touching one folder \u2014 no ripple effect across the codebase.", tags: ["modules", "nestjs"] },
|
|
721
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "No circular module dependencies", content: "Modules must not depend on each other in a cycle. If A imports B and B imports A, extract the shared logic into a SharedModule or CoreModule.", reason: "Circular module dependencies cause NestJS to fail at startup with a cryptic error. More importantly, they reveal a design problem \u2014 two modules that must know about each other are probably one module, or they share something that belongs in a third.", tags: ["modules", "architecture", "nestjs"] },
|
|
722
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "CoreModule for app-wide singletons", content: "Create a CoreModule (imported once in AppModule) for global singletons: database connection, config, logger, event emitter. Never import infrastructure providers in feature modules directly.", reason: "Importing infrastructure into every feature module creates hidden coupling between features and infrastructure. CoreModule is the single place that wires infrastructure \u2014 feature modules consume it through DI without knowing where it came from.", tags: ["modules", "core-module", "nestjs"] },
|
|
723
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "SharedModule for reusable utilities", content: "Put reusable services, helpers, and custom pipes into SharedModule and export them. Feature modules import SharedModule, not individual providers.", reason: "Providing the same utility class in multiple modules creates separate singleton instances \u2014 state is not shared and behavior is inconsistent. SharedModule ensures one instance is shared across every importer.", tags: ["modules", "shared-module", "nestjs"] },
|
|
724
|
-
{ type: "pattern", scope: "global", architecture: "nestjs", title: "Use forRootAsync for config-dependent modules", content: "Dynamic modules (database, cache) use forRoot() for static config and forRootAsync() when config depends on other providers like ConfigService.", reason: "Passing hardcoded config to TypeOrmModule.forRoot() means different values per environment require code changes. forRootAsync() injects ConfigService \u2014 the same code runs in every environment, values come from the environment, not the source.", tags: ["modules", "config", "nestjs"] },
|
|
725
|
-
// ── Controllers ──
|
|
726
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Thin controllers \u2014 HTTP only", content: "Controllers parse HTTP request, call one service method, return the result. No business logic, no conditionals, no DB calls. If a controller method has more than 10 lines of logic, it is too fat.", reason: "Logic in controllers is untestable without spinning up an HTTP server and cannot be reused by CLI commands, queue consumers, or scheduled jobs. Thin controllers mean your business logic is always in services \u2014 portable and unit-testable.", tags: ["controllers", "nestjs"] },
|
|
727
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Never use @Res() in controllers", content: "Do not inject the raw Express/Fastify response object with @Res(). Return values from controller methods and let NestJS serialise them. Exception: streaming responses only.", reason: "Using @Res() bypasses NestJS interceptors, exception filters, and response serialisation. Any interceptor that transforms the response silently stops working. The entire NestJS response pipeline is designed around return values, not manual res.send().", tags: ["controllers", "interceptors", "nestjs"] },
|
|
728
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Swagger decorators on every endpoint", content: "Decorate every controller with @ApiTags and every endpoint with @ApiOperation, @ApiResponse, and @ApiBearerAuth where applicable. Swagger must always be accurate.", reason: "An inaccurate Swagger doc is worse than no doc \u2014 consumers build integrations against it and discover the mismatch in production. Decorators on the controller keep the doc co-located with the code so they update together.", tags: ["swagger", "controllers", "nestjs"] },
|
|
729
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Global API prefix and versioning", content: "Set a global API prefix (api) and version prefix (v1) in main.ts via app.setGlobalPrefix(). Do not mix versioning strategies across controllers.", reason: "Inconsistent versioning \u2014 some routes with v1, some without \u2014 forces clients to maintain a map of which endpoints are versioned. A global prefix applied in main.ts means every route is versioned uniformly with one configuration change.", tags: ["versioning", "controllers", "nestjs"] },
|
|
730
|
-
// ── Services & Business Logic ──
|
|
731
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Services own all business logic", content: "Every business rule, calculation, and orchestration lives in a service. Services are @Injectable() and framework-agnostic \u2014 they never import @nestjs/common HTTP decorators or Express/Fastify types.", reason: "Business logic that imports HTTP types cannot run in a CLI, queue worker, or test without an HTTP context. Framework-agnostic services can be tested with plain unit tests \u2014 no HTTP setup, no mocking of request objects.", tags: ["services", "nestjs"] },
|
|
732
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "One service per aggregate root", content: "Create one service per domain aggregate: UserService, OrderService, ProductService. Never create a GenericService or UtilityService that spans multiple domains.", reason: "A service that handles users AND orders AND notifications is doing three jobs. Single-responsibility services are smaller, easier to test, easier to reason about, and can be replaced without touching unrelated functionality.", tags: ["services", "srp", "nestjs"] },
|
|
733
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Services depend on repository interfaces", content: "Services import IUserRepository (interface), not UserRepository (concrete class) or TypeORM Repository<User>. Bind the interface to the implementation via a custom provider token in the module.", reason: "A service that directly imports TypeORM is untestable without a real database. Depending on an interface lets you inject a mock in tests and swap PostgreSQL for another database without touching business logic.", tags: ["services", "repository", "dependency-inversion", "nestjs"] },
|
|
734
|
-
// ── DTOs & Validation ──
|
|
735
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "class-validator on every DTO property", content: "Every DTO property has at least one class-validator decorator: @IsString(), @IsEmail(), @IsUUID(), etc. ValidationPipe is registered globally with whitelist: true and forbidNonWhitelisted: true.", reason: "whitelist: true strips undeclared properties \u2014 clients cannot sneak extra fields into your payload. forbidNonWhitelisted: true rejects requests with unknown properties rather than silently ignoring them \u2014 it makes API contracts explicit and rejects bad input at the boundary.", tags: ["validation", "dto", "nestjs"] },
|
|
736
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Separate request and response DTOs", content: "CreateUserDto and UserResponseDto are different classes. Never reuse the same DTO for input and output. Response DTOs use @Exclude() and @Expose() from class-transformer to control what fields are returned.", reason: "Input and output have different shapes \u2014 input has passwords, output never should. Reusing the same DTO means accidentally exposing a field that should be hidden. Separate DTOs make each direction explicit and safe.", tags: ["dto", "security", "nestjs"] },
|
|
737
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Never return entities directly from controllers", content: "Controller methods never return TypeORM/Prisma entities. Always map to a response DTO before returning. Use plainToInstance(ResponseDto, entity) or a dedicated mapper class.", reason: "Returning a TypeORM entity sends all columns to the client \u2014 including passwords, internal IDs, and audit fields. It also couples your API shape to your database schema: any column rename becomes a breaking API change.", tags: ["dto", "security", "entities", "nestjs"] },
|
|
738
|
-
{ type: "pattern", scope: "global", architecture: "nestjs", title: "PartialType for update DTOs", content: "Extend CreateUserDto with PartialType(CreateUserDto) to create UpdateUserDto. All fields become optional automatically with no duplication of validators.", reason: "Duplicating DTO properties in an Update class means every change to the Create DTO must be manually mirrored in the Update DTO. PartialType inherits all properties and validators and makes them optional \u2014 the definition lives in one place.", tags: ["dto", "nestjs"] },
|
|
739
|
-
// ── Guards & Auth ──
|
|
740
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Guards enforce all authentication", content: "Authentication is enforced by @UseGuards(JwtAuthGuard) at the global level in main.ts. Never validate tokens inside controllers or services.", reason: "Auth logic in controllers is invisible to the NestJS guard pipeline \u2014 it bypasses @Public() decorators, cannot be globally toggled, and duplicates logic across every handler. Guards run before handlers and can be applied globally with one line.", tags: ["auth", "guards", "security", "nestjs"] },
|
|
741
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Use @Public() decorator for open endpoints", content: "Apply a global JwtAuthGuard and mark public endpoints with a custom @Public() decorator that skips the guard. Never omit the guard from individual private endpoints.", reason: "Opting out of auth per-endpoint means new endpoints are unprotected by default \u2014 a new developer adds a route and forgets the guard. @Public() opt-in means new routes are protected by default. The safe choice is the default.", tags: ["auth", "guards", "security", "nestjs"] },
|
|
742
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "RBAC via RolesGuard and @Roles() decorator", content: "Role-based access is enforced by a RolesGuard that reads allowed roles from a @Roles() decorator. Never check req.user.role inside controllers or services.", reason: "Role checks inside service methods are invisible to the authorization layer \u2014 they cannot be audited or changed without modifying business logic. A RolesGuard makes authorization declarative: @Roles(Role.ADMIN) is self-documenting and auditable.", tags: ["auth", "rbac", "guards", "nestjs"] },
|
|
743
|
-
// ── Pipes ──
|
|
744
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Global ValidationPipe in main.ts", content: "Register ValidationPipe globally in main.ts: app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })). Never register per-controller.", reason: "A per-controller ValidationPipe is one forgotten decorator away from an unvalidated endpoint. The global pipe is the safety net \u2014 every incoming payload is validated without any per-route action. transform: true auto-converts plain objects to DTO class instances.", tags: ["validation", "pipes", "nestjs"] },
|
|
745
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "ParseUUIDPipe on all UUID params", content: 'Apply ParseUUIDPipe to every route parameter that expects a UUID: @Param("id", ParseUUIDPipe) id: string. Never validate UUID format manually inside the handler.', reason: "Without ParseUUIDPipe, a non-UUID string reaches the service and causes a cryptic database error. ParseUUIDPipe rejects invalid UUIDs at the HTTP boundary with a 400 \u2014 the error message is clear, the service never sees garbage input.", tags: ["pipes", "validation", "nestjs"] },
|
|
746
|
-
// ── Exception Filters ──
|
|
747
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Global exception filter for consistent errors", content: "Register a global HttpExceptionFilter in main.ts that catches all exceptions and formats a consistent { error: { code, message } } response. Never catch exceptions inside controllers.", reason: "try/catch in controllers produces inconsistent error shapes per endpoint and makes error handling impossible to audit. A global filter is the single place all errors flow through \u2014 one change to the error format updates every endpoint simultaneously.", tags: ["exceptions", "filters", "nestjs"] },
|
|
748
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Throw NestJS exceptions from services", content: "Services throw NotFoundException, ForbiddenException, BadRequestException, ConflictException \u2014 not raw Error objects. The global exception filter catches and formats them with the correct HTTP status automatically.", reason: "Throwing raw Error from a service forces the controller or filter to guess the status code. NestJS exceptions carry the status code with them \u2014 the filter reads it and sets the correct HTTP status. NotFoundException makes intent obvious.", tags: ["exceptions", "services", "nestjs"] },
|
|
749
|
-
// ── Interceptors ──
|
|
750
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "TransformInterceptor for response envelope", content: "Wrap all success responses in a standard envelope using a global TransformInterceptor: { data: T, timestamp, path }. Controllers return raw data \u2014 the interceptor wraps it.", reason: "Wrapping responses inside controllers duplicates the envelope code across every method. An interceptor transforms the response after the controller returns \u2014 the controller stays clean, and the envelope shape is guaranteed to be consistent with no per-method effort.", tags: ["interceptors", "response", "nestjs"] },
|
|
751
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "LoggingInterceptor for request tracing", content: "Use a LoggingInterceptor to log every incoming request and its response time. Include method, URL, status, and duration. Never log inside individual controllers.", reason: "Logging per-controller is duplicated and inconsistent \u2014 some requests get logged, some do not. An interceptor wraps every request automatically, gives a complete audit trail, and keeps controllers free of observability concerns.", tags: ["logging", "interceptors", "nestjs"] },
|
|
752
|
-
// ── Repository Pattern ──
|
|
753
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Repository interface defined in domain", content: "Define IUserRepository as an interface with methods like findById, findByEmail, save, delete. The concrete implementation (TypeOrmUserRepository) lives in the infrastructure layer and is bound via custom provider.", reason: "An interface in the domain layer defines what the service needs. The implementation is an infrastructure detail. Swapping PostgreSQL for MongoDB, or adding a cache layer, requires zero changes to the service or the interface.", tags: ["repository", "dependency-inversion", "nestjs"] },
|
|
754
|
-
{ type: "pattern", scope: "global", architecture: "nestjs", title: "Query objects for complex queries", content: "Encapsulate complex database queries in dedicated query classes (GetActiveUsersQuery) instead of growing service methods with query-building logic. Query objects are reusable and individually testable.", reason: "A service method that builds a 10-condition query is doing two jobs: business orchestration and query construction. Query objects are single-purpose \u2014 they build one query, can be tested in isolation, and can be reused across multiple services.", tags: ["repository", "query-objects", "nestjs"] },
|
|
755
|
-
// ── Configuration ──
|
|
756
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "ConfigModule with schema validation at startup", content: "Use ConfigModule.forRoot() with a Joi or zod validation schema. The app must fail to start if required env vars are missing or have the wrong type. Never access process.env directly anywhere in the app.", reason: "An app that starts without a required env var crashes at runtime when the var is first accessed \u2014 possibly minutes after deployment. Schema validation at startup fails immediately with a clear message. The error happens before any traffic hits the app.", tags: ["config", "environment", "nestjs"] },
|
|
757
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Typed config with registerAs namespaces", content: 'Group related config using registerAs("database", () => ({ url: process.env.DATABASE_URL })). Inject with @InjectConfig("database") for typed access. Never use magic strings to access config keys.', reason: 'this.configService.get("DATABASE_URL") returns unknown \u2014 a typo in the key name returns undefined and causes a silent runtime crash. Typed namespaced configs are autocomplete-friendly and catch key name errors at compile time.', tags: ["config", "typescript", "nestjs"] },
|
|
758
|
-
// ── Database & Entities ──
|
|
759
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Migrations for all schema changes \u2014 never synchronize", content: "Never use synchronize: true in any environment other than a throw-away local database. All schema changes are made through migrations generated via TypeORM CLI and committed to source control.", reason: "synchronize: true drops and recreates columns to match your entity definition. On a production database this destroys data. Migrations are explicit, reversible, and reviewable \u2014 every schema change is tracked in source control alongside the code that requires it.", tags: ["database", "migrations", "nestjs"] },
|
|
760
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Entities are data containers only", content: "TypeORM entities contain columns, relations, and basic column options. No business logic, no service calls, no complex instance methods. Business rules live in services.", reason: "Business logic in entities creates hidden coupling \u2014 the entity knows about services, making it impossible to hydrate without a full DI container. Entities are plain data objects; services operate on them. This makes both individually testable.", tags: ["entities", "database", "nestjs"] },
|
|
761
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Transactions at the service layer", content: "Database transactions are managed in services using DataSource.transaction() or a UnitOfWork pattern. Never start transactions in controllers or repositories.", reason: "A transaction that starts in a controller ties the HTTP request lifecycle to the transaction lifecycle \u2014 any exception from an interceptor or filter runs outside the transaction. Service-level transactions wrap exactly the business operation that needs atomicity.", tags: ["database", "transactions", "nestjs"] },
|
|
762
|
-
// ── Testing ──
|
|
763
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Unit test services with mocked repositories", content: "Unit tests for services use Test.createTestingModule() with mocked repository providers. Never start a real database connection for service unit tests.", reason: "A service unit test that hits a real database is an integration test that runs slowly, requires DB setup, and fails on CI without a database. Mocked repositories run in milliseconds and test the service logic in complete isolation.", tags: ["testing", "services", "nestjs"] },
|
|
764
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "E2E tests with SuperTest against real app", content: "E2E tests use SuperTest against the full NestJS app bootstrapped with Test.createTestingModule(). Use a separate test database. E2E tests cover critical user flows end to end.", reason: "E2E tests catch integration bugs that unit tests miss \u2014 wiring errors, guard ordering, pipe and filter interactions. Running against the real NestJS stack means the test exercises the same code path as production.", tags: ["testing", "e2e", "nestjs"] },
|
|
765
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "Co-locate test files with source files", content: "Place unit test files next to the files they test: user.service.spec.ts beside user.service.ts. E2E tests live in a top-level test/ directory.", reason: "Test files in a separate folder force developers to navigate away from the implementation to find the test. Co-located tests are found instantly and are obviously related to their implementation \u2014 developers are more likely to keep them updated.", tags: ["testing", "nestjs"] },
|
|
766
|
-
// ── Custom Decorators ──
|
|
767
|
-
{ type: "pattern", scope: "global", architecture: "nestjs", title: "@CurrentUser() decorator for authenticated user", content: "Extract the authenticated user from the request with a custom @CurrentUser() decorator instead of @Req() req.user inside handlers.", reason: "@Req() exposes the entire request object to the controller \u2014 far more access than it needs. A @CurrentUser() decorator returns exactly the typed user object, making the controller's dependency on auth explicit and the method signature self-documenting.", tags: ["decorators", "auth", "nestjs"] },
|
|
768
|
-
{ type: "pattern", scope: "global", architecture: "nestjs", title: "Custom decorators for repeated param extraction", content: "If you access the same request property in multiple handlers (tenant ID, correlation ID, device ID), extract it into a custom decorator. Never duplicate request parsing across handlers.", reason: 'Repeating @Headers("x-tenant-id") and parsing it in every handler that needs it is fragile \u2014 a header name change requires updating every handler. A custom decorator centralises the extraction and is changed in one place.', tags: ["decorators", "nestjs"] },
|
|
769
|
-
// ── Async & Events ──
|
|
770
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "EventEmitter for in-process side effects", content: "Use @nestjs/event-emitter to decouple side effects (send email, invalidate cache, log analytics) from the main business operation. Emit events from services, handle them in dedicated listener classes.", reason: "Calling EmailService directly from UserService creates a direct dependency \u2014 if the email service throws, the user creation fails. An event emitter decouples them: the primary operation succeeds regardless of what happens in listeners, and listeners can be added without touching the service.", tags: ["events", "async", "nestjs"] },
|
|
771
|
-
{ type: "rule", scope: "global", architecture: "nestjs", title: "BullMQ for heavy async operations", content: "Offload time-consuming tasks (image processing, bulk emails, report generation) to a BullMQ queue. Never run operations that take more than 200ms synchronously inside an HTTP handler.", reason: "A handler that takes 5 seconds blocks the event loop and times out under load. Queues move the work off the HTTP thread \u2014 the handler returns immediately with a job ID, the queue worker processes it in the background, and the app stays responsive.", tags: ["queues", "async", "performance", "nestjs"] },
|
|
772
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
773
|
-
// SVELTE 5 + SVELTEKIT — frontend framework
|
|
774
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
775
|
-
// ── Runes & Reactivity ───────────────────────────────────────────────────
|
|
776
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Use $state only for reactive variables", content: "Use $state only for variables that need to be reactive. Plain let or const for everything else.", reason: "Every $state variable carries reactivity overhead. Marking non-reactive data as $state adds memory cost and obscures which values actually drive UI updates \u2014 making the component harder to reason about.", tags: ["svelte", "runes", "reactivity"] },
|
|
777
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Prefer $derived over $effect for computed values", content: "Use $derived (or $derived.by() for multi-statement logic) for all computed values. Never derive values inside $effect.", reason: "$derived is memoized and side-effect free \u2014 it recalculates only when its dependencies change and never triggers extra renders. $effect runs after DOM updates and computing values inside it creates subtle timing bugs and circular loops.", tags: ["svelte", "runes", "reactivity"] },
|
|
778
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Never update $state inside $effect", content: "Never update $state inside a $effect callback. Derive the value with $derived instead, or restructure the logic so no effect is needed.", reason: "Updating state inside an effect creates a reactivity loop: state changes \u2192 effect runs \u2192 state changes again. Svelte will warn about this and the component will re-render unpredictably. $derived eliminates the loop entirely.", tags: ["svelte", "runes", "reactivity"] },
|
|
779
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Treat $effect as an escape hatch", content: "Use $effect only for interacting with external systems (DOM APIs, third-party libraries, WebSockets). Never use it as the default way to react to state changes.", reason: "Overusing $effect leads to components that are hard to trace \u2014 the execution flow jumps from renders to effects unpredictably. Most logic belongs in $derived, event handlers, or templates.", tags: ["svelte", "runes", "reactivity"] },
|
|
780
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Use $props for all component props", content: "Declare component props with $props() in Svelte 5. Never use the legacy export let syntax in runes mode.", reason: "$props is the Svelte 5 standard \u2014 it integrates with the runes reactivity system, works correctly with TypeScript inference, and is the only prop syntax that will receive future improvements.", tags: ["svelte", "runes", "props"] },
|
|
781
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Mark two-way bindable props with $bindable", content: "Mark props that parents can bind to with $bindable(). All other props are read-only by convention \u2014 never mutate a non-bindable prop from the child.", reason: "Mutating a non-bindable prop in the child is undefined behaviour in Svelte 5 \u2014 Svelte warns and the parent state is not updated. $bindable() makes the two-way contract explicit and self-documenting.", tags: ["svelte", "runes", "props", "binding"] },
|
|
782
|
-
// ── State Management ─────────────────────────────────────────────────────
|
|
783
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Use runes for 95% of state management", content: "Use $state and $derived for all local and shared state in new Svelte 5 code. Stores are legacy \u2014 use them only when integrating with third-party libraries that require the store contract.", reason: "Runes use signals, work identically in .svelte and .ts files, and have no subscription/unsubscription boilerplate. Mixing runes and stores creates two mental models \u2014 prefer one system throughout.", tags: ["svelte", "state", "runes"] },
|
|
784
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Use context API for scoped layout state", content: "Use setContext/getContext for state shared across a component subtree (auth, theme, user preferences). Export typed getter functions from a context module for type safety.", reason: "Context avoids prop drilling for state that belongs to a whole page or layout section. Typed getter functions catch missing-context bugs at compile time instead of throwing at runtime.", tags: ["svelte", "context", "state"] },
|
|
785
|
-
{ type: "pattern", scope: "global", architecture: "svelte", title: "Extract shared state into .svelte.ts modules", content: "Put reusable state logic (counters, toggles, form state) in .svelte.ts files using $state and $derived. Import and instantiate them in components.", reason: "Runes work identically in .svelte.ts as in components \u2014 this lets you extract and test state logic independently from the component template, and reuse it across multiple components without duplicating code.", tags: ["svelte", "state", "reusability"] },
|
|
786
|
-
// ── SvelteKit Load Functions ─────────────────────────────────────────────
|
|
787
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Use +page.server.ts for private data fetching", content: "Fetch data that requires secrets, database access, or server-only logic in +page.server.ts load functions. Use +page.ts only for public API calls that can run on both client and server.", reason: "+page.server.ts code never ships to the browser \u2014 secrets and DB credentials stay server-side. +page.ts is sent to the client as a JS bundle, leaking any secrets embedded in it.", tags: ["sveltekit", "load", "server", "security"] },
|
|
788
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Fetch shared data in +layout.server.ts", content: "Fetch data needed by every page in a route group (auth session, user profile, navigation) in +layout.server.ts. Never duplicate layout-level data fetching in individual page loaders.", reason: "Layout loaders run once per navigation and apply to all child routes. Duplicating this fetch in every page loader causes redundant network calls on every navigation and diverging data if not kept in sync.", tags: ["sveltekit", "load", "layout"] },
|
|
789
|
-
{ type: "pattern", scope: "global", architecture: "svelte", title: "Stream slow data by returning promises from load", content: "Return slow data as unresolved promises from load functions rather than awaiting them. SvelteKit streams the fast data immediately and resolves slow promises in the background.", reason: "Awaiting all data before rendering makes the user wait for the slowest query. Streaming sends the page shell immediately \u2014 the user sees content within milliseconds and slow data fills in progressively, dramatically improving perceived performance.", tags: ["sveltekit", "load", "streaming", "performance"] },
|
|
790
|
-
// ── Form Actions ─────────────────────────────────────────────────────────
|
|
791
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Use use:enhance on all forms", content: "Always add use:enhance to <form> elements in SvelteKit. This enables JS-enhanced submission while keeping full-page-load as the fallback when JS is unavailable.", reason: "Without use:enhance every form submission causes a full page reload \u2014 poor UX in a SPA. use:enhance adds progressive enhancement: JS users get instant feedback, JS-disabled users still get working forms.", tags: ["sveltekit", "forms", "progressive-enhancement"] },
|
|
792
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Return typed errors from form actions", content: "Return field-keyed error objects from form actions using the fail() helper. Access them via form.errors in the template to display inline validation messages.", reason: "Without structured errors, you cannot show inline field errors \u2014 you can only display a generic failure message at the top of the form. Keyed errors let you show exactly which field failed and why, next to the field itself.", tags: ["sveltekit", "forms", "validation"] },
|
|
793
|
-
// ── Component Structure ───────────────────────────────────────────────────
|
|
794
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Order: script \u2192 markup \u2192 style", content: "Order every Svelte component as: <script> block, then markup/template, then <style> block. Never mix the ordering across a codebase.", reason: "Consistent ordering means every developer knows exactly where to look \u2014 props and state are always at the top, styles at the bottom. Prettier enforces this convention automatically.", tags: ["svelte", "structure", "conventions"] },
|
|
795
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: 'Use <script lang="ts"> always', content: 'Always use <script lang="ts"> in Svelte components. Enable strict TypeScript checking via svelte-check in CI.', reason: "JavaScript components have no type safety \u2014 prop shapes, event payloads, and store values are unchecked. TypeScript catches refactoring breaks, missing required props, and API shape mismatches before they reach production.", tags: ["svelte", "typescript", "quality"] },
|
|
796
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Keep components single-responsibility", content: "Each Svelte component does one thing. Split components when they exceed ~150 lines or contain unrelated concerns.", reason: "Large multi-concern components accumulate state entanglement \u2014 changing one feature risks breaking another. Small focused components are independently testable, reusable, and replaceable.", tags: ["svelte", "structure", "maintainability"] },
|
|
797
|
-
// ── Snippets & Composition ────────────────────────────────────────────────
|
|
798
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Prefer snippets over slots for composition", content: "Use Svelte 5 snippets ({#snippet} / {@render}) for component composition. Avoid the legacy <slot> API in new components.", reason: "Snippets are more powerful than slots \u2014 they support parameters, work inside loops, can be passed as props, and enable recursive patterns. Slots are a legacy API and will receive no new features.", tags: ["svelte", "snippets", "composition"] },
|
|
799
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Provide fallbacks for optional snippets", content: "Always check if an optional snippet was passed before rendering it: {#if children}{@render children()}{:else}<DefaultContent />{/if}.", reason: "Calling @render on an undefined snippet throws a runtime error. An explicit fallback makes the component usable without every optional snippet filled in \u2014 components are more robust and easier to adopt incrementally.", tags: ["svelte", "snippets", "composition"] },
|
|
800
|
-
// ── Performance ───────────────────────────────────────────────────────────
|
|
801
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Always key {#each} blocks", content: "Always provide a unique key expression in {#each items as item (item.id)} blocks. Never use array index as a key for lists that can be reordered or filtered.", reason: "Without keys, Svelte reuses DOM nodes by position \u2014 reordering or removing an item updates every node after it. With keys, Svelte moves existing DOM nodes, skipping unneeded updates. Index keys break this entirely when items are removed or reordered.", tags: ["svelte", "performance", "lists"] },
|
|
802
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Lazy load below-the-fold components", content: "Use dynamic import() for heavy components that appear only on interaction or below the fold: const HeavyChart = await import('./HeavyChart.svelte').", reason: "Every component in a static import is included in the initial JS bundle, increasing Time to Interactive for all users even if they never see the heavy component. Dynamic imports code-split automatically in Vite.", tags: ["svelte", "performance", "code-splitting"] },
|
|
803
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Never use querySelector inside components", content: "Never use document.querySelector or document.querySelectorAll inside Svelte components. Use bind:this to get element references, or Svelte actions for DOM manipulation.", reason: "querySelector queries the entire document \u2014 it bypasses Svelte's component boundary, can match elements from other components, and breaks when components are server-rendered (no document on the server).", tags: ["svelte", "dom", "performance"] },
|
|
804
|
-
// ── Error Handling ────────────────────────────────────────────────────────
|
|
805
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Custom +error.svelte for every route group", content: "Create a +error.svelte page for each route group that needs a custom error UI. Use page.status and page.error.message to display contextual messages.", reason: "Without a custom error page, SvelteKit shows a bare default error UI with no branding or recovery path. Custom error pages guide users toward recovery actions (go home, retry, contact support) and match the app's design.", tags: ["sveltekit", "errors", "ux"] },
|
|
806
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Use handleError hook for unexpected errors", content: "Implement the handleError hook in hooks.server.ts to sanitize error messages and send unexpected errors to a monitoring service (Sentry, Datadog). Never expose raw error details to the client.", reason: "Unhandled errors can leak stack traces, file paths, and internal logic to the browser. The handleError hook intercepts these before they reach the client \u2014 you control what the user sees and send the full error to your monitoring system.", tags: ["sveltekit", "errors", "security", "monitoring"] },
|
|
807
|
-
// ── Accessibility ─────────────────────────────────────────────────────────
|
|
808
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Never use positive tabindex values", content: 'Never use tabindex values greater than 0. Use tabindex="0" to add an element to focus order, tabindex="-1" to remove it. Let the browser manage natural tab order.', reason: "Positive tabindex values create a separate, confusing tab order that overrides the natural document flow. Keyboard users Tab through elements in a logical, surprising-to-no-one order \u2014 positive tabindex breaks that guarantee.", tags: ["svelte", "accessibility", "a11y"] },
|
|
809
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Use ARIA live regions for dynamic updates", content: 'Wrap dynamically updated content (notifications, status messages, search results count) in aria-live="polite" or aria-live="assertive" regions so screen readers announce the change.', reason: "Screen readers only read content the user focuses on or that's in a live region. Dynamic DOM changes \u2014 even if clearly visible \u2014 are invisible to screen reader users without a live region announcement.", tags: ["svelte", "accessibility", "a11y"] },
|
|
810
|
-
// ── Testing ───────────────────────────────────────────────────────────────
|
|
811
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Extract logic into .ts files for unit testing", content: "Extract business logic, transformations, and validation from components into plain .ts functions. Test those functions directly with Vitest \u2014 components should be tested for behaviour, not internal state.", reason: "Testing pure functions is orders of magnitude simpler than mounting a component. Logic extracted to .ts is framework-agnostic, instantly testable, and reusable. Components that contain logic tightly couple test setup to UI implementation details.", tags: ["svelte", "testing", "vitest"] },
|
|
812
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Use @testing-library/svelte for component tests", content: "Test Svelte components with @testing-library/svelte and Vitest. Query by role, label, and text \u2014 never by class or internal implementation details.", reason: "Testing Library queries mirror how real users interact with the UI \u2014 by what they see and can click, not by CSS class names. This makes tests resilient to markup refactors and actually verifies user-facing behaviour.", tags: ["svelte", "testing", "testing-library"] },
|
|
813
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Run svelte-check in CI", content: "Run npx svelte-check --tsconfig ./tsconfig.json in your CI pipeline to catch type errors across all .svelte files before merging.", reason: "The VS Code extension only checks open files. svelte-check validates the entire project \u2014 catching type errors in components you haven't recently opened. This is the only way to guarantee the full project is type-safe on every merge.", tags: ["svelte", "typescript", "ci"] },
|
|
814
|
-
// ── Anti-Patterns ─────────────────────────────────────────────────────────
|
|
815
|
-
{ type: "rule", scope: "global", architecture: "svelte", title: "Do not return stores from load functions", content: "Never return writable stores from SvelteKit load functions. Return plain data and let components initialize their own reactive state from it.", reason: "Stores returned from load functions cannot be migrated to runes \u2014 they create a pattern incompatible with Svelte 5's reactivity model. Writing to shared state from load is also unsafe in SSR where multiple requests share the module scope.", tags: ["svelte", "sveltekit", "anti-pattern"] },
|
|
816
|
-
{ 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"] }
|
|
817
|
-
];
|
|
818
|
-
|
|
819
46
|
// src/hook.ts
|
|
820
47
|
import { execSync, spawnSync } from "child_process";
|
|
821
|
-
import { writeFileSync as
|
|
822
|
-
import { join as
|
|
48
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync2, unlinkSync, readFileSync as readFileSync2, chmodSync } from "fs";
|
|
49
|
+
import { join as join2 } from "path";
|
|
823
50
|
import chalk from "chalk";
|
|
824
51
|
|
|
825
|
-
// src/chat.ts
|
|
826
|
-
function getChatConfig() {
|
|
827
|
-
const provider2 = process.env.CHAT_PROVIDER ?? "ollama";
|
|
828
|
-
const model2 = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
|
|
829
|
-
return {
|
|
830
|
-
provider: provider2,
|
|
831
|
-
model: model2,
|
|
832
|
-
ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
|
|
833
|
-
apiKey: process.env.CHAT_API_KEY ?? ""
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
async function callOllama(cfg, messages) {
|
|
837
|
-
const res = await fetch(`${cfg.ollamaUrl}/api/chat`, {
|
|
838
|
-
method: "POST",
|
|
839
|
-
headers: { "Content-Type": "application/json" },
|
|
840
|
-
body: JSON.stringify({ model: cfg.model, messages, stream: false, format: "json" })
|
|
841
|
-
});
|
|
842
|
-
if (!res.ok) {
|
|
843
|
-
const body = await res.text();
|
|
844
|
-
if (body.includes("not found") || body.includes("model")) {
|
|
845
|
-
throw new Error(`MODEL_NOT_FOUND:${cfg.model}`);
|
|
846
|
-
}
|
|
847
|
-
throw new Error(body);
|
|
848
|
-
}
|
|
849
|
-
const data = await res.json();
|
|
850
|
-
return data.message.content.trim();
|
|
851
|
-
}
|
|
852
|
-
async function callOpenAI(cfg, messages) {
|
|
853
|
-
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
854
|
-
method: "POST",
|
|
855
|
-
headers: {
|
|
856
|
-
"Content-Type": "application/json",
|
|
857
|
-
"Authorization": `Bearer ${cfg.apiKey}`
|
|
858
|
-
},
|
|
859
|
-
body: JSON.stringify({
|
|
860
|
-
model: cfg.model,
|
|
861
|
-
messages,
|
|
862
|
-
response_format: { type: "json_object" }
|
|
863
|
-
})
|
|
864
|
-
});
|
|
865
|
-
if (!res.ok) throw new Error(`OpenAI API error ${res.status}: ${await res.text()}`);
|
|
866
|
-
const data = await res.json();
|
|
867
|
-
return data.choices[0].message.content.trim();
|
|
868
|
-
}
|
|
869
|
-
async function callAnthropic(cfg, messages) {
|
|
870
|
-
const system = messages.find((m) => m.role === "system")?.content ?? "";
|
|
871
|
-
const userMessages = messages.filter((m) => m.role !== "system");
|
|
872
|
-
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
873
|
-
method: "POST",
|
|
874
|
-
headers: {
|
|
875
|
-
"Content-Type": "application/json",
|
|
876
|
-
"x-api-key": cfg.apiKey,
|
|
877
|
-
"anthropic-version": "2023-06-01"
|
|
878
|
-
},
|
|
879
|
-
body: JSON.stringify({
|
|
880
|
-
model: cfg.model,
|
|
881
|
-
max_tokens: 4096,
|
|
882
|
-
system,
|
|
883
|
-
messages: userMessages
|
|
884
|
-
})
|
|
885
|
-
});
|
|
886
|
-
if (!res.ok) throw new Error(`Anthropic API error ${res.status}: ${await res.text()}`);
|
|
887
|
-
const data = await res.json();
|
|
888
|
-
return data.content[0].text.trim();
|
|
889
|
-
}
|
|
890
|
-
async function callMiniMax(cfg, messages) {
|
|
891
|
-
const res = await fetch("https://api.minimax.io/v1/chat/completions", {
|
|
892
|
-
method: "POST",
|
|
893
|
-
headers: {
|
|
894
|
-
"Content-Type": "application/json",
|
|
895
|
-
"Authorization": `Bearer ${cfg.apiKey}`
|
|
896
|
-
},
|
|
897
|
-
body: JSON.stringify({
|
|
898
|
-
model: cfg.model,
|
|
899
|
-
messages,
|
|
900
|
-
response_format: { type: "json_object" }
|
|
901
|
-
})
|
|
902
|
-
});
|
|
903
|
-
if (!res.ok) throw new Error(`MiniMax API error ${res.status}: ${await res.text()}`);
|
|
904
|
-
const data = await res.json();
|
|
905
|
-
return data.choices[0].message.content.trim();
|
|
906
|
-
}
|
|
907
|
-
async function callChatModel(messages) {
|
|
908
|
-
const cfg = getChatConfig();
|
|
909
|
-
switch (cfg.provider) {
|
|
910
|
-
case "openai":
|
|
911
|
-
return callOpenAI(cfg, messages);
|
|
912
|
-
case "anthropic":
|
|
913
|
-
return callAnthropic(cfg, messages);
|
|
914
|
-
case "minimax":
|
|
915
|
-
return callMiniMax(cfg, messages);
|
|
916
|
-
default:
|
|
917
|
-
return callOllama(cfg, messages);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
function getChatProviderLabel() {
|
|
921
|
-
const cfg = getChatConfig();
|
|
922
|
-
if (cfg.provider === "ollama") return `ollama (${cfg.model})`;
|
|
923
|
-
return `${cfg.provider} (${cfg.model})`;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
52
|
// src/memory-file.ts
|
|
927
|
-
import { existsSync
|
|
928
|
-
import { join
|
|
53
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
54
|
+
import { join } from "path";
|
|
929
55
|
var MEMORY_FILE = "memories.json";
|
|
930
56
|
function toPortableMemory(memory) {
|
|
931
57
|
return {
|
|
@@ -959,16 +85,16 @@ function parseContext(value) {
|
|
|
959
85
|
return Object.keys(context).length ? context : void 0;
|
|
960
86
|
}
|
|
961
87
|
function writeMemoryFile(memories, cwd = process.cwd()) {
|
|
962
|
-
const path =
|
|
963
|
-
|
|
88
|
+
const path = join(cwd, MEMORY_FILE);
|
|
89
|
+
writeFileSync(path, JSON.stringify(memories, null, 2) + "\n", "utf-8");
|
|
964
90
|
return path;
|
|
965
91
|
}
|
|
966
92
|
function readMemoryFile(cwd = process.cwd()) {
|
|
967
|
-
const path =
|
|
968
|
-
if (!
|
|
93
|
+
const path = join(cwd, MEMORY_FILE);
|
|
94
|
+
if (!existsSync(path)) {
|
|
969
95
|
throw new Error(`${MEMORY_FILE} not found. Run: memory-core export`);
|
|
970
96
|
}
|
|
971
|
-
return parseMemoryFile(
|
|
97
|
+
return parseMemoryFile(readFileSync(path, "utf-8"));
|
|
972
98
|
}
|
|
973
99
|
async function readMemoryFileFromUrl(url) {
|
|
974
100
|
const res = await fetch(url, { signal: AbortSignal.timeout(15e3) });
|
|
@@ -1006,7 +132,7 @@ function parseMemoryFile(raw) {
|
|
|
1006
132
|
var reasonMap = new Map(
|
|
1007
133
|
seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
|
|
1008
134
|
);
|
|
1009
|
-
var HOOK_PATH =
|
|
135
|
+
var HOOK_PATH = join2(".git", "hooks", "pre-commit");
|
|
1010
136
|
var HOOK_MARKER = "# archmind-memory-core";
|
|
1011
137
|
function buildHookScript(advisory) {
|
|
1012
138
|
const suffix = advisory ? " || true" : "";
|
|
@@ -1024,11 +150,11 @@ fi
|
|
|
1024
150
|
`;
|
|
1025
151
|
}
|
|
1026
152
|
function recordViolations(violations) {
|
|
1027
|
-
const statsPath =
|
|
153
|
+
const statsPath = join2(process.cwd(), ".memory-core-stats.json");
|
|
1028
154
|
let stats = { rules: {}, files: {} };
|
|
1029
|
-
if (
|
|
155
|
+
if (existsSync2(statsPath)) {
|
|
1030
156
|
try {
|
|
1031
|
-
stats = JSON.parse(
|
|
157
|
+
stats = JSON.parse(readFileSync2(statsPath, "utf-8"));
|
|
1032
158
|
} catch {
|
|
1033
159
|
stats = { rules: {}, files: {} };
|
|
1034
160
|
}
|
|
@@ -1037,7 +163,7 @@ function recordViolations(violations) {
|
|
|
1037
163
|
stats.rules[violation.rule] = (stats.rules[violation.rule] ?? 0) + 1;
|
|
1038
164
|
if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
|
|
1039
165
|
}
|
|
1040
|
-
|
|
166
|
+
writeFileSync2(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
1041
167
|
}
|
|
1042
168
|
async function promptToSaveViolations(violations) {
|
|
1043
169
|
if (!process.stdin.isTTY || violations.length === 0) return;
|
|
@@ -1282,25 +408,25 @@ ${JSON.stringify(violations, null, 2)}`;
|
|
|
1282
408
|
}
|
|
1283
409
|
}
|
|
1284
410
|
function installHook(advisory = true) {
|
|
1285
|
-
if (!
|
|
411
|
+
if (!existsSync2(".git")) {
|
|
1286
412
|
console.error(chalk.red("\n Not a git repository. Run from project root.\n"));
|
|
1287
413
|
process.exit(1);
|
|
1288
414
|
}
|
|
1289
415
|
const script = buildHookScript(advisory);
|
|
1290
|
-
if (
|
|
1291
|
-
const existing =
|
|
416
|
+
if (existsSync2(HOOK_PATH)) {
|
|
417
|
+
const existing = readFileSync2(HOOK_PATH, "utf-8");
|
|
1292
418
|
if (existing.includes(HOOK_MARKER)) {
|
|
1293
419
|
const markerIndex = existing.indexOf(HOOK_MARKER);
|
|
1294
420
|
const before = markerIndex > 1 ? existing.slice(0, markerIndex).trimEnd() + "\n\n" : "";
|
|
1295
|
-
|
|
421
|
+
writeFileSync2(HOOK_PATH, before + script);
|
|
1296
422
|
chmodSync(HOOK_PATH, 493);
|
|
1297
423
|
const modeLabel2 = advisory ? chalk.cyan("advisory") : chalk.yellow("strict");
|
|
1298
424
|
console.log(chalk.green("\n \u2713 Pre-commit hook updated") + chalk.dim(` (${modeLabel2} mode)`));
|
|
1299
425
|
return;
|
|
1300
426
|
}
|
|
1301
|
-
|
|
427
|
+
writeFileSync2(HOOK_PATH, existing.trimEnd() + "\n\n" + script);
|
|
1302
428
|
} else {
|
|
1303
|
-
|
|
429
|
+
writeFileSync2(HOOK_PATH, script);
|
|
1304
430
|
}
|
|
1305
431
|
chmodSync(HOOK_PATH, 493);
|
|
1306
432
|
const modeLabel = advisory ? "advisory (logs violations, never blocks)" : "strict (blocks commits on violations)";
|
|
@@ -1309,29 +435,31 @@ function installHook(advisory = true) {
|
|
|
1309
435
|
console.log(chalk.gray(" To uninstall: memory-core hook uninstall\n"));
|
|
1310
436
|
}
|
|
1311
437
|
function uninstallHook() {
|
|
1312
|
-
if (!
|
|
438
|
+
if (!existsSync2(HOOK_PATH)) {
|
|
1313
439
|
console.log(chalk.yellow("\n No pre-commit hook found.\n"));
|
|
1314
440
|
return;
|
|
1315
441
|
}
|
|
1316
|
-
const content =
|
|
442
|
+
const content = readFileSync2(HOOK_PATH, "utf-8");
|
|
1317
443
|
if (!content.includes(HOOK_MARKER)) {
|
|
1318
444
|
console.log(chalk.yellow("\n ArchMind hook not found in pre-commit \u2014 nothing to remove.\n"));
|
|
1319
445
|
return;
|
|
1320
446
|
}
|
|
1321
447
|
const markerIndex = content.indexOf(HOOK_MARKER);
|
|
1322
|
-
|
|
1323
|
-
|
|
448
|
+
const before = markerIndex > 1 ? content.slice(0, markerIndex).trimEnd() : "";
|
|
449
|
+
if (before && before !== "#!/bin/sh") {
|
|
450
|
+
writeFileSync2(HOOK_PATH, `${before}
|
|
451
|
+
`);
|
|
1324
452
|
} else {
|
|
1325
453
|
unlinkSync(HOOK_PATH);
|
|
1326
454
|
}
|
|
1327
455
|
console.log(chalk.green("\n \u2713 Pre-commit hook removed\n"));
|
|
1328
456
|
}
|
|
1329
457
|
async function checkStaged(options = {}) {
|
|
1330
|
-
const
|
|
458
|
+
const SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
|
|
1331
459
|
let diff;
|
|
1332
460
|
let stagedFiles = [];
|
|
1333
461
|
try {
|
|
1334
|
-
stagedFiles = execSync("git diff --cached --name-only", { encoding: "utf-8" }).split("\n").filter((f) => f &&
|
|
462
|
+
stagedFiles = execSync("git diff --cached --name-only", { encoding: "utf-8" }).split("\n").filter((f) => f && SOURCE_EXTENSIONS.test(f));
|
|
1335
463
|
if (stagedFiles.length === 0) {
|
|
1336
464
|
if (options.verbose) console.log(chalk.gray(" No source files staged \u2014 skipping rule check."));
|
|
1337
465
|
return;
|
|
@@ -1346,9 +474,9 @@ async function checkStaged(options = {}) {
|
|
|
1346
474
|
if (options.verbose) console.log(chalk.gray(" No staged changes to check."));
|
|
1347
475
|
return;
|
|
1348
476
|
}
|
|
1349
|
-
const configPath =
|
|
1350
|
-
if (!
|
|
1351
|
-
const config = JSON.parse(
|
|
477
|
+
const configPath = join2(process.cwd(), ".memory-core.json");
|
|
478
|
+
if (!existsSync2(configPath)) return;
|
|
479
|
+
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
1352
480
|
const { rules: fallbackRules, avoids } = getProfileRules(config);
|
|
1353
481
|
const rules = await loadRelevantRules(config, diff, stagedFiles, fallbackRules);
|
|
1354
482
|
const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...await loadIgnorePatterns()])];
|
|
@@ -1553,332 +681,8 @@ function printModelMissing(model2) {
|
|
|
1553
681
|
console.log(chalk.gray(" Recommended: llama3.2 | qwen2.5-coder:3b | mistral\n"));
|
|
1554
682
|
}
|
|
1555
683
|
|
|
1556
|
-
// src/watcher.ts
|
|
1557
|
-
import { watch } from "chokidar";
|
|
1558
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
1559
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
1560
|
-
import { join as join5, relative } from "path";
|
|
1561
|
-
import chalk2 from "chalk";
|
|
1562
|
-
function getFileLines(filePath) {
|
|
1563
|
-
try {
|
|
1564
|
-
return readFileSync5(filePath, "utf-8").split("\n");
|
|
1565
|
-
} catch {
|
|
1566
|
-
return [];
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
function printCodeContext(filePath, line, contextLines = 2) {
|
|
1570
|
-
const lines = getFileLines(filePath);
|
|
1571
|
-
if (lines.length === 0) return;
|
|
1572
|
-
const start = Math.max(0, line - 1 - contextLines);
|
|
1573
|
-
const end = Math.min(lines.length - 1, line - 1 + contextLines);
|
|
1574
|
-
console.log(chalk2.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"));
|
|
1575
|
-
for (let i = start; i <= end; i++) {
|
|
1576
|
-
const lineNum = String(i + 1).padStart(4, " ");
|
|
1577
|
-
const isViolation = i === line - 1;
|
|
1578
|
-
if (isViolation) {
|
|
1579
|
-
console.log(chalk2.red(` \u2502 ${lineNum} \u25B6 ${lines[i]}`));
|
|
1580
|
-
} else {
|
|
1581
|
-
console.log(chalk2.dim(` \u2502 ${lineNum} ${lines[i]}`));
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
console.log(chalk2.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"));
|
|
1585
|
-
}
|
|
1586
|
-
var SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
|
|
1587
|
-
var reasonMap2 = new Map(
|
|
1588
|
-
seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
|
|
1589
|
-
);
|
|
1590
|
-
function recordViolations2(violations) {
|
|
1591
|
-
const statsPath = join5(process.cwd(), ".memory-core-stats.json");
|
|
1592
|
-
let stats = { rules: {}, files: {} };
|
|
1593
|
-
if (existsSync5(statsPath)) {
|
|
1594
|
-
try {
|
|
1595
|
-
stats = JSON.parse(readFileSync5(statsPath, "utf-8"));
|
|
1596
|
-
} catch {
|
|
1597
|
-
stats = { rules: {}, files: {} };
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
for (const violation of violations) {
|
|
1601
|
-
stats.rules[violation.rule] = (stats.rules[violation.rule] ?? 0) + 1;
|
|
1602
|
-
if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
|
|
1603
|
-
}
|
|
1604
|
-
writeFileSync4(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
1605
|
-
}
|
|
1606
|
-
function loadConfig(cwd) {
|
|
1607
|
-
const configPath = join5(cwd, ".memory-core.json");
|
|
1608
|
-
if (!existsSync5(configPath)) return null;
|
|
1609
|
-
try {
|
|
1610
|
-
return JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
1611
|
-
} catch {
|
|
1612
|
-
return null;
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
function getProfileRules2(config) {
|
|
1616
|
-
const rules = [];
|
|
1617
|
-
const avoids = [];
|
|
1618
|
-
if (config.backendArchitecture) {
|
|
1619
|
-
const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
|
|
1620
|
-
if (profile) {
|
|
1621
|
-
rules.push(...profile.rules);
|
|
1622
|
-
avoids.push(...profile.avoid);
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
if (config.frontendFramework) {
|
|
1626
|
-
const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
|
|
1627
|
-
if (profile) {
|
|
1628
|
-
rules.push(...profile.rules);
|
|
1629
|
-
avoids.push(...profile.avoid);
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
return { rules, avoids };
|
|
1633
|
-
}
|
|
1634
|
-
async function loadRelevantRules2(config, rel, diff, fallbackRules) {
|
|
1635
|
-
try {
|
|
1636
|
-
const query = buildContextQuery([
|
|
1637
|
-
rel,
|
|
1638
|
-
diff.slice(0, 1200),
|
|
1639
|
-
config.backendArchitecture,
|
|
1640
|
-
config.frontendFramework,
|
|
1641
|
-
config.language
|
|
1642
|
-
]);
|
|
1643
|
-
const memories = await retrieveContextualMemories({
|
|
1644
|
-
query,
|
|
1645
|
-
cwd: process.cwd(),
|
|
1646
|
-
config,
|
|
1647
|
-
limit: 15
|
|
1648
|
-
});
|
|
1649
|
-
const selected = memories.filter((memory) => ["rule", "pattern", "decision"].includes(memory.type)).map((memory) => memory.content);
|
|
1650
|
-
return selected.length > 0 ? selected : fallbackRules;
|
|
1651
|
-
} catch {
|
|
1652
|
-
return fallbackRules;
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
function applyAllowPatterns2(violations, allowPatterns) {
|
|
1656
|
-
if (allowPatterns.length === 0) return violations;
|
|
1657
|
-
return violations.filter((violation) => {
|
|
1658
|
-
const haystack = `${violation.rule}
|
|
1659
|
-
${violation.issue}
|
|
1660
|
-
${violation.file}`.toLowerCase();
|
|
1661
|
-
return !allowPatterns.some((pattern) => haystack.includes(pattern.toLowerCase()));
|
|
1662
|
-
});
|
|
1663
|
-
}
|
|
1664
|
-
async function verifyViolations2(diff, violations, allowPatterns, debug) {
|
|
1665
|
-
if (violations.length === 0) return violations;
|
|
1666
|
-
const systemPrompt = `You are verifying candidate architecture violations.
|
|
1667
|
-
Only keep violations that are directly supported by the diff.
|
|
1668
|
-
Reject speculative or weak matches.
|
|
1669
|
-
Treat these allowlisted patterns as intentional and valid:
|
|
1670
|
-
${allowPatterns.length ? allowPatterns.map((pattern, index) => `${index + 1}. ${pattern}`).join("\n") : "(none)"}
|
|
1671
|
-
|
|
1672
|
-
Return strict JSON:
|
|
1673
|
-
{"violations":[{"rule":"...","file":"...","line":1,"issue":"...","suggestion":"...","reason":"..."}]}
|
|
1674
|
-
Do not include any text outside the JSON.`;
|
|
1675
|
-
const userPrompt = `Diff:
|
|
1676
|
-
${diff.slice(0, 6e3)}
|
|
1677
|
-
|
|
1678
|
-
Candidate violations:
|
|
1679
|
-
${JSON.stringify(violations, null, 2)}`;
|
|
1680
|
-
if (debug) {
|
|
1681
|
-
console.log(chalk2.gray("\n [debug] verifier prompt:"));
|
|
1682
|
-
console.log(chalk2.dim(systemPrompt));
|
|
1683
|
-
console.log(chalk2.dim(userPrompt));
|
|
1684
|
-
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"));
|
|
1685
|
-
}
|
|
1686
|
-
try {
|
|
1687
|
-
const raw = await callChatModel([
|
|
1688
|
-
{ role: "system", content: systemPrompt },
|
|
1689
|
-
{ role: "user", content: userPrompt }
|
|
1690
|
-
]);
|
|
1691
|
-
const parsed = JSON.parse(raw);
|
|
1692
|
-
if (Array.isArray(parsed?.violations)) return parsed.violations;
|
|
1693
|
-
if (Array.isArray(parsed)) return parsed;
|
|
1694
|
-
return violations;
|
|
1695
|
-
} catch {
|
|
1696
|
-
return violations;
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
async function loadIgnorePatterns2() {
|
|
1700
|
-
try {
|
|
1701
|
-
const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-MF3VKVKH.js");
|
|
1702
|
-
const ignores = await listMemories2({ type: "ignore", limit: 1e3 });
|
|
1703
|
-
await closePool2();
|
|
1704
|
-
return ignores.map((ignore) => ignore.content);
|
|
1705
|
-
} catch {
|
|
1706
|
-
return [];
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
async function checkFile(filePath, cwd, config, verbose, debug) {
|
|
1710
|
-
const rel = relative(cwd, filePath);
|
|
1711
|
-
let diff;
|
|
1712
|
-
const headResult = spawnSync2("git", ["diff", "HEAD", "--", rel], { encoding: "utf-8", cwd });
|
|
1713
|
-
if (headResult.stdout?.trim()) {
|
|
1714
|
-
diff = headResult.stdout;
|
|
1715
|
-
} else {
|
|
1716
|
-
const noIndexResult = spawnSync2("git", ["diff", "--no-index", "/dev/null", rel], { encoding: "utf-8", cwd });
|
|
1717
|
-
diff = noIndexResult.stdout ?? "";
|
|
1718
|
-
}
|
|
1719
|
-
if (!diff.trim()) return;
|
|
1720
|
-
const { rules: fallbackRules, avoids } = getProfileRules2(config);
|
|
1721
|
-
const rules = await loadRelevantRules2(config, rel, diff, fallbackRules);
|
|
1722
|
-
if (rules.length === 0) return;
|
|
1723
|
-
const MAX_DIFF = 6e3;
|
|
1724
|
-
const truncated = diff.length > MAX_DIFF;
|
|
1725
|
-
const diffToSend = truncated ? diff.slice(0, MAX_DIFF) + "\n\n[diff truncated]" : diff;
|
|
1726
|
-
if (verbose || debug) {
|
|
1727
|
-
console.log(chalk2.dim(`
|
|
1728
|
-
[watch] checking ${rel} (${diff.length} chars)\u2026`));
|
|
1729
|
-
}
|
|
1730
|
-
const rulesWithReasons = rules.map((r, i) => {
|
|
1731
|
-
const why = reasonMap2.get(r);
|
|
1732
|
-
return why ? `${i + 1}. ${r}
|
|
1733
|
-
WHY: ${why}` : `${i + 1}. ${r}`;
|
|
1734
|
-
}).join("\n");
|
|
1735
|
-
const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...await loadIgnorePatterns2()])];
|
|
1736
|
-
const systemPrompt = `You are a strict code reviewer enforcing architecture rules.
|
|
1737
|
-
Analyze the file diff and identify ONLY clear, definite rule violations.
|
|
1738
|
-
Use the WHY for each rule to understand intent and judge edge cases.
|
|
1739
|
-
|
|
1740
|
-
Rules to enforce:
|
|
1741
|
-
${rulesWithReasons}
|
|
1742
|
-
|
|
1743
|
-
Things that must never appear:
|
|
1744
|
-
${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
|
|
1745
|
-
|
|
1746
|
-
Never flag these accepted project patterns:
|
|
1747
|
-
${allowPatterns.length ? allowPatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
|
|
1748
|
-
|
|
1749
|
-
IMPORTANT: Respond with JSON: {"violations":[...]} or {"violations":[]}.
|
|
1750
|
-
Each violation: {"rule":"...","file":"...","line":N,"issue":"...","suggestion":"...","reason":"..."}.
|
|
1751
|
-
No text outside the JSON.`;
|
|
1752
|
-
if (debug) {
|
|
1753
|
-
console.log(chalk2.gray("\n [debug] prompt:"));
|
|
1754
|
-
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"));
|
|
1755
|
-
console.log(systemPrompt);
|
|
1756
|
-
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"));
|
|
1757
|
-
console.log(chalk2.gray(` [debug] diff length: ${diff.length} chars`));
|
|
1758
|
-
console.log(chalk2.dim(diffToSend));
|
|
1759
|
-
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"));
|
|
1760
|
-
}
|
|
1761
|
-
try {
|
|
1762
|
-
const raw = await callChatModel([
|
|
1763
|
-
{ role: "system", content: systemPrompt },
|
|
1764
|
-
{ role: "user", content: `Review this diff for ${rel}:
|
|
1765
|
-
|
|
1766
|
-
${diffToSend}` }
|
|
1767
|
-
]);
|
|
1768
|
-
if (debug) {
|
|
1769
|
-
console.log(chalk2.gray(" [debug] raw response:"));
|
|
1770
|
-
console.log(chalk2.dim(raw));
|
|
1771
|
-
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"));
|
|
1772
|
-
}
|
|
1773
|
-
let violations = [];
|
|
1774
|
-
try {
|
|
1775
|
-
const parsed = JSON.parse(raw);
|
|
1776
|
-
if (Array.isArray(parsed)) {
|
|
1777
|
-
violations = parsed;
|
|
1778
|
-
} else if (Array.isArray(parsed?.violations)) {
|
|
1779
|
-
violations = parsed.violations;
|
|
1780
|
-
} else if (parsed?.rule) {
|
|
1781
|
-
violations = [parsed];
|
|
1782
|
-
}
|
|
1783
|
-
} catch {
|
|
1784
|
-
violations = [];
|
|
1785
|
-
}
|
|
1786
|
-
violations = await verifyViolations2(diff, violations, allowPatterns, debug);
|
|
1787
|
-
violations = applyAllowPatterns2(violations, allowPatterns);
|
|
1788
|
-
if (violations.length === 0) {
|
|
1789
|
-
console.log(chalk2.green(` \u2713 ${rel}`) + chalk2.dim(" \u2014 no violations"));
|
|
1790
|
-
return;
|
|
1791
|
-
}
|
|
1792
|
-
console.log(
|
|
1793
|
-
chalk2.red.bold(`
|
|
1794
|
-
\u2717 ${violations.length} violation${violations.length > 1 ? "s" : ""} in ${rel}
|
|
1795
|
-
`)
|
|
1796
|
-
);
|
|
1797
|
-
violations.forEach((v, i) => {
|
|
1798
|
-
const loc = v.line ? `${v.file ?? rel}:${v.line}` : v.file ?? rel;
|
|
1799
|
-
console.log(chalk2.bold(` [${i + 1}] ${loc}`));
|
|
1800
|
-
console.log(chalk2.yellow(" Rule: ") + v.rule);
|
|
1801
|
-
const why = v.reason ?? reasonMap2.get(v.rule);
|
|
1802
|
-
if (why) console.log(chalk2.dim(" Why: ") + chalk2.dim(why));
|
|
1803
|
-
if (v.line && existsSync5(filePath)) {
|
|
1804
|
-
printCodeContext(filePath, v.line, 1);
|
|
1805
|
-
}
|
|
1806
|
-
if (v.issue) console.log(chalk2.red(" Issue: ") + v.issue);
|
|
1807
|
-
if (v.suggestion) console.log(chalk2.green(" Fix: ") + v.suggestion);
|
|
1808
|
-
console.log();
|
|
1809
|
-
});
|
|
1810
|
-
recordViolations2(violations);
|
|
1811
|
-
console.log(chalk2.dim(' Fix violations or run: memory-core remember "<lesson>"'));
|
|
1812
|
-
console.log();
|
|
1813
|
-
} catch (err) {
|
|
1814
|
-
if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
|
|
1815
|
-
return;
|
|
1816
|
-
}
|
|
1817
|
-
if (verbose) {
|
|
1818
|
-
console.log(chalk2.yellow(` \u26A0 Check failed for ${rel}: ${err.message}`));
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
async function startWatch(options = {}) {
|
|
1823
|
-
const cwd = process.cwd();
|
|
1824
|
-
const config = loadConfig(cwd);
|
|
1825
|
-
if (!config) {
|
|
1826
|
-
console.error(chalk2.red("\n No .memory-core.json found. Run: memory-core init\n"));
|
|
1827
|
-
process.exit(1);
|
|
1828
|
-
}
|
|
1829
|
-
const { rules } = getProfileRules2(config);
|
|
1830
|
-
if (rules.length === 0) {
|
|
1831
|
-
console.log(chalk2.yellow("\n No architecture rules configured in .memory-core.json \u2014 nothing to watch.\n"));
|
|
1832
|
-
process.exit(0);
|
|
1833
|
-
}
|
|
1834
|
-
const watchPath = options.path ?? cwd;
|
|
1835
|
-
console.log(chalk2.cyan("\n archmind watch \u2014 real-time rule enforcement\n"));
|
|
1836
|
-
console.log(chalk2.dim(` watching: ${watchPath}`));
|
|
1837
|
-
console.log(chalk2.dim(` model: ${getChatProviderLabel()}`));
|
|
1838
|
-
console.log(chalk2.dim(` rules: ${rules.length}`));
|
|
1839
|
-
console.log(chalk2.dim(" ctrl+c to stop\n"));
|
|
1840
|
-
const pending = /* @__PURE__ */ new Map();
|
|
1841
|
-
const watcher = watch(watchPath, {
|
|
1842
|
-
ignored: [
|
|
1843
|
-
"**/node_modules/**",
|
|
1844
|
-
"**/.git/**",
|
|
1845
|
-
"**/dist/**",
|
|
1846
|
-
"**/build/**",
|
|
1847
|
-
"**/coverage/**",
|
|
1848
|
-
"**/.memory-core*"
|
|
1849
|
-
],
|
|
1850
|
-
ignoreInitial: true,
|
|
1851
|
-
persistent: true,
|
|
1852
|
-
awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50 }
|
|
1853
|
-
});
|
|
1854
|
-
const keepAlive = setInterval(() => {
|
|
1855
|
-
}, 1 << 30);
|
|
1856
|
-
const handle = (filePath) => {
|
|
1857
|
-
if (!SOURCE_EXTENSIONS.test(filePath)) return;
|
|
1858
|
-
if (pending.has(filePath)) clearTimeout(pending.get(filePath));
|
|
1859
|
-
const timer = setTimeout(async () => {
|
|
1860
|
-
pending.delete(filePath);
|
|
1861
|
-
console.log(chalk2.dim(`
|
|
1862
|
-
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] saved: ${relative(cwd, filePath)}`));
|
|
1863
|
-
await checkFile(filePath, cwd, config, options.verbose ?? false, options.debug ?? false);
|
|
1864
|
-
}, 300);
|
|
1865
|
-
pending.set(filePath, timer);
|
|
1866
|
-
};
|
|
1867
|
-
watcher.on("add", handle);
|
|
1868
|
-
watcher.on("change", handle);
|
|
1869
|
-
watcher.on("error", (err) => {
|
|
1870
|
-
console.error(chalk2.red(` watcher error: ${err instanceof Error ? err.message : String(err)}`));
|
|
1871
|
-
});
|
|
1872
|
-
process.on("SIGINT", () => {
|
|
1873
|
-
console.log(chalk2.dim("\n\n archmind watch stopped.\n"));
|
|
1874
|
-
clearInterval(keepAlive);
|
|
1875
|
-
watcher.close();
|
|
1876
|
-
process.exit(0);
|
|
1877
|
-
});
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
684
|
// src/remote-install.ts
|
|
1881
|
-
import { spawnSync as
|
|
685
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1882
686
|
var CAVEMAN_INSTALL_URL = "https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh";
|
|
1883
687
|
var MAX_INSTALLER_BYTES = 2e5;
|
|
1884
688
|
var TRUSTED_INSTALL_HOSTS = /* @__PURE__ */ new Set(["raw.githubusercontent.com"]);
|
|
@@ -1892,7 +696,7 @@ function assertTrustedInstallerUrl(url) {
|
|
|
1892
696
|
}
|
|
1893
697
|
}
|
|
1894
698
|
function defaultRunScript(script) {
|
|
1895
|
-
return
|
|
699
|
+
return spawnSync2("bash", ["-s"], {
|
|
1896
700
|
input: script,
|
|
1897
701
|
encoding: "utf-8",
|
|
1898
702
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1928,33 +732,33 @@ async function installCavemanTokenSaver(options = {}) {
|
|
|
1928
732
|
|
|
1929
733
|
// src/cli.ts
|
|
1930
734
|
function printBanner(projectName, agentCount, status) {
|
|
1931
|
-
const pg = status ? status.postgresOk ?
|
|
1932
|
-
const ol = status ? status.ollamaOk ?
|
|
735
|
+
const pg = status ? status.postgresOk ? chalk2.green(" \u2713 PostgreSQL ") + chalk2.bold("connected") : chalk2.red(" \u2717 PostgreSQL ") + chalk2.bold("not connected \u2014 check DATABASE_URL") : chalk2.green(" \u2713 Memory ") + chalk2.bold("PostgreSQL + pgvector ready");
|
|
736
|
+
const ol = status ? status.ollamaOk ? chalk2.green(" \u2713 Ollama ") + chalk2.bold(`connected (model: ${status.chatModel})`) : chalk2.red(" \u2717 Ollama ") + chalk2.bold("not running \u2014 start with: ollama serve") : null;
|
|
1933
737
|
const lines = [
|
|
1934
738
|
"",
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
739
|
+
chalk2.cyan(" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 "),
|
|
740
|
+
chalk2.cyan(" \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557"),
|
|
741
|
+
chalk2.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551"),
|
|
742
|
+
chalk2.cyan(" \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551"),
|
|
743
|
+
chalk2.cyan(" \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D"),
|
|
744
|
+
chalk2.cyan(" \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D"),
|
|
1941
745
|
"",
|
|
1942
|
-
|
|
746
|
+
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 ") + chalk2.bold.white("C O R E") + 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"),
|
|
1943
747
|
"",
|
|
1944
|
-
|
|
1945
|
-
|
|
748
|
+
chalk2.green(` \u2713 Project `) + chalk2.bold(projectName),
|
|
749
|
+
chalk2.green(` \u2713 Agents `) + chalk2.bold(`${agentCount} AI agents configured`),
|
|
1946
750
|
pg,
|
|
1947
751
|
...ol ? [ol] : [],
|
|
1948
752
|
"",
|
|
1949
|
-
|
|
1950
|
-
|
|
753
|
+
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\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"),
|
|
754
|
+
chalk2.dim(" Built by ") + chalk2.bold.white("Shahmil Saari"),
|
|
1951
755
|
"",
|
|
1952
|
-
|
|
756
|
+
chalk2.bold(" Every AI agent in this project now follows your rules."),
|
|
1953
757
|
"",
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
758
|
+
chalk2.gray(" Next steps:"),
|
|
759
|
+
chalk2.gray(' memory-core remember "Your architectural decision"'),
|
|
760
|
+
chalk2.gray(' memory-core search "query"'),
|
|
761
|
+
chalk2.gray(" memory-core sync"),
|
|
1958
762
|
""
|
|
1959
763
|
];
|
|
1960
764
|
lines.forEach((l) => console.log(l));
|
|
@@ -1980,25 +784,28 @@ async function checkConnections(dbUrl, ollamaUrl, chatModel) {
|
|
|
1980
784
|
}
|
|
1981
785
|
spinner.stop();
|
|
1982
786
|
console.log(
|
|
1983
|
-
postgresOk ?
|
|
787
|
+
postgresOk ? chalk2.green(" \u2713 PostgreSQL") + chalk2.dim(" \u2014 connected") : chalk2.red(" \u2717 PostgreSQL") + chalk2.dim(" \u2014 cannot connect. Check DATABASE_URL and that PostgreSQL is running.")
|
|
1984
788
|
);
|
|
1985
789
|
console.log(
|
|
1986
|
-
ollamaOk ?
|
|
790
|
+
ollamaOk ? chalk2.green(" \u2713 Ollama ") + chalk2.dim(` \u2014 connected (${chatModel})`) : chalk2.red(" \u2717 Ollama ") + chalk2.dim(" \u2014 not reachable. Run: ollama serve")
|
|
1987
791
|
);
|
|
1988
792
|
console.log();
|
|
1989
793
|
return { postgresOk, ollamaOk, chatModel };
|
|
1990
794
|
}
|
|
1991
|
-
var { version } = JSON.parse(
|
|
795
|
+
var { version } = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf-8"));
|
|
1992
796
|
var CONFIG_FILE = ".memory-core.json";
|
|
1993
797
|
var LOCAL_GENERATED_FILES = [".memory-core-stats.json"];
|
|
798
|
+
var LOCAL_STATE_FILES = [CONFIG_FILE, ".memory-core.env", ...LOCAL_GENERATED_FILES];
|
|
799
|
+
var CI_WORKFLOW_FILE = ".github/workflows/memory-core.yml";
|
|
800
|
+
var GITIGNORE_HEADING = "# memory-core generated files";
|
|
1994
801
|
var DEFAULT_OLLAMA_URL = "http://localhost:11434";
|
|
1995
802
|
var DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
|
|
1996
803
|
var DEFAULT_CHAT_MODEL = "llama3.2";
|
|
1997
804
|
function getEnvPath() {
|
|
1998
|
-
const memoryEnv =
|
|
1999
|
-
if (
|
|
2000
|
-
const dotEnv =
|
|
2001
|
-
return
|
|
805
|
+
const memoryEnv = join3(process.cwd(), ".memory-core.env");
|
|
806
|
+
if (existsSync3(memoryEnv)) return memoryEnv;
|
|
807
|
+
const dotEnv = join3(process.cwd(), ".env");
|
|
808
|
+
return existsSync3(dotEnv) ? dotEnv : memoryEnv;
|
|
2002
809
|
}
|
|
2003
810
|
function parseEnvFile(raw) {
|
|
2004
811
|
const lines = raw.split(/\r?\n/);
|
|
@@ -2016,7 +823,7 @@ function parseEnvFile(raw) {
|
|
|
2016
823
|
}
|
|
2017
824
|
function readRuntimeEnv() {
|
|
2018
825
|
const envPath = getEnvPath();
|
|
2019
|
-
const fileValues =
|
|
826
|
+
const fileValues = existsSync3(envPath) ? parseEnvFile(readFileSync3(envPath, "utf-8")) : {};
|
|
2020
827
|
const values = {
|
|
2021
828
|
...fileValues
|
|
2022
829
|
};
|
|
@@ -2049,7 +856,7 @@ function writeRuntimeEnv(values, envPath = getEnvPath()) {
|
|
|
2049
856
|
const value = values[key];
|
|
2050
857
|
if (value) lines.push(`${key}=${value}`);
|
|
2051
858
|
}
|
|
2052
|
-
|
|
859
|
+
writeFileSync3(envPath, `${lines.join("\n")}
|
|
2053
860
|
`, "utf-8");
|
|
2054
861
|
}
|
|
2055
862
|
function applyRuntimeEnv(values) {
|
|
@@ -2059,16 +866,16 @@ function applyRuntimeEnv(values) {
|
|
|
2059
866
|
}
|
|
2060
867
|
function ensureEnvFileIgnored(envPath = getEnvPath()) {
|
|
2061
868
|
const envFileName = envPath.split("/").pop() ?? ".memory-core.env";
|
|
2062
|
-
const gitignorePath =
|
|
2063
|
-
const existing =
|
|
869
|
+
const gitignorePath = join3(process.cwd(), ".gitignore");
|
|
870
|
+
const existing = existsSync3(gitignorePath) ? readFileSync3(gitignorePath, "utf-8") : "";
|
|
2064
871
|
if (!existing.includes(envFileName)) {
|
|
2065
872
|
appendFileSync(gitignorePath, `${existing ? "\n" : ""}${envFileName}
|
|
2066
873
|
`);
|
|
2067
874
|
}
|
|
2068
875
|
}
|
|
2069
876
|
function appendMissingGitignoreEntries(entries, heading) {
|
|
2070
|
-
const gitignorePath =
|
|
2071
|
-
const existing =
|
|
877
|
+
const gitignorePath = join3(process.cwd(), ".gitignore");
|
|
878
|
+
const existing = existsSync3(gitignorePath) ? readFileSync3(gitignorePath, "utf-8") : "";
|
|
2072
879
|
const existingEntries = new Set(
|
|
2073
880
|
existing.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
2074
881
|
);
|
|
@@ -2082,6 +889,52 @@ ${toAdd.join("\n")}
|
|
|
2082
889
|
`);
|
|
2083
890
|
return toAdd.length;
|
|
2084
891
|
}
|
|
892
|
+
function removeMemoryCoreGitignoreBlock(entries, heading = GITIGNORE_HEADING) {
|
|
893
|
+
const gitignorePath = join3(process.cwd(), ".gitignore");
|
|
894
|
+
if (!existsSync3(gitignorePath)) return false;
|
|
895
|
+
const existing = readFileSync3(gitignorePath, "utf-8");
|
|
896
|
+
const entrySet = new Set(entries);
|
|
897
|
+
const lines = existing.split(/\r?\n/);
|
|
898
|
+
const kept = [];
|
|
899
|
+
let changed = false;
|
|
900
|
+
let inMemoryCoreBlock = false;
|
|
901
|
+
for (const line of lines) {
|
|
902
|
+
const trimmed = line.trim();
|
|
903
|
+
if (trimmed === heading) {
|
|
904
|
+
inMemoryCoreBlock = true;
|
|
905
|
+
changed = true;
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
if (inMemoryCoreBlock) {
|
|
909
|
+
if (!trimmed) {
|
|
910
|
+
inMemoryCoreBlock = false;
|
|
911
|
+
changed = true;
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
if (entrySet.has(trimmed)) {
|
|
915
|
+
changed = true;
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
inMemoryCoreBlock = false;
|
|
919
|
+
}
|
|
920
|
+
kept.push(line);
|
|
921
|
+
}
|
|
922
|
+
if (!changed) return false;
|
|
923
|
+
const content = kept.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/u, "");
|
|
924
|
+
writeFileSync3(gitignorePath, content ? `${content}
|
|
925
|
+
` : "", "utf-8");
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
function removeProjectFiles(relativePaths) {
|
|
929
|
+
const removed = [];
|
|
930
|
+
for (const relativePath of [...new Set(relativePaths)]) {
|
|
931
|
+
const target = join3(process.cwd(), relativePath);
|
|
932
|
+
if (!existsSync3(target)) continue;
|
|
933
|
+
rmSync(target, { force: true, recursive: true });
|
|
934
|
+
removed.push(relativePath);
|
|
935
|
+
}
|
|
936
|
+
return removed;
|
|
937
|
+
}
|
|
2085
938
|
function normalizeProvider(value) {
|
|
2086
939
|
const provider2 = value.trim().toLowerCase();
|
|
2087
940
|
if (provider2 === "ollama" || provider2 === "openai" || provider2 === "anthropic" || provider2 === "minimax") {
|
|
@@ -2141,16 +994,16 @@ async function verifyOllamaConnection(ollamaUrl) {
|
|
|
2141
994
|
}
|
|
2142
995
|
}
|
|
2143
996
|
function readProjectConfig() {
|
|
2144
|
-
const path =
|
|
2145
|
-
if (!
|
|
997
|
+
const path = join3(process.cwd(), CONFIG_FILE);
|
|
998
|
+
if (!existsSync3(path)) return null;
|
|
2146
999
|
try {
|
|
2147
|
-
return JSON.parse(
|
|
1000
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
2148
1001
|
} catch {
|
|
2149
1002
|
return null;
|
|
2150
1003
|
}
|
|
2151
1004
|
}
|
|
2152
1005
|
function writeProjectConfig(config) {
|
|
2153
|
-
|
|
1006
|
+
writeFileSync3(join3(process.cwd(), CONFIG_FILE), JSON.stringify(config, null, 2));
|
|
2154
1007
|
}
|
|
2155
1008
|
function updateProjectConfig(mutator) {
|
|
2156
1009
|
const current = readProjectConfig() ?? {
|
|
@@ -2188,11 +1041,11 @@ function truncate(value, length) {
|
|
|
2188
1041
|
return value.length > length ? `${value.slice(0, Math.max(0, length - 1))}\u2026` : value;
|
|
2189
1042
|
}
|
|
2190
1043
|
function printMemoryTable(memories, title = "Rules in memory") {
|
|
2191
|
-
console.log(
|
|
1044
|
+
console.log(chalk2.bold(`
|
|
2192
1045
|
${title} (${memories.length} total)
|
|
2193
1046
|
`));
|
|
2194
|
-
console.log(
|
|
2195
|
-
console.log(
|
|
1047
|
+
console.log(chalk2.dim(" ID Type Scope Title / Content"));
|
|
1048
|
+
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\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\u2500\u2500"));
|
|
2196
1049
|
memories.forEach((memory) => {
|
|
2197
1050
|
const id = String(memory.id).padEnd(4);
|
|
2198
1051
|
const type = memory.type.padEnd(10);
|
|
@@ -2200,13 +1053,13 @@ function printMemoryTable(memories, title = "Rules in memory") {
|
|
|
2200
1053
|
const label = truncate(memory.title || memory.content, 64);
|
|
2201
1054
|
console.log(` ${id} ${type} ${scope} ${label}`);
|
|
2202
1055
|
});
|
|
2203
|
-
console.log(
|
|
1056
|
+
console.log(chalk2.gray("\n Use: memory-core remove <id> | memory-core edit <id>\n"));
|
|
2204
1057
|
}
|
|
2205
1058
|
function getCurrentListArchitectures(config) {
|
|
2206
1059
|
return inferProjectArchitectures(process.cwd(), config).filter((architecture) => architecture !== "global");
|
|
2207
1060
|
}
|
|
2208
1061
|
function printStatusLine(label, value) {
|
|
2209
|
-
console.log(` ${
|
|
1062
|
+
console.log(` ${chalk2.dim(label.padEnd(18))} ${value}`);
|
|
2210
1063
|
}
|
|
2211
1064
|
async function runModelDoctor() {
|
|
2212
1065
|
const { envPath, values } = readRuntimeEnv();
|
|
@@ -2215,8 +1068,8 @@ async function runModelDoctor() {
|
|
|
2215
1068
|
const ollamaUrl = values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
|
|
2216
1069
|
const embeddingModel = values.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL;
|
|
2217
1070
|
const dbUrl = values.DATABASE_URL ?? "";
|
|
2218
|
-
console.log(
|
|
2219
|
-
printStatusLine("Env file",
|
|
1071
|
+
console.log(chalk2.bold("\n memory-core model doctor\n"));
|
|
1072
|
+
printStatusLine("Env file", existsSync3(envPath) ? envPath : `${envPath} ${chalk2.yellow("(will be created on first write)")}`);
|
|
2220
1073
|
printStatusLine("Provider", provider2);
|
|
2221
1074
|
printStatusLine("Chat model", model2);
|
|
2222
1075
|
printStatusLine("Embedding model", embeddingModel);
|
|
@@ -2226,56 +1079,56 @@ async function runModelDoctor() {
|
|
|
2226
1079
|
const dbError = await verifyDatabaseConnection(dbUrl);
|
|
2227
1080
|
if (dbError) {
|
|
2228
1081
|
ok = false;
|
|
2229
|
-
console.log(
|
|
1082
|
+
console.log(chalk2.red(" \u2717 PostgreSQL ") + chalk2.dim(dbError));
|
|
2230
1083
|
} else {
|
|
2231
|
-
console.log(
|
|
1084
|
+
console.log(chalk2.green(" \u2713 PostgreSQL ") + chalk2.dim("connected"));
|
|
2232
1085
|
}
|
|
2233
1086
|
const ollamaError = await verifyOllamaConnection(ollamaUrl);
|
|
2234
1087
|
if (ollamaError) {
|
|
2235
1088
|
ok = false;
|
|
2236
|
-
console.log(
|
|
1089
|
+
console.log(chalk2.red(" \u2717 Ollama ") + chalk2.dim(ollamaError));
|
|
2237
1090
|
} else {
|
|
2238
|
-
console.log(
|
|
1091
|
+
console.log(chalk2.green(" \u2713 Ollama ") + chalk2.dim("reachable"));
|
|
2239
1092
|
}
|
|
2240
1093
|
if (!ollamaError) {
|
|
2241
1094
|
try {
|
|
2242
1095
|
const installedEmbeddingModel = await resolveOllamaInstalledModel(ollamaUrl, embeddingModel);
|
|
2243
1096
|
if (installedEmbeddingModel) {
|
|
2244
|
-
console.log(
|
|
1097
|
+
console.log(chalk2.green(" \u2713 Embedding ") + chalk2.dim(`${installedEmbeddingModel} installed`));
|
|
2245
1098
|
} else {
|
|
2246
1099
|
ok = false;
|
|
2247
|
-
console.log(
|
|
1100
|
+
console.log(chalk2.red(" \u2717 Embedding ") + chalk2.dim(`${embeddingModel} not installed in Ollama`));
|
|
2248
1101
|
}
|
|
2249
1102
|
} catch (err) {
|
|
2250
1103
|
ok = false;
|
|
2251
|
-
console.log(
|
|
1104
|
+
console.log(chalk2.red(" \u2717 Embedding ") + chalk2.dim(err.message));
|
|
2252
1105
|
}
|
|
2253
1106
|
}
|
|
2254
1107
|
if (provider2 === "ollama") {
|
|
2255
1108
|
if (ollamaError) {
|
|
2256
1109
|
ok = false;
|
|
2257
|
-
console.log(
|
|
1110
|
+
console.log(chalk2.red(" \u2717 Chat model ") + chalk2.dim("cannot verify while Ollama is unreachable"));
|
|
2258
1111
|
} else {
|
|
2259
1112
|
try {
|
|
2260
1113
|
const installedChatModel = await resolveOllamaInstalledModel(ollamaUrl, model2);
|
|
2261
1114
|
if (installedChatModel) {
|
|
2262
|
-
console.log(
|
|
1115
|
+
console.log(chalk2.green(" \u2713 Chat model ") + chalk2.dim(`${installedChatModel} installed`));
|
|
2263
1116
|
} else {
|
|
2264
1117
|
ok = false;
|
|
2265
|
-
console.log(
|
|
1118
|
+
console.log(chalk2.red(" \u2717 Chat model ") + chalk2.dim(`${model2} not installed in Ollama`));
|
|
2266
1119
|
}
|
|
2267
1120
|
} catch (err) {
|
|
2268
1121
|
ok = false;
|
|
2269
|
-
console.log(
|
|
1122
|
+
console.log(chalk2.red(" \u2717 Chat model ") + chalk2.dim(err.message));
|
|
2270
1123
|
}
|
|
2271
1124
|
}
|
|
2272
1125
|
} else {
|
|
2273
1126
|
if (!values.CHAT_API_KEY) {
|
|
2274
1127
|
ok = false;
|
|
2275
|
-
console.log(
|
|
1128
|
+
console.log(chalk2.red(` \u2717 ${providerLabel(provider2)} API`) + chalk2.dim(" CHAT_API_KEY is missing"));
|
|
2276
1129
|
} else {
|
|
2277
|
-
console.log(
|
|
2278
|
-
console.log(
|
|
1130
|
+
console.log(chalk2.green(` \u2713 ${providerLabel(provider2)} API`) + chalk2.dim(" key configured"));
|
|
1131
|
+
console.log(chalk2.gray(" Remote provider connectivity is not verified live by doctor."));
|
|
2279
1132
|
}
|
|
2280
1133
|
}
|
|
2281
1134
|
console.log();
|
|
@@ -2287,29 +1140,29 @@ async function printProjectStatus() {
|
|
|
2287
1140
|
const provider2 = getConfiguredProvider(values);
|
|
2288
1141
|
const model2 = getConfiguredChatModel(values);
|
|
2289
1142
|
const architectures = inferProjectArchitectures(process.cwd(), config);
|
|
2290
|
-
const generatedFiles = OUTPUT_FILES.map((entry) => entry.path).filter((relativePath) =>
|
|
2291
|
-
const hookPath =
|
|
2292
|
-
const memoryFilePath =
|
|
2293
|
-
const statsPath =
|
|
1143
|
+
const generatedFiles = OUTPUT_FILES.map((entry) => entry.path).filter((relativePath) => existsSync3(join3(process.cwd(), relativePath)));
|
|
1144
|
+
const hookPath = join3(process.cwd(), ".git", "hooks", "pre-commit");
|
|
1145
|
+
const memoryFilePath = join3(process.cwd(), MEMORY_FILE);
|
|
1146
|
+
const statsPath = join3(process.cwd(), ".memory-core-stats.json");
|
|
2294
1147
|
const dbError = await verifyDatabaseConnection(values.DATABASE_URL ?? "");
|
|
2295
1148
|
const ollamaError = await verifyOllamaConnection(values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL);
|
|
2296
|
-
console.log(
|
|
1149
|
+
console.log(chalk2.bold("\n memory-core status\n"));
|
|
2297
1150
|
printStatusLine("Project", config?.projectName ?? process.cwd().split("/").pop() ?? "unknown");
|
|
2298
|
-
printStatusLine("Project type", config?.projectType ??
|
|
1151
|
+
printStatusLine("Project type", config?.projectType ?? chalk2.yellow("not initialized"));
|
|
2299
1152
|
printStatusLine("Language", config?.language ?? detectProject().language);
|
|
2300
|
-
printStatusLine("Backend arch", config?.backendArchitecture ??
|
|
2301
|
-
printStatusLine("Frontend fw", config?.frontendFramework ??
|
|
2302
|
-
printStatusLine("Architectures", architectures.length ? architectures.join(", ") :
|
|
2303
|
-
printStatusLine("Agents", config?.agents?.length ? `${config.agents.length} selected` :
|
|
1153
|
+
printStatusLine("Backend arch", config?.backendArchitecture ?? chalk2.gray("\u2014"));
|
|
1154
|
+
printStatusLine("Frontend fw", config?.frontendFramework ?? chalk2.gray("\u2014"));
|
|
1155
|
+
printStatusLine("Architectures", architectures.length ? architectures.join(", ") : chalk2.gray("none detected"));
|
|
1156
|
+
printStatusLine("Agents", config?.agents?.length ? `${config.agents.length} selected` : chalk2.gray("none saved"));
|
|
2304
1157
|
printStatusLine("Caveman", config?.caveman?.enabled ? `enabled (${config.caveman.intensity})` : "disabled");
|
|
2305
1158
|
printStatusLine("Auto sync", config?.autoSync === false ? "disabled" : "enabled");
|
|
2306
1159
|
printStatusLine("Allow patterns", String(getAllowPatterns(config).length));
|
|
2307
|
-
printStatusLine("Env file", `${
|
|
2308
|
-
printStatusLine("Memory file",
|
|
2309
|
-
printStatusLine("Project config",
|
|
1160
|
+
printStatusLine("Env file", `${existsSync3(envPath) ? "present" : "missing"} (${envPath.split("/").pop()})`);
|
|
1161
|
+
printStatusLine("Memory file", existsSync3(memoryFilePath) ? MEMORY_FILE : chalk2.gray("not exported"));
|
|
1162
|
+
printStatusLine("Project config", existsSync3(join3(process.cwd(), CONFIG_FILE)) ? CONFIG_FILE : chalk2.gray("missing"));
|
|
2310
1163
|
printStatusLine("Generated files", String(generatedFiles.length));
|
|
2311
|
-
printStatusLine("Hook",
|
|
2312
|
-
printStatusLine("Stats file",
|
|
1164
|
+
printStatusLine("Hook", existsSync3(hookPath) ? "installed" : "not installed");
|
|
1165
|
+
printStatusLine("Stats file", existsSync3(statsPath) ? ".memory-core-stats.json" : chalk2.gray("none"));
|
|
2313
1166
|
console.log();
|
|
2314
1167
|
printStatusLine("Database URL", redactDatabaseUrl(values.DATABASE_URL ?? ""));
|
|
2315
1168
|
printStatusLine("Ollama URL", values.OLLAMA_URL ?? DEFAULT_OLLAMA_URL);
|
|
@@ -2318,38 +1171,38 @@ async function printProjectStatus() {
|
|
|
2318
1171
|
printStatusLine("Chat model", model2);
|
|
2319
1172
|
console.log();
|
|
2320
1173
|
console.log(
|
|
2321
|
-
dbError ?
|
|
1174
|
+
dbError ? chalk2.red(" \u2717 PostgreSQL ") + chalk2.dim(dbError) : chalk2.green(" \u2713 PostgreSQL ") + chalk2.dim("connected")
|
|
2322
1175
|
);
|
|
2323
1176
|
console.log(
|
|
2324
|
-
ollamaError ?
|
|
1177
|
+
ollamaError ? chalk2.red(" \u2717 Ollama ") + chalk2.dim(ollamaError) : chalk2.green(" \u2713 Ollama ") + chalk2.dim("reachable")
|
|
2325
1178
|
);
|
|
2326
1179
|
if (provider2 !== "ollama") {
|
|
2327
1180
|
console.log(
|
|
2328
|
-
values.CHAT_API_KEY ?
|
|
1181
|
+
values.CHAT_API_KEY ? chalk2.green(` \u2713 ${providerLabel(provider2)} API`) + chalk2.dim(" key configured") : chalk2.red(` \u2717 ${providerLabel(provider2)} API`) + chalk2.dim(" CHAT_API_KEY is missing")
|
|
2329
1182
|
);
|
|
2330
1183
|
}
|
|
2331
1184
|
console.log();
|
|
2332
1185
|
}
|
|
2333
1186
|
function printMemorySelection(selection, limit = 4) {
|
|
2334
1187
|
const active = selection.activeArchitectures.join(", ") || "none detected";
|
|
2335
|
-
console.log(
|
|
1188
|
+
console.log(chalk2.gray(` Stack filter: ${active}`));
|
|
2336
1189
|
const included = selection.decisions.filter((decision) => decision.status === "included");
|
|
2337
1190
|
if (included.length > 0) {
|
|
2338
|
-
console.log(
|
|
1191
|
+
console.log(chalk2.gray(` Included ${included.length}:`));
|
|
2339
1192
|
for (const decision of included.slice(0, limit)) {
|
|
2340
|
-
console.log(
|
|
1193
|
+
console.log(chalk2.gray(` + ${decision.memory.content} (${decision.reason})`));
|
|
2341
1194
|
}
|
|
2342
1195
|
if (included.length > limit) {
|
|
2343
|
-
console.log(
|
|
1196
|
+
console.log(chalk2.gray(` \u2026 ${included.length - limit} more included`));
|
|
2344
1197
|
}
|
|
2345
1198
|
}
|
|
2346
1199
|
if (selection.excluded.length > 0) {
|
|
2347
|
-
console.log(
|
|
1200
|
+
console.log(chalk2.gray(` Excluded ${selection.excluded.length}:`));
|
|
2348
1201
|
for (const decision of selection.excluded.slice(0, limit)) {
|
|
2349
|
-
console.log(
|
|
1202
|
+
console.log(chalk2.gray(` - ${decision.memory.content} (${decision.reason})`));
|
|
2350
1203
|
}
|
|
2351
1204
|
if (selection.excluded.length > limit) {
|
|
2352
|
-
console.log(
|
|
1205
|
+
console.log(chalk2.gray(` \u2026 ${selection.excluded.length - limit} more excluded`));
|
|
2353
1206
|
}
|
|
2354
1207
|
}
|
|
2355
1208
|
}
|
|
@@ -2391,22 +1244,22 @@ async function syncGeneratedFiles(config, agents, options = {}) {
|
|
|
2391
1244
|
agents
|
|
2392
1245
|
);
|
|
2393
1246
|
spinner.succeed(
|
|
2394
|
-
`Synced \u2014 ${
|
|
1247
|
+
`Synced \u2014 ${chalk2.green(`${result.written.length} updated`)}, ${chalk2.dim(`${result.skipped.length} already up to date`)}`
|
|
2395
1248
|
);
|
|
2396
1249
|
if (result.written.length > 0) {
|
|
2397
|
-
result.written.forEach((file) => console.log(
|
|
1250
|
+
result.written.forEach((file) => console.log(chalk2.gray(` \u2713 ${file}`)));
|
|
2398
1251
|
}
|
|
2399
1252
|
}
|
|
2400
1253
|
async function autoSyncGeneratedFiles(config, action, enabled = true) {
|
|
2401
1254
|
if (!enabled) {
|
|
2402
|
-
console.log(
|
|
1255
|
+
console.log(chalk2.gray(" Auto-sync skipped (--no-sync). Run memory-core sync when ready."));
|
|
2403
1256
|
return;
|
|
2404
1257
|
}
|
|
2405
1258
|
if (!config) {
|
|
2406
1259
|
return;
|
|
2407
1260
|
}
|
|
2408
1261
|
if (config.autoSync === false) {
|
|
2409
|
-
console.log(
|
|
1262
|
+
console.log(chalk2.gray(" Auto-sync disabled for this project. Run memory-core sync when ready."));
|
|
2410
1263
|
return;
|
|
2411
1264
|
}
|
|
2412
1265
|
try {
|
|
@@ -2414,18 +1267,18 @@ async function autoSyncGeneratedFiles(config, action, enabled = true) {
|
|
|
2414
1267
|
label: `Auto-syncing agent files after ${action}\u2026`
|
|
2415
1268
|
});
|
|
2416
1269
|
} catch (err) {
|
|
2417
|
-
console.log(
|
|
2418
|
-
console.log(
|
|
1270
|
+
console.log(chalk2.yellow(` Auto-sync skipped: ${err.message}`));
|
|
1271
|
+
console.log(chalk2.gray(" Run memory-core sync manually when ready."));
|
|
2419
1272
|
}
|
|
2420
1273
|
}
|
|
2421
1274
|
var program = new Command();
|
|
2422
1275
|
program.name("memory-core").description("Universal AI memory core \u2014 generate AI context files for all coding agents").version(version);
|
|
2423
1276
|
program.command("init").description("Initialize memory-core in the current project").option("--quick", "Use smart defaults and skip optional prompts").action(async (opts) => {
|
|
2424
|
-
console.log(
|
|
1277
|
+
console.log(chalk2.bold.cyan("\n memory-core init\n"));
|
|
2425
1278
|
const detected = detectProject();
|
|
2426
1279
|
const quick = opts.quick ?? false;
|
|
2427
|
-
const envPath =
|
|
2428
|
-
const hasEnv =
|
|
1280
|
+
const envPath = join3(process.cwd(), ".memory-core.env");
|
|
1281
|
+
const hasEnv = existsSync3(envPath) || existsSync3(join3(process.cwd(), ".env")) || !!process.env.DATABASE_URL;
|
|
2429
1282
|
if (!hasEnv && quick) {
|
|
2430
1283
|
const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
2431
1284
|
const dbUrl = `postgresql://${dbUser}@localhost:5432/memory_core`;
|
|
@@ -2442,9 +1295,9 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2442
1295
|
writeRuntimeEnv(envValues, envPath);
|
|
2443
1296
|
applyRuntimeEnv(envValues);
|
|
2444
1297
|
ensureEnvFileIgnored(envPath);
|
|
2445
|
-
console.log(
|
|
1298
|
+
console.log(chalk2.green(" \u2713 .memory-core.env created with local defaults"));
|
|
2446
1299
|
} else if (!hasEnv) {
|
|
2447
|
-
console.log(
|
|
1300
|
+
console.log(chalk2.dim(" No .memory-core.env found \u2014 let's set up your connection.\n"));
|
|
2448
1301
|
const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
2449
1302
|
let dbUrl = "";
|
|
2450
1303
|
while (true) {
|
|
@@ -2458,11 +1311,11 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2458
1311
|
const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
|
|
2459
1312
|
await testPool.query("SELECT 1");
|
|
2460
1313
|
await testPool.end();
|
|
2461
|
-
pgSpinner.succeed(
|
|
1314
|
+
pgSpinner.succeed(chalk2.green("PostgreSQL connected"));
|
|
2462
1315
|
break;
|
|
2463
1316
|
} catch (err) {
|
|
2464
|
-
pgSpinner.fail(
|
|
2465
|
-
console.log(
|
|
1317
|
+
pgSpinner.fail(chalk2.red(`Cannot connect: ${err.message}`));
|
|
1318
|
+
console.log(chalk2.yellow(" Please check that PostgreSQL is running and the URL is correct.\n"));
|
|
2466
1319
|
}
|
|
2467
1320
|
}
|
|
2468
1321
|
let ollamaUrl = "";
|
|
@@ -2475,11 +1328,11 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2475
1328
|
try {
|
|
2476
1329
|
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
2477
1330
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2478
|
-
ollamaSpinner.succeed(
|
|
1331
|
+
ollamaSpinner.succeed(chalk2.green("Ollama connected"));
|
|
2479
1332
|
break;
|
|
2480
1333
|
} catch (err) {
|
|
2481
|
-
ollamaSpinner.fail(
|
|
2482
|
-
console.log(
|
|
1334
|
+
ollamaSpinner.fail(chalk2.red(`Cannot reach Ollama: ${err.message}`));
|
|
1335
|
+
console.log(chalk2.yellow(" Make sure Ollama is running: ollama serve\n"));
|
|
2483
1336
|
}
|
|
2484
1337
|
}
|
|
2485
1338
|
const chatProvider = await select({
|
|
@@ -2516,15 +1369,15 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2516
1369
|
const match = exact ?? prefixed;
|
|
2517
1370
|
if (match) {
|
|
2518
1371
|
chatModel = match.name;
|
|
2519
|
-
modelSpinner.succeed(
|
|
1372
|
+
modelSpinner.succeed(chalk2.green(`${chatModel} is installed and ready`));
|
|
2520
1373
|
break;
|
|
2521
1374
|
} else {
|
|
2522
|
-
modelSpinner.fail(
|
|
2523
|
-
console.log(
|
|
1375
|
+
modelSpinner.fail(chalk2.red(`${chatModel} is not installed in your Ollama`));
|
|
1376
|
+
console.log(chalk2.yellow(` Run: ollama pull ${chatModel} \u2014 or pick a different model.
|
|
2524
1377
|
`));
|
|
2525
1378
|
}
|
|
2526
1379
|
} catch {
|
|
2527
|
-
modelSpinner.warn(
|
|
1380
|
+
modelSpinner.warn(chalk2.yellow("Could not verify model \u2014 continuing anyway"));
|
|
2528
1381
|
break;
|
|
2529
1382
|
}
|
|
2530
1383
|
}
|
|
@@ -2555,7 +1408,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2555
1408
|
chatApiKey = await input({
|
|
2556
1409
|
message: `${chatProvider.charAt(0).toUpperCase() + chatProvider.slice(1)} API key?`
|
|
2557
1410
|
});
|
|
2558
|
-
console.log(
|
|
1411
|
+
console.log(chalk2.green(` \u2713 ${chatProvider} / ${chatModel} configured`));
|
|
2559
1412
|
}
|
|
2560
1413
|
const envValues = {
|
|
2561
1414
|
DATABASE_URL: dbUrl,
|
|
@@ -2569,8 +1422,8 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2569
1422
|
writeRuntimeEnv(envValues, envPath);
|
|
2570
1423
|
applyRuntimeEnv(envValues);
|
|
2571
1424
|
ensureEnvFileIgnored(envPath);
|
|
2572
|
-
console.log(
|
|
2573
|
-
console.log(
|
|
1425
|
+
console.log(chalk2.green("\n \u2713 .memory-core.env created"));
|
|
1426
|
+
console.log(chalk2.gray(" Added to .gitignore \u2014 your DB credentials stay local.\n"));
|
|
2574
1427
|
}
|
|
2575
1428
|
const projectName = quick ? process.cwd().split("/").pop() ?? "my-project" : await input({
|
|
2576
1429
|
message: "Project name?",
|
|
@@ -2724,9 +1577,9 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2724
1577
|
spinner.succeed(`Generated ${written.written.length} files`);
|
|
2725
1578
|
const gitignoreEntries = [...written.written, ...LOCAL_GENERATED_FILES];
|
|
2726
1579
|
if (gitignoreEntries.length > 0) {
|
|
2727
|
-
const added = appendMissingGitignoreEntries(gitignoreEntries,
|
|
1580
|
+
const added = appendMissingGitignoreEntries(gitignoreEntries, GITIGNORE_HEADING);
|
|
2728
1581
|
if (added > 0) {
|
|
2729
|
-
console.log(
|
|
1582
|
+
console.log(chalk2.green(` \u2713 Added ${added} generated files to .gitignore`));
|
|
2730
1583
|
}
|
|
2731
1584
|
}
|
|
2732
1585
|
if (enableHook) {
|
|
@@ -2743,7 +1596,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
2743
1596
|
program.command("sync").description("Re-pull memories and regenerate AI agent files").action(async () => {
|
|
2744
1597
|
const config = readProjectConfig();
|
|
2745
1598
|
if (!config) {
|
|
2746
|
-
console.error(
|
|
1599
|
+
console.error(chalk2.red("No .memory-core.json found. Run: memory-core init"));
|
|
2747
1600
|
process.exit(1);
|
|
2748
1601
|
}
|
|
2749
1602
|
const { checkbox } = await import("@inquirer/prompts");
|
|
@@ -2758,7 +1611,7 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
|
|
|
2758
1611
|
instructions: " (Space to toggle, A to select all, Enter to confirm)"
|
|
2759
1612
|
});
|
|
2760
1613
|
if (selectedAgents.length === 0) {
|
|
2761
|
-
console.log(
|
|
1614
|
+
console.log(chalk2.yellow(" No agents selected \u2014 nothing to sync."));
|
|
2762
1615
|
process.exit(0);
|
|
2763
1616
|
}
|
|
2764
1617
|
await syncGeneratedFiles(config, [...selectedAgents, "Shared"], { showSelection: true });
|
|
@@ -2767,31 +1620,31 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
|
|
|
2767
1620
|
program.command("auto-sync [mode]").description("Show or change automatic agent file sync (on|off)").action((mode) => {
|
|
2768
1621
|
const config = readProjectConfig();
|
|
2769
1622
|
if (!config) {
|
|
2770
|
-
console.error(
|
|
1623
|
+
console.error(chalk2.red("No .memory-core.json found. Run: memory-core init"));
|
|
2771
1624
|
process.exit(1);
|
|
2772
1625
|
}
|
|
2773
1626
|
const normalized = mode?.trim().toLowerCase();
|
|
2774
1627
|
if (!normalized || normalized === "status") {
|
|
2775
|
-
console.log(
|
|
2776
|
-
console.log(` Status: ${config.autoSync === false ?
|
|
2777
|
-
console.log(
|
|
1628
|
+
console.log(chalk2.bold("\n Auto-sync\n"));
|
|
1629
|
+
console.log(` Status: ${config.autoSync === false ? chalk2.yellow("disabled") : chalk2.green("enabled")}`);
|
|
1630
|
+
console.log(chalk2.gray(" Manual sync is always available: memory-core sync\n"));
|
|
2778
1631
|
return;
|
|
2779
1632
|
}
|
|
2780
1633
|
if (normalized !== "on" && normalized !== "off") {
|
|
2781
|
-
console.error(
|
|
1634
|
+
console.error(chalk2.red("Use: memory-core auto-sync [on|off|status]"));
|
|
2782
1635
|
process.exit(1);
|
|
2783
1636
|
}
|
|
2784
1637
|
const enabled = normalized === "on";
|
|
2785
1638
|
writeProjectConfig({ ...config, autoSync: enabled });
|
|
2786
|
-
console.log(
|
|
2787
|
-
console.log(
|
|
1639
|
+
console.log(chalk2.green(`Auto-sync ${enabled ? "enabled" : "disabled"}`));
|
|
1640
|
+
console.log(chalk2.gray(" Manual sync is always available: memory-core sync"));
|
|
2788
1641
|
});
|
|
2789
1642
|
program.command("remember <text>").description("Save a new memory to the central database").option("-t, --type <type>", "Memory type (decision|rule|pattern|note)", "decision").option("-s, --scope <scope>", "Scope (global|project)", "project").option("--tags <tags>", "Comma-separated tags").option("-r, --reason <reason>", "Why this rule exists \u2014 helps agents understand intent and debug violations").option("--applies-to <items>", "Comma-separated situations where this memory applies").option("--avoid-when <items>", "Comma-separated situations where this memory should not be used").option("--example <items>", "Comma-separated examples that teach agents how to apply this memory").option("--source <source>", "Human-readable source for this memory").option("--no-sync", "Skip automatic agent file sync after saving").action(async (text, opts) => {
|
|
2790
1643
|
const config = readProjectConfig();
|
|
2791
1644
|
let reason = opts.reason;
|
|
2792
1645
|
if (!reason) {
|
|
2793
1646
|
reason = await input({
|
|
2794
|
-
message:
|
|
1647
|
+
message: chalk2.dim("Why does this rule exist? (optional \u2014 helps agents debug violations)"),
|
|
2795
1648
|
default: ""
|
|
2796
1649
|
});
|
|
2797
1650
|
}
|
|
@@ -2809,9 +1662,9 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
2809
1662
|
tags: parseTags(opts.tags),
|
|
2810
1663
|
embedding
|
|
2811
1664
|
});
|
|
2812
|
-
const reasonLine = reason ?
|
|
1665
|
+
const reasonLine = reason ? chalk2.gray(`
|
|
2813
1666
|
Why: ${reason}`) : "";
|
|
2814
|
-
spinner.succeed(
|
|
1667
|
+
spinner.succeed(chalk2.green(`Memory saved: "${text}"`) + reasonLine);
|
|
2815
1668
|
await autoSyncGeneratedFiles(config, "remember", opts.sync);
|
|
2816
1669
|
} catch (err) {
|
|
2817
1670
|
spinner.fail(`Failed: ${err.message}`);
|
|
@@ -2831,20 +1684,20 @@ program.command("search <query>").description("Search memories using semantic si
|
|
|
2831
1684
|
);
|
|
2832
1685
|
spinner.stop();
|
|
2833
1686
|
if (results.length === 0) {
|
|
2834
|
-
console.log(
|
|
1687
|
+
console.log(chalk2.yellow("No memories found."));
|
|
2835
1688
|
} else {
|
|
2836
|
-
console.log(
|
|
1689
|
+
console.log(chalk2.bold(`
|
|
2837
1690
|
${results.length} results for "${query}"
|
|
2838
1691
|
`));
|
|
2839
1692
|
results.forEach((m, i) => {
|
|
2840
|
-
const sim = m.similarity ?
|
|
2841
|
-
console.log(
|
|
2842
|
-
console.log(
|
|
2843
|
-
if (m.reason) console.log(
|
|
2844
|
-
if (m.context?.appliesTo?.length) console.log(
|
|
2845
|
-
if (m.context?.avoidWhen?.length) console.log(
|
|
2846
|
-
if (m.context?.examples?.length) console.log(
|
|
2847
|
-
if (m.tags?.length) console.log(
|
|
1693
|
+
const sim = m.similarity ? chalk2.gray(` (${(m.similarity * 100).toFixed(0)}% match)`) : "";
|
|
1694
|
+
console.log(chalk2.cyan(` ${i + 1}. [${m.type}] ${m.title ?? ""}`));
|
|
1695
|
+
console.log(chalk2.white(` ${m.content}`) + sim);
|
|
1696
|
+
if (m.reason) console.log(chalk2.gray(` why: ${m.reason}`));
|
|
1697
|
+
if (m.context?.appliesTo?.length) console.log(chalk2.gray(` use when: ${m.context.appliesTo.join("; ")}`));
|
|
1698
|
+
if (m.context?.avoidWhen?.length) console.log(chalk2.gray(` avoid when: ${m.context.avoidWhen.join("; ")}`));
|
|
1699
|
+
if (m.context?.examples?.length) console.log(chalk2.gray(` examples: ${m.context.examples.join("; ")}`));
|
|
1700
|
+
if (m.tags?.length) console.log(chalk2.gray(` tags: ${m.tags.join(", ")}`));
|
|
2848
1701
|
console.log();
|
|
2849
1702
|
});
|
|
2850
1703
|
}
|
|
@@ -2859,9 +1712,9 @@ program.command("export").description(`Export DB memories to ${MEMORY_FILE}`).op
|
|
|
2859
1712
|
try {
|
|
2860
1713
|
const memories = await listMemories({ limit: 1e4 });
|
|
2861
1714
|
const portable = memories.map(toPortableMemory);
|
|
2862
|
-
const outputPath = opts.output ?
|
|
1715
|
+
const outputPath = opts.output ? join3(process.cwd(), opts.output) : writeMemoryFile(portable);
|
|
2863
1716
|
if (opts.output) {
|
|
2864
|
-
|
|
1717
|
+
writeFileSync3(outputPath, JSON.stringify(portable, null, 2) + "\n", "utf-8");
|
|
2865
1718
|
}
|
|
2866
1719
|
spinner.succeed(`Exported ${portable.length} memories to ${outputPath}`);
|
|
2867
1720
|
} catch (err) {
|
|
@@ -2875,7 +1728,7 @@ program.command("import").description(`Import memories from ${MEMORY_FILE}`).opt
|
|
|
2875
1728
|
const spinner = ora("Reading memories\u2026").start();
|
|
2876
1729
|
try {
|
|
2877
1730
|
const config = readProjectConfig();
|
|
2878
|
-
const memories = opts.url ? await readMemoryFileFromUrl(opts.url) : opts.file ? parseMemoryFile(
|
|
1731
|
+
const memories = opts.url ? await readMemoryFileFromUrl(opts.url) : opts.file ? parseMemoryFile(readFileSync3(join3(process.cwd(), opts.file), "utf-8")) : readMemoryFile();
|
|
2879
1732
|
let inserted = 0;
|
|
2880
1733
|
let skipped = 0;
|
|
2881
1734
|
spinner.text = `Importing ${memories.length} memories\u2026`;
|
|
@@ -2923,10 +1776,10 @@ program.command("list").description("List memories from the local database").opt
|
|
|
2923
1776
|
const title = opts.all ? "All memories" : `Current project memories${architectures ? ` (${Array.isArray(architectures) ? architectures.join(", ") : architectures})` : ""}`;
|
|
2924
1777
|
printMemoryTable(memories, title);
|
|
2925
1778
|
if (!opts.all) {
|
|
2926
|
-
console.log(
|
|
1779
|
+
console.log(chalk2.gray(" Showing current project context plus shared/global memories. Use --all for the full database.\n"));
|
|
2927
1780
|
}
|
|
2928
1781
|
} catch (err) {
|
|
2929
|
-
console.error(
|
|
1782
|
+
console.error(chalk2.red(`List failed: ${err.message}`));
|
|
2930
1783
|
process.exit(1);
|
|
2931
1784
|
} finally {
|
|
2932
1785
|
await closePool();
|
|
@@ -2937,13 +1790,13 @@ program.command("remove <id>").description("Remove a memory by ID").option("--no
|
|
|
2937
1790
|
const config = readProjectConfig();
|
|
2938
1791
|
const deleted = await deleteMemory(parseInt(id, 10));
|
|
2939
1792
|
if (!deleted) {
|
|
2940
|
-
console.log(
|
|
1793
|
+
console.log(chalk2.yellow(`No memory found with ID ${id}`));
|
|
2941
1794
|
process.exit(1);
|
|
2942
1795
|
}
|
|
2943
|
-
console.log(
|
|
1796
|
+
console.log(chalk2.green(`Removed memory ${id}`));
|
|
2944
1797
|
await autoSyncGeneratedFiles(config, "remove", opts.sync);
|
|
2945
1798
|
} catch (err) {
|
|
2946
|
-
console.error(
|
|
1799
|
+
console.error(chalk2.red(`Remove failed: ${err.message}`));
|
|
2947
1800
|
process.exit(1);
|
|
2948
1801
|
} finally {
|
|
2949
1802
|
await closePool();
|
|
@@ -2958,12 +1811,12 @@ program.command("forget").description("Bulk-delete memories by tag, scope, type,
|
|
|
2958
1811
|
type: opts.type,
|
|
2959
1812
|
architecture: opts.arch
|
|
2960
1813
|
});
|
|
2961
|
-
console.log(
|
|
1814
|
+
console.log(chalk2.green(`Deleted ${deleted} memories`));
|
|
2962
1815
|
if (deleted > 0) {
|
|
2963
1816
|
await autoSyncGeneratedFiles(config, "forget", opts.sync);
|
|
2964
1817
|
}
|
|
2965
1818
|
} catch (err) {
|
|
2966
|
-
console.error(
|
|
1819
|
+
console.error(chalk2.red(`Forget failed: ${err.message}`));
|
|
2967
1820
|
process.exit(1);
|
|
2968
1821
|
} finally {
|
|
2969
1822
|
await closePool();
|
|
@@ -2975,7 +1828,7 @@ program.command("edit <id>").description("Edit a memory interactively").option("
|
|
|
2975
1828
|
const config = readProjectConfig();
|
|
2976
1829
|
const existing = await getMemory(memoryId);
|
|
2977
1830
|
if (!existing) {
|
|
2978
|
-
console.log(
|
|
1831
|
+
console.log(chalk2.yellow(`No memory found with ID ${id}`));
|
|
2979
1832
|
process.exit(1);
|
|
2980
1833
|
}
|
|
2981
1834
|
const type = await input({ message: "Type?", default: existing.type });
|
|
@@ -2999,10 +1852,10 @@ program.command("edit <id>").description("Edit a memory interactively").option("
|
|
|
2999
1852
|
tags: parseTags(tags),
|
|
3000
1853
|
embedding
|
|
3001
1854
|
});
|
|
3002
|
-
console.log(
|
|
1855
|
+
console.log(chalk2.green(`Updated memory ${id}`));
|
|
3003
1856
|
await autoSyncGeneratedFiles(config, "edit", opts.sync);
|
|
3004
1857
|
} catch (err) {
|
|
3005
|
-
console.error(
|
|
1858
|
+
console.error(chalk2.red(`Edit failed: ${err.message}`));
|
|
3006
1859
|
process.exit(1);
|
|
3007
1860
|
} finally {
|
|
3008
1861
|
await closePool();
|
|
@@ -3018,15 +1871,15 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
|
|
|
3018
1871
|
if (opts.remove) {
|
|
3019
1872
|
const deleted = await deleteMemory(parseInt(opts.remove, 10));
|
|
3020
1873
|
if (!deleted) {
|
|
3021
|
-
console.log(
|
|
1874
|
+
console.log(chalk2.yellow(`No ignore pattern found with ID ${opts.remove}`));
|
|
3022
1875
|
process.exit(1);
|
|
3023
1876
|
}
|
|
3024
|
-
console.log(
|
|
1877
|
+
console.log(chalk2.green(`Removed ignore pattern ${opts.remove}`));
|
|
3025
1878
|
await autoSyncGeneratedFiles(config, "ignore remove", opts.sync);
|
|
3026
1879
|
return;
|
|
3027
1880
|
}
|
|
3028
1881
|
if (!pattern) {
|
|
3029
|
-
console.error(
|
|
1882
|
+
console.error(chalk2.red("Provide a pattern, --list, or --remove <id>"));
|
|
3030
1883
|
process.exit(1);
|
|
3031
1884
|
}
|
|
3032
1885
|
const embedding = await embed(pattern);
|
|
@@ -3039,10 +1892,10 @@ program.command("ignore [pattern]").description("Manage project-scoped false-pos
|
|
|
3039
1892
|
tags: ["ignore"],
|
|
3040
1893
|
embedding
|
|
3041
1894
|
});
|
|
3042
|
-
console.log(
|
|
1895
|
+
console.log(chalk2.green(`Ignored pattern saved: "${pattern}"`));
|
|
3043
1896
|
await autoSyncGeneratedFiles(config, "ignore", opts.sync);
|
|
3044
1897
|
} catch (err) {
|
|
3045
|
-
console.error(
|
|
1898
|
+
console.error(chalk2.red(`Ignore failed: ${err.message}`));
|
|
3046
1899
|
process.exit(1);
|
|
3047
1900
|
} finally {
|
|
3048
1901
|
await closePool();
|
|
@@ -3052,10 +1905,10 @@ program.command("allow [pattern]").description("Manage project allow patterns in
|
|
|
3052
1905
|
if (opts.list) {
|
|
3053
1906
|
const patterns = getAllowPatterns(readProjectConfig());
|
|
3054
1907
|
if (patterns.length === 0) {
|
|
3055
|
-
console.log(
|
|
1908
|
+
console.log(chalk2.yellow("\n No allow patterns configured.\n"));
|
|
3056
1909
|
return;
|
|
3057
1910
|
}
|
|
3058
|
-
console.log(
|
|
1911
|
+
console.log(chalk2.bold("\n Allow patterns\n"));
|
|
3059
1912
|
patterns.forEach((entry, index) => console.log(` ${index + 1}. ${entry}`));
|
|
3060
1913
|
console.log();
|
|
3061
1914
|
return;
|
|
@@ -3065,23 +1918,23 @@ program.command("allow [pattern]").description("Manage project allow patterns in
|
|
|
3065
1918
|
...config,
|
|
3066
1919
|
allowPatterns: getAllowPatterns(config).filter((entry) => entry !== opts.remove)
|
|
3067
1920
|
}));
|
|
3068
|
-
console.log(
|
|
1921
|
+
console.log(chalk2.green(`Removed allow pattern: "${opts.remove}"`));
|
|
3069
1922
|
return;
|
|
3070
1923
|
}
|
|
3071
1924
|
if (!pattern) {
|
|
3072
|
-
console.error(
|
|
1925
|
+
console.error(chalk2.red("Provide a pattern, --list, or --remove <pattern>"));
|
|
3073
1926
|
process.exit(1);
|
|
3074
1927
|
}
|
|
3075
1928
|
updateProjectConfig((config) => ({
|
|
3076
1929
|
...config,
|
|
3077
1930
|
allowPatterns: [.../* @__PURE__ */ new Set([...getAllowPatterns(config), pattern])]
|
|
3078
1931
|
}));
|
|
3079
|
-
console.log(
|
|
1932
|
+
console.log(chalk2.green(`Allow pattern saved: "${pattern}"`));
|
|
3080
1933
|
});
|
|
3081
1934
|
program.command("ci-setup").description("Generate GitHub Actions workflow for memory-core").action(() => {
|
|
3082
|
-
const workflowPath =
|
|
3083
|
-
|
|
3084
|
-
|
|
1935
|
+
const workflowPath = join3(process.cwd(), ".github", "workflows", "memory-core.yml");
|
|
1936
|
+
mkdirSync(dirname(workflowPath), { recursive: true });
|
|
1937
|
+
writeFileSync3(workflowPath, `name: memory-core
|
|
3085
1938
|
on: [pull_request]
|
|
3086
1939
|
jobs:
|
|
3087
1940
|
check:
|
|
@@ -3092,24 +1945,12 @@ jobs:
|
|
|
3092
1945
|
fetch-depth: 0
|
|
3093
1946
|
- run: npx @shahmilsaari/memory-core check --ci
|
|
3094
1947
|
`, "utf-8");
|
|
3095
|
-
console.log(
|
|
1948
|
+
console.log(chalk2.green(`Generated ${workflowPath}`));
|
|
3096
1949
|
});
|
|
3097
1950
|
program.command("reset").description("Remove memory-core generated files and local project config").option("--soft", "Only remove generated files; keep config and DB").option("--db", "Also drop the memories table after confirmation").action(async (opts) => {
|
|
3098
|
-
|
|
3099
|
-
let removed = 0;
|
|
3100
|
-
for (const relativePath of generated) {
|
|
3101
|
-
const target = join6(process.cwd(), relativePath);
|
|
3102
|
-
if (existsSync6(target)) {
|
|
3103
|
-
rmSync(target, { force: true });
|
|
3104
|
-
removed++;
|
|
3105
|
-
}
|
|
3106
|
-
}
|
|
1951
|
+
let removed = removeProjectFiles(OUTPUT_FILES.map((file) => file.path)).length;
|
|
3107
1952
|
if (!opts.soft) {
|
|
3108
|
-
|
|
3109
|
-
if (existsSync6(configPath)) {
|
|
3110
|
-
unlinkSync2(configPath);
|
|
3111
|
-
removed++;
|
|
3112
|
-
}
|
|
1953
|
+
removed += removeProjectFiles([CONFIG_FILE]).length;
|
|
3113
1954
|
uninstallHook();
|
|
3114
1955
|
}
|
|
3115
1956
|
if (opts.db) {
|
|
@@ -3120,20 +1961,49 @@ program.command("reset").description("Remove memory-core generated files and loc
|
|
|
3120
1961
|
if (ok) {
|
|
3121
1962
|
await getPool().query("DROP TABLE IF EXISTS memories");
|
|
3122
1963
|
await closePool();
|
|
3123
|
-
console.log(
|
|
1964
|
+
console.log(chalk2.yellow("Dropped memories table"));
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
console.log(chalk2.green(`Reset complete. Removed ${removed} files.`));
|
|
1968
|
+
});
|
|
1969
|
+
program.command("uninstall").description("Remove memory-core from the current project").option("--db", "Also drop the memories table after confirmation").action(async (opts) => {
|
|
1970
|
+
const generatedFiles = OUTPUT_FILES.map((file) => file.path);
|
|
1971
|
+
const gitignoreEntries = [...generatedFiles, ...LOCAL_GENERATED_FILES];
|
|
1972
|
+
const removed = removeProjectFiles([
|
|
1973
|
+
...generatedFiles,
|
|
1974
|
+
...LOCAL_STATE_FILES,
|
|
1975
|
+
CI_WORKFLOW_FILE
|
|
1976
|
+
]);
|
|
1977
|
+
uninstallHook();
|
|
1978
|
+
const cleanedGitignore = removeMemoryCoreGitignoreBlock(gitignoreEntries);
|
|
1979
|
+
if (opts.db) {
|
|
1980
|
+
const ok = await confirm({
|
|
1981
|
+
message: "Drop the memories table from the configured database?",
|
|
1982
|
+
default: false
|
|
1983
|
+
});
|
|
1984
|
+
if (ok) {
|
|
1985
|
+
await getPool().query("DROP TABLE IF EXISTS memories");
|
|
1986
|
+
await closePool();
|
|
1987
|
+
console.log(chalk2.yellow("Dropped memories table"));
|
|
3124
1988
|
}
|
|
3125
1989
|
}
|
|
3126
|
-
console.log(
|
|
1990
|
+
console.log(chalk2.green(`Uninstall complete. Removed ${removed.length} files.`));
|
|
1991
|
+
if (removed.length > 0) {
|
|
1992
|
+
removed.forEach((file) => console.log(chalk2.gray(` \u2713 ${file}`)));
|
|
1993
|
+
}
|
|
1994
|
+
if (cleanedGitignore) {
|
|
1995
|
+
console.log(chalk2.gray(" \u2713 cleaned .gitignore memory-core block"));
|
|
1996
|
+
}
|
|
3127
1997
|
});
|
|
3128
1998
|
program.command("stats").description("Show violation counters recorded by check and watch").action(() => {
|
|
3129
|
-
const statsPath =
|
|
3130
|
-
if (!
|
|
3131
|
-
console.log(
|
|
1999
|
+
const statsPath = join3(process.cwd(), ".memory-core-stats.json");
|
|
2000
|
+
if (!existsSync3(statsPath)) {
|
|
2001
|
+
console.log(chalk2.yellow("\n No violation stats recorded yet.\n"));
|
|
3132
2002
|
return;
|
|
3133
2003
|
}
|
|
3134
|
-
const stats = JSON.parse(
|
|
2004
|
+
const stats = JSON.parse(readFileSync3(statsPath, "utf-8"));
|
|
3135
2005
|
const printTop = (label, values = {}) => {
|
|
3136
|
-
console.log(
|
|
2006
|
+
console.log(chalk2.bold(`
|
|
3137
2007
|
${label}
|
|
3138
2008
|
`));
|
|
3139
2009
|
Object.entries(values).sort((a, b) => b[1] - a[1]).slice(0, 10).forEach(([name, count], index) => {
|
|
@@ -3144,10 +2014,18 @@ program.command("stats").description("Show violation counters recorded by check
|
|
|
3144
2014
|
printTop("Top files", stats.files);
|
|
3145
2015
|
console.log();
|
|
3146
2016
|
});
|
|
2017
|
+
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) => {
|
|
2018
|
+
const { startDashboard } = await import("./dashboard-server-EVN4FL4L.js");
|
|
2019
|
+
await startDashboard({
|
|
2020
|
+
port: parseInt(opts.port, 10),
|
|
2021
|
+
path: opts.path,
|
|
2022
|
+
watch: opts.watch
|
|
2023
|
+
});
|
|
2024
|
+
});
|
|
3147
2025
|
program.command("seed").description("Load all predefined memories into the database").option("--arch <architecture>", "Only seed a specific architecture (e.g. clean-architecture)").option("--force", "Re-seed even if memories already exist", false).action(async (opts) => {
|
|
3148
2026
|
await runMigrations();
|
|
3149
2027
|
const filtered = opts.arch ? seeds.filter((s) => s.architecture === opts.arch || s.architecture === "global") : seeds;
|
|
3150
|
-
console.log(
|
|
2028
|
+
console.log(chalk2.bold.cyan(`
|
|
3151
2029
|
Seeding ${filtered.length} memories\u2026
|
|
3152
2030
|
`));
|
|
3153
2031
|
let saved = 0;
|
|
@@ -3169,15 +2047,15 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
3169
2047
|
if (opts.force) {
|
|
3170
2048
|
await saveMemory(payload);
|
|
3171
2049
|
saved++;
|
|
3172
|
-
spinner.succeed(
|
|
2050
|
+
spinner.succeed(chalk2.gray(`[${seed.architecture}] ${seed.title}`));
|
|
3173
2051
|
} else {
|
|
3174
2052
|
const result = await upsertMemory(payload);
|
|
3175
2053
|
if (result === "inserted") {
|
|
3176
2054
|
saved++;
|
|
3177
|
-
spinner.succeed(
|
|
2055
|
+
spinner.succeed(chalk2.gray(`[${seed.architecture}] ${seed.title}`));
|
|
3178
2056
|
} else {
|
|
3179
2057
|
skipped++;
|
|
3180
|
-
spinner.info(
|
|
2058
|
+
spinner.info(chalk2.gray(`Already exists \u2014 [${seed.architecture}] ${seed.title}`));
|
|
3181
2059
|
}
|
|
3182
2060
|
}
|
|
3183
2061
|
} catch (err) {
|
|
@@ -3185,7 +2063,7 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
3185
2063
|
skipped++;
|
|
3186
2064
|
}
|
|
3187
2065
|
}
|
|
3188
|
-
console.log(
|
|
2066
|
+
console.log(chalk2.bold.green(`
|
|
3189
2067
|
Done. ${saved} memories seeded, ${skipped} skipped.
|
|
3190
2068
|
`));
|
|
3191
2069
|
await closePool();
|
|
@@ -3194,21 +2072,21 @@ program.command("global").description("Sync your memory into every AI agent glob
|
|
|
3194
2072
|
const home = homedir();
|
|
3195
2073
|
const GLOBAL_TARGETS = [
|
|
3196
2074
|
// Claude Code
|
|
3197
|
-
{ label: "Claude Code", path:
|
|
2075
|
+
{ label: "Claude Code", path: join3(home, ".claude/CLAUDE.md"), type: "md" },
|
|
3198
2076
|
// GitHub Copilot (VS Code)
|
|
3199
|
-
{ label: "Copilot", path:
|
|
2077
|
+
{ label: "Copilot", path: join3(home, "Library/Application Support/Code/User/settings.json"), type: "vscode-copilot" },
|
|
3200
2078
|
// Cursor global rules
|
|
3201
|
-
{ label: "Cursor", path:
|
|
2079
|
+
{ label: "Cursor", path: join3(home, ".cursor/rules/memory-core.mdc"), type: "md" },
|
|
3202
2080
|
// Cline (VS Code)
|
|
3203
|
-
{ label: "Cline", path:
|
|
2081
|
+
{ label: "Cline", path: join3(home, "Library/Application Support/Code/User/settings.json"), type: "vscode-cline" },
|
|
3204
2082
|
// Continue.dev global config
|
|
3205
|
-
{ label: "Continue.dev", path:
|
|
2083
|
+
{ label: "Continue.dev", path: join3(home, ".continue/config.json"), type: "continue" },
|
|
3206
2084
|
// Aider global config
|
|
3207
|
-
{ label: "Aider", path:
|
|
2085
|
+
{ label: "Aider", path: join3(home, ".aider.conf.yml"), type: "aider" },
|
|
3208
2086
|
// Zed global settings
|
|
3209
|
-
{ label: "Zed AI", path:
|
|
2087
|
+
{ label: "Zed AI", path: join3(home, ".config/zed/settings.json"), type: "zed" },
|
|
3210
2088
|
// Windsurf global rules
|
|
3211
|
-
{ label: "Windsurf", path:
|
|
2089
|
+
{ label: "Windsurf", path: join3(home, ".windsurf/rules/memory-core.md"), type: "md" }
|
|
3212
2090
|
];
|
|
3213
2091
|
const spinner = ora("Fetching global memories\u2026").start();
|
|
3214
2092
|
let memories = [];
|
|
@@ -3232,14 +2110,14 @@ ${rulesText}
|
|
|
3232
2110
|
`;
|
|
3233
2111
|
const written = [];
|
|
3234
2112
|
const skipped = [];
|
|
3235
|
-
const
|
|
3236
|
-
|
|
3237
|
-
|
|
2113
|
+
const writeFile = (filePath, content) => {
|
|
2114
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
2115
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
3238
2116
|
};
|
|
3239
2117
|
const readJson = (filePath) => {
|
|
3240
|
-
if (!
|
|
2118
|
+
if (!existsSync3(filePath)) return {};
|
|
3241
2119
|
try {
|
|
3242
|
-
return JSON.parse(
|
|
2120
|
+
return JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
3243
2121
|
} catch {
|
|
3244
2122
|
return {};
|
|
3245
2123
|
}
|
|
@@ -3248,7 +2126,7 @@ ${rulesText}
|
|
|
3248
2126
|
for (const target of GLOBAL_TARGETS) {
|
|
3249
2127
|
try {
|
|
3250
2128
|
if (target.type === "md") {
|
|
3251
|
-
|
|
2129
|
+
writeFile(target.path, mdContent);
|
|
3252
2130
|
written.push(target.label);
|
|
3253
2131
|
} else if (target.type === "vscode-copilot" || target.type === "vscode-cline") {
|
|
3254
2132
|
if (!processedVscode.done) {
|
|
@@ -3261,40 +2139,40 @@ ${rulesText}
|
|
|
3261
2139
|
if (target.type === "vscode-cline") {
|
|
3262
2140
|
processedVscode.settings["cline.customInstructions"] = systemPrompt;
|
|
3263
2141
|
}
|
|
3264
|
-
|
|
2142
|
+
writeFile(target.path, JSON.stringify(processedVscode.settings, null, 2));
|
|
3265
2143
|
written.push(target.label);
|
|
3266
2144
|
} else if (target.type === "continue") {
|
|
3267
2145
|
const config = readJson(target.path);
|
|
3268
2146
|
config["systemMessage"] = systemPrompt;
|
|
3269
|
-
|
|
2147
|
+
writeFile(target.path, JSON.stringify(config, null, 2));
|
|
3270
2148
|
written.push(target.label);
|
|
3271
2149
|
} else if (target.type === "aider") {
|
|
3272
2150
|
const aiderContent = `# Aider global config \u2014 synced by memory-core
|
|
3273
2151
|
read:
|
|
3274
2152
|
- ~/.claude/CLAUDE.md
|
|
3275
2153
|
`;
|
|
3276
|
-
|
|
2154
|
+
writeFile(target.path, aiderContent);
|
|
3277
2155
|
written.push(target.label);
|
|
3278
2156
|
} else if (target.type === "zed") {
|
|
3279
2157
|
const config = readJson(target.path);
|
|
3280
2158
|
const assistant = config["assistant"] ?? {};
|
|
3281
2159
|
assistant["system_prompt"] = systemPrompt;
|
|
3282
2160
|
config["assistant"] = assistant;
|
|
3283
|
-
|
|
2161
|
+
writeFile(target.path, JSON.stringify(config, null, 2));
|
|
3284
2162
|
written.push(target.label);
|
|
3285
2163
|
}
|
|
3286
2164
|
} catch {
|
|
3287
2165
|
skipped.push(target.label);
|
|
3288
2166
|
}
|
|
3289
2167
|
}
|
|
3290
|
-
spinner.succeed(
|
|
3291
|
-
console.log(
|
|
3292
|
-
written.forEach((l) => console.log(
|
|
2168
|
+
spinner.succeed(chalk2.green(`Synced ${memories.length} memories \u2192 ${written.length} agents`));
|
|
2169
|
+
console.log(chalk2.green("\n Updated:"));
|
|
2170
|
+
written.forEach((l) => console.log(chalk2.gray(` \u2713 ${l}`)));
|
|
3293
2171
|
if (skipped.length) {
|
|
3294
|
-
console.log(
|
|
3295
|
-
skipped.forEach((l) => console.log(
|
|
2172
|
+
console.log(chalk2.yellow("\n Skipped (not installed):"));
|
|
2173
|
+
skipped.forEach((l) => console.log(chalk2.gray(` \u2717 ${l}`)));
|
|
3296
2174
|
}
|
|
3297
|
-
console.log(
|
|
2175
|
+
console.log(chalk2.bold("\n Every AI agent now follows your memory globally.\n"));
|
|
3298
2176
|
await closePool();
|
|
3299
2177
|
});
|
|
3300
2178
|
var provider = program.command("provider").description("Manage the code-checking provider configuration");
|
|
@@ -3329,10 +2207,10 @@ provider.command("set <name>").description("Set the code-checking provider (olla
|
|
|
3329
2207
|
writeRuntimeEnv(values, runtimeEnv.envPath);
|
|
3330
2208
|
applyRuntimeEnv(values);
|
|
3331
2209
|
ensureEnvFileIgnored(runtimeEnv.envPath);
|
|
3332
|
-
console.log(
|
|
3333
|
-
console.log(
|
|
2210
|
+
console.log(chalk2.green(`Updated provider: ${providerName}`));
|
|
2211
|
+
console.log(chalk2.gray(` Chat model: ${getConfiguredChatModel(values)}`));
|
|
3334
2212
|
} catch (err) {
|
|
3335
|
-
console.error(
|
|
2213
|
+
console.error(chalk2.red(`Provider update failed: ${err.message}`));
|
|
3336
2214
|
process.exit(1);
|
|
3337
2215
|
}
|
|
3338
2216
|
});
|
|
@@ -3354,10 +2232,10 @@ model.command("set <name>").description("Set the chat model used for code checki
|
|
|
3354
2232
|
writeRuntimeEnv(values, runtimeEnv.envPath);
|
|
3355
2233
|
applyRuntimeEnv(values);
|
|
3356
2234
|
ensureEnvFileIgnored(runtimeEnv.envPath);
|
|
3357
|
-
console.log(
|
|
3358
|
-
console.log(
|
|
2235
|
+
console.log(chalk2.green(`Updated ${opts.embedding ? "embedding" : "chat"} model: ${name}`));
|
|
2236
|
+
console.log(chalk2.gray(` Provider: ${providerName}`));
|
|
3359
2237
|
} catch (err) {
|
|
3360
|
-
console.error(
|
|
2238
|
+
console.error(chalk2.red(`Model update failed: ${err.message}`));
|
|
3361
2239
|
process.exit(1);
|
|
3362
2240
|
}
|
|
3363
2241
|
});
|
|
@@ -3369,7 +2247,7 @@ program.command("status").description("Show the current memory-core project and
|
|
|
3369
2247
|
try {
|
|
3370
2248
|
await printProjectStatus();
|
|
3371
2249
|
} catch (err) {
|
|
3372
|
-
console.error(
|
|
2250
|
+
console.error(chalk2.red(`Status failed: ${err.message}`));
|
|
3373
2251
|
process.exit(1);
|
|
3374
2252
|
}
|
|
3375
2253
|
});
|