mindlore 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,7 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
 
15
15
  const { DB_NAME } = require('./lib/constants.cjs');
16
+ const { openDatabase } = require('../hooks/lib/mindlore-common.cjs');
16
17
  const MAX_RESULTS = 3;
17
18
 
18
19
  // ── Helpers ────────────────────────────────────────────────────────────
@@ -46,16 +47,12 @@ function main() {
46
47
  process.exit(1);
47
48
  }
48
49
 
49
- let Database;
50
- try {
51
- Database = require('better-sqlite3');
52
- } catch (_err) {
50
+ const db = openDatabase(dbPath, { readonly: true });
51
+ if (!db) {
53
52
  console.error(' better-sqlite3 not installed.');
54
53
  process.exit(1);
55
54
  }
56
55
 
57
- const db = new Database(dbPath, { readonly: true });
58
-
59
56
  try {
60
57
  // Sanitize query for FTS5 (escape special chars)
61
58
  const sanitized = query.replace(/['"(){}[\]*:^~!]/g, ' ').trim();
@@ -15,35 +15,19 @@ const path = require('path');
15
15
  // ── Constants ──────────────────────────────────────────────────────────
16
16
 
17
17
  const { DIRECTORIES, TYPE_TO_DIR } = require('./lib/constants.cjs');
18
+ const { parseFrontmatter: _parseFm, getAllMdFiles: _getAllMd } = require('../hooks/lib/mindlore-common.cjs');
18
19
 
19
20
  // ── Helpers ────────────────────────────────────────────────────────────
20
21
 
22
+ // Wrapper: shared parseFrontmatter returns { meta, body }, health-check expects flat object or null
21
23
  function parseFrontmatter(content) {
22
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
23
- if (!match) return null;
24
-
25
- const fm = {};
26
- const lines = match[1].split('\n');
27
- for (const line of lines) {
28
- const colonIdx = line.indexOf(':');
29
- if (colonIdx === -1) continue;
30
- const key = line.slice(0, colonIdx).trim();
31
- let value = line.slice(colonIdx + 1).trim();
32
- // Handle arrays
33
- if (value.startsWith('[') && value.endsWith(']')) {
34
- value = value
35
- .slice(1, -1)
36
- .split(',')
37
- .map((s) => s.trim());
38
- }
39
- fm[key] = value;
40
- }
41
- return fm;
24
+ const { meta } = _parseFm(content);
25
+ return Object.keys(meta).length > 0 ? meta : null;
42
26
  }
43
27
 
44
28
  // Health check needs ALL .md files (no skip), so pass empty set
45
29
  function getAllMdFiles(dir) {
46
- return require('../hooks/lib/mindlore-common.cjs').getAllMdFiles(dir, new Set());
30
+ return _getAllMd(dir, new Set());
47
31
  }
48
32
 
49
33
  // ── Checks ─────────────────────────────────────────────────────────────
@@ -116,10 +100,10 @@ class HealthChecker {
116
100
  }
117
101
  const content = fs.readFileSync(indexPath, 'utf8');
118
102
  const lines = content.trim().split('\n');
119
- if (lines.length > 30) {
103
+ if (lines.length > 60) {
120
104
  return {
121
105
  warn: true,
122
- detail: `${lines.length} lines (should be ~15-20, consider trimming)`,
106
+ detail: `${lines.length} lines (should be ~15-60, consider trimming)`,
123
107
  };
124
108
  }
125
109
  return { ok: true, detail: `${lines.length} lines` };
@@ -147,9 +131,31 @@ class HealthChecker {
147
131
  const hashResult = db
148
132
  .prepare('SELECT count(*) as cnt FROM file_hashes')
149
133
  .get();
134
+
135
+ // Verify 9-column schema (slug, description, type, category, title, content, tags, quality + path)
136
+ let schemaVersion = 0;
137
+ try {
138
+ db.prepare('SELECT tags, quality FROM mindlore_fts LIMIT 0').run();
139
+ schemaVersion = 9;
140
+ } catch (_err) {
141
+ try {
142
+ db.prepare('SELECT slug, description, category, title FROM mindlore_fts LIMIT 0').run();
143
+ schemaVersion = 7;
144
+ } catch (_err2) {
145
+ schemaVersion = 2;
146
+ }
147
+ }
148
+
149
+ if (schemaVersion < 9) {
150
+ return {
151
+ warn: true,
152
+ detail: `${result.cnt} indexed, ${hashResult.cnt} hashes — ${schemaVersion}-col schema (run: npx mindlore init to upgrade to 9-col)`,
153
+ };
154
+ }
155
+
150
156
  return {
151
157
  ok: true,
152
- detail: `${result.cnt} indexed, ${hashResult.cnt} hashes`,
158
+ detail: `${result.cnt} indexed, ${hashResult.cnt} hashes, 9-col schema`,
153
159
  };
154
160
  } catch (err) {
155
161
  return { ok: false, detail: `FTS5 error: ${err.message}` };
@@ -263,11 +269,72 @@ class HealthChecker {
263
269
  detail: `${mdFiles.length} files validated`,
264
270
  };
265
271
  }
266
- return {
267
- ok: wrongDir > 0 ? false : undefined,
268
- warn: wrongDir === 0,
269
- detail: issues.join(', '),
270
- };
272
+ if (wrongDir > 0) {
273
+ return { ok: false, detail: issues.join(', ') };
274
+ }
275
+ return { warn: true, detail: issues.join(', ') };
276
+ });
277
+ }
278
+
279
+ // Check 17: Stale deltas (30+ days without archived: true)
280
+ checkStaleDeltas() {
281
+ this.check('Stale deltas', () => {
282
+ const diaryDir = path.join(this.baseDir, 'diary');
283
+ if (!fs.existsSync(diaryDir)) return { ok: true, detail: 'no diary dir' };
284
+
285
+ const now = Date.now();
286
+ const thirtyDays = 30 * 24 * 60 * 60 * 1000;
287
+ let stale = 0;
288
+
289
+ const files = fs.readdirSync(diaryDir).filter((f) => f.startsWith('delta-') && f.endsWith('.md'));
290
+ for (const file of files) {
291
+ const fullPath = path.join(diaryDir, file);
292
+ const content = fs.readFileSync(fullPath, 'utf8').replace(/\r\n/g, '\n');
293
+ const fm = parseFrontmatter(content);
294
+ if (fm && fm.archived === 'true') continue;
295
+
296
+ const stat = fs.statSync(fullPath);
297
+ if (now - stat.mtimeMs > thirtyDays) stale++;
298
+ }
299
+
300
+ if (stale === 0) return { ok: true, detail: `${files.length} deltas, none stale` };
301
+ return { warn: true, detail: `${stale} deltas older than 30 days without archived flag — run /mindlore-log reflect` };
302
+ });
303
+ }
304
+
305
+ // Check 18: Conflicting analyses (same tags, different confidence)
306
+ checkConflictingAnalyses() {
307
+ this.check('Conflicting analyses', () => {
308
+ const analysesDir = path.join(this.baseDir, 'analyses');
309
+ if (!fs.existsSync(analysesDir)) return { ok: true, detail: 'no analyses dir' };
310
+
311
+ const files = fs.readdirSync(analysesDir).filter((f) => f.endsWith('.md'));
312
+ if (files.length < 2) return { ok: true, detail: `${files.length} analyses, no conflict possible` };
313
+
314
+ const tagMap = {};
315
+ for (const file of files) {
316
+ const content = fs.readFileSync(path.join(analysesDir, file), 'utf8').replace(/\r\n/g, '\n');
317
+ const fm = parseFrontmatter(content);
318
+ if (!fm || !fm.tags || !fm.confidence) continue;
319
+
320
+ const tags = Array.isArray(fm.tags) ? fm.tags : String(fm.tags).split(',').map((t) => t.trim());
321
+ for (const tag of tags) {
322
+ if (!tagMap[tag]) tagMap[tag] = [];
323
+ tagMap[tag].push({ file, confidence: fm.confidence });
324
+ }
325
+ }
326
+
327
+ const conflicts = [];
328
+ for (const [tag, entries] of Object.entries(tagMap)) {
329
+ if (entries.length < 2) continue;
330
+ const confidences = new Set(entries.map((e) => e.confidence));
331
+ if (confidences.size > 1) {
332
+ conflicts.push(`${tag}: ${entries.map((e) => `${e.file}(${e.confidence})`).join(' vs ')}`);
333
+ }
334
+ }
335
+
336
+ if (conflicts.length === 0) return { ok: true, detail: `${files.length} analyses, no conflicts` };
337
+ return { warn: true, detail: `${conflicts.length} tag conflicts: ${conflicts.slice(0, 2).join('; ')}` };
271
338
  });
272
339
  }
273
340
 
@@ -280,6 +347,8 @@ class HealthChecker {
280
347
  this.checkDatabase();
281
348
  this.checkOrphans();
282
349
  this.checkFrontmatter();
350
+ this.checkStaleDeltas();
351
+ this.checkConflictingAnalyses();
283
352
  return this;
284
353
  }
285
354
 
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * mindlore uninstall — Remove Mindlore hooks, skills, and optionally project data.
6
+ *
7
+ * Usage: npx mindlore uninstall [--all]
8
+ *
9
+ * --all: also remove .mindlore/ project data (without flag, only hooks + skills)
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { homedir } = require('./lib/constants.cjs');
15
+
16
+ function log(msg) {
17
+ console.log(` ${msg}`);
18
+ }
19
+
20
+ // ── Remove hooks from settings.json ────────────────────────────────────
21
+
22
+ function removeHooks() {
23
+ const settingsPath = path.join(homedir(), '.claude', 'settings.json');
24
+
25
+ if (!fs.existsSync(settingsPath)) {
26
+ log('No settings.json found, skipping hooks');
27
+ return 0;
28
+ }
29
+
30
+ let settings;
31
+ try {
32
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
33
+ } catch (_err) {
34
+ log('Could not parse settings.json, skipping hooks');
35
+ return 0;
36
+ }
37
+
38
+ if (!settings.hooks) return 0;
39
+
40
+ let removed = 0;
41
+ for (const event of Object.keys(settings.hooks)) {
42
+ const entries = settings.hooks[event];
43
+ if (!Array.isArray(entries)) continue;
44
+
45
+ const filtered = entries.filter((entry) => {
46
+ const hooks = entry.hooks || [];
47
+ const hasMindlore = hooks.some(
48
+ (h) => (h.command || '').includes('mindlore-')
49
+ );
50
+ // Also check flat format (legacy)
51
+ const flatMindlore = (entry.command || '').includes('mindlore-');
52
+
53
+ if (hasMindlore || flatMindlore) {
54
+ removed++;
55
+ return false;
56
+ }
57
+ return true;
58
+ });
59
+
60
+ settings.hooks[event] = filtered;
61
+
62
+ // Clean up empty arrays
63
+ if (settings.hooks[event].length === 0) {
64
+ delete settings.hooks[event];
65
+ }
66
+ }
67
+
68
+ if (removed > 0) {
69
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
70
+ }
71
+
72
+ return removed;
73
+ }
74
+
75
+ // ── Remove skills from ~/.claude/skills/ ───────────────────────────────
76
+
77
+ function removeSkills() {
78
+ const skillsDir = path.join(homedir(), '.claude', 'skills');
79
+ if (!fs.existsSync(skillsDir)) return 0;
80
+
81
+ const mindloreSkills = fs
82
+ .readdirSync(skillsDir)
83
+ .filter((d) => d.startsWith('mindlore-'));
84
+
85
+ let removed = 0;
86
+ for (const skill of mindloreSkills) {
87
+ const skillPath = path.join(skillsDir, skill);
88
+ fs.rmSync(skillPath, { recursive: true, force: true });
89
+ removed++;
90
+ }
91
+
92
+ return removed;
93
+ }
94
+
95
+ // ── Remove SCHEMA.md from projectDocFiles ──────────────────────────────
96
+
97
+ function removeFromProjectDocs() {
98
+ const projectSettingsPath = path.join(process.cwd(), '.claude', 'settings.json');
99
+ if (!fs.existsSync(projectSettingsPath)) return false;
100
+
101
+ let settings;
102
+ try {
103
+ settings = JSON.parse(fs.readFileSync(projectSettingsPath, 'utf8'));
104
+ } catch (_err) {
105
+ return false;
106
+ }
107
+
108
+ if (!settings.projectDocFiles) return false;
109
+
110
+ const before = settings.projectDocFiles.length;
111
+ settings.projectDocFiles = settings.projectDocFiles.filter(
112
+ (p) => !p.includes('mindlore')
113
+ );
114
+
115
+ if (settings.projectDocFiles.length < before) {
116
+ fs.writeFileSync(
117
+ projectSettingsPath,
118
+ JSON.stringify(settings, null, 2),
119
+ 'utf8'
120
+ );
121
+ return true;
122
+ }
123
+ return false;
124
+ }
125
+
126
+ // ── Remove .mindlore/ project data ─────────────────────────────────────
127
+
128
+ function removeProjectData() {
129
+ const mindloreDir = path.join(process.cwd(), '.mindlore');
130
+ if (!fs.existsSync(mindloreDir)) {
131
+ log('No .mindlore/ directory in current project');
132
+ return false;
133
+ }
134
+
135
+ fs.rmSync(mindloreDir, { recursive: true, force: true });
136
+ return true;
137
+ }
138
+
139
+ // ── Main ───────────────────────────────────────────────────────────────
140
+
141
+ function main() {
142
+ const args = process.argv.slice(2);
143
+ const removeAll = args.includes('--all');
144
+
145
+ console.log('\n Mindlore — Uninstall\n');
146
+
147
+ // Hooks
148
+ const hooksRemoved = removeHooks();
149
+ log(
150
+ hooksRemoved > 0
151
+ ? `Removed ${hooksRemoved} hooks from ~/.claude/settings.json`
152
+ : 'No hooks found'
153
+ );
154
+
155
+ // Skills
156
+ const skillsRemoved = removeSkills();
157
+ log(
158
+ skillsRemoved > 0
159
+ ? `Removed ${skillsRemoved} skills from ~/.claude/skills/`
160
+ : 'No skills found'
161
+ );
162
+
163
+ // Project doc files
164
+ const docsRemoved = removeFromProjectDocs();
165
+ log(
166
+ docsRemoved
167
+ ? 'Removed SCHEMA.md from project settings'
168
+ : 'No project doc references found'
169
+ );
170
+
171
+ // Project data (only with --all)
172
+ if (removeAll) {
173
+ const dataRemoved = removeProjectData();
174
+ log(
175
+ dataRemoved
176
+ ? 'Removed .mindlore/ project data'
177
+ : 'No .mindlore/ directory found'
178
+ );
179
+ } else {
180
+ log('.mindlore/ project data kept (use --all to remove)');
181
+ }
182
+
183
+ console.log('\n Done! Mindlore has been uninstalled.\n');
184
+ }
185
+
186
+ main();
@@ -0,0 +1,71 @@
1
+ # Skill: Mindlore Decide
2
+
3
+ Record and list decisions in the `.mindlore/decisions/` directory.
4
+
5
+ ## Trigger
6
+
7
+ User says `/mindlore-decide record` or `/mindlore-decide list`.
8
+
9
+ ## Modes
10
+
11
+ ### record
12
+
13
+ Record a new decision.
14
+
15
+ **Flow:**
16
+ 1. Ask user (or extract from context): What was decided? What alternatives were considered? Why this choice?
17
+ 2. Generate slug from decision title (kebab-case, max 5 words)
18
+ 3. Check if a previous decision on same topic exists → set `supersedes` field
19
+ 4. Write to `.mindlore/decisions/{slug}.md` with frontmatter:
20
+
21
+ ```yaml
22
+ ---
23
+ slug: use-fts5-over-vector
24
+ type: decision
25
+ title: Use FTS5 over vector search for v0.1
26
+ tags: [search, fts5, architecture]
27
+ date: 2026-04-11
28
+ supersedes: null
29
+ status: active
30
+ description: Chose FTS5 keyword search as primary engine, vector deferred to v0.4
31
+ ---
32
+ ```
33
+
34
+ 5. Body structure:
35
+ ```markdown
36
+ # {title}
37
+
38
+ ## Context
39
+ Why this decision was needed.
40
+
41
+ ## Alternatives Considered
42
+ 1. **Option A** — pros/cons
43
+ 2. **Option B** — pros/cons
44
+
45
+ ## Decision
46
+ What was chosen and why.
47
+
48
+ ## Consequences
49
+ What this means going forward.
50
+ ```
51
+
52
+ 6. Append to `log.md`: `| {date} | decide | {slug}.md |`
53
+ 7. FTS5 auto-indexes via FileChanged hook
54
+
55
+ ### list
56
+
57
+ List active decisions.
58
+
59
+ **Flow:**
60
+ 1. Read all `.md` files in `.mindlore/decisions/`
61
+ 2. Parse frontmatter, filter `status: active`
62
+ 3. Display as table: slug, title, date, tags
63
+ 4. Show supersedes chain if any (A → B → C)
64
+
65
+ ## Rules
66
+
67
+ - Slug must be unique in decisions/
68
+ - `supersedes` field points to the slug of the replaced decision
69
+ - When a decision is superseded, update old one: `status: superseded`
70
+ - Tags should match domain topics for FTS5 discoverability
71
+ - Keep decision body concise — context + alternatives + choice + consequences
@@ -94,9 +94,20 @@ Only update the stats line: increment source count and total count.
94
94
  N source, N analysis, N total
95
95
  ```
96
96
 
97
- ## Post-Ingest Verification
97
+ ## Post-Ingest Quality Gate
98
98
 
99
- After ingest, run health check:
99
+ After every ingest, verify all 6 checkpoints before reporting success:
100
+
101
+ 1. **raw/ file exists** — immutable capture written with frontmatter (slug, type, source_url)
102
+ 2. **sources/ summary exists** — processed summary with full frontmatter (slug, type, title, tags, quality, description)
103
+ 3. **INDEX.md updated** — stats line incremented, Recent section has new entry
104
+ 4. **Domain updated** — if relevant domain exists, new finding added (max 1 domain per ingest)
105
+ 5. **log.md entry** — append `| {date} | ingest | {slug}.md |`
106
+ 6. **FTS5 indexed** — FileChanged hook auto-triggers, but verify: `node scripts/mindlore-fts5-search.cjs "{keyword}"` returns the new file
107
+
108
+ If any checkpoint fails, fix it before reporting "ingest complete". Do NOT skip steps.
109
+
110
+ Optional: run full health check for structural integrity:
100
111
  ```bash
101
112
  node scripts/mindlore-health-check.cjs
102
113
  ```
@@ -0,0 +1,79 @@
1
+ # Skill: Mindlore Log
2
+
3
+ Session logging, pattern extraction, and wiki updates.
4
+
5
+ ## Trigger
6
+
7
+ `/mindlore-log <mode>` where mode is `log`, `reflect`, `status`, or `save`.
8
+
9
+ ## Modes
10
+
11
+ ### log
12
+
13
+ Write a manual diary entry.
14
+
15
+ **Flow:**
16
+ 1. User provides note/observation (or extract from conversation context)
17
+ 2. Generate slug: `note-YYYY-MM-DD-HHmm`
18
+ 3. Write to `.mindlore/diary/{slug}.md`:
19
+
20
+ ```yaml
21
+ ---
22
+ slug: note-2026-04-11-1530
23
+ type: diary
24
+ date: 2026-04-11
25
+ ---
26
+ ```
27
+
28
+ 4. Body: user's note as-is
29
+ 5. Append to `log.md`: `| {date} | log | {slug}.md |`
30
+
31
+ ### reflect
32
+
33
+ Scan old deltas, extract patterns, write to learnings/.
34
+
35
+ **Flow (v0.2 — basic):**
36
+ 1. Read all non-archived delta files in `diary/` (no `archived: true` frontmatter)
37
+ 2. Present summary to user: "Found N unprocessed deltas spanning DATE1 to DATE2"
38
+ 3. For each delta, extract: repeated topics, recurring decisions, common file changes
39
+ 4. Propose learnings to user: "I found these patterns: ..."
40
+ 5. User approves → write to `learnings/{topic}.md` (append if exists, create if not)
41
+ 6. Format: `YAPMA:` / `BEST PRACTICE:` / `KRITIK:` prefixed rules
42
+ 7. Mark processed deltas: add `archived: true` to their frontmatter
43
+ 8. Append to `log.md`: `| {date} | reflect | {N} deltas processed, {M} learnings written |`
44
+
45
+ **Important:** Do NOT auto-extract patterns. Present findings, user approves. This is v0.2 basic mode — automated pattern extraction deferred to v0.2.1.
46
+
47
+ ### status
48
+
49
+ Show recent session summary.
50
+
51
+ **Flow:**
52
+ 1. Read last 5 delta files from `diary/` (sorted by date, newest first)
53
+ 2. For each delta, extract: date, commits count, changed files count
54
+ 3. Display as compact table
55
+ 4. Show any open items (from delta "Yarım Kalan" sections if present)
56
+ 5. Show total: "N sessions, M commits, K unique files changed"
57
+
58
+ ### save
59
+
60
+ Structured delta + log.md append + domain wiki update.
61
+
62
+ **Flow:**
63
+ 1. Gather current session context:
64
+ - Recent git commits (last 5)
65
+ - Changed files
66
+ - Decisions made (if decision-detector captured any)
67
+ 2. Write structured delta to `diary/delta-YYYY-MM-DD-HHmm.md` (same format as session-end hook)
68
+ 3. Append to `log.md`
69
+ 4. Ask user: "Which domain pages should be updated with this session's findings?"
70
+ 5. If user specifies domains → update those `.mindlore/domains/{slug}.md` pages with new findings
71
+ 6. Max 2 domain updates per save (prevent sprawl)
72
+
73
+ ## Rules
74
+
75
+ - Diary files are session-scoped (temporary), learnings are permanent
76
+ - reflect marks deltas as `archived: true` — they stay in diary/ but are not processed again
77
+ - Health check warns on deltas older than 30 days without `archived: true`
78
+ - learnings/ files are topic-based (one per topic), append-only
79
+ - save mode is the manual equivalent of what session-end hook does automatically
@@ -0,0 +1,125 @@
1
+ # Skill: Mindlore Query
2
+
3
+ Search, ask, analyze, and retrieve knowledge from `.mindlore/`.
4
+
5
+ ## Trigger
6
+
7
+ `/mindlore-query <mode> [query]` where mode is `search`, `ask`, `stats`, or `brief`.
8
+
9
+ ## Modes
10
+
11
+ ### search
12
+
13
+ FTS5 keyword search with direct results.
14
+
15
+ **Flow:**
16
+ 1. Parse user query into keywords (strip stop words)
17
+ 2. Run FTS5 MATCH on `mindlore_fts` table
18
+ 3. Return top 5 results (configurable) with: path, title, description, category, rank score
19
+ 4. Display as table with snippet preview
20
+ 5. If `--tags <tag>` flag provided: `WHERE tags MATCH '<tag>'` filter
21
+
22
+ **Output format:**
23
+ ```
24
+ | # | Category | Title | Description | Score |
25
+ |---|----------|-------|-------------|-------|
26
+ | 1 | sources | React Hooks | useEffect cleanup patterns | -2.34 |
27
+ ```
28
+
29
+ ### ask
30
+
31
+ Compounding query pipeline — knowledge grows with each answer.
32
+
33
+ **Flow:**
34
+ 1. Parse user question
35
+ 2. FTS5 search → find relevant files (sources + domains + analyses + insights — previous answers INCLUDED)
36
+ 3. Read top 3-5 relevant files using ctx_execute_file if context-mode available, else Read
37
+ 4. Synthesize answer from found knowledge
38
+ 5. Cite sources: `[kaynak: sources/x.md]` format
39
+ 6. Ask user: "Bu cevabı kaydetmemi ister misin?"
40
+ 7. If yes → determine target:
41
+ - Short answer (<200 lines, 1-2 sources) → `insights/{slug}.md`
42
+ - Large synthesis (200+ lines, 3+ sources) → `analyses/{slug}.md`
43
+ 8. Write with frontmatter:
44
+
45
+ ```yaml
46
+ ---
47
+ slug: react-hooks-cleanup-comparison
48
+ type: insight
49
+ title: React Hooks Cleanup Comparison
50
+ tags: [react, hooks, useEffect]
51
+ confidence: high
52
+ sources_used: [react-hooks, typescript-generics]
53
+ description: Comparison of cleanup patterns in useEffect vs useLayoutEffect
54
+ ---
55
+ ```
56
+
57
+ 9. FTS5 auto-indexes via FileChanged hook → next query finds this answer
58
+ 10. Update relevant domain page (max 1) with backlink if applicable
59
+ 11. Append to `log.md`: `| {date} | query-ask | {slug}.md |`
60
+
61
+ **Compounding effect:** Step 2 searches ALL of `.mindlore/` including previous `insights/` and `analyses/`. Each saved answer enriches the next query.
62
+
63
+ **Error compounding prevention:**
64
+ - `confidence` field is REQUIRED on writebacks (high/medium/low)
65
+ - `sources_used` lists exact slugs — traceability
66
+ - Health check flags conflicting analyses on same topic (different confidence)
67
+ - User approval is the quality gate — low-quality answers are not saved
68
+
69
+ ### stats
70
+
71
+ Knowledge base statistics.
72
+
73
+ **Flow:**
74
+ 1. Count files per directory (9 directories)
75
+ 2. Count FTS5 entries and file_hashes entries
76
+ 3. Find most recent ingest (latest file by modified date per directory)
77
+ 4. Count tags frequency (parse all frontmatter, aggregate tags)
78
+ 5. Display summary:
79
+
80
+ ```
81
+ Mindlore Stats
82
+ ─────────────
83
+ Total files: 47 (FTS5: 47 indexed)
84
+ - sources: 18
85
+ - domains: 5
86
+ - analyses: 6
87
+ - insights: 3
88
+ - connections: 2
89
+ - learnings: 4
90
+ - diary: 8
91
+ - decisions: 1
92
+ - raw: 0
93
+
94
+ Top tags: security (12), hooks (8), fts5 (6), testing (5)
95
+ Last ingest: 2026-04-11 (sources/react-hooks.md)
96
+ DB size: 1.2 MB
97
+ ```
98
+
99
+ ### brief
100
+
101
+ Quick context on a topic — token-efficient (~50 tokens output).
102
+
103
+ **Flow:**
104
+ 1. FTS5 search for topic
105
+ 2. If domain page exists → read first 3 lines of body (after frontmatter)
106
+ 3. If no domain → read description field from top FTS5 match
107
+ 4. Return: title + description + "Read full: {path}" pointer
108
+ 5. Do NOT read full file — this mode is for quick "do I need to open this?" decisions
109
+
110
+ **Output format:**
111
+ ```
112
+ [Mindlore Brief: Security]
113
+ SSH hardening, firewall rules, audit checks. 5 sources, 2 analyses linked.
114
+ → Read full: .mindlore/domains/security.md
115
+ ```
116
+
117
+ ## Rules
118
+
119
+ - All modes respect the SCHEMA.md writeback rules (Section 6: Wiki vs Diary)
120
+ - search and brief are read-only — no writes
121
+ - ask writes only with user approval
122
+ - stats is read-only
123
+ - Token strategy: prefer ctx_execute_file (if context-mode installed), fallback to Read
124
+ - Tags filter: `--tags security` works in search and ask modes
125
+ - Max results: search=5, ask=3-5 (for synthesis), brief=1