@shahmilsaari/memory-core 0.1.0
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 +538 -0
- package/dist/cli.js +1171 -0
- package/package.json +51 -0
- package/profiles/angular.yml +33 -0
- package/profiles/clean-architecture.yml +33 -0
- package/profiles/hexagonal.yml +28 -0
- package/profiles/laravel-service-repository.yml +32 -0
- package/profiles/modular-monolith.yml +28 -0
- package/profiles/mvc.yml +27 -0
- package/profiles/nextjs.yml +32 -0
- package/profiles/nuxt.yml +32 -0
- package/profiles/react-native.yml +35 -0
- package/profiles/react.yml +37 -0
- package/profiles/svelte.yml +29 -0
- package/profiles/vue.yml +35 -0
- package/templates/AGENTS.md.hbs +42 -0
- package/templates/AI_RULES.md.hbs +39 -0
- package/templates/ARCHITECTURE.md.hbs +51 -0
- package/templates/CLAUDE.md.hbs +52 -0
- package/templates/DEVIN.md.hbs +26 -0
- package/templates/PROJECT_MEMORY.md.hbs +31 -0
- package/templates/aider.conf.yml.hbs +8 -0
- package/templates/amazonq-guidelines.md.hbs +26 -0
- package/templates/clinerules.hbs +26 -0
- package/templates/continue-config.json.hbs +5 -0
- package/templates/copilot-instructions.md.hbs +32 -0
- package/templates/cursor-rule.mdc.hbs +30 -0
- package/templates/cursorrules.hbs +28 -0
- package/templates/gemini-styleguide.md.hbs +26 -0
- package/templates/jetbrains-ai.md.hbs +26 -0
- package/templates/roo-rule.md.hbs +28 -0
- package/templates/windsurfrules.hbs +26 -0
- package/templates/zed-settings.json.hbs +9 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { input, select, confirm } from "@inquirer/prompts";
|
|
6
|
+
import chalk2 from "chalk";
|
|
7
|
+
import ora from "ora";
|
|
8
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
9
|
+
import { join as join5, dirname as dirname2 } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { execSync as execSync2 } from "child_process";
|
|
12
|
+
|
|
13
|
+
// src/generator.ts
|
|
14
|
+
import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
15
|
+
import { join, dirname } from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
import Handlebars from "handlebars";
|
|
18
|
+
import yaml from "js-yaml";
|
|
19
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
var __dirname = dirname(__filename);
|
|
21
|
+
var PKG_ROOT = join(__dirname, "..");
|
|
22
|
+
var OUTPUT_FILES = [
|
|
23
|
+
{ template: "CLAUDE.md.hbs", path: "CLAUDE.md" },
|
|
24
|
+
{ template: "copilot-instructions.md.hbs", path: ".github/copilot-instructions.md" },
|
|
25
|
+
{ template: "cursorrules.hbs", path: ".cursorrules" },
|
|
26
|
+
{ template: "cursor-rule.mdc.hbs", path: ".cursor/rules/memory-core.mdc" },
|
|
27
|
+
{ template: "windsurfrules.hbs", path: ".windsurfrules" },
|
|
28
|
+
{ template: "clinerules.hbs", path: ".clinerules" },
|
|
29
|
+
{ template: "roo-rule.md.hbs", path: ".roo/rules/memory-core.md" },
|
|
30
|
+
{ template: "aider.conf.yml.hbs", path: ".aider.conf.yml" },
|
|
31
|
+
{ template: "continue-config.json.hbs", path: ".continue/config.json", skipIfExists: true },
|
|
32
|
+
{ template: "DEVIN.md.hbs", path: "DEVIN.md" },
|
|
33
|
+
{ template: "amazonq-guidelines.md.hbs", path: ".amazonq/dev/guidelines.md" },
|
|
34
|
+
{ template: "gemini-styleguide.md.hbs", path: ".gemini/styleguide.md" },
|
|
35
|
+
{ template: "zed-settings.json.hbs", path: ".zed/settings.json", skipIfExists: true },
|
|
36
|
+
{ template: "jetbrains-ai.md.hbs", path: ".idea/ai-instructions.md" },
|
|
37
|
+
{ template: "AGENTS.md.hbs", path: "AGENTS.md" },
|
|
38
|
+
{ template: "AI_RULES.md.hbs", path: "AI_RULES.md" },
|
|
39
|
+
{ template: "ARCHITECTURE.md.hbs", path: "ARCHITECTURE.md" },
|
|
40
|
+
{ template: "PROJECT_MEMORY.md.hbs", path: "PROJECT_MEMORY.md" }
|
|
41
|
+
];
|
|
42
|
+
Handlebars.registerHelper(
|
|
43
|
+
"join",
|
|
44
|
+
(arr, sep) => Array.isArray(arr) ? arr.join(sep) : ""
|
|
45
|
+
);
|
|
46
|
+
Handlebars.registerHelper(
|
|
47
|
+
"bullet",
|
|
48
|
+
(arr) => Array.isArray(arr) ? arr.map((i) => `- ${i}`).join("\n") : ""
|
|
49
|
+
);
|
|
50
|
+
Handlebars.registerHelper(
|
|
51
|
+
"numbered",
|
|
52
|
+
(arr) => Array.isArray(arr) ? arr.map((i, idx) => `${idx + 1}. ${i}`).join("\n") : ""
|
|
53
|
+
);
|
|
54
|
+
Handlebars.registerHelper("json", (val) => JSON.stringify(val, null, 2));
|
|
55
|
+
function loadProfile(name) {
|
|
56
|
+
const profilePath = join(PKG_ROOT, "profiles", `${name}.yml`);
|
|
57
|
+
if (!existsSync(profilePath)) throw new Error(`Profile not found: ${name}`);
|
|
58
|
+
return yaml.load(readFileSync(profilePath, "utf-8"));
|
|
59
|
+
}
|
|
60
|
+
function listProfiles(layer) {
|
|
61
|
+
const files = readdirSync(join(PKG_ROOT, "profiles")).filter((f) => f.endsWith(".yml"));
|
|
62
|
+
const all = files.map((f) => yaml.load(readFileSync(join(PKG_ROOT, "profiles", f), "utf-8")));
|
|
63
|
+
if (!layer) return all;
|
|
64
|
+
if (layer === "backend") return all.filter((p) => p.layer === "backend" || p.layer === "fullstack");
|
|
65
|
+
if (layer === "frontend") return all.filter((p) => p.layer === "frontend" || p.layer === "fullstack");
|
|
66
|
+
return all;
|
|
67
|
+
}
|
|
68
|
+
function buildTemplateData(options) {
|
|
69
|
+
const backend = options.backendArchitecture ? loadProfile(options.backendArchitecture) : null;
|
|
70
|
+
const frontend = options.frontendFramework ? loadProfile(options.frontendFramework) : null;
|
|
71
|
+
const allRules = [
|
|
72
|
+
...backend?.rules ?? [],
|
|
73
|
+
...frontend?.rules ?? []
|
|
74
|
+
];
|
|
75
|
+
const allFolders = [
|
|
76
|
+
...backend?.folders ?? [],
|
|
77
|
+
...frontend?.folders ?? []
|
|
78
|
+
];
|
|
79
|
+
const allAvoid = [
|
|
80
|
+
...backend?.avoid ?? [],
|
|
81
|
+
...frontend?.avoid ?? []
|
|
82
|
+
];
|
|
83
|
+
const archLabel = [
|
|
84
|
+
backend ? `Backend: ${backend.displayName}` : null,
|
|
85
|
+
frontend ? `Frontend: ${frontend.displayName}` : null
|
|
86
|
+
].filter(Boolean).join(" \xB7 ");
|
|
87
|
+
return {
|
|
88
|
+
projectName: options.projectName,
|
|
89
|
+
projectType: options.projectType,
|
|
90
|
+
isBackend: options.projectType === "backend" || options.projectType === "fullstack",
|
|
91
|
+
isFrontend: options.projectType === "frontend" || options.projectType === "fullstack",
|
|
92
|
+
isFullstack: options.projectType === "fullstack",
|
|
93
|
+
// backend
|
|
94
|
+
hasBackend: !!backend,
|
|
95
|
+
backendArchitecture: backend?.displayName,
|
|
96
|
+
backendDescription: backend?.description,
|
|
97
|
+
backendRules: backend?.rules ?? [],
|
|
98
|
+
backendFolders: backend?.folders ?? [],
|
|
99
|
+
backendAvoid: backend?.avoid ?? [],
|
|
100
|
+
// frontend
|
|
101
|
+
hasFrontend: !!frontend,
|
|
102
|
+
frontendFramework: frontend?.displayName,
|
|
103
|
+
frontendDescription: frontend?.description,
|
|
104
|
+
frontendRules: frontend?.rules ?? [],
|
|
105
|
+
frontendFolders: frontend?.folders ?? [],
|
|
106
|
+
frontendAvoid: frontend?.avoid ?? [],
|
|
107
|
+
// combined — used by simple templates
|
|
108
|
+
architecture: archLabel,
|
|
109
|
+
rules: allRules,
|
|
110
|
+
folders: allFolders,
|
|
111
|
+
avoid: allAvoid,
|
|
112
|
+
description: [backend?.description, frontend?.description].filter(Boolean).join(" | "),
|
|
113
|
+
// memories
|
|
114
|
+
memories: options.memories,
|
|
115
|
+
hasMemories: options.memories.length > 0,
|
|
116
|
+
// misc
|
|
117
|
+
language: options.language,
|
|
118
|
+
caveman: options.caveman,
|
|
119
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function renderTemplate(templateName, data) {
|
|
123
|
+
const templatePath = join(PKG_ROOT, "templates", templateName);
|
|
124
|
+
if (!existsSync(templatePath)) throw new Error(`Template not found: ${templateName}`);
|
|
125
|
+
return Handlebars.compile(readFileSync(templatePath, "utf-8"))(data);
|
|
126
|
+
}
|
|
127
|
+
function writeFile(filePath, content) {
|
|
128
|
+
const dir = dirname(filePath);
|
|
129
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
130
|
+
writeFileSync(filePath, content, "utf-8");
|
|
131
|
+
}
|
|
132
|
+
async function generate(options, cwd = process.cwd()) {
|
|
133
|
+
const data = buildTemplateData(options);
|
|
134
|
+
const written = [];
|
|
135
|
+
for (const output of OUTPUT_FILES) {
|
|
136
|
+
const targetPath = join(cwd, output.path);
|
|
137
|
+
if (output.skipIfExists && existsSync(targetPath)) continue;
|
|
138
|
+
try {
|
|
139
|
+
const content = renderTemplate(output.template, data);
|
|
140
|
+
writeFile(targetPath, content);
|
|
141
|
+
written.push(output.path);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (!(err instanceof Error && err.message.includes("Template not found"))) throw err;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return written;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/seeds.ts
|
|
150
|
+
var seeds = [
|
|
151
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
152
|
+
// GLOBAL — applies to every architecture
|
|
153
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
154
|
+
// ── API Design ───────────────────────────────────────────────────────────
|
|
155
|
+
{ 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"] },
|
|
156
|
+
{ 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"] },
|
|
157
|
+
{ 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"] },
|
|
158
|
+
{ 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"] },
|
|
159
|
+
{ 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"] },
|
|
160
|
+
{ 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"] },
|
|
161
|
+
{ 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"] },
|
|
162
|
+
{ 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"] },
|
|
163
|
+
// ── Validation & Input ───────────────────────────────────────────────────
|
|
164
|
+
{ 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"] },
|
|
165
|
+
{ 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"] },
|
|
166
|
+
{ 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"] },
|
|
167
|
+
// ── Security ─────────────────────────────────────────────────────────────
|
|
168
|
+
{ 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"] },
|
|
169
|
+
{ 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"] },
|
|
170
|
+
{ 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"] },
|
|
171
|
+
{ 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"] },
|
|
172
|
+
{ 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"] },
|
|
173
|
+
{ 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"] },
|
|
174
|
+
{ 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"] },
|
|
175
|
+
{ 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"] },
|
|
176
|
+
{ 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"] },
|
|
177
|
+
// ── Error Handling ───────────────────────────────────────────────────────
|
|
178
|
+
{ 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"] },
|
|
179
|
+
{ 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"] },
|
|
180
|
+
{ 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"] },
|
|
181
|
+
// ── Database ─────────────────────────────────────────────────────────────
|
|
182
|
+
{ 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"] },
|
|
183
|
+
{ 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"] },
|
|
184
|
+
{ 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"] },
|
|
185
|
+
{ 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"] },
|
|
186
|
+
{ 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"] },
|
|
187
|
+
{ 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"] },
|
|
188
|
+
{ 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"] },
|
|
189
|
+
// ── Caching ──────────────────────────────────────────────────────────────
|
|
190
|
+
{ 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"] },
|
|
191
|
+
{ 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"] },
|
|
192
|
+
{ 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"] },
|
|
193
|
+
// ── Logging & Observability ───────────────────────────────────────────────
|
|
194
|
+
{ 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"] },
|
|
195
|
+
{ 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"] },
|
|
196
|
+
{ 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"] },
|
|
197
|
+
{ 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"] },
|
|
198
|
+
// ── Testing ──────────────────────────────────────────────────────────────
|
|
199
|
+
{ 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"] },
|
|
200
|
+
{ 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"] },
|
|
201
|
+
{ 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"] },
|
|
202
|
+
{ 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"] },
|
|
203
|
+
{ 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"] },
|
|
204
|
+
{ 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"] },
|
|
205
|
+
// ── Code Quality ─────────────────────────────────────────────────────────
|
|
206
|
+
{ 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"] },
|
|
207
|
+
{ 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"] },
|
|
208
|
+
{ 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"] },
|
|
209
|
+
{ 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"] },
|
|
210
|
+
{ 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"] },
|
|
211
|
+
{ 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"] },
|
|
212
|
+
// ── DevOps & Reliability ─────────────────────────────────────────────────
|
|
213
|
+
{ 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"] },
|
|
214
|
+
{ 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"] },
|
|
215
|
+
{ 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"] },
|
|
216
|
+
{ 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"] },
|
|
217
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
218
|
+
// CLEAN ARCHITECTURE
|
|
219
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
220
|
+
// ── Core Rules ───────────────────────────────────────────────────────────
|
|
221
|
+
{ 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"] },
|
|
222
|
+
{ 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"] },
|
|
223
|
+
{ 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"] },
|
|
224
|
+
{ 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"] },
|
|
225
|
+
{ 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"] },
|
|
226
|
+
{ 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"] },
|
|
227
|
+
// ── Domain ───────────────────────────────────────────────────────────────
|
|
228
|
+
{ 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"] },
|
|
229
|
+
{ 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"] },
|
|
230
|
+
{ 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"] },
|
|
231
|
+
{ 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"] },
|
|
232
|
+
{ 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"] },
|
|
233
|
+
{ 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"] },
|
|
234
|
+
// ── Application ──────────────────────────────────────────────────────────
|
|
235
|
+
{ 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"] },
|
|
236
|
+
{ 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"] },
|
|
237
|
+
{ 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"] },
|
|
238
|
+
{ 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"] },
|
|
239
|
+
// ── Infrastructure ────────────────────────────────────────────────────────
|
|
240
|
+
{ 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"] },
|
|
241
|
+
{ 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"] },
|
|
242
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
243
|
+
// MODULAR MONOLITH
|
|
244
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
245
|
+
{ 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"] },
|
|
246
|
+
{ 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"] },
|
|
247
|
+
{ 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"] },
|
|
248
|
+
{ 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"] },
|
|
249
|
+
{ 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"] },
|
|
250
|
+
{ 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"] },
|
|
251
|
+
{ 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"] },
|
|
252
|
+
{ 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"] },
|
|
253
|
+
{ 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"] },
|
|
254
|
+
{ 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"] },
|
|
255
|
+
{ 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"] },
|
|
256
|
+
{ 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"] },
|
|
257
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
258
|
+
// MVC
|
|
259
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
260
|
+
{ 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"] },
|
|
261
|
+
{ 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"] },
|
|
262
|
+
{ 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"] },
|
|
263
|
+
{ 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"] },
|
|
264
|
+
{ 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"] },
|
|
265
|
+
{ 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"] },
|
|
266
|
+
{ 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"] },
|
|
267
|
+
{ 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"] },
|
|
268
|
+
{ 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"] },
|
|
269
|
+
{ 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"] },
|
|
270
|
+
{ 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"] },
|
|
271
|
+
{ 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"] },
|
|
272
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
273
|
+
// HEXAGONAL ARCHITECTURE
|
|
274
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
275
|
+
{ 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"] },
|
|
276
|
+
{ 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"] },
|
|
277
|
+
{ 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"] },
|
|
278
|
+
{ 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"] },
|
|
279
|
+
{ 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"] },
|
|
280
|
+
{ 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"] },
|
|
281
|
+
{ 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"] },
|
|
282
|
+
{ 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"] },
|
|
283
|
+
{ 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"] },
|
|
284
|
+
{ 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"] },
|
|
285
|
+
{ 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"] },
|
|
286
|
+
{ 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"] },
|
|
287
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
288
|
+
// NEXT.JS APP ROUTER
|
|
289
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
290
|
+
// ── Rendering Strategy ───────────────────────────────────────────────────
|
|
291
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "Server Components by default", content: 'Every component is a Server Component unless it requires interactivity (onClick, useState, useEffect). Add "use client" only when necessary, as low in the tree as possible.', tags: ["server-components", "nextjs"] },
|
|
292
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "Fetch in Server Components", content: "Fetch data directly in async Server Components. Never use useEffect, SWR, or React Query for initial server-side data. Client-side fetching is for user-triggered updates only.", tags: ["data-fetching", "nextjs"] },
|
|
293
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "Server Actions for mutations", content: "All form submissions and data mutations use Server Actions. API routes are only for third-party webhooks or browser clients that cannot use Server Actions.", tags: ["server-actions", "mutations", "nextjs"] },
|
|
294
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "Static by default, dynamic when needed", content: 'Pages are statically rendered unless they use cookies(), headers(), or searchParams. Opt into dynamic rendering explicitly with export const dynamic = "force-dynamic".', tags: ["rendering", "performance", "nextjs"] },
|
|
295
|
+
{ type: "pattern", scope: "global", architecture: "nextjs", title: "Suspense boundaries for streaming", content: "Wrap slow data-fetching sections in <Suspense fallback={<Skeleton />}>. This enables streaming and progressive hydration. Do not block the entire page on one slow query.", tags: ["suspense", "streaming", "nextjs"] },
|
|
296
|
+
{ type: "pattern", scope: "global", architecture: "nextjs", title: "Parallel data fetching", content: "Use Promise.all() for independent data fetches in the same Server Component. Sequential awaits in a single component waterfall unnecessarily.", tags: ["performance", "data-fetching", "nextjs"] },
|
|
297
|
+
// ── Structure ─────────────────────────────────────────────────────────────
|
|
298
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "Thin page.tsx files", content: "page.tsx fetches data and passes it to a feature component. No JSX logic, no conditionals, no business logic in page files.", tags: ["structure", "nextjs"] },
|
|
299
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "Colocate by route", content: "Components used only by one route live next to that route. Components used across 3+ routes move to src/components/. Utils used across routes go to src/lib/.", tags: ["structure", "colocation", "nextjs"] },
|
|
300
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "Route groups for organization", content: "Use (groupName) folders to organize routes without affecting URLs. Group by concern: (marketing), (dashboard), (auth). Each group can have its own layout.", tags: ["route-groups", "structure", "nextjs"] },
|
|
301
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "loading.tsx and error.tsx always", content: "Every dynamic route segment must have a loading.tsx (Suspense fallback) and error.tsx (error boundary). Never let routes fail or hang without user feedback.", tags: ["loading", "error-handling", "nextjs"] },
|
|
302
|
+
// ── Security & Config ────────────────────────────────────────────────────
|
|
303
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "No secrets in NEXT_PUBLIC_", content: "NEXT_PUBLIC_ variables are embedded in the client bundle. Never put API keys, tokens, or DB credentials there. Server-only secrets stay in Server Components and Actions.", tags: ["security", "env", "nextjs"] },
|
|
304
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "Auth in middleware.ts", content: "Route protection and session validation happens in middleware.ts at the edge. Never rely on page-level checks as the only auth guard.", tags: ["auth", "middleware", "security", "nextjs"] },
|
|
305
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "Validate Server Action inputs", content: "Always validate and sanitize inputs inside Server Actions using zod or similar. Server Actions are public endpoints \u2014 treat them like API routes.", tags: ["validation", "server-actions", "security", "nextjs"] },
|
|
306
|
+
// ── Performance ──────────────────────────────────────────────────────────
|
|
307
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "next/image for all images", content: "Use next/image for all images. It provides automatic WebP conversion, lazy loading, and size optimization. Never use raw <img> tags.", tags: ["performance", "images", "nextjs"] },
|
|
308
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "next/font for typography", content: "Use next/font for fonts. It eliminates layout shift, self-hosts, and applies font-display: swap automatically. Never load fonts from Google Fonts CDN directly.", tags: ["performance", "fonts", "nextjs"] },
|
|
309
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "Cache fetch calls with tags", content: 'Tag fetch() calls with cache tags: fetch(url, { next: { tags: ["products"] } }). Revalidate by tag on mutation with revalidateTag("products").', tags: ["caching", "performance", "nextjs"] },
|
|
310
|
+
{ type: "pattern", scope: "global", architecture: "nextjs", title: "ISR for semi-static pages", content: "Use Incremental Static Regeneration for pages that change infrequently: export const revalidate = 3600. Combine with revalidatePath() on writes.", tags: ["isr", "performance", "nextjs"] },
|
|
311
|
+
{ type: "rule", scope: "global", architecture: "nextjs", title: "Metadata API for SEO", content: "Define page metadata using the Metadata API (export const metadata) or generateMetadata(). Never use next/head or raw <Head> in App Router.", tags: ["seo", "metadata", "nextjs"] },
|
|
312
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
313
|
+
// LARAVEL SERVICE REPOSITORY
|
|
314
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
315
|
+
// ── Core Rules ───────────────────────────────────────────────────────────
|
|
316
|
+
{ 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"] },
|
|
317
|
+
{ 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"] },
|
|
318
|
+
{ 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"] },
|
|
319
|
+
{ 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"] },
|
|
320
|
+
{ 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"] },
|
|
321
|
+
{ 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"] },
|
|
322
|
+
// ── Patterns ─────────────────────────────────────────────────────────────
|
|
323
|
+
{ 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"] },
|
|
324
|
+
{ 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"] },
|
|
325
|
+
{ 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"] },
|
|
326
|
+
{ 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"] },
|
|
327
|
+
{ 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"] },
|
|
328
|
+
{ 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"] },
|
|
329
|
+
// ── Advanced ─────────────────────────────────────────────────────────────
|
|
330
|
+
{ 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"] },
|
|
331
|
+
{ 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"] },
|
|
332
|
+
{ 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"] },
|
|
333
|
+
{ 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"] },
|
|
334
|
+
{ 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"] },
|
|
335
|
+
{ 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"] },
|
|
336
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
337
|
+
// REACT
|
|
338
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
339
|
+
{ 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"] },
|
|
340
|
+
{ 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"] },
|
|
341
|
+
{ 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"] },
|
|
342
|
+
{ 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"] },
|
|
343
|
+
{ 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"] },
|
|
344
|
+
{ 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"] },
|
|
345
|
+
{ 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"] },
|
|
346
|
+
{ 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"] },
|
|
347
|
+
{ 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"] },
|
|
348
|
+
{ 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"] },
|
|
349
|
+
{ 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"] },
|
|
350
|
+
{ 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"] },
|
|
351
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
352
|
+
// VUE 3
|
|
353
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
354
|
+
{ 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"] },
|
|
355
|
+
{ 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"] },
|
|
356
|
+
{ 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"] },
|
|
357
|
+
{ 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"] },
|
|
358
|
+
{ 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"] },
|
|
359
|
+
{ 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"] },
|
|
360
|
+
{ 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"] },
|
|
361
|
+
{ 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"] },
|
|
362
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
363
|
+
// ANGULAR
|
|
364
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
365
|
+
{ 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"] },
|
|
366
|
+
{ 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"] },
|
|
367
|
+
{ 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"] },
|
|
368
|
+
{ 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"] },
|
|
369
|
+
{ 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"] },
|
|
370
|
+
{ 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"] },
|
|
371
|
+
{ 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"] },
|
|
372
|
+
{ 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"] },
|
|
373
|
+
{ 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"] },
|
|
374
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
375
|
+
// SVELTE / SVELTEKIT
|
|
376
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
377
|
+
{ 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"] },
|
|
378
|
+
{ 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"] },
|
|
379
|
+
{ 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"] },
|
|
380
|
+
{ 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"] },
|
|
381
|
+
{ 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"] },
|
|
382
|
+
{ 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"] },
|
|
383
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
384
|
+
// NUXT 3
|
|
385
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
386
|
+
{ 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"] },
|
|
387
|
+
{ 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"] },
|
|
388
|
+
{ 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"] },
|
|
389
|
+
{ 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"] },
|
|
390
|
+
{ 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"] },
|
|
391
|
+
{ 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"] },
|
|
392
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
393
|
+
// REACT NATIVE
|
|
394
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
395
|
+
{ 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"] },
|
|
396
|
+
{ 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"] },
|
|
397
|
+
{ 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"] },
|
|
398
|
+
{ 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"] },
|
|
399
|
+
{ 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"] },
|
|
400
|
+
{ 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"] },
|
|
401
|
+
{ 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"] },
|
|
402
|
+
{ 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"] }
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
// src/config.ts
|
|
406
|
+
import { config } from "dotenv";
|
|
407
|
+
import { existsSync as existsSync2 } from "fs";
|
|
408
|
+
import { join as join2 } from "path";
|
|
409
|
+
var localEnv = join2(process.cwd(), ".memory-core.env");
|
|
410
|
+
config({ path: existsSync2(localEnv) ? localEnv : join2(process.cwd(), ".env") });
|
|
411
|
+
var Config = {
|
|
412
|
+
databaseUrl: process.env.DATABASE_URL ?? "",
|
|
413
|
+
ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
|
|
414
|
+
ollamaModel: process.env.OLLAMA_MODEL ?? "nomic-embed-text"
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// src/embedding.ts
|
|
418
|
+
async function embed(text) {
|
|
419
|
+
let response;
|
|
420
|
+
try {
|
|
421
|
+
response = await fetch(`${Config.ollamaUrl}/api/embeddings`, {
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: { "Content-Type": "application/json" },
|
|
424
|
+
body: JSON.stringify({ model: Config.ollamaModel, prompt: text })
|
|
425
|
+
});
|
|
426
|
+
} catch {
|
|
427
|
+
throw new Error(
|
|
428
|
+
`Cannot reach Ollama at ${Config.ollamaUrl}. Run: ollama serve`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
if (!response.ok) {
|
|
432
|
+
const body = await response.text();
|
|
433
|
+
throw new Error(`Ollama embedding failed (${response.status}): ${body}`);
|
|
434
|
+
}
|
|
435
|
+
const data = await response.json();
|
|
436
|
+
return data.embedding;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/db.ts
|
|
440
|
+
import pg from "pg";
|
|
441
|
+
var { Pool } = pg;
|
|
442
|
+
var pool = null;
|
|
443
|
+
function getPool() {
|
|
444
|
+
if (!pool) {
|
|
445
|
+
if (!Config.databaseUrl) {
|
|
446
|
+
throw new Error("DATABASE_URL is not set. Add it to your .env or .memory-core.env file.");
|
|
447
|
+
}
|
|
448
|
+
pool = new Pool({ connectionString: Config.databaseUrl });
|
|
449
|
+
}
|
|
450
|
+
return pool;
|
|
451
|
+
}
|
|
452
|
+
async function runMigrations() {
|
|
453
|
+
await getPool().query(
|
|
454
|
+
`ALTER TABLE memories ADD COLUMN IF NOT EXISTS reason TEXT`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
async function saveMemory(memory) {
|
|
458
|
+
const { type, scope, architecture, projectName, title, content, reason, tags, embedding } = memory;
|
|
459
|
+
await getPool().query(
|
|
460
|
+
`INSERT INTO memories (type, scope, architecture, project_name, title, content, reason, tags, embedding)
|
|
461
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
|
462
|
+
[type, scope, architecture ?? null, projectName ?? null, title ?? null, content, reason ?? null, tags ?? [], `[${embedding.join(",")}]`]
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
async function searchMemories(embedding, architecture, limit = 10) {
|
|
466
|
+
const vector = `[${embedding.join(",")}]`;
|
|
467
|
+
const params = [vector];
|
|
468
|
+
let whereClause = "";
|
|
469
|
+
if (architecture) {
|
|
470
|
+
whereClause = `WHERE (architecture = $2 OR scope = 'global')`;
|
|
471
|
+
params.push(architecture);
|
|
472
|
+
}
|
|
473
|
+
const client = await getPool().connect();
|
|
474
|
+
try {
|
|
475
|
+
await client.query("BEGIN");
|
|
476
|
+
await client.query("SET LOCAL ivfflat.probes = 10");
|
|
477
|
+
const result = await client.query(
|
|
478
|
+
`SELECT id, type, scope, architecture, project_name, title, content, reason, tags,
|
|
479
|
+
1 - (embedding <=> $1) AS similarity
|
|
480
|
+
FROM memories
|
|
481
|
+
${whereClause}
|
|
482
|
+
ORDER BY embedding <=> $1
|
|
483
|
+
LIMIT $${params.length + 1}`,
|
|
484
|
+
[...params, limit]
|
|
485
|
+
);
|
|
486
|
+
await client.query("COMMIT");
|
|
487
|
+
return result.rows;
|
|
488
|
+
} finally {
|
|
489
|
+
client.release();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async function closePool() {
|
|
493
|
+
if (pool) {
|
|
494
|
+
await pool.end();
|
|
495
|
+
pool = null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/retriever.ts
|
|
500
|
+
async function retrieve(query, architecture, limit = 10) {
|
|
501
|
+
const embedding = await embed(query);
|
|
502
|
+
return searchMemories(embedding, architecture, limit);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/project-detector.ts
|
|
506
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
507
|
+
import { join as join3 } from "path";
|
|
508
|
+
function detectProject(cwd = process.cwd()) {
|
|
509
|
+
const has = (file) => existsSync3(join3(cwd, file));
|
|
510
|
+
const readJson = (file) => {
|
|
511
|
+
try {
|
|
512
|
+
return JSON.parse(readFileSync2(join3(cwd, file), "utf-8"));
|
|
513
|
+
} catch {
|
|
514
|
+
return {};
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
if (has("next.config.js") || has("next.config.ts") || has("next.config.mjs")) {
|
|
518
|
+
return { language: "TypeScript", framework: "Next.js" };
|
|
519
|
+
}
|
|
520
|
+
if (has("artisan") && has("composer.json")) {
|
|
521
|
+
return { language: "PHP", framework: "Laravel" };
|
|
522
|
+
}
|
|
523
|
+
if (has("nuxt.config.ts") || has("nuxt.config.js")) {
|
|
524
|
+
return { language: "TypeScript", framework: "Nuxt.js" };
|
|
525
|
+
}
|
|
526
|
+
if (has("manage.py")) {
|
|
527
|
+
if (has("requirements.txt")) {
|
|
528
|
+
const req = readFileSync2(join3(cwd, "requirements.txt"), "utf-8");
|
|
529
|
+
if (req.includes("djangorestframework")) {
|
|
530
|
+
return { language: "Python", framework: "Django REST Framework" };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return { language: "Python", framework: "Django" };
|
|
534
|
+
}
|
|
535
|
+
if (has("go.mod")) {
|
|
536
|
+
return { language: "Go", framework: "Go" };
|
|
537
|
+
}
|
|
538
|
+
if (has("Cargo.toml")) {
|
|
539
|
+
return { language: "Rust", framework: "Rust" };
|
|
540
|
+
}
|
|
541
|
+
if (has("pubspec.yaml")) {
|
|
542
|
+
return { language: "Dart", framework: "Flutter" };
|
|
543
|
+
}
|
|
544
|
+
if (has("pom.xml")) {
|
|
545
|
+
return { language: "Java", framework: "Spring Boot" };
|
|
546
|
+
}
|
|
547
|
+
if (has("build.gradle") || has("build.gradle.kts")) {
|
|
548
|
+
return { language: "Kotlin", framework: "Kotlin/JVM" };
|
|
549
|
+
}
|
|
550
|
+
if (has("package.json")) {
|
|
551
|
+
const pkg = readJson("package.json");
|
|
552
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
553
|
+
if (deps["@nestjs/core"]) return { language: "TypeScript", framework: "NestJS" };
|
|
554
|
+
if (deps["express"]) return { language: "TypeScript", framework: "Express.js" };
|
|
555
|
+
if (deps["fastify"]) return { language: "TypeScript", framework: "Fastify" };
|
|
556
|
+
if (deps["react"]) return { language: "TypeScript", framework: "React" };
|
|
557
|
+
if (deps["vue"]) return { language: "TypeScript", framework: "Vue.js" };
|
|
558
|
+
if (deps["svelte"]) return { language: "TypeScript", framework: "Svelte" };
|
|
559
|
+
return { language: "TypeScript/JavaScript", framework: "Node.js" };
|
|
560
|
+
}
|
|
561
|
+
if (has("requirements.txt") || has("pyproject.toml")) {
|
|
562
|
+
return { language: "Python", framework: "Python" };
|
|
563
|
+
}
|
|
564
|
+
return { language: "Unknown", framework: "Unknown" };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/hook.ts
|
|
568
|
+
import { execSync } from "child_process";
|
|
569
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync4, unlinkSync, readFileSync as readFileSync3, chmodSync } from "fs";
|
|
570
|
+
import { join as join4 } from "path";
|
|
571
|
+
import chalk from "chalk";
|
|
572
|
+
var reasonMap = new Map(
|
|
573
|
+
seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
|
|
574
|
+
);
|
|
575
|
+
var HOOK_PATH = join4(".git", "hooks", "pre-commit");
|
|
576
|
+
var HOOK_MARKER = "# archmind-memory-core";
|
|
577
|
+
var HOOK_SCRIPT = `#!/bin/sh
|
|
578
|
+
${HOOK_MARKER}
|
|
579
|
+
if command -v memory-core >/dev/null 2>&1; then
|
|
580
|
+
memory-core check --staged
|
|
581
|
+
elif [ -f "./node_modules/.bin/memory-core" ]; then
|
|
582
|
+
./node_modules/.bin/memory-core check --staged
|
|
583
|
+
elif [ -f "./dist/cli.js" ]; then
|
|
584
|
+
node ./dist/cli.js check --staged
|
|
585
|
+
else
|
|
586
|
+
npx --no-install memory-core check --staged 2>/dev/null || exit 0
|
|
587
|
+
fi
|
|
588
|
+
`;
|
|
589
|
+
function installHook() {
|
|
590
|
+
if (!existsSync4(".git")) {
|
|
591
|
+
console.error(chalk.red("\n Not a git repository. Run from project root.\n"));
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
if (existsSync4(HOOK_PATH)) {
|
|
595
|
+
const existing = readFileSync3(HOOK_PATH, "utf-8");
|
|
596
|
+
if (existing.includes(HOOK_MARKER)) {
|
|
597
|
+
console.log(chalk.yellow("\n Hook already installed.\n"));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
writeFileSync2(HOOK_PATH, existing.trimEnd() + "\n\n" + HOOK_SCRIPT);
|
|
601
|
+
} else {
|
|
602
|
+
writeFileSync2(HOOK_PATH, HOOK_SCRIPT);
|
|
603
|
+
}
|
|
604
|
+
chmodSync(HOOK_PATH, 493);
|
|
605
|
+
console.log(chalk.green("\n \u2713 Pre-commit hook installed"));
|
|
606
|
+
console.log(chalk.gray(" Every commit will be checked against your architecture rules."));
|
|
607
|
+
console.log(chalk.gray(` Chat model: ${process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"}`));
|
|
608
|
+
console.log(chalk.gray(" To uninstall: memory-core hook uninstall\n"));
|
|
609
|
+
}
|
|
610
|
+
function uninstallHook() {
|
|
611
|
+
if (!existsSync4(HOOK_PATH)) {
|
|
612
|
+
console.log(chalk.yellow("\n No pre-commit hook found.\n"));
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const content = readFileSync3(HOOK_PATH, "utf-8");
|
|
616
|
+
if (!content.includes(HOOK_MARKER)) {
|
|
617
|
+
console.log(chalk.yellow("\n ArchMind hook not found in pre-commit \u2014 nothing to remove.\n"));
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const markerIndex = content.indexOf(HOOK_MARKER);
|
|
621
|
+
if (markerIndex > 1) {
|
|
622
|
+
writeFileSync2(HOOK_PATH, content.slice(0, markerIndex).trimEnd() + "\n");
|
|
623
|
+
} else {
|
|
624
|
+
unlinkSync(HOOK_PATH);
|
|
625
|
+
}
|
|
626
|
+
console.log(chalk.green("\n \u2713 Pre-commit hook removed\n"));
|
|
627
|
+
}
|
|
628
|
+
async function checkStaged(options = {}) {
|
|
629
|
+
const SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|svelte)$/;
|
|
630
|
+
let diff;
|
|
631
|
+
try {
|
|
632
|
+
const stagedFiles = execSync("git diff --cached --name-only", { encoding: "utf-8" }).split("\n").filter((f) => f && SOURCE_EXTENSIONS.test(f));
|
|
633
|
+
if (stagedFiles.length === 0) {
|
|
634
|
+
if (options.verbose) console.log(chalk.gray(" No source files staged \u2014 skipping rule check."));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
diff = execSync(`git diff --cached -- ${stagedFiles.map((f) => `"${f}"`).join(" ")}`, { encoding: "utf-8" });
|
|
638
|
+
} catch {
|
|
639
|
+
console.error(chalk.red(" Failed to read staged diff."));
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
if (!diff.trim()) {
|
|
643
|
+
if (options.verbose) console.log(chalk.gray(" No staged changes to check."));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const configPath = join4(process.cwd(), ".memory-core.json");
|
|
647
|
+
if (!existsSync4(configPath)) return;
|
|
648
|
+
const config2 = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
649
|
+
const rules = [];
|
|
650
|
+
const avoids = [];
|
|
651
|
+
if (config2.backendArchitecture) {
|
|
652
|
+
const profile = listProfiles("backend").find((p) => p.name === config2.backendArchitecture);
|
|
653
|
+
if (profile) {
|
|
654
|
+
rules.push(...profile.rules);
|
|
655
|
+
avoids.push(...profile.avoid);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (config2.frontendFramework) {
|
|
659
|
+
const profile = listProfiles("frontend").find((p) => p.name === config2.frontendFramework);
|
|
660
|
+
if (profile) {
|
|
661
|
+
rules.push(...profile.rules);
|
|
662
|
+
avoids.push(...profile.avoid);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (rules.length === 0) return;
|
|
666
|
+
const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
667
|
+
const chatModel = process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
|
|
668
|
+
const MAX_DIFF = 8e3;
|
|
669
|
+
const truncated = diff.length > MAX_DIFF;
|
|
670
|
+
const diffToSend = truncated ? diff.slice(0, MAX_DIFF) + "\n\n[diff truncated]" : diff;
|
|
671
|
+
console.log(chalk.cyan("\n archmind \u2014 checking staged changes against rules\u2026"));
|
|
672
|
+
if (options.verbose) {
|
|
673
|
+
console.log(chalk.gray(` model: ${chatModel} rules: ${rules.length} diff: ${diff.length} chars${truncated ? " (truncated)" : ""}`));
|
|
674
|
+
}
|
|
675
|
+
const rulesWithReasons = rules.map((r, i) => {
|
|
676
|
+
const why = reasonMap.get(r);
|
|
677
|
+
return why ? `${i + 1}. ${r}
|
|
678
|
+
WHY: ${why}` : `${i + 1}. ${r}`;
|
|
679
|
+
}).join("\n");
|
|
680
|
+
const systemPrompt = `You are a strict code reviewer enforcing architecture and framework rules.
|
|
681
|
+
Analyze the git diff and identify ONLY clear, definite rule violations \u2014 not style preferences.
|
|
682
|
+
Use the WHY for each rule to understand intent and judge edge cases correctly.
|
|
683
|
+
|
|
684
|
+
Rules to enforce:
|
|
685
|
+
${rulesWithReasons}
|
|
686
|
+
|
|
687
|
+
Things that must never appear:
|
|
688
|
+
${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
|
|
689
|
+
|
|
690
|
+
IMPORTANT: You MUST respond with a JSON object that has a "violations" key containing an array.
|
|
691
|
+
For each violation include a "reason" field \u2014 copy the WHY from the rule to explain to the developer why this matters.
|
|
692
|
+
Example with violations: {"violations":[{"rule":"Use functional components only","file":"User.tsx","line":3,"issue":"Class component used","suggestion":"Convert to a function component using hooks","reason":"Class components cannot use hooks and the entire React ecosystem now assumes functional components"}]}
|
|
693
|
+
Example with no violations: {"violations":[]}
|
|
694
|
+
Do not include any text outside the JSON object.`;
|
|
695
|
+
let violations = [];
|
|
696
|
+
try {
|
|
697
|
+
const res = await fetch(`${ollamaUrl}/api/chat`, {
|
|
698
|
+
method: "POST",
|
|
699
|
+
headers: { "Content-Type": "application/json" },
|
|
700
|
+
body: JSON.stringify({
|
|
701
|
+
model: chatModel,
|
|
702
|
+
messages: [
|
|
703
|
+
{ role: "system", content: systemPrompt },
|
|
704
|
+
{ role: "user", content: `Review this diff:
|
|
705
|
+
|
|
706
|
+
${diffToSend}` }
|
|
707
|
+
],
|
|
708
|
+
stream: false,
|
|
709
|
+
format: "json"
|
|
710
|
+
})
|
|
711
|
+
});
|
|
712
|
+
if (!res.ok) {
|
|
713
|
+
const body = await res.text();
|
|
714
|
+
if (body.includes("not found") || body.includes("model")) {
|
|
715
|
+
printModelMissing(chatModel);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
throw new Error(body);
|
|
719
|
+
}
|
|
720
|
+
const data = await res.json();
|
|
721
|
+
const raw = data.message.content.trim();
|
|
722
|
+
try {
|
|
723
|
+
const parsed = JSON.parse(raw);
|
|
724
|
+
if (Array.isArray(parsed)) {
|
|
725
|
+
violations = parsed;
|
|
726
|
+
} else if (Array.isArray(parsed?.violations)) {
|
|
727
|
+
violations = parsed.violations;
|
|
728
|
+
} else if (parsed?.rule) {
|
|
729
|
+
violations = [parsed];
|
|
730
|
+
} else {
|
|
731
|
+
violations = [];
|
|
732
|
+
}
|
|
733
|
+
} catch {
|
|
734
|
+
violations = [];
|
|
735
|
+
}
|
|
736
|
+
if (options.verbose) {
|
|
737
|
+
console.log(chalk.gray(` raw response: ${raw.slice(0, 200)}`));
|
|
738
|
+
}
|
|
739
|
+
} catch (err) {
|
|
740
|
+
if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
|
|
741
|
+
console.log(chalk.yellow("\n \u26A0 Ollama not running \u2014 skipping rule check."));
|
|
742
|
+
console.log(chalk.gray(" Start it: ollama serve\n"));
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
console.log(chalk.yellow(`
|
|
746
|
+
\u26A0 Rule check failed: ${err.message}
|
|
747
|
+
`));
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (violations.length === 0) {
|
|
751
|
+
console.log(chalk.green(" \u2713 No rule violations \u2014 commit allowed.\n"));
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
console.log(
|
|
755
|
+
chalk.red.bold(
|
|
756
|
+
`
|
|
757
|
+
\u2717 ${violations.length} rule violation${violations.length > 1 ? "s" : ""} found \u2014 commit blocked
|
|
758
|
+
`
|
|
759
|
+
)
|
|
760
|
+
);
|
|
761
|
+
violations.forEach((v, i) => {
|
|
762
|
+
const loc = v.file ? v.line ? `${v.file}:${v.line}` : v.file : "unknown location";
|
|
763
|
+
console.log(chalk.bold(` [${i + 1}] ${loc}`));
|
|
764
|
+
console.log(chalk.yellow(" Rule: ") + v.rule);
|
|
765
|
+
const why = v.reason ?? reasonMap.get(v.rule);
|
|
766
|
+
if (why) console.log(chalk.dim(" Why: ") + chalk.dim(why));
|
|
767
|
+
if (v.issue) console.log(chalk.red(" Issue: ") + v.issue);
|
|
768
|
+
if (v.suggestion) console.log(chalk.green(" Fix: ") + v.suggestion);
|
|
769
|
+
console.log();
|
|
770
|
+
});
|
|
771
|
+
console.log(chalk.dim(" Fix the violations above, then commit again."));
|
|
772
|
+
console.log(chalk.dim(" To bypass (not recommended): git commit --no-verify"));
|
|
773
|
+
console.log(chalk.dim(' To save as memory: memory-core remember "<lesson>"'));
|
|
774
|
+
console.log();
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
function printModelMissing(model) {
|
|
778
|
+
console.log(chalk.yellow(`
|
|
779
|
+
\u26A0 Chat model "${model}" not found in Ollama.`));
|
|
780
|
+
console.log(chalk.gray(` Pull a model: ollama pull ${model}`));
|
|
781
|
+
console.log(chalk.gray(" Or set OLLAMA_CHAT_MODEL=<model> in .env"));
|
|
782
|
+
console.log(chalk.gray(" Recommended: llama3.2 | qwen2.5-coder:3b | mistral\n"));
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/cli.ts
|
|
786
|
+
function printBanner(projectName, agentCount) {
|
|
787
|
+
const lines = [
|
|
788
|
+
"",
|
|
789
|
+
chalk2.cyan(" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 "),
|
|
790
|
+
chalk2.cyan(" \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557"),
|
|
791
|
+
chalk2.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551"),
|
|
792
|
+
chalk2.cyan(" \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551"),
|
|
793
|
+
chalk2.cyan(" \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D"),
|
|
794
|
+
chalk2.cyan(" \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D"),
|
|
795
|
+
"",
|
|
796
|
+
chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 ") + chalk2.bold.white("C O R E") + chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),
|
|
797
|
+
"",
|
|
798
|
+
chalk2.green(` \u2713 Project `) + chalk2.bold(projectName),
|
|
799
|
+
chalk2.green(` \u2713 Agents `) + chalk2.bold(`${agentCount} AI agents configured`),
|
|
800
|
+
chalk2.green(` \u2713 Memory `) + chalk2.bold("PostgreSQL + pgvector ready"),
|
|
801
|
+
"",
|
|
802
|
+
chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),
|
|
803
|
+
"",
|
|
804
|
+
chalk2.bold(" Every AI agent in this project now follows your rules."),
|
|
805
|
+
"",
|
|
806
|
+
chalk2.gray(" Next steps:"),
|
|
807
|
+
chalk2.gray(' memory-core remember "Your architectural decision"'),
|
|
808
|
+
chalk2.gray(' memory-core search "query"'),
|
|
809
|
+
chalk2.gray(" memory-core sync"),
|
|
810
|
+
""
|
|
811
|
+
];
|
|
812
|
+
lines.forEach((l) => console.log(l));
|
|
813
|
+
}
|
|
814
|
+
var CONFIG_FILE = ".memory-core.json";
|
|
815
|
+
function readProjectConfig() {
|
|
816
|
+
const path = join5(process.cwd(), CONFIG_FILE);
|
|
817
|
+
if (!existsSync5(path)) return null;
|
|
818
|
+
try {
|
|
819
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
820
|
+
} catch {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function writeProjectConfig(config2) {
|
|
825
|
+
writeFileSync3(join5(process.cwd(), CONFIG_FILE), JSON.stringify(config2, null, 2));
|
|
826
|
+
}
|
|
827
|
+
var program = new Command();
|
|
828
|
+
program.name("memory-core").description("Universal AI memory core \u2014 generate AI context files for all coding agents").version("0.1.0");
|
|
829
|
+
program.command("init").description("Initialize memory-core in the current project").action(async () => {
|
|
830
|
+
console.log(chalk2.bold.cyan("\n memory-core init\n"));
|
|
831
|
+
const detected = detectProject();
|
|
832
|
+
const projectName = await input({
|
|
833
|
+
message: "Project name?",
|
|
834
|
+
default: process.cwd().split("/").pop() ?? "my-project"
|
|
835
|
+
});
|
|
836
|
+
const projectType = await select({
|
|
837
|
+
message: "Project type?",
|
|
838
|
+
choices: [
|
|
839
|
+
{ value: "backend", name: "Backend \u2014 API, server, microservice" },
|
|
840
|
+
{ value: "frontend", name: "Frontend \u2014 UI, web, mobile app" },
|
|
841
|
+
{ value: "fullstack", name: "Fullstack \u2014 Both backend and frontend" }
|
|
842
|
+
]
|
|
843
|
+
});
|
|
844
|
+
let backendArchitecture;
|
|
845
|
+
if (projectType === "backend" || projectType === "fullstack") {
|
|
846
|
+
const backendProfiles = listProfiles("backend");
|
|
847
|
+
backendArchitecture = await select({
|
|
848
|
+
message: "Backend architecture?",
|
|
849
|
+
choices: backendProfiles.map((p) => ({
|
|
850
|
+
value: p.name,
|
|
851
|
+
name: `${p.displayName} \u2014 ${p.description}`
|
|
852
|
+
}))
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
let frontendFramework;
|
|
856
|
+
if (projectType === "frontend" || projectType === "fullstack") {
|
|
857
|
+
const frontendProfiles = listProfiles("frontend");
|
|
858
|
+
frontendFramework = await select({
|
|
859
|
+
message: "Frontend framework?",
|
|
860
|
+
choices: frontendProfiles.map((p) => ({
|
|
861
|
+
value: p.name,
|
|
862
|
+
name: `${p.displayName} \u2014 ${p.description}`
|
|
863
|
+
}))
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
const language = await input({
|
|
867
|
+
message: "Language?",
|
|
868
|
+
default: detected.language
|
|
869
|
+
});
|
|
870
|
+
const pullMemories = await confirm({
|
|
871
|
+
message: "Pull relevant memories from previous projects?",
|
|
872
|
+
default: true
|
|
873
|
+
});
|
|
874
|
+
const installCaveman = await confirm({
|
|
875
|
+
message: "Install caveman token saver? (~65-75% fewer tokens)",
|
|
876
|
+
default: false
|
|
877
|
+
});
|
|
878
|
+
let cavemanIntensity = "full";
|
|
879
|
+
if (installCaveman) {
|
|
880
|
+
cavemanIntensity = await select({
|
|
881
|
+
message: "Caveman intensity?",
|
|
882
|
+
choices: [
|
|
883
|
+
{ value: "full", name: "Full \u2014 caveman mode (default)" },
|
|
884
|
+
{ value: "lite", name: "Lite \u2014 professional terseness" },
|
|
885
|
+
{ value: "ultra", name: "Ultra \u2014 telegraphic, minimum words" }
|
|
886
|
+
]
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
const config2 = {
|
|
890
|
+
projectName,
|
|
891
|
+
projectType,
|
|
892
|
+
backendArchitecture,
|
|
893
|
+
frontendFramework,
|
|
894
|
+
language,
|
|
895
|
+
caveman: { enabled: installCaveman, intensity: cavemanIntensity }
|
|
896
|
+
};
|
|
897
|
+
let memories = [];
|
|
898
|
+
if (pullMemories) {
|
|
899
|
+
const spinner2 = ora("Retrieving relevant memories\u2026").start();
|
|
900
|
+
try {
|
|
901
|
+
const archQuery = [backendArchitecture, frontendFramework, language].filter(Boolean).join(" ");
|
|
902
|
+
memories = await retrieve(archQuery, backendArchitecture ?? frontendFramework, 10);
|
|
903
|
+
spinner2.succeed(`Found ${memories.length} relevant memories`);
|
|
904
|
+
} catch (err) {
|
|
905
|
+
spinner2.warn(`Could not retrieve memories: ${err.message}`);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (installCaveman) {
|
|
909
|
+
const spinner2 = ora("Installing caveman token saver\u2026").start();
|
|
910
|
+
try {
|
|
911
|
+
execSync2(
|
|
912
|
+
"curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash",
|
|
913
|
+
{ stdio: "pipe", cwd: process.cwd() }
|
|
914
|
+
);
|
|
915
|
+
spinner2.succeed("Caveman installed");
|
|
916
|
+
} catch (err) {
|
|
917
|
+
spinner2.warn(`Caveman install failed: ${err.message}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const spinner = ora("Generating AI agent context files\u2026").start();
|
|
921
|
+
const written = await generate(
|
|
922
|
+
{ projectName, projectType, backendArchitecture, frontendFramework, language, memories, caveman: config2.caveman },
|
|
923
|
+
process.cwd()
|
|
924
|
+
);
|
|
925
|
+
writeProjectConfig(config2);
|
|
926
|
+
spinner.succeed(`Generated ${written.length} files`);
|
|
927
|
+
printBanner(config2.projectName, written.length);
|
|
928
|
+
await closePool();
|
|
929
|
+
});
|
|
930
|
+
program.command("sync").description("Re-pull memories and regenerate all AI agent files").action(async () => {
|
|
931
|
+
const config2 = readProjectConfig();
|
|
932
|
+
if (!config2) {
|
|
933
|
+
console.error(chalk2.red("No .memory-core.json found. Run: memory-core init"));
|
|
934
|
+
process.exit(1);
|
|
935
|
+
}
|
|
936
|
+
const spinner = ora("Syncing memories\u2026").start();
|
|
937
|
+
let memories = [];
|
|
938
|
+
try {
|
|
939
|
+
const archQuery = [config2.backendArchitecture, config2.frontendFramework, config2.language].filter(Boolean).join(" ");
|
|
940
|
+
memories = await retrieve(archQuery, config2.backendArchitecture ?? config2.frontendFramework, 10);
|
|
941
|
+
spinner.text = `Found ${memories.length} memories \u2014 regenerating files\u2026`;
|
|
942
|
+
} catch (err) {
|
|
943
|
+
spinner.warn(`Could not retrieve memories: ${err.message}`);
|
|
944
|
+
}
|
|
945
|
+
const written = await generate(
|
|
946
|
+
{
|
|
947
|
+
projectName: config2.projectName,
|
|
948
|
+
projectType: config2.projectType,
|
|
949
|
+
backendArchitecture: config2.backendArchitecture,
|
|
950
|
+
frontendFramework: config2.frontendFramework,
|
|
951
|
+
language: config2.language,
|
|
952
|
+
memories,
|
|
953
|
+
caveman: config2.caveman
|
|
954
|
+
},
|
|
955
|
+
process.cwd()
|
|
956
|
+
);
|
|
957
|
+
spinner.succeed(`Synced \u2014 regenerated ${written.length} files`);
|
|
958
|
+
await closePool();
|
|
959
|
+
});
|
|
960
|
+
program.command("remember <text>").description("Save a new memory to the central database").option("-t, --type <type>", "Memory type (decision|rule|pattern|note)", "decision").option("-s, --scope <scope>", "Scope (global|project)", "project").option("--tags <tags>", "Comma-separated tags").option("-r, --reason <reason>", "Why this rule exists \u2014 helps agents understand intent and debug violations").action(async (text, opts) => {
|
|
961
|
+
const config2 = readProjectConfig();
|
|
962
|
+
let reason = opts.reason;
|
|
963
|
+
if (!reason) {
|
|
964
|
+
reason = await input({
|
|
965
|
+
message: chalk2.dim("Why does this rule exist? (optional \u2014 helps agents debug violations)"),
|
|
966
|
+
default: ""
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
const spinner = ora("Saving memory\u2026").start();
|
|
970
|
+
try {
|
|
971
|
+
const embedding = await embed(text);
|
|
972
|
+
await saveMemory({
|
|
973
|
+
type: opts.type,
|
|
974
|
+
scope: opts.scope,
|
|
975
|
+
architecture: config2?.backendArchitecture ?? config2?.frontendFramework,
|
|
976
|
+
projectName: config2?.projectName,
|
|
977
|
+
content: text,
|
|
978
|
+
reason: reason || void 0,
|
|
979
|
+
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [],
|
|
980
|
+
embedding
|
|
981
|
+
});
|
|
982
|
+
const reasonLine = reason ? chalk2.gray(`
|
|
983
|
+
Why: ${reason}`) : "";
|
|
984
|
+
spinner.succeed(chalk2.green(`Memory saved: "${text}"`) + reasonLine);
|
|
985
|
+
} catch (err) {
|
|
986
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
987
|
+
process.exit(1);
|
|
988
|
+
}
|
|
989
|
+
await closePool();
|
|
990
|
+
});
|
|
991
|
+
program.command("search <query>").description("Search memories using semantic similarity").option("-n, --limit <n>", "Number of results", "5").action(async (query, opts) => {
|
|
992
|
+
const config2 = readProjectConfig();
|
|
993
|
+
const spinner = ora("Searching\u2026").start();
|
|
994
|
+
try {
|
|
995
|
+
const results = await retrieve(
|
|
996
|
+
query,
|
|
997
|
+
config2?.backendArchitecture ?? config2?.frontendFramework,
|
|
998
|
+
parseInt(opts.limit, 10)
|
|
999
|
+
);
|
|
1000
|
+
spinner.stop();
|
|
1001
|
+
if (results.length === 0) {
|
|
1002
|
+
console.log(chalk2.yellow("No memories found."));
|
|
1003
|
+
} else {
|
|
1004
|
+
console.log(chalk2.bold(`
|
|
1005
|
+
${results.length} results for "${query}"
|
|
1006
|
+
`));
|
|
1007
|
+
results.forEach((m, i) => {
|
|
1008
|
+
const sim = m.similarity ? chalk2.gray(` (${(m.similarity * 100).toFixed(0)}% match)`) : "";
|
|
1009
|
+
console.log(chalk2.cyan(` ${i + 1}. [${m.type}] ${m.title ?? ""}`));
|
|
1010
|
+
console.log(chalk2.white(` ${m.content}`) + sim);
|
|
1011
|
+
if (m.tags?.length) console.log(chalk2.gray(` tags: ${m.tags.join(", ")}`));
|
|
1012
|
+
console.log();
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
1017
|
+
process.exit(1);
|
|
1018
|
+
}
|
|
1019
|
+
await closePool();
|
|
1020
|
+
});
|
|
1021
|
+
program.command("seed").description("Load all predefined memories into the database").option("--arch <architecture>", "Only seed a specific architecture (e.g. clean-architecture)").option("--force", "Re-seed even if memories already exist", false).action(async (opts) => {
|
|
1022
|
+
await runMigrations();
|
|
1023
|
+
const filtered = opts.arch ? seeds.filter((s) => s.architecture === opts.arch || s.architecture === "global") : seeds;
|
|
1024
|
+
console.log(chalk2.bold.cyan(`
|
|
1025
|
+
Seeding ${filtered.length} memories\u2026
|
|
1026
|
+
`));
|
|
1027
|
+
let saved = 0;
|
|
1028
|
+
let skipped = 0;
|
|
1029
|
+
for (const seed of filtered) {
|
|
1030
|
+
const spinner = ora(`[${seed.architecture}] ${seed.title}`).start();
|
|
1031
|
+
try {
|
|
1032
|
+
const embedding = await embed(seed.content);
|
|
1033
|
+
await saveMemory({
|
|
1034
|
+
type: seed.type,
|
|
1035
|
+
scope: seed.scope,
|
|
1036
|
+
architecture: seed.architecture === "global" ? void 0 : seed.architecture,
|
|
1037
|
+
title: seed.title,
|
|
1038
|
+
content: seed.content,
|
|
1039
|
+
reason: seed.reason,
|
|
1040
|
+
tags: seed.tags,
|
|
1041
|
+
embedding
|
|
1042
|
+
});
|
|
1043
|
+
spinner.succeed(chalk2.gray(`[${seed.architecture}] ${seed.title}`));
|
|
1044
|
+
saved++;
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
spinner.warn(`Skipped \u2014 ${err.message}`);
|
|
1047
|
+
skipped++;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
console.log(chalk2.bold.green(`
|
|
1051
|
+
Done. ${saved} memories seeded, ${skipped} skipped.
|
|
1052
|
+
`));
|
|
1053
|
+
await closePool();
|
|
1054
|
+
});
|
|
1055
|
+
program.command("global").description("Sync your memory into every AI agent globally \u2014 Claude, Copilot, Cursor, Cline, Continue, Aider, Zed").option("--arch <architecture>", "Filter memories by architecture").action(async (opts) => {
|
|
1056
|
+
const home = homedir();
|
|
1057
|
+
const GLOBAL_TARGETS = [
|
|
1058
|
+
// Claude Code
|
|
1059
|
+
{ label: "Claude Code", path: join5(home, ".claude/CLAUDE.md"), type: "md" },
|
|
1060
|
+
// GitHub Copilot (VS Code)
|
|
1061
|
+
{ label: "Copilot", path: join5(home, "Library/Application Support/Code/User/settings.json"), type: "vscode-copilot" },
|
|
1062
|
+
// Cursor global rules
|
|
1063
|
+
{ label: "Cursor", path: join5(home, ".cursor/rules/memory-core.mdc"), type: "md" },
|
|
1064
|
+
// Cline (VS Code)
|
|
1065
|
+
{ label: "Cline", path: join5(home, "Library/Application Support/Code/User/settings.json"), type: "vscode-cline" },
|
|
1066
|
+
// Continue.dev global config
|
|
1067
|
+
{ label: "Continue.dev", path: join5(home, ".continue/config.json"), type: "continue" },
|
|
1068
|
+
// Aider global config
|
|
1069
|
+
{ label: "Aider", path: join5(home, ".aider.conf.yml"), type: "aider" },
|
|
1070
|
+
// Zed global settings
|
|
1071
|
+
{ label: "Zed AI", path: join5(home, ".config/zed/settings.json"), type: "zed" },
|
|
1072
|
+
// Windsurf global rules
|
|
1073
|
+
{ label: "Windsurf", path: join5(home, ".windsurf/rules/memory-core.md"), type: "md" }
|
|
1074
|
+
];
|
|
1075
|
+
const spinner = ora("Fetching global memories\u2026").start();
|
|
1076
|
+
let memories = [];
|
|
1077
|
+
try {
|
|
1078
|
+
memories = await retrieve("architecture rules coding standards", opts.arch, 20);
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
spinner.fail(`Could not fetch memories: ${err.message}`);
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
}
|
|
1083
|
+
spinner.text = `Found ${memories.length} memories \u2014 syncing to all agents\u2026`;
|
|
1084
|
+
const rulesText = memories.map((m) => `- [${m.type}] ${m.content}`).join("\n");
|
|
1085
|
+
const systemPrompt = `You are an AI coding assistant. Always follow these rules:
|
|
1086
|
+
|
|
1087
|
+
${rulesText}`;
|
|
1088
|
+
const mdContent = `# Memory Core \u2014 Global Rules
|
|
1089
|
+
<!-- Synced by memory-core \u2014 run: memory-core global to refresh -->
|
|
1090
|
+
|
|
1091
|
+
## Rules
|
|
1092
|
+
${rulesText}
|
|
1093
|
+
`;
|
|
1094
|
+
const written = [];
|
|
1095
|
+
const skipped = [];
|
|
1096
|
+
const writeFile2 = (filePath, content) => {
|
|
1097
|
+
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
1098
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
1099
|
+
};
|
|
1100
|
+
const readJson = (filePath) => {
|
|
1101
|
+
if (!existsSync5(filePath)) return {};
|
|
1102
|
+
try {
|
|
1103
|
+
return JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
1104
|
+
} catch {
|
|
1105
|
+
return {};
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
const processedVscode = { done: false, settings: {} };
|
|
1109
|
+
for (const target of GLOBAL_TARGETS) {
|
|
1110
|
+
try {
|
|
1111
|
+
if (target.type === "md") {
|
|
1112
|
+
writeFile2(target.path, mdContent);
|
|
1113
|
+
written.push(target.label);
|
|
1114
|
+
} else if (target.type === "vscode-copilot" || target.type === "vscode-cline") {
|
|
1115
|
+
if (!processedVscode.done) {
|
|
1116
|
+
processedVscode.settings = readJson(target.path);
|
|
1117
|
+
processedVscode.done = true;
|
|
1118
|
+
}
|
|
1119
|
+
if (target.type === "vscode-copilot") {
|
|
1120
|
+
processedVscode.settings["github.copilot.chat.codeGeneration.instructions"] = [{ text: systemPrompt }];
|
|
1121
|
+
}
|
|
1122
|
+
if (target.type === "vscode-cline") {
|
|
1123
|
+
processedVscode.settings["cline.customInstructions"] = systemPrompt;
|
|
1124
|
+
}
|
|
1125
|
+
writeFile2(target.path, JSON.stringify(processedVscode.settings, null, 2));
|
|
1126
|
+
written.push(target.label);
|
|
1127
|
+
} else if (target.type === "continue") {
|
|
1128
|
+
const config2 = readJson(target.path);
|
|
1129
|
+
config2["systemMessage"] = systemPrompt;
|
|
1130
|
+
writeFile2(target.path, JSON.stringify(config2, null, 2));
|
|
1131
|
+
written.push(target.label);
|
|
1132
|
+
} else if (target.type === "aider") {
|
|
1133
|
+
const aiderContent = `# Aider global config \u2014 synced by memory-core
|
|
1134
|
+
read:
|
|
1135
|
+
- ~/.claude/CLAUDE.md
|
|
1136
|
+
`;
|
|
1137
|
+
writeFile2(target.path, aiderContent);
|
|
1138
|
+
written.push(target.label);
|
|
1139
|
+
} else if (target.type === "zed") {
|
|
1140
|
+
const config2 = readJson(target.path);
|
|
1141
|
+
const assistant = config2["assistant"] ?? {};
|
|
1142
|
+
assistant["system_prompt"] = systemPrompt;
|
|
1143
|
+
config2["assistant"] = assistant;
|
|
1144
|
+
writeFile2(target.path, JSON.stringify(config2, null, 2));
|
|
1145
|
+
written.push(target.label);
|
|
1146
|
+
}
|
|
1147
|
+
} catch {
|
|
1148
|
+
skipped.push(target.label);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
spinner.succeed(chalk2.green(`Synced ${memories.length} memories \u2192 ${written.length} agents`));
|
|
1152
|
+
console.log(chalk2.green("\n Updated:"));
|
|
1153
|
+
written.forEach((l) => console.log(chalk2.gray(` \u2713 ${l}`)));
|
|
1154
|
+
if (skipped.length) {
|
|
1155
|
+
console.log(chalk2.yellow("\n Skipped (not installed):"));
|
|
1156
|
+
skipped.forEach((l) => console.log(chalk2.gray(` \u2717 ${l}`)));
|
|
1157
|
+
}
|
|
1158
|
+
console.log(chalk2.bold("\n Every AI agent now follows your memory globally.\n"));
|
|
1159
|
+
await closePool();
|
|
1160
|
+
});
|
|
1161
|
+
var hook = program.command("hook").description("Manage the pre-commit rule enforcement hook");
|
|
1162
|
+
hook.command("install").description("Install pre-commit hook \u2014 blocks commits that violate your architecture rules").action(() => {
|
|
1163
|
+
installHook();
|
|
1164
|
+
});
|
|
1165
|
+
hook.command("uninstall").description("Remove the pre-commit hook").action(() => {
|
|
1166
|
+
uninstallHook();
|
|
1167
|
+
});
|
|
1168
|
+
program.command("check").description("Check staged changes against architecture rules (used by pre-commit hook)").option("--staged", "Check git staged diff (default behaviour)").option("--verbose", "Show model and diff details").action(async (opts) => {
|
|
1169
|
+
await checkStaged({ verbose: opts.verbose ?? false });
|
|
1170
|
+
});
|
|
1171
|
+
program.parseAsync(process.argv);
|