@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
|
@@ -0,0 +1,1287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
embed
|
|
4
|
+
} from "./chunk-HAGRPKR3.js";
|
|
5
|
+
import {
|
|
6
|
+
searchMemories
|
|
7
|
+
} from "./chunk-WUL7HLAA.js";
|
|
8
|
+
|
|
9
|
+
// src/project-detector.ts
|
|
10
|
+
import { existsSync, readFileSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
function detectProject(cwd = process.cwd()) {
|
|
13
|
+
const has = (file) => existsSync(join(cwd, file));
|
|
14
|
+
const readJson = (file) => {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(join(cwd, file), "utf-8"));
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
if (has("next.config.js") || has("next.config.ts") || has("next.config.mjs")) {
|
|
22
|
+
return { language: "TypeScript", framework: "Next.js" };
|
|
23
|
+
}
|
|
24
|
+
if (has("artisan") && has("composer.json")) {
|
|
25
|
+
return { language: "PHP", framework: "Laravel" };
|
|
26
|
+
}
|
|
27
|
+
if (has("nuxt.config.ts") || has("nuxt.config.js")) {
|
|
28
|
+
return { language: "TypeScript", framework: "Nuxt.js" };
|
|
29
|
+
}
|
|
30
|
+
if (has("manage.py")) {
|
|
31
|
+
if (has("requirements.txt")) {
|
|
32
|
+
const req = readFileSync(join(cwd, "requirements.txt"), "utf-8");
|
|
33
|
+
if (req.includes("djangorestframework")) {
|
|
34
|
+
return { language: "Python", framework: "Django REST Framework" };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { language: "Python", framework: "Django" };
|
|
38
|
+
}
|
|
39
|
+
if (has("go.mod")) {
|
|
40
|
+
return { language: "Go", framework: "Go" };
|
|
41
|
+
}
|
|
42
|
+
if (has("Cargo.toml")) {
|
|
43
|
+
return { language: "Rust", framework: "Rust" };
|
|
44
|
+
}
|
|
45
|
+
if (has("pubspec.yaml")) {
|
|
46
|
+
return { language: "Dart", framework: "Flutter" };
|
|
47
|
+
}
|
|
48
|
+
if (has("pom.xml")) {
|
|
49
|
+
return { language: "Java", framework: "Spring Boot" };
|
|
50
|
+
}
|
|
51
|
+
if (has("build.gradle") || has("build.gradle.kts")) {
|
|
52
|
+
return { language: "Kotlin", framework: "Kotlin/JVM" };
|
|
53
|
+
}
|
|
54
|
+
if (has("package.json")) {
|
|
55
|
+
const pkg = readJson("package.json");
|
|
56
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
57
|
+
if (deps["@nestjs/core"]) return { language: "TypeScript", framework: "NestJS" };
|
|
58
|
+
if (deps["express"]) return { language: "TypeScript", framework: "Express.js" };
|
|
59
|
+
if (deps["fastify"]) return { language: "TypeScript", framework: "Fastify" };
|
|
60
|
+
if (deps["react"]) return { language: "TypeScript", framework: "React" };
|
|
61
|
+
if (deps["vue"]) return { language: "TypeScript", framework: "Vue.js" };
|
|
62
|
+
if (deps["svelte"]) return { language: "TypeScript", framework: "Svelte" };
|
|
63
|
+
return { language: "TypeScript/JavaScript", framework: "Node.js" };
|
|
64
|
+
}
|
|
65
|
+
if (has("requirements.txt") || has("pyproject.toml")) {
|
|
66
|
+
return { language: "Python", framework: "Python" };
|
|
67
|
+
}
|
|
68
|
+
return { language: "Unknown", framework: "Unknown" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/retriever.ts
|
|
72
|
+
async function retrieve(query, architecture, limit = 10) {
|
|
73
|
+
const embedding = await embed(query);
|
|
74
|
+
return searchMemories(embedding, architecture, limit);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/memory-selection.ts
|
|
78
|
+
var FRAMEWORK_ARCHITECTURE_MAP = {
|
|
79
|
+
Laravel: ["laravel-service-repository"],
|
|
80
|
+
"Next.js": ["nextjs"],
|
|
81
|
+
"Nuxt.js": ["nuxt"],
|
|
82
|
+
Go: ["go-api"],
|
|
83
|
+
NestJS: ["nestjs"],
|
|
84
|
+
React: ["react"],
|
|
85
|
+
"Vue.js": ["vue"],
|
|
86
|
+
Svelte: ["svelte"]
|
|
87
|
+
};
|
|
88
|
+
var KNOWN_ARCHITECTURE_KEYS = /* @__PURE__ */ new Set([
|
|
89
|
+
...Object.values(FRAMEWORK_ARCHITECTURE_MAP).flat(),
|
|
90
|
+
"angular",
|
|
91
|
+
"clean-architecture",
|
|
92
|
+
"express",
|
|
93
|
+
"fastify",
|
|
94
|
+
"hexagonal",
|
|
95
|
+
"modular-monolith",
|
|
96
|
+
"mvc",
|
|
97
|
+
"react-native"
|
|
98
|
+
]);
|
|
99
|
+
function normalizeText(value) {
|
|
100
|
+
return value.toLowerCase().replace(/[`"'()[\]{}.,:;!?/\\<>|=*+-]/g, " ").replace(/\s+/g, " ").trim();
|
|
101
|
+
}
|
|
102
|
+
function tokenSet(value) {
|
|
103
|
+
return new Set(
|
|
104
|
+
normalizeText(value).split(" ").filter((token) => token.length > 2)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
function similarityScore(a, b) {
|
|
108
|
+
const left = tokenSet(a);
|
|
109
|
+
const right = tokenSet(b);
|
|
110
|
+
if (left.size === 0 || right.size === 0) return 0;
|
|
111
|
+
let intersection = 0;
|
|
112
|
+
for (const token of left) {
|
|
113
|
+
if (right.has(token)) intersection++;
|
|
114
|
+
}
|
|
115
|
+
return 2 * intersection / (left.size + right.size);
|
|
116
|
+
}
|
|
117
|
+
function mergeMemory(primary, secondary) {
|
|
118
|
+
const mergedTags = [.../* @__PURE__ */ new Set([...primary.tags ?? [], ...secondary.tags ?? []])];
|
|
119
|
+
const reason = [primary.reason, secondary.reason].filter(Boolean).join(" | ") || void 0;
|
|
120
|
+
return {
|
|
121
|
+
...primary,
|
|
122
|
+
tags: mergedTags,
|
|
123
|
+
reason
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function memoryArchitectureKeys(memory) {
|
|
127
|
+
if (memory.architecture && memory.architecture !== "global") {
|
|
128
|
+
return [memory.architecture];
|
|
129
|
+
}
|
|
130
|
+
return (memory.tags ?? []).filter((tag) => KNOWN_ARCHITECTURE_KEYS.has(tag));
|
|
131
|
+
}
|
|
132
|
+
function getStackReason(memory, activeArchitectures) {
|
|
133
|
+
const architectureKeys = memoryArchitectureKeys(memory);
|
|
134
|
+
if (architectureKeys.length === 0) {
|
|
135
|
+
return {
|
|
136
|
+
included: true,
|
|
137
|
+
reason: "global memory: no architecture-specific tag"
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const matched = architectureKeys.filter((architecture) => activeArchitectures.has(architecture));
|
|
141
|
+
if (matched.length > 0) {
|
|
142
|
+
return {
|
|
143
|
+
included: true,
|
|
144
|
+
reason: `matched active architecture: ${matched.join(", ")}`
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const active = [...activeArchitectures].join(", ") || "none detected";
|
|
148
|
+
return {
|
|
149
|
+
included: false,
|
|
150
|
+
reason: `excluded: tagged for ${architectureKeys.join(", ")}; active stack is ${active}`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function dedupeMemories(memories, threshold = 0.8) {
|
|
154
|
+
const deduped = [];
|
|
155
|
+
for (const memory of memories) {
|
|
156
|
+
const existingIndex = deduped.findIndex((candidate) => {
|
|
157
|
+
if (candidate.content_hash && memory.content_hash && candidate.content_hash === memory.content_hash) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return similarityScore(candidate.content, memory.content) >= threshold;
|
|
161
|
+
});
|
|
162
|
+
if (existingIndex === -1) {
|
|
163
|
+
deduped.push(memory);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
deduped[existingIndex] = mergeMemory(deduped[existingIndex], memory);
|
|
167
|
+
}
|
|
168
|
+
return deduped;
|
|
169
|
+
}
|
|
170
|
+
function inferProjectArchitectures(cwd = process.cwd(), config) {
|
|
171
|
+
const inferred = /* @__PURE__ */ new Set();
|
|
172
|
+
if (config?.backendArchitecture) inferred.add(config.backendArchitecture);
|
|
173
|
+
if (config?.frontendFramework) inferred.add(config.frontendFramework);
|
|
174
|
+
if (config?.projectType === "backend" && !config.backendArchitecture) {
|
|
175
|
+
inferred.add("clean-architecture");
|
|
176
|
+
}
|
|
177
|
+
const detected = detectProject(cwd);
|
|
178
|
+
for (const architecture of FRAMEWORK_ARCHITECTURE_MAP[detected.framework] ?? []) {
|
|
179
|
+
inferred.add(architecture);
|
|
180
|
+
}
|
|
181
|
+
return [...inferred];
|
|
182
|
+
}
|
|
183
|
+
function getAllowPatterns(config) {
|
|
184
|
+
return [...new Set(config?.allowPatterns?.filter(Boolean) ?? [])];
|
|
185
|
+
}
|
|
186
|
+
function filterRelevantMemories(memories, config, cwd = process.cwd()) {
|
|
187
|
+
return explainMemorySelection(memories, config, cwd).included;
|
|
188
|
+
}
|
|
189
|
+
function explainMemorySelection(memories, config, cwd = process.cwd(), threshold = 0.8) {
|
|
190
|
+
const activeArchitectures = inferProjectArchitectures(cwd, config);
|
|
191
|
+
const activeSet = new Set(activeArchitectures);
|
|
192
|
+
const included = [];
|
|
193
|
+
const decisions = [];
|
|
194
|
+
for (const memory of memories) {
|
|
195
|
+
const stackDecision = getStackReason(memory, activeSet);
|
|
196
|
+
if (!stackDecision.included) {
|
|
197
|
+
decisions.push({
|
|
198
|
+
memory,
|
|
199
|
+
status: "excluded",
|
|
200
|
+
reason: stackDecision.reason
|
|
201
|
+
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const existingIndex = included.findIndex((candidate) => {
|
|
205
|
+
if (candidate.content_hash && memory.content_hash && candidate.content_hash === memory.content_hash) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return similarityScore(candidate.content, memory.content) >= threshold;
|
|
209
|
+
});
|
|
210
|
+
if (existingIndex === -1) {
|
|
211
|
+
included.push(memory);
|
|
212
|
+
decisions.push({
|
|
213
|
+
memory,
|
|
214
|
+
status: "included",
|
|
215
|
+
reason: stackDecision.reason
|
|
216
|
+
});
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
included[existingIndex] = mergeMemory(included[existingIndex], memory);
|
|
220
|
+
decisions.push({
|
|
221
|
+
memory,
|
|
222
|
+
status: "excluded",
|
|
223
|
+
reason: `duplicate or near-duplicate of memory #${included[existingIndex].id}`
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
included,
|
|
228
|
+
excluded: decisions.filter((decision) => decision.status === "excluded"),
|
|
229
|
+
decisions,
|
|
230
|
+
activeArchitectures
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function buildContextQuery(parts, maxLength = 1200) {
|
|
234
|
+
return parts.filter(Boolean).join("\n").slice(0, maxLength);
|
|
235
|
+
}
|
|
236
|
+
async function retrieveContextualMemories(options) {
|
|
237
|
+
return (await retrieveMemorySelection(options)).included;
|
|
238
|
+
}
|
|
239
|
+
async function retrieveMemorySelection(options) {
|
|
240
|
+
const architectures = inferProjectArchitectures(options.cwd, options.config);
|
|
241
|
+
const memories = await retrieve(options.query, architectures, options.limit ?? 15);
|
|
242
|
+
return explainMemorySelection(memories, options.config, options.cwd);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/generator.ts
|
|
246
|
+
import { readFileSync as readFileSync2, readdirSync, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
|
|
247
|
+
import { join as join2, dirname } from "path";
|
|
248
|
+
import { fileURLToPath } from "url";
|
|
249
|
+
import Handlebars from "handlebars";
|
|
250
|
+
import yaml from "js-yaml";
|
|
251
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
252
|
+
var __dirname = dirname(__filename);
|
|
253
|
+
var PKG_ROOT = join2(__dirname, "..");
|
|
254
|
+
var OUTPUT_FILES = [
|
|
255
|
+
{ template: "CLAUDE.md.hbs", path: "CLAUDE.md", agent: "Claude Code" },
|
|
256
|
+
{ template: "copilot-instructions.md.hbs", path: ".github/copilot-instructions.md", agent: "GitHub Copilot" },
|
|
257
|
+
{ template: "cursorrules.hbs", path: ".cursorrules", agent: "Cursor" },
|
|
258
|
+
{ template: "cursor-rule.mdc.hbs", path: ".cursor/rules/memory-core.mdc", agent: "Cursor" },
|
|
259
|
+
{ template: "windsurfrules.hbs", path: ".windsurfrules", agent: "Windsurf" },
|
|
260
|
+
{ template: "clinerules.hbs", path: ".clinerules", agent: "Cline" },
|
|
261
|
+
{ template: "roo-rule.md.hbs", path: ".roo/rules/memory-core.md", agent: "Roo Code" },
|
|
262
|
+
{ template: "aider.conf.yml.hbs", path: ".aider.conf.yml", agent: "Aider" },
|
|
263
|
+
{ template: "continue-config.json.hbs", path: ".continue/config.json", agent: "Continue.dev", skipIfExists: true },
|
|
264
|
+
{ template: "DEVIN.md.hbs", path: "DEVIN.md", agent: "Devin" },
|
|
265
|
+
{ template: "amazonq-guidelines.md.hbs", path: ".amazonq/dev/guidelines.md", agent: "Amazon Q" },
|
|
266
|
+
{ template: "gemini-styleguide.md.hbs", path: ".gemini/styleguide.md", agent: "Gemini Code Assist" },
|
|
267
|
+
{ template: "zed-settings.json.hbs", path: ".zed/settings.json", agent: "Zed AI", skipIfExists: true },
|
|
268
|
+
{ template: "jetbrains-ai.md.hbs", path: ".idea/ai-instructions.md", agent: "JetBrains AI" },
|
|
269
|
+
{ template: "AGENTS.md.hbs", path: "AGENTS.md", agent: "OpenHands" },
|
|
270
|
+
{ template: "AI_RULES.md.hbs", path: "AI_RULES.md", agent: "Shared" },
|
|
271
|
+
{ template: "ARCHITECTURE.md.hbs", path: "ARCHITECTURE.md", agent: "Shared" },
|
|
272
|
+
{ template: "PROJECT_MEMORY.md.hbs", path: "PROJECT_MEMORY.md", agent: "Shared" }
|
|
273
|
+
];
|
|
274
|
+
var AGENT_NAMES = [...new Set(OUTPUT_FILES.map((f) => f.agent))];
|
|
275
|
+
Handlebars.registerHelper(
|
|
276
|
+
"join",
|
|
277
|
+
(arr, sep) => Array.isArray(arr) ? arr.join(sep) : ""
|
|
278
|
+
);
|
|
279
|
+
Handlebars.registerHelper(
|
|
280
|
+
"bullet",
|
|
281
|
+
(arr) => Array.isArray(arr) ? arr.map((i) => `- ${i}`).join("\n") : ""
|
|
282
|
+
);
|
|
283
|
+
Handlebars.registerHelper(
|
|
284
|
+
"numbered",
|
|
285
|
+
(arr) => Array.isArray(arr) ? arr.map((i, idx) => `${idx + 1}. ${i}`).join("\n") : ""
|
|
286
|
+
);
|
|
287
|
+
Handlebars.registerHelper("json", (val) => JSON.stringify(val, null, 2));
|
|
288
|
+
Handlebars.registerHelper("memoryBlock", (memory) => {
|
|
289
|
+
const meta = [memory.type, memory.architecture].filter(Boolean).join(" \xB7 ");
|
|
290
|
+
const label = memory.title ? `${memory.title}: ${memory.content}` : memory.content;
|
|
291
|
+
const lines = [`- [${meta || "memory"}] ${label}`];
|
|
292
|
+
if (memory.reason) lines.push(` Why: ${memory.reason}`);
|
|
293
|
+
if (memory.context?.appliesTo?.length) lines.push(` Use when: ${memory.context.appliesTo.join("; ")}`);
|
|
294
|
+
if (memory.context?.avoidWhen?.length) lines.push(` Avoid when: ${memory.context.avoidWhen.join("; ")}`);
|
|
295
|
+
if (memory.context?.examples?.length) {
|
|
296
|
+
lines.push(" Examples:");
|
|
297
|
+
for (const example of memory.context.examples) lines.push(` - ${example}`);
|
|
298
|
+
}
|
|
299
|
+
if (memory.tags?.length) lines.push(` Tags: ${memory.tags.join(", ")}`);
|
|
300
|
+
if (memory.project_name || memory.context?.source) {
|
|
301
|
+
lines.push(` Source: ${memory.context?.source ?? memory.project_name}`);
|
|
302
|
+
}
|
|
303
|
+
return new Handlebars.SafeString(lines.join("\n"));
|
|
304
|
+
});
|
|
305
|
+
function loadProfile(name) {
|
|
306
|
+
const profilePath = join2(PKG_ROOT, "profiles", `${name}.yml`);
|
|
307
|
+
if (!existsSync2(profilePath)) throw new Error(`Profile not found: ${name}`);
|
|
308
|
+
return yaml.load(readFileSync2(profilePath, "utf-8"));
|
|
309
|
+
}
|
|
310
|
+
function listProfiles(layer) {
|
|
311
|
+
const files = readdirSync(join2(PKG_ROOT, "profiles")).filter((f) => f.endsWith(".yml"));
|
|
312
|
+
const all = files.map((f) => yaml.load(readFileSync2(join2(PKG_ROOT, "profiles", f), "utf-8")));
|
|
313
|
+
if (!layer) return all;
|
|
314
|
+
if (layer === "backend") return all.filter((p) => p.layer === "backend" || p.layer === "fullstack");
|
|
315
|
+
if (layer === "frontend") return all.filter((p) => p.layer === "frontend" || p.layer === "fullstack");
|
|
316
|
+
return all;
|
|
317
|
+
}
|
|
318
|
+
function buildTemplateData(options, cwd = process.cwd()) {
|
|
319
|
+
const backend = options.backendArchitecture ? loadProfile(options.backendArchitecture) : null;
|
|
320
|
+
const frontend = options.frontendFramework ? loadProfile(options.frontendFramework) : null;
|
|
321
|
+
const dedupedMemories = filterRelevantMemories(options.memories, {
|
|
322
|
+
projectType: options.projectType,
|
|
323
|
+
backendArchitecture: options.backendArchitecture,
|
|
324
|
+
frontendFramework: options.frontendFramework,
|
|
325
|
+
language: options.language
|
|
326
|
+
}, cwd);
|
|
327
|
+
const allRules = [
|
|
328
|
+
...backend?.rules ?? [],
|
|
329
|
+
...frontend?.rules ?? []
|
|
330
|
+
];
|
|
331
|
+
const allFolders = [
|
|
332
|
+
...backend?.folders ?? [],
|
|
333
|
+
...frontend?.folders ?? []
|
|
334
|
+
];
|
|
335
|
+
const allAvoid = [
|
|
336
|
+
...backend?.avoid ?? [],
|
|
337
|
+
...frontend?.avoid ?? []
|
|
338
|
+
];
|
|
339
|
+
const archLabel = [
|
|
340
|
+
backend ? `Backend: ${backend.displayName}` : null,
|
|
341
|
+
frontend ? `Frontend: ${frontend.displayName}` : null
|
|
342
|
+
].filter(Boolean).join(" \xB7 ");
|
|
343
|
+
return {
|
|
344
|
+
projectName: options.projectName,
|
|
345
|
+
projectType: options.projectType,
|
|
346
|
+
isBackend: options.projectType === "backend" || options.projectType === "fullstack",
|
|
347
|
+
isFrontend: options.projectType === "frontend" || options.projectType === "fullstack",
|
|
348
|
+
isFullstack: options.projectType === "fullstack",
|
|
349
|
+
// backend
|
|
350
|
+
hasBackend: !!backend,
|
|
351
|
+
backendArchitecture: backend?.displayName,
|
|
352
|
+
backendDescription: backend?.description,
|
|
353
|
+
backendRules: backend?.rules ?? [],
|
|
354
|
+
backendFolders: backend?.folders ?? [],
|
|
355
|
+
backendAvoid: backend?.avoid ?? [],
|
|
356
|
+
// frontend
|
|
357
|
+
hasFrontend: !!frontend,
|
|
358
|
+
frontendFramework: frontend?.displayName,
|
|
359
|
+
frontendDescription: frontend?.description,
|
|
360
|
+
frontendRules: frontend?.rules ?? [],
|
|
361
|
+
frontendFolders: frontend?.folders ?? [],
|
|
362
|
+
frontendAvoid: frontend?.avoid ?? [],
|
|
363
|
+
// combined — used by simple templates
|
|
364
|
+
architecture: archLabel,
|
|
365
|
+
rules: allRules,
|
|
366
|
+
folders: allFolders,
|
|
367
|
+
avoid: allAvoid,
|
|
368
|
+
description: [backend?.description, frontend?.description].filter(Boolean).join(" | "),
|
|
369
|
+
// memories
|
|
370
|
+
memories: dedupedMemories,
|
|
371
|
+
hasMemories: dedupedMemories.length > 0,
|
|
372
|
+
// misc
|
|
373
|
+
language: options.language,
|
|
374
|
+
caveman: options.caveman,
|
|
375
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function renderTemplate(templateName, data) {
|
|
379
|
+
const templatePath = join2(PKG_ROOT, "templates", templateName);
|
|
380
|
+
if (!existsSync2(templatePath)) throw new Error(`Template not found: ${templateName}`);
|
|
381
|
+
return Handlebars.compile(readFileSync2(templatePath, "utf-8"))(data);
|
|
382
|
+
}
|
|
383
|
+
function writeFile(filePath, content) {
|
|
384
|
+
const dir = dirname(filePath);
|
|
385
|
+
if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
|
|
386
|
+
if (existsSync2(filePath)) {
|
|
387
|
+
const existing = readFileSync2(filePath, "utf-8");
|
|
388
|
+
if (existing === content) return "skipped";
|
|
389
|
+
}
|
|
390
|
+
writeFileSync(filePath, content, "utf-8");
|
|
391
|
+
return "written";
|
|
392
|
+
}
|
|
393
|
+
async function generate(options, cwd = process.cwd(), onlyAgents) {
|
|
394
|
+
const data = buildTemplateData(options, cwd);
|
|
395
|
+
const written = [];
|
|
396
|
+
const skipped = [];
|
|
397
|
+
const files = onlyAgents ? OUTPUT_FILES.filter((f) => onlyAgents.includes(f.agent)) : OUTPUT_FILES;
|
|
398
|
+
for (const output of files) {
|
|
399
|
+
const targetPath = join2(cwd, output.path);
|
|
400
|
+
if (output.skipIfExists && existsSync2(targetPath)) {
|
|
401
|
+
skipped.push(output.path);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const content = renderTemplate(output.template, data);
|
|
406
|
+
const result = writeFile(targetPath, content);
|
|
407
|
+
if (result === "written") written.push(output.path);
|
|
408
|
+
else skipped.push(output.path);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
if (!(err instanceof Error && err.message.includes("Template not found"))) throw err;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return { written, skipped };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/seeds.ts
|
|
417
|
+
var seeds = [
|
|
418
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
419
|
+
// GLOBAL — applies to every architecture
|
|
420
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
421
|
+
// ── API Design ───────────────────────────────────────────────────────────
|
|
422
|
+
{ 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"] },
|
|
423
|
+
{ 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"] },
|
|
424
|
+
{ 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"] },
|
|
425
|
+
{ 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"] },
|
|
426
|
+
{ 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"] },
|
|
427
|
+
{ 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"] },
|
|
428
|
+
{ 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"] },
|
|
429
|
+
{ 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"] },
|
|
430
|
+
// ── Validation & Input ───────────────────────────────────────────────────
|
|
431
|
+
{ 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"] },
|
|
432
|
+
{ 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"] },
|
|
433
|
+
{ 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"] },
|
|
434
|
+
// ── Security ─────────────────────────────────────────────────────────────
|
|
435
|
+
{ 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"] },
|
|
436
|
+
{ 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"] },
|
|
437
|
+
{ 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"] },
|
|
438
|
+
{ 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"] },
|
|
439
|
+
{ 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"] },
|
|
440
|
+
{ 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"] },
|
|
441
|
+
{ 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"] },
|
|
442
|
+
{ 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"] },
|
|
443
|
+
{ 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"] },
|
|
444
|
+
// ── Error Handling ───────────────────────────────────────────────────────
|
|
445
|
+
{ 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"] },
|
|
446
|
+
{ 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"] },
|
|
447
|
+
{ 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"] },
|
|
448
|
+
// ── Database ─────────────────────────────────────────────────────────────
|
|
449
|
+
{ 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"] },
|
|
450
|
+
{ 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"] },
|
|
451
|
+
{ 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"] },
|
|
452
|
+
{ 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"] },
|
|
453
|
+
{ 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"] },
|
|
454
|
+
{ 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"] },
|
|
455
|
+
{ 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"] },
|
|
456
|
+
// ── Caching ──────────────────────────────────────────────────────────────
|
|
457
|
+
{ 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"] },
|
|
458
|
+
{ 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"] },
|
|
459
|
+
{ 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"] },
|
|
460
|
+
// ── Logging & Observability ───────────────────────────────────────────────
|
|
461
|
+
{ 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"] },
|
|
462
|
+
{ 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"] },
|
|
463
|
+
{ 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"] },
|
|
464
|
+
{ 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"] },
|
|
465
|
+
// ── Testing ──────────────────────────────────────────────────────────────
|
|
466
|
+
{ 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"] },
|
|
467
|
+
{ 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"] },
|
|
468
|
+
{ 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"] },
|
|
469
|
+
{ 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"] },
|
|
470
|
+
{ 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"] },
|
|
471
|
+
{ 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"] },
|
|
472
|
+
// ── Code Quality ─────────────────────────────────────────────────────────
|
|
473
|
+
{ 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"] },
|
|
474
|
+
{ 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"] },
|
|
475
|
+
{ 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"] },
|
|
476
|
+
{ 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"] },
|
|
477
|
+
{ 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"] },
|
|
478
|
+
{ 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"] },
|
|
479
|
+
// ── DevOps & Reliability ─────────────────────────────────────────────────
|
|
480
|
+
{ 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"] },
|
|
481
|
+
{ 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"] },
|
|
482
|
+
{ 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"] },
|
|
483
|
+
{ 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"] },
|
|
484
|
+
// ── Universal Coding Practices ────────────────────────────────────────────
|
|
485
|
+
{ 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"] },
|
|
486
|
+
{ 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"] },
|
|
487
|
+
{ 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"] },
|
|
488
|
+
{ 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"] },
|
|
489
|
+
{ 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"] },
|
|
490
|
+
{ 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"] },
|
|
491
|
+
{ 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"] },
|
|
492
|
+
{ 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"] },
|
|
493
|
+
{ 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"] },
|
|
494
|
+
{ 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"] },
|
|
495
|
+
{ 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"] },
|
|
496
|
+
{ 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"] },
|
|
497
|
+
{ 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"] },
|
|
498
|
+
{ 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"] },
|
|
499
|
+
{ 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"] },
|
|
500
|
+
{ 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"] },
|
|
501
|
+
{ 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"] },
|
|
502
|
+
{ 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"] },
|
|
503
|
+
{ 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"] },
|
|
504
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
505
|
+
// CLEAN ARCHITECTURE
|
|
506
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
507
|
+
// ── Core Rules ───────────────────────────────────────────────────────────
|
|
508
|
+
{ 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"] },
|
|
509
|
+
{ 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"] },
|
|
510
|
+
{ 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"] },
|
|
511
|
+
{ 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"] },
|
|
512
|
+
{ 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"] },
|
|
513
|
+
{ 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"] },
|
|
514
|
+
// ── Domain ───────────────────────────────────────────────────────────────
|
|
515
|
+
{ 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"] },
|
|
516
|
+
{ 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"] },
|
|
517
|
+
{ 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"] },
|
|
518
|
+
{ 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"] },
|
|
519
|
+
{ 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"] },
|
|
520
|
+
{ 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"] },
|
|
521
|
+
// ── Application ──────────────────────────────────────────────────────────
|
|
522
|
+
{ 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"] },
|
|
523
|
+
{ 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"] },
|
|
524
|
+
{ 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"] },
|
|
525
|
+
{ 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"] },
|
|
526
|
+
// ── Infrastructure ────────────────────────────────────────────────────────
|
|
527
|
+
{ 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"] },
|
|
528
|
+
{ 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"] },
|
|
529
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
530
|
+
// MODULAR MONOLITH
|
|
531
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
532
|
+
{ 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"] },
|
|
533
|
+
{ 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"] },
|
|
534
|
+
{ 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"] },
|
|
535
|
+
{ 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"] },
|
|
536
|
+
{ 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"] },
|
|
537
|
+
{ 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"] },
|
|
538
|
+
{ 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"] },
|
|
539
|
+
{ 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"] },
|
|
540
|
+
{ 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"] },
|
|
541
|
+
{ 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"] },
|
|
542
|
+
{ 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"] },
|
|
543
|
+
{ 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"] },
|
|
544
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
545
|
+
// MVC
|
|
546
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
547
|
+
{ 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"] },
|
|
548
|
+
{ 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"] },
|
|
549
|
+
{ 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"] },
|
|
550
|
+
{ 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"] },
|
|
551
|
+
{ 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"] },
|
|
552
|
+
{ 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"] },
|
|
553
|
+
{ 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"] },
|
|
554
|
+
{ 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"] },
|
|
555
|
+
{ 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"] },
|
|
556
|
+
{ 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"] },
|
|
557
|
+
{ 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"] },
|
|
558
|
+
{ 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"] },
|
|
559
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
560
|
+
// HEXAGONAL ARCHITECTURE
|
|
561
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
562
|
+
{ 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"] },
|
|
563
|
+
{ 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"] },
|
|
564
|
+
{ 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"] },
|
|
565
|
+
{ 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"] },
|
|
566
|
+
{ 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"] },
|
|
567
|
+
{ 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"] },
|
|
568
|
+
{ 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"] },
|
|
569
|
+
{ 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"] },
|
|
570
|
+
{ 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"] },
|
|
571
|
+
{ 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"] },
|
|
572
|
+
{ 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"] },
|
|
573
|
+
{ 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"] },
|
|
574
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
575
|
+
// GO REST API
|
|
576
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
577
|
+
// ── Package Structure ────────────────────────────────────────────────────
|
|
578
|
+
{ 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"] },
|
|
579
|
+
{ 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"] },
|
|
580
|
+
{ 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"] },
|
|
581
|
+
{ 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"] },
|
|
582
|
+
// ── Error Handling ───────────────────────────────────────────────────────
|
|
583
|
+
{ 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"] },
|
|
584
|
+
{ 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"] },
|
|
585
|
+
{ 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"] },
|
|
586
|
+
{ 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"] },
|
|
587
|
+
// ── Interfaces & Types ───────────────────────────────────────────────────
|
|
588
|
+
{ 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"] },
|
|
589
|
+
{ 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"] },
|
|
590
|
+
// ── Context & Concurrency ────────────────────────────────────────────────
|
|
591
|
+
{ 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"] },
|
|
592
|
+
{ 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"] },
|
|
593
|
+
// ── Configuration & Logging ──────────────────────────────────────────────
|
|
594
|
+
{ 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"] },
|
|
595
|
+
{ 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"] },
|
|
596
|
+
// ── Testing ──────────────────────────────────────────────────────────────
|
|
597
|
+
{ 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"] },
|
|
598
|
+
{ 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"] },
|
|
599
|
+
{ 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"] },
|
|
600
|
+
// ── Middleware & Security ────────────────────────────────────────────────
|
|
601
|
+
{ 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"] },
|
|
602
|
+
{ 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"] },
|
|
603
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
604
|
+
// LARAVEL SERVICE REPOSITORY
|
|
605
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
606
|
+
// ── Core Rules ───────────────────────────────────────────────────────────
|
|
607
|
+
{ 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"] },
|
|
608
|
+
{ 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"] },
|
|
609
|
+
{ 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"] },
|
|
610
|
+
{ 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"] },
|
|
611
|
+
{ 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"] },
|
|
612
|
+
{ 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"] },
|
|
613
|
+
// ── Patterns ─────────────────────────────────────────────────────────────
|
|
614
|
+
{ 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"] },
|
|
615
|
+
{ 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"] },
|
|
616
|
+
{ 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"] },
|
|
617
|
+
{ 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"] },
|
|
618
|
+
{ 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"] },
|
|
619
|
+
{ 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"] },
|
|
620
|
+
// ── Advanced ─────────────────────────────────────────────────────────────
|
|
621
|
+
{ 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"] },
|
|
622
|
+
{ 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"] },
|
|
623
|
+
{ 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"] },
|
|
624
|
+
{ 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"] },
|
|
625
|
+
{ 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"] },
|
|
626
|
+
{ 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"] },
|
|
627
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
628
|
+
// REACT
|
|
629
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
630
|
+
{ 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"] },
|
|
631
|
+
{ 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"] },
|
|
632
|
+
{ 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"] },
|
|
633
|
+
{ 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"] },
|
|
634
|
+
{ 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"] },
|
|
635
|
+
{ 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"] },
|
|
636
|
+
{ 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"] },
|
|
637
|
+
{ 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"] },
|
|
638
|
+
{ 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"] },
|
|
639
|
+
{ 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"] },
|
|
640
|
+
{ 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"] },
|
|
641
|
+
{ 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"] },
|
|
642
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
643
|
+
// VUE 3
|
|
644
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
645
|
+
{ 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"] },
|
|
646
|
+
{ 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"] },
|
|
647
|
+
{ 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"] },
|
|
648
|
+
{ 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"] },
|
|
649
|
+
{ 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"] },
|
|
650
|
+
{ 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"] },
|
|
651
|
+
{ 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"] },
|
|
652
|
+
{ 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"] },
|
|
653
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
654
|
+
// ANGULAR
|
|
655
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
656
|
+
{ 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"] },
|
|
657
|
+
{ 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"] },
|
|
658
|
+
{ 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"] },
|
|
659
|
+
{ 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"] },
|
|
660
|
+
{ 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"] },
|
|
661
|
+
{ 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"] },
|
|
662
|
+
{ 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"] },
|
|
663
|
+
{ 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"] },
|
|
664
|
+
{ 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"] },
|
|
665
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
666
|
+
// SVELTE / SVELTEKIT
|
|
667
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
668
|
+
{ 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"] },
|
|
669
|
+
{ 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"] },
|
|
670
|
+
{ 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"] },
|
|
671
|
+
{ 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"] },
|
|
672
|
+
{ 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"] },
|
|
673
|
+
{ 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"] },
|
|
674
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
675
|
+
// NUXT 3
|
|
676
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
677
|
+
{ 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"] },
|
|
678
|
+
{ 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"] },
|
|
679
|
+
{ 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"] },
|
|
680
|
+
{ 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"] },
|
|
681
|
+
{ 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"] },
|
|
682
|
+
{ 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"] },
|
|
683
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
684
|
+
// REACT NATIVE
|
|
685
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
686
|
+
{ 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"] },
|
|
687
|
+
{ 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"] },
|
|
688
|
+
{ 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"] },
|
|
689
|
+
{ 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"] },
|
|
690
|
+
{ 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"] },
|
|
691
|
+
{ 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"] },
|
|
692
|
+
{ 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"] },
|
|
693
|
+
{ 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"] },
|
|
694
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
695
|
+
// NESTJS
|
|
696
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
697
|
+
// ── Modules ──
|
|
698
|
+
{ 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"] },
|
|
699
|
+
{ 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"] },
|
|
700
|
+
{ 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"] },
|
|
701
|
+
{ 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"] },
|
|
702
|
+
{ 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"] },
|
|
703
|
+
// ── Controllers ──
|
|
704
|
+
{ 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"] },
|
|
705
|
+
{ 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"] },
|
|
706
|
+
{ 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"] },
|
|
707
|
+
{ 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"] },
|
|
708
|
+
// ── Services & Business Logic ──
|
|
709
|
+
{ 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"] },
|
|
710
|
+
{ 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"] },
|
|
711
|
+
{ 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"] },
|
|
712
|
+
// ── DTOs & Validation ──
|
|
713
|
+
{ 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"] },
|
|
714
|
+
{ 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"] },
|
|
715
|
+
{ 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"] },
|
|
716
|
+
{ 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"] },
|
|
717
|
+
// ── Guards & Auth ──
|
|
718
|
+
{ 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"] },
|
|
719
|
+
{ 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"] },
|
|
720
|
+
{ 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"] },
|
|
721
|
+
// ── Pipes ──
|
|
722
|
+
{ 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"] },
|
|
723
|
+
{ 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"] },
|
|
724
|
+
// ── Exception Filters ──
|
|
725
|
+
{ 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"] },
|
|
726
|
+
{ 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"] },
|
|
727
|
+
// ── Interceptors ──
|
|
728
|
+
{ 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"] },
|
|
729
|
+
{ 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"] },
|
|
730
|
+
// ── Repository Pattern ──
|
|
731
|
+
{ 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"] },
|
|
732
|
+
{ 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"] },
|
|
733
|
+
// ── Configuration ──
|
|
734
|
+
{ 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"] },
|
|
735
|
+
{ 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"] },
|
|
736
|
+
// ── Database & Entities ──
|
|
737
|
+
{ 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"] },
|
|
738
|
+
{ 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"] },
|
|
739
|
+
{ 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"] },
|
|
740
|
+
// ── Testing ──
|
|
741
|
+
{ 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"] },
|
|
742
|
+
{ 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"] },
|
|
743
|
+
{ 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"] },
|
|
744
|
+
// ── Custom Decorators ──
|
|
745
|
+
{ 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"] },
|
|
746
|
+
{ 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"] },
|
|
747
|
+
// ── Async & Events ──
|
|
748
|
+
{ 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"] },
|
|
749
|
+
{ 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"] },
|
|
750
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
751
|
+
// SVELTE 5 + SVELTEKIT — frontend framework
|
|
752
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
753
|
+
// ── Runes & Reactivity ───────────────────────────────────────────────────
|
|
754
|
+
{ 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"] },
|
|
755
|
+
{ 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"] },
|
|
756
|
+
{ 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"] },
|
|
757
|
+
{ 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"] },
|
|
758
|
+
{ 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"] },
|
|
759
|
+
{ 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"] },
|
|
760
|
+
// ── State Management ─────────────────────────────────────────────────────
|
|
761
|
+
{ 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"] },
|
|
762
|
+
{ 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"] },
|
|
763
|
+
{ 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"] },
|
|
764
|
+
// ── SvelteKit Load Functions ─────────────────────────────────────────────
|
|
765
|
+
{ 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"] },
|
|
766
|
+
{ 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"] },
|
|
767
|
+
{ 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"] },
|
|
768
|
+
// ── Form Actions ─────────────────────────────────────────────────────────
|
|
769
|
+
{ 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"] },
|
|
770
|
+
{ 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"] },
|
|
771
|
+
// ── Component Structure ───────────────────────────────────────────────────
|
|
772
|
+
{ 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"] },
|
|
773
|
+
{ 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"] },
|
|
774
|
+
{ 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"] },
|
|
775
|
+
// ── Snippets & Composition ────────────────────────────────────────────────
|
|
776
|
+
{ 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"] },
|
|
777
|
+
{ 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"] },
|
|
778
|
+
// ── Performance ───────────────────────────────────────────────────────────
|
|
779
|
+
{ 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"] },
|
|
780
|
+
{ 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"] },
|
|
781
|
+
{ 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"] },
|
|
782
|
+
// ── Error Handling ────────────────────────────────────────────────────────
|
|
783
|
+
{ 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"] },
|
|
784
|
+
{ 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"] },
|
|
785
|
+
// ── Accessibility ─────────────────────────────────────────────────────────
|
|
786
|
+
{ 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"] },
|
|
787
|
+
{ 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"] },
|
|
788
|
+
// ── Testing ───────────────────────────────────────────────────────────────
|
|
789
|
+
{ 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"] },
|
|
790
|
+
{ 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"] },
|
|
791
|
+
{ 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"] },
|
|
792
|
+
// ── Anti-Patterns ─────────────────────────────────────────────────────────
|
|
793
|
+
{ 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"] },
|
|
794
|
+
{ 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"] }
|
|
795
|
+
];
|
|
796
|
+
|
|
797
|
+
// src/chat.ts
|
|
798
|
+
function getChatConfig() {
|
|
799
|
+
const provider = process.env.CHAT_PROVIDER ?? "ollama";
|
|
800
|
+
const model = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
|
|
801
|
+
return {
|
|
802
|
+
provider,
|
|
803
|
+
model,
|
|
804
|
+
ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
|
|
805
|
+
apiKey: process.env.CHAT_API_KEY ?? ""
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
async function callOllama(cfg, messages) {
|
|
809
|
+
const res = await fetch(`${cfg.ollamaUrl}/api/chat`, {
|
|
810
|
+
method: "POST",
|
|
811
|
+
headers: { "Content-Type": "application/json" },
|
|
812
|
+
body: JSON.stringify({ model: cfg.model, messages, stream: false, format: "json" })
|
|
813
|
+
});
|
|
814
|
+
if (!res.ok) {
|
|
815
|
+
const body = await res.text();
|
|
816
|
+
if (body.includes("not found") || body.includes("model")) {
|
|
817
|
+
throw new Error(`MODEL_NOT_FOUND:${cfg.model}`);
|
|
818
|
+
}
|
|
819
|
+
throw new Error(body);
|
|
820
|
+
}
|
|
821
|
+
const data = await res.json();
|
|
822
|
+
return data.message.content.trim();
|
|
823
|
+
}
|
|
824
|
+
async function callOpenAI(cfg, messages) {
|
|
825
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
826
|
+
method: "POST",
|
|
827
|
+
headers: {
|
|
828
|
+
"Content-Type": "application/json",
|
|
829
|
+
"Authorization": `Bearer ${cfg.apiKey}`
|
|
830
|
+
},
|
|
831
|
+
body: JSON.stringify({
|
|
832
|
+
model: cfg.model,
|
|
833
|
+
messages,
|
|
834
|
+
response_format: { type: "json_object" }
|
|
835
|
+
})
|
|
836
|
+
});
|
|
837
|
+
if (!res.ok) throw new Error(`OpenAI API error ${res.status}: ${await res.text()}`);
|
|
838
|
+
const data = await res.json();
|
|
839
|
+
return data.choices[0].message.content.trim();
|
|
840
|
+
}
|
|
841
|
+
async function callAnthropic(cfg, messages) {
|
|
842
|
+
const system = messages.find((m) => m.role === "system")?.content ?? "";
|
|
843
|
+
const userMessages = messages.filter((m) => m.role !== "system");
|
|
844
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
845
|
+
method: "POST",
|
|
846
|
+
headers: {
|
|
847
|
+
"Content-Type": "application/json",
|
|
848
|
+
"x-api-key": cfg.apiKey,
|
|
849
|
+
"anthropic-version": "2023-06-01"
|
|
850
|
+
},
|
|
851
|
+
body: JSON.stringify({
|
|
852
|
+
model: cfg.model,
|
|
853
|
+
max_tokens: 4096,
|
|
854
|
+
system,
|
|
855
|
+
messages: userMessages
|
|
856
|
+
})
|
|
857
|
+
});
|
|
858
|
+
if (!res.ok) throw new Error(`Anthropic API error ${res.status}: ${await res.text()}`);
|
|
859
|
+
const data = await res.json();
|
|
860
|
+
return data.content[0].text.trim();
|
|
861
|
+
}
|
|
862
|
+
async function callMiniMax(cfg, messages) {
|
|
863
|
+
const res = await fetch("https://api.minimax.io/v1/chat/completions", {
|
|
864
|
+
method: "POST",
|
|
865
|
+
headers: {
|
|
866
|
+
"Content-Type": "application/json",
|
|
867
|
+
"Authorization": `Bearer ${cfg.apiKey}`
|
|
868
|
+
},
|
|
869
|
+
body: JSON.stringify({
|
|
870
|
+
model: cfg.model,
|
|
871
|
+
messages,
|
|
872
|
+
response_format: { type: "json_object" }
|
|
873
|
+
})
|
|
874
|
+
});
|
|
875
|
+
if (!res.ok) throw new Error(`MiniMax API error ${res.status}: ${await res.text()}`);
|
|
876
|
+
const data = await res.json();
|
|
877
|
+
return data.choices[0].message.content.trim();
|
|
878
|
+
}
|
|
879
|
+
async function callChatModel(messages) {
|
|
880
|
+
const cfg = getChatConfig();
|
|
881
|
+
switch (cfg.provider) {
|
|
882
|
+
case "openai":
|
|
883
|
+
return callOpenAI(cfg, messages);
|
|
884
|
+
case "anthropic":
|
|
885
|
+
return callAnthropic(cfg, messages);
|
|
886
|
+
case "minimax":
|
|
887
|
+
return callMiniMax(cfg, messages);
|
|
888
|
+
default:
|
|
889
|
+
return callOllama(cfg, messages);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function getChatProviderLabel() {
|
|
893
|
+
const cfg = getChatConfig();
|
|
894
|
+
if (cfg.provider === "ollama") return `ollama (${cfg.model})`;
|
|
895
|
+
return `${cfg.provider} (${cfg.model})`;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/watcher.ts
|
|
899
|
+
import { watch } from "chokidar";
|
|
900
|
+
import { spawnSync } from "child_process";
|
|
901
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
902
|
+
import { join as join3, relative } from "path";
|
|
903
|
+
import chalk from "chalk";
|
|
904
|
+
function getFileLines(filePath) {
|
|
905
|
+
try {
|
|
906
|
+
return readFileSync3(filePath, "utf-8").split("\n");
|
|
907
|
+
} catch {
|
|
908
|
+
return [];
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
function printCodeContext(filePath, line, contextLines = 2) {
|
|
912
|
+
const lines = getFileLines(filePath);
|
|
913
|
+
if (lines.length === 0) return;
|
|
914
|
+
const start = Math.max(0, line - 1 - contextLines);
|
|
915
|
+
const end = Math.min(lines.length - 1, line - 1 + contextLines);
|
|
916
|
+
console.log(chalk.dim(" \u250C\u2500 code \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
917
|
+
for (let i = start; i <= end; i++) {
|
|
918
|
+
const lineNum = String(i + 1).padStart(4, " ");
|
|
919
|
+
const isViolation = i === line - 1;
|
|
920
|
+
if (isViolation) {
|
|
921
|
+
console.log(chalk.red(` \u2502 ${lineNum} \u25B6 ${lines[i]}`));
|
|
922
|
+
} else {
|
|
923
|
+
console.log(chalk.dim(` \u2502 ${lineNum} ${lines[i]}`));
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
console.log(chalk.dim(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
927
|
+
}
|
|
928
|
+
function formatCodeContext(filePath, line, contextLines = 2) {
|
|
929
|
+
const lines = getFileLines(filePath);
|
|
930
|
+
if (lines.length === 0) return void 0;
|
|
931
|
+
const start = Math.max(0, line - 1 - contextLines);
|
|
932
|
+
const end = Math.min(lines.length - 1, line - 1 + contextLines);
|
|
933
|
+
return Array.from({ length: end - start + 1 }, (_, index) => {
|
|
934
|
+
const current = start + index;
|
|
935
|
+
const lineNum = String(current + 1).padStart(4, " ");
|
|
936
|
+
const marker = current === line - 1 ? ">" : " ";
|
|
937
|
+
return `${lineNum} ${marker} ${lines[current]}`;
|
|
938
|
+
}).join("\n");
|
|
939
|
+
}
|
|
940
|
+
var SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
|
|
941
|
+
var reasonMap = new Map(
|
|
942
|
+
seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
|
|
943
|
+
);
|
|
944
|
+
function recordViolations(violations) {
|
|
945
|
+
const statsPath = join3(process.cwd(), ".memory-core-stats.json");
|
|
946
|
+
let stats = { rules: {}, files: {} };
|
|
947
|
+
if (existsSync3(statsPath)) {
|
|
948
|
+
try {
|
|
949
|
+
stats = JSON.parse(readFileSync3(statsPath, "utf-8"));
|
|
950
|
+
} catch {
|
|
951
|
+
stats = { rules: {}, files: {} };
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
for (const violation of violations) {
|
|
955
|
+
stats.rules[violation.rule] = (stats.rules[violation.rule] ?? 0) + 1;
|
|
956
|
+
if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
|
|
957
|
+
}
|
|
958
|
+
writeFileSync2(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
959
|
+
}
|
|
960
|
+
function loadConfig(cwd) {
|
|
961
|
+
const configPath = join3(cwd, ".memory-core.json");
|
|
962
|
+
if (!existsSync3(configPath)) return null;
|
|
963
|
+
try {
|
|
964
|
+
return JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
965
|
+
} catch {
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
function getProfileRules(config) {
|
|
970
|
+
const rules = [];
|
|
971
|
+
const avoids = [];
|
|
972
|
+
if (config.backendArchitecture) {
|
|
973
|
+
const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
|
|
974
|
+
if (profile) {
|
|
975
|
+
rules.push(...profile.rules);
|
|
976
|
+
avoids.push(...profile.avoid);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (config.frontendFramework) {
|
|
980
|
+
const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
|
|
981
|
+
if (profile) {
|
|
982
|
+
rules.push(...profile.rules);
|
|
983
|
+
avoids.push(...profile.avoid);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return { rules, avoids };
|
|
987
|
+
}
|
|
988
|
+
async function loadRelevantRules(config, rel, diff, fallbackRules) {
|
|
989
|
+
try {
|
|
990
|
+
const query = buildContextQuery([
|
|
991
|
+
rel,
|
|
992
|
+
diff.slice(0, 1200),
|
|
993
|
+
config.backendArchitecture,
|
|
994
|
+
config.frontendFramework,
|
|
995
|
+
config.language
|
|
996
|
+
]);
|
|
997
|
+
const memories = await retrieveContextualMemories({
|
|
998
|
+
query,
|
|
999
|
+
cwd: process.cwd(),
|
|
1000
|
+
config,
|
|
1001
|
+
limit: 15
|
|
1002
|
+
});
|
|
1003
|
+
const selected = memories.filter((memory) => ["rule", "pattern", "decision"].includes(memory.type)).map((memory) => memory.content);
|
|
1004
|
+
return selected.length > 0 ? selected : fallbackRules;
|
|
1005
|
+
} catch {
|
|
1006
|
+
return fallbackRules;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
function applyAllowPatterns(violations, allowPatterns) {
|
|
1010
|
+
if (allowPatterns.length === 0) return violations;
|
|
1011
|
+
return violations.filter((violation) => {
|
|
1012
|
+
const haystack = `${violation.rule}
|
|
1013
|
+
${violation.issue}
|
|
1014
|
+
${violation.file}`.toLowerCase();
|
|
1015
|
+
return !allowPatterns.some((pattern) => haystack.includes(pattern.toLowerCase()));
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
async function verifyViolations(diff, violations, allowPatterns, debug) {
|
|
1019
|
+
if (violations.length === 0) return violations;
|
|
1020
|
+
const systemPrompt = `You are verifying candidate architecture violations.
|
|
1021
|
+
Only keep violations that are directly supported by the diff.
|
|
1022
|
+
Reject speculative or weak matches.
|
|
1023
|
+
Treat these allowlisted patterns as intentional and valid:
|
|
1024
|
+
${allowPatterns.length ? allowPatterns.map((pattern, index) => `${index + 1}. ${pattern}`).join("\n") : "(none)"}
|
|
1025
|
+
|
|
1026
|
+
Return strict JSON:
|
|
1027
|
+
{"violations":[{"rule":"...","file":"...","line":1,"issue":"...","suggestion":"...","reason":"..."}]}
|
|
1028
|
+
Do not include any text outside the JSON.`;
|
|
1029
|
+
const userPrompt = `Diff:
|
|
1030
|
+
${diff.slice(0, 6e3)}
|
|
1031
|
+
|
|
1032
|
+
Candidate violations:
|
|
1033
|
+
${JSON.stringify(violations, null, 2)}`;
|
|
1034
|
+
if (debug) {
|
|
1035
|
+
console.log(chalk.gray("\n [debug] verifier prompt:"));
|
|
1036
|
+
console.log(chalk.dim(systemPrompt));
|
|
1037
|
+
console.log(chalk.dim(userPrompt));
|
|
1038
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1039
|
+
}
|
|
1040
|
+
try {
|
|
1041
|
+
const raw = await callChatModel([
|
|
1042
|
+
{ role: "system", content: systemPrompt },
|
|
1043
|
+
{ role: "user", content: userPrompt }
|
|
1044
|
+
]);
|
|
1045
|
+
const parsed = JSON.parse(raw);
|
|
1046
|
+
if (Array.isArray(parsed?.violations)) return parsed.violations;
|
|
1047
|
+
if (Array.isArray(parsed)) return parsed;
|
|
1048
|
+
return violations;
|
|
1049
|
+
} catch {
|
|
1050
|
+
return violations;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
async function loadIgnorePatterns() {
|
|
1054
|
+
try {
|
|
1055
|
+
const { listMemories, closePool } = await import("./db-MF3VKVKH.js");
|
|
1056
|
+
const ignores = await listMemories({ type: "ignore", limit: 1e3 });
|
|
1057
|
+
await closePool();
|
|
1058
|
+
return ignores.map((ignore) => ignore.content);
|
|
1059
|
+
} catch {
|
|
1060
|
+
return [];
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
async function checkFile(filePath, cwd, config, verbose, debug) {
|
|
1064
|
+
const rel = relative(cwd, filePath);
|
|
1065
|
+
let diff;
|
|
1066
|
+
const headResult = spawnSync("git", ["diff", "HEAD", "--", rel], { encoding: "utf-8", cwd });
|
|
1067
|
+
if (headResult.stdout?.trim()) {
|
|
1068
|
+
diff = headResult.stdout;
|
|
1069
|
+
} else {
|
|
1070
|
+
const noIndexResult = spawnSync("git", ["diff", "--no-index", "/dev/null", rel], { encoding: "utf-8", cwd });
|
|
1071
|
+
diff = noIndexResult.stdout ?? "";
|
|
1072
|
+
}
|
|
1073
|
+
if (!diff.trim()) return null;
|
|
1074
|
+
const { rules: fallbackRules, avoids } = getProfileRules(config);
|
|
1075
|
+
const rules = await loadRelevantRules(config, rel, diff, fallbackRules);
|
|
1076
|
+
if (rules.length === 0) return null;
|
|
1077
|
+
const MAX_DIFF = 6e3;
|
|
1078
|
+
const truncated = diff.length > MAX_DIFF;
|
|
1079
|
+
const diffToSend = truncated ? diff.slice(0, MAX_DIFF) + "\n\n[diff truncated]" : diff;
|
|
1080
|
+
if (verbose || debug) {
|
|
1081
|
+
console.log(chalk.dim(`
|
|
1082
|
+
[watch] checking ${rel} (${diff.length} chars)\u2026`));
|
|
1083
|
+
}
|
|
1084
|
+
const rulesWithReasons = rules.map((r, i) => {
|
|
1085
|
+
const why = reasonMap.get(r);
|
|
1086
|
+
return why ? `${i + 1}. ${r}
|
|
1087
|
+
WHY: ${why}` : `${i + 1}. ${r}`;
|
|
1088
|
+
}).join("\n");
|
|
1089
|
+
const allowPatterns = [.../* @__PURE__ */ new Set([...getAllowPatterns(config), ...await loadIgnorePatterns()])];
|
|
1090
|
+
const systemPrompt = `You are a strict code reviewer enforcing architecture rules.
|
|
1091
|
+
Analyze the file diff and identify ONLY clear, definite rule violations.
|
|
1092
|
+
Use the WHY for each rule to understand intent and judge edge cases.
|
|
1093
|
+
|
|
1094
|
+
Rules to enforce:
|
|
1095
|
+
${rulesWithReasons}
|
|
1096
|
+
|
|
1097
|
+
Things that must never appear:
|
|
1098
|
+
${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
|
|
1099
|
+
|
|
1100
|
+
Never flag these accepted project patterns:
|
|
1101
|
+
${allowPatterns.length ? allowPatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
|
|
1102
|
+
|
|
1103
|
+
IMPORTANT: Respond with JSON: {"violations":[...]} or {"violations":[]}.
|
|
1104
|
+
Each violation: {"rule":"...","file":"...","line":N,"issue":"...","suggestion":"...","reason":"..."}.
|
|
1105
|
+
No text outside the JSON.`;
|
|
1106
|
+
if (debug) {
|
|
1107
|
+
console.log(chalk.gray("\n [debug] prompt:"));
|
|
1108
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1109
|
+
console.log(systemPrompt);
|
|
1110
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1111
|
+
console.log(chalk.gray(` [debug] diff length: ${diff.length} chars`));
|
|
1112
|
+
console.log(chalk.dim(diffToSend));
|
|
1113
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1114
|
+
}
|
|
1115
|
+
try {
|
|
1116
|
+
const raw = await callChatModel([
|
|
1117
|
+
{ role: "system", content: systemPrompt },
|
|
1118
|
+
{ role: "user", content: `Review this diff for ${rel}:
|
|
1119
|
+
|
|
1120
|
+
${diffToSend}` }
|
|
1121
|
+
]);
|
|
1122
|
+
if (debug) {
|
|
1123
|
+
console.log(chalk.gray(" [debug] raw response:"));
|
|
1124
|
+
console.log(chalk.dim(raw));
|
|
1125
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1126
|
+
}
|
|
1127
|
+
let violations = [];
|
|
1128
|
+
try {
|
|
1129
|
+
const parsed = JSON.parse(raw);
|
|
1130
|
+
if (Array.isArray(parsed)) {
|
|
1131
|
+
violations = parsed;
|
|
1132
|
+
} else if (Array.isArray(parsed?.violations)) {
|
|
1133
|
+
violations = parsed.violations;
|
|
1134
|
+
} else if (parsed?.rule) {
|
|
1135
|
+
violations = [parsed];
|
|
1136
|
+
}
|
|
1137
|
+
} catch {
|
|
1138
|
+
violations = [];
|
|
1139
|
+
}
|
|
1140
|
+
violations = await verifyViolations(diff, violations, allowPatterns, debug);
|
|
1141
|
+
violations = applyAllowPatterns(violations, allowPatterns);
|
|
1142
|
+
violations = violations.map((violation) => ({
|
|
1143
|
+
...violation,
|
|
1144
|
+
code: violation.code ?? (violation.line ? formatCodeContext(filePath, violation.line, 1) : void 0)
|
|
1145
|
+
}));
|
|
1146
|
+
if (violations.length === 0) {
|
|
1147
|
+
console.log(chalk.green(` \u2713 ${rel}`) + chalk.dim(" \u2014 no violations"));
|
|
1148
|
+
return [];
|
|
1149
|
+
}
|
|
1150
|
+
console.log(
|
|
1151
|
+
chalk.red.bold(`
|
|
1152
|
+
\u2717 ${violations.length} violation${violations.length > 1 ? "s" : ""} in ${rel}
|
|
1153
|
+
`)
|
|
1154
|
+
);
|
|
1155
|
+
violations.forEach((v, i) => {
|
|
1156
|
+
const loc = v.line ? `${v.file ?? rel}:${v.line}` : v.file ?? rel;
|
|
1157
|
+
console.log(chalk.bold(` [${i + 1}] ${loc}`));
|
|
1158
|
+
console.log(chalk.yellow(" Rule: ") + v.rule);
|
|
1159
|
+
const why = v.reason ?? reasonMap.get(v.rule);
|
|
1160
|
+
if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
|
|
1161
|
+
if (v.line && existsSync3(filePath)) {
|
|
1162
|
+
printCodeContext(filePath, v.line, 1);
|
|
1163
|
+
}
|
|
1164
|
+
if (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
|
|
1165
|
+
if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
|
|
1166
|
+
console.log();
|
|
1167
|
+
});
|
|
1168
|
+
recordViolations(violations);
|
|
1169
|
+
console.log(chalk.dim(' Fix violations or run: memory-core remember "<lesson>"'));
|
|
1170
|
+
console.log();
|
|
1171
|
+
return violations;
|
|
1172
|
+
} catch (err) {
|
|
1173
|
+
if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
if (verbose) {
|
|
1177
|
+
console.log(chalk.yellow(` \u26A0 Check failed for ${rel}: ${err.message}`));
|
|
1178
|
+
}
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async function startWatch(options = {}) {
|
|
1183
|
+
const cwd = process.cwd();
|
|
1184
|
+
const config = loadConfig(cwd);
|
|
1185
|
+
const exitOnSetupFailure = options.exitOnSetupFailure ?? true;
|
|
1186
|
+
if (!config) {
|
|
1187
|
+
const message = "No .memory-core.json found. Run: memory-core init";
|
|
1188
|
+
console.error(chalk.red(`
|
|
1189
|
+
${message}
|
|
1190
|
+
`));
|
|
1191
|
+
options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message });
|
|
1192
|
+
if (exitOnSetupFailure) process.exit(1);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
const { rules } = getProfileRules(config);
|
|
1196
|
+
if (rules.length === 0) {
|
|
1197
|
+
const message = "No architecture rules configured in .memory-core.json \u2014 nothing to watch.";
|
|
1198
|
+
console.log(chalk.yellow(`
|
|
1199
|
+
${message}
|
|
1200
|
+
`));
|
|
1201
|
+
options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message });
|
|
1202
|
+
if (exitOnSetupFailure) process.exit(0);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const watchPath = options.path ?? cwd;
|
|
1206
|
+
console.log(chalk.cyan("\n archmind watch \u2014 real-time rule enforcement\n"));
|
|
1207
|
+
console.log(chalk.dim(` watching: ${watchPath}`));
|
|
1208
|
+
console.log(chalk.dim(` model: ${getChatProviderLabel()}`));
|
|
1209
|
+
console.log(chalk.dim(` rules: ${rules.length}`));
|
|
1210
|
+
console.log(chalk.dim(" ctrl+c to stop\n"));
|
|
1211
|
+
options.onEvent?.({
|
|
1212
|
+
type: "ready",
|
|
1213
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1214
|
+
path: watchPath,
|
|
1215
|
+
model: getChatProviderLabel(),
|
|
1216
|
+
rules: rules.length
|
|
1217
|
+
});
|
|
1218
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1219
|
+
const watcher = watch(watchPath, {
|
|
1220
|
+
ignored: [
|
|
1221
|
+
"**/node_modules/**",
|
|
1222
|
+
"**/.git/**",
|
|
1223
|
+
"**/dist/**",
|
|
1224
|
+
"**/build/**",
|
|
1225
|
+
"**/coverage/**",
|
|
1226
|
+
"**/.memory-core*"
|
|
1227
|
+
],
|
|
1228
|
+
ignoreInitial: true,
|
|
1229
|
+
persistent: true,
|
|
1230
|
+
awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50 }
|
|
1231
|
+
});
|
|
1232
|
+
const keepAlive = setInterval(() => {
|
|
1233
|
+
}, 1 << 30);
|
|
1234
|
+
const handle = (filePath) => {
|
|
1235
|
+
if (!SOURCE_EXTENSIONS.test(filePath)) return;
|
|
1236
|
+
if (pending.has(filePath)) clearTimeout(pending.get(filePath));
|
|
1237
|
+
const timer = setTimeout(async () => {
|
|
1238
|
+
pending.delete(filePath);
|
|
1239
|
+
const rel = relative(cwd, filePath);
|
|
1240
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
1241
|
+
console.log(chalk.dim(`
|
|
1242
|
+
[${timestamp.toLocaleTimeString()}] saved: ${rel}`));
|
|
1243
|
+
options.onEvent?.({ type: "saved", timestamp: timestamp.toISOString(), file: rel });
|
|
1244
|
+
const violations = await checkFile(filePath, cwd, config, options.verbose ?? false, options.debug ?? false);
|
|
1245
|
+
if (violations === null) return;
|
|
1246
|
+
if (violations.length === 0) {
|
|
1247
|
+
options.onEvent?.({ type: "clean", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel });
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
options.onEvent?.({ type: "violations", timestamp: (/* @__PURE__ */ new Date()).toISOString(), file: rel, violations });
|
|
1251
|
+
}, 300);
|
|
1252
|
+
pending.set(filePath, timer);
|
|
1253
|
+
};
|
|
1254
|
+
watcher.on("add", handle);
|
|
1255
|
+
watcher.on("change", handle);
|
|
1256
|
+
watcher.on("error", (err) => {
|
|
1257
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1258
|
+
console.error(chalk.red(` watcher error: ${message}`));
|
|
1259
|
+
options.onEvent?.({ type: "error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), message });
|
|
1260
|
+
});
|
|
1261
|
+
process.on("SIGINT", () => {
|
|
1262
|
+
console.log(chalk.dim("\n\n archmind watch stopped.\n"));
|
|
1263
|
+
options.onEvent?.({ type: "stopped", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1264
|
+
clearInterval(keepAlive);
|
|
1265
|
+
watcher.close();
|
|
1266
|
+
process.exit(0);
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
export {
|
|
1271
|
+
detectProject,
|
|
1272
|
+
retrieve,
|
|
1273
|
+
dedupeMemories,
|
|
1274
|
+
inferProjectArchitectures,
|
|
1275
|
+
getAllowPatterns,
|
|
1276
|
+
buildContextQuery,
|
|
1277
|
+
retrieveContextualMemories,
|
|
1278
|
+
retrieveMemorySelection,
|
|
1279
|
+
OUTPUT_FILES,
|
|
1280
|
+
AGENT_NAMES,
|
|
1281
|
+
listProfiles,
|
|
1282
|
+
generate,
|
|
1283
|
+
seeds,
|
|
1284
|
+
callChatModel,
|
|
1285
|
+
getChatProviderLabel,
|
|
1286
|
+
startWatch
|
|
1287
|
+
};
|