@shahmilsaari/memory-core 0.2.16 → 0.2.18

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