gitmem-mcp 1.2.2 → 1.3.1

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/bin/uninstall.js CHANGED
@@ -28,6 +28,35 @@ const deleteAll = args.includes("--all");
28
28
  const clientIdx = args.indexOf("--client");
29
29
  const clientFlag = clientIdx !== -1 ? args[clientIdx + 1]?.toLowerCase() : null;
30
30
 
31
+ // ── Colors (brand-matched to init wizard) ──
32
+
33
+ const _color =
34
+ !process.env.NO_COLOR &&
35
+ !process.env.GITMEM_NO_COLOR &&
36
+ process.stdout.isTTY;
37
+
38
+ const C = {
39
+ reset: _color ? "\x1b[0m" : "",
40
+ bold: _color ? "\x1b[1m" : "",
41
+ dim: _color ? "\x1b[2m" : "",
42
+ red: _color ? "\x1b[31m" : "",
43
+ green: _color ? "\x1b[32m" : "",
44
+ yellow: _color ? "\x1b[33m" : "",
45
+ };
46
+
47
+ const RIPPLE = `${C.dim}(${C.reset}${C.red}(${C.reset}${C.bold}\u25cf${C.reset}${C.red})${C.reset}${C.dim})${C.reset}`;
48
+ const PRODUCT = `${RIPPLE} ${C.red}gitmem${C.reset}`;
49
+ const CHECK = `${C.bold}\u2714${C.reset}`;
50
+ const SKIP = `${C.dim}\u00b7${C.reset}`;
51
+
52
+ let actionsTaken = 0;
53
+
54
+ function log(icon, msg, extra) {
55
+ if (icon === CHECK) actionsTaken++;
56
+ const suffix = extra ? ` ${C.dim}${extra}${C.reset}` : "";
57
+ console.log(`${icon} ${msg}${suffix}`);
58
+ }
59
+
31
60
  // ── Client Configuration ──
32
61
 
33
62
  const CLIENT_CONFIGS = {
@@ -125,13 +154,11 @@ function writeJson(path, data) {
125
154
  }
126
155
 
127
156
  function isGitmemHook(entry) {
128
- // Claude Code format: entry.hooks is an array of {command: "..."}
129
157
  if (entry.hooks && Array.isArray(entry.hooks)) {
130
158
  return entry.hooks.some(
131
159
  (h) => typeof h.command === "string" && h.command.includes("gitmem")
132
160
  );
133
161
  }
134
- // Cursor format: entry itself has {command: "..."}
135
162
  if (typeof entry.command === "string") {
136
163
  return entry.command.includes("gitmem");
137
164
  }
@@ -142,49 +169,47 @@ function isGitmemHook(entry) {
142
169
 
143
170
  function stepInstructions() {
144
171
  if (!existsSync(cc.instructionsFile)) {
145
- console.log(` No ${cc.instructionsName} found. Skipping.`);
172
+ log(SKIP, `No ${cc.instructionsName} found`);
146
173
  return;
147
174
  }
148
175
 
149
176
  let content = readFileSync(cc.instructionsFile, "utf-8");
150
177
 
151
178
  if (!content.includes(cc.startMarker)) {
152
- console.log(` No gitmem section in ${cc.instructionsName}. Skipping.`);
179
+ log(SKIP, `No gitmem section in ${cc.instructionsName}`);
153
180
  return;
154
181
  }
155
182
 
156
183
  const startIdx = content.indexOf(cc.startMarker);
157
184
  const endIdx = content.indexOf(cc.endMarker);
158
185
  if (startIdx === -1 || endIdx === -1) {
159
- console.log(` Malformed gitmem markers in ${cc.instructionsName}. Skipping.`);
186
+ log(SKIP, `Malformed gitmem markers in ${cc.instructionsName}`);
160
187
  return;
161
188
  }
162
189
 
163
- // Remove the block including markers and surrounding whitespace
164
190
  const before = content.slice(0, startIdx).trimEnd();
165
191
  const after = content.slice(endIdx + cc.endMarker.length).trimStart();
166
-
167
192
  const result = before + (before && after ? "\n\n" : "") + after;
168
193
 
169
194
  if (result.trim() === "") {
170
195
  rmSync(cc.instructionsFile);
171
- console.log(` Removed ${cc.instructionsName} (was gitmem-only)`);
196
+ log(CHECK, `Removed ${cc.instructionsName}`, "(was gitmem-only)");
172
197
  } else {
173
198
  writeFileSync(cc.instructionsFile, result.trimEnd() + "\n");
174
- console.log(` Stripped gitmem section from ${cc.instructionsName}`);
199
+ log(CHECK, `Stripped gitmem section from ${cc.instructionsName}`, "(your content preserved)");
175
200
  }
176
201
  }
177
202
 
178
203
  function stepMcpJson() {
179
204
  const config = readJson(cc.mcpConfigPath);
180
205
  if (!config?.mcpServers) {
181
- console.log(` No ${cc.mcpConfigName} found. Skipping.`);
206
+ log(SKIP, `No ${cc.mcpConfigName} found`);
182
207
  return;
183
208
  }
184
209
 
185
210
  const had = !!config.mcpServers.gitmem || !!config.mcpServers["gitmem-mcp"];
186
211
  if (!had) {
187
- console.log(` No gitmem in ${cc.mcpConfigName}. Skipping.`);
212
+ log(SKIP, `No gitmem in ${cc.mcpConfigName}`);
188
213
  return;
189
214
  }
190
215
 
@@ -193,9 +218,11 @@ function stepMcpJson() {
193
218
 
194
219
  const remaining = Object.keys(config.mcpServers).length;
195
220
  writeJson(cc.mcpConfigPath, config);
196
- console.log(
197
- ` Removed gitmem server (${remaining} other server${remaining !== 1 ? "s" : ""} preserved)`
198
- );
221
+ if (remaining > 0) {
222
+ log(CHECK, `Removed gitmem server`, `(${remaining} other server${remaining !== 1 ? "s" : ""} preserved)`);
223
+ } else {
224
+ log(CHECK, `Removed gitmem server from ${cc.mcpConfigName}`);
225
+ }
199
226
  }
200
227
 
201
228
  function stepHooks() {
@@ -208,7 +235,7 @@ function stepHooks() {
208
235
  function stepHooksClaude() {
209
236
  const settings = readJson(cc.settingsFile);
210
237
  if (!settings?.hooks) {
211
- console.log(" No hooks in .claude/settings.json. Skipping.");
238
+ log(SKIP, "No hooks in .claude/settings.json");
212
239
  return;
213
240
  }
214
241
 
@@ -232,7 +259,7 @@ function stepHooksClaude() {
232
259
  }
233
260
 
234
261
  if (removed === 0) {
235
- console.log(" No gitmem hooks found. Skipping.");
262
+ log(SKIP, "No gitmem hooks found");
236
263
  return;
237
264
  }
238
265
 
@@ -243,18 +270,17 @@ function stepHooksClaude() {
243
270
  }
244
271
 
245
272
  writeJson(cc.settingsFile, settings);
246
- console.log(
247
- ` Removed gitmem hooks` +
248
- (preserved > 0
249
- ? ` (${preserved} other hook${preserved !== 1 ? "s" : ""} preserved)`
250
- : "")
251
- );
273
+ if (preserved > 0) {
274
+ log(CHECK, "Removed automatic memory hooks", `(${preserved} other hook${preserved !== 1 ? "s" : ""} preserved)`);
275
+ } else {
276
+ log(CHECK, "Removed automatic memory hooks");
277
+ }
252
278
  }
253
279
 
254
280
  function stepHooksCursor() {
255
281
  const config = readJson(cc.hooksFile);
256
282
  if (!config?.hooks) {
257
- console.log(` No hooks in ${cc.hooksFileName}. Skipping.`);
283
+ log(SKIP, `No hooks in ${cc.hooksFileName}`);
258
284
  return;
259
285
  }
260
286
 
@@ -278,7 +304,7 @@ function stepHooksCursor() {
278
304
  }
279
305
 
280
306
  if (removed === 0) {
281
- console.log(" No gitmem hooks found. Skipping.");
307
+ log(SKIP, "No gitmem hooks found");
282
308
  return;
283
309
  }
284
310
 
@@ -289,38 +315,36 @@ function stepHooksCursor() {
289
315
  }
290
316
 
291
317
  writeJson(cc.hooksFile, config);
292
- console.log(
293
- ` Removed gitmem hooks` +
294
- (preserved > 0
295
- ? ` (${preserved} other hook${preserved !== 1 ? "s" : ""} preserved)`
296
- : "")
297
- );
318
+ if (preserved > 0) {
319
+ log(CHECK, "Removed automatic memory hooks", `(${preserved} other hook${preserved !== 1 ? "s" : ""} preserved)`);
320
+ } else {
321
+ log(CHECK, "Removed automatic memory hooks");
322
+ }
298
323
  }
299
324
 
300
325
  function stepPermissions() {
301
326
  if (!cc.hasPermissions) {
302
- console.log(` Not needed for ${cc.name}. Skipping.`);
327
+ log(SKIP, `Not needed for ${cc.name}`);
303
328
  return;
304
329
  }
305
330
 
306
331
  const settings = readJson(cc.settingsFile);
307
332
  const allow = settings?.permissions?.allow;
308
333
  if (!Array.isArray(allow)) {
309
- console.log(" No permissions in .claude/settings.json. Skipping.");
334
+ log(SKIP, "No permissions in .claude/settings.json");
310
335
  return;
311
336
  }
312
337
 
313
338
  const pattern = "mcp__gitmem__*";
314
339
  const idx = allow.indexOf(pattern);
315
340
  if (idx === -1) {
316
- console.log(" No gitmem permissions found. Skipping.");
341
+ log(SKIP, "No gitmem permissions found");
317
342
  return;
318
343
  }
319
344
 
320
345
  allow.splice(idx, 1);
321
346
  settings.permissions.allow = allow;
322
347
 
323
- // Clean up empty permissions
324
348
  if (allow.length === 0) {
325
349
  delete settings.permissions.allow;
326
350
  }
@@ -332,31 +356,26 @@ function stepPermissions() {
332
356
  }
333
357
 
334
358
  writeJson(cc.settingsFile, settings);
335
- console.log(" Removed mcp__gitmem__* from permissions.allow");
359
+ log(CHECK, "Removed tool permissions");
336
360
  }
337
361
 
338
362
  async function stepGitmemDir() {
339
363
  if (!existsSync(gitmemDir)) {
340
- console.log(" No .gitmem/ directory. Skipping.");
364
+ log(SKIP, "No .gitmem/ directory");
341
365
  return;
342
366
  }
343
367
 
344
368
  if (deleteAll) {
345
369
  rmSync(gitmemDir, { recursive: true, force: true });
346
- console.log(" Deleted .gitmem/ directory");
370
+ log(CHECK, "Deleted .gitmem/ directory");
347
371
  return;
348
372
  }
349
373
 
350
- console.log(
351
- " This contains your memory data (scars, sessions, decisions)."
352
- );
353
-
354
- // Default to No for data deletion
355
- if (await confirm("Delete .gitmem/?", false)) {
356
- rmSync(gitmemDir, { recursive: true, force: true });
357
- console.log(" Deleted .gitmem/ directory");
374
+ if (await confirm("Keep .gitmem/ memory data for future use?", true)) {
375
+ log(CHECK, ".gitmem/ preserved your memories will be here if you reinstall");
358
376
  } else {
359
- console.log(" Skipped .gitmem/ preserved.");
377
+ rmSync(gitmemDir, { recursive: true, force: true });
378
+ log(CHECK, "Deleted .gitmem/ directory");
360
379
  }
361
380
  }
362
381
 
@@ -366,58 +385,38 @@ function stepGitignore() {
366
385
  let content = readFileSync(gitignorePath, "utf-8");
367
386
  if (!content.includes(".gitmem/")) return;
368
387
 
369
- // Remove the .gitmem/ line
370
388
  const lines = content.split("\n");
371
389
  const filtered = lines.filter((line) => line.trim() !== ".gitmem/");
372
390
  writeFileSync(gitignorePath, filtered.join("\n"));
391
+ log(CHECK, "Cleaned .gitignore");
373
392
  }
374
393
 
375
394
  // ── Main ──
376
395
 
377
396
  async function main() {
397
+ const pkg = readJson(join(__dirname, "..", "package.json"));
398
+ const version = pkg?.version || "1.0.0";
399
+
378
400
  console.log("");
379
- console.log(` gitmem Uninstall (${cc.name})`);
380
- if (clientFlag) {
381
- console.log(` (client: ${client} — via --client flag)`);
382
- } else {
383
- console.log(` (client: ${client} — auto-detected)`);
384
- }
401
+ console.log(`${PRODUCT} \u2500\u2500 uninstall v${version}`);
402
+ console.log(`${C.dim}Removing gitmem from ${cc.name}${clientFlag ? "" : " (auto-detected)"}${C.reset}`);
385
403
  console.log("");
386
404
 
387
- const stepCount = cc.hasPermissions ? 5 : 4;
388
- let step = 1;
389
-
390
- console.log(` Step ${step}/${stepCount} — Remove gitmem section from ${cc.instructionsName}`);
391
405
  stepInstructions();
392
- console.log("");
393
- step++;
394
-
395
- console.log(` Step ${step}/${stepCount} — Remove gitmem from ${cc.mcpConfigName}`);
396
406
  stepMcpJson();
397
- console.log("");
398
- step++;
399
-
400
- const hooksTarget = cc.hooksInSettings ? ".claude/settings.json" : cc.hooksFileName;
401
- console.log(` Step ${step}/${stepCount} — Remove gitmem hooks from ${hooksTarget}`);
402
407
  stepHooks();
403
- console.log("");
404
- step++;
405
-
406
- if (cc.hasPermissions) {
407
- console.log(` Step ${step}/${stepCount} — Remove gitmem permissions from .claude/settings.json`);
408
- stepPermissions();
409
- console.log("");
410
- step++;
411
- }
412
-
413
- console.log(` Step ${step}/${stepCount} — Delete .gitmem/ directory?`);
408
+ stepPermissions();
414
409
  await stepGitmemDir();
415
-
416
- // Also clean .gitignore entry
417
410
  stepGitignore();
418
411
 
419
412
  console.log("");
420
- console.log(" Uninstall complete.");
413
+ if (actionsTaken > 0) {
414
+ console.log(`${C.dim}gitmem-mcp has been removed.${C.reset}`);
415
+ console.log(`${C.dim}Reinstall anytime: ${C.reset}${C.red}npx gitmem-mcp init${C.reset}`);
416
+ } else {
417
+ console.log(`${C.dim}gitmem-mcp is not installed in this project.${C.reset}`);
418
+ console.log(`${C.dim}Install: ${C.reset}${C.red}npx gitmem-mcp init${C.reset}`);
419
+ }
421
420
  console.log("");
422
421
 
423
422
  if (rl) rl.close();
@@ -10,7 +10,7 @@
10
10
  * - Universal: works in ALL MCP clients, no IDE hooks needed
11
11
  * - Lightweight: pure in-memory checks, no I/O
12
12
  */
13
- import { getCurrentSession, hasUnconfirmedScars, getSurfacedScars } from "./session-state.js";
13
+ import { getCurrentSession, hasUnconfirmedScars, getSurfacedScars, isRecallCalled } from "./session-state.js";
14
14
  /**
15
15
  * Tools that require an active session to function properly.
16
16
  * Read-only/administrative tools are excluded.
@@ -107,19 +107,17 @@ export function checkEnforcement(toolName) {
107
107
  };
108
108
  }
109
109
  // Check 3: No recall before consequential action
110
- if (CONSEQUENTIAL_TOOLS.has(toolName)) {
111
- const recallScars = getSurfacedScars().filter(s => s.source === "recall");
112
- if (recallScars.length === 0) {
113
- return {
114
- warning: [
115
- "--- gitmem enforcement ---",
116
- "No recall() was run this session before this action.",
117
- "Consider calling recall() first to check for relevant institutional memory.",
118
- "Past mistakes and patterns may prevent repeating known issues.",
119
- "---",
120
- ].join("\n"),
121
- };
122
- }
110
+ // Uses recallCalled boolean to avoid false positives when recall returns 0 scars
111
+ if (CONSEQUENTIAL_TOOLS.has(toolName) && !isRecallCalled()) {
112
+ return {
113
+ warning: [
114
+ "--- gitmem enforcement ---",
115
+ "No recall() was run this session before this action.",
116
+ "Consider calling recall() first to check for relevant institutional memory.",
117
+ "Past mistakes and patterns may prevent repeating known issues.",
118
+ "---",
119
+ ].join("\n"),
120
+ };
123
121
  }
124
122
  return { warning: null };
125
123
  }
@@ -123,7 +123,8 @@ export class LocalFileStorage {
123
123
  * Uses stemming, IDF weighting, and document length normalization.
124
124
  */
125
125
  async keywordSearch(query, k = 5) {
126
- const learnings = this.readCollection("learnings");
126
+ const learnings = this.readCollection("learnings")
127
+ .filter((l) => l.is_active !== false);
127
128
  if (learnings.length === 0)
128
129
  return [];
129
130
  // Build BM25 documents with field boosting
@@ -18,6 +18,7 @@ interface SessionContext {
18
18
  agent?: string;
19
19
  project?: string;
20
20
  startedAt: Date;
21
+ recallCalled: boolean;
21
22
  surfacedScars: SurfacedScar[];
22
23
  confirmations: ScarConfirmation[];
23
24
  reflections: ScarReflection[];
@@ -29,7 +30,7 @@ interface SessionContext {
29
30
  * Set the current active session
30
31
  * Called by session_start
31
32
  */
32
- export declare function setCurrentSession(context: Omit<SessionContext, 'surfacedScars' | 'confirmations' | 'reflections' | 'observations' | 'children' | 'threads'> & {
33
+ export declare function setCurrentSession(context: Omit<SessionContext, 'recallCalled' | 'surfacedScars' | 'confirmations' | 'reflections' | 'observations' | 'children' | 'threads'> & {
33
34
  surfacedScars?: SurfacedScar[];
34
35
  observations?: Observation[];
35
36
  children?: SessionChild[];
@@ -54,6 +55,16 @@ export declare function getProject(): string | null;
54
55
  * Check if currently working on a Linear issue
55
56
  */
56
57
  export declare function hasActiveIssue(): boolean;
58
+ /**
59
+ * Mark that recall() was called this session (independent of whether it returned scars).
60
+ * Called by recall tool before any early return.
61
+ */
62
+ export declare function setRecallCalled(): void;
63
+ /**
64
+ * Check if recall() was called this session.
65
+ * Used by enforcement to avoid false positives when recall returns 0 scars.
66
+ */
67
+ export declare function isRecallCalled(): boolean;
57
68
  /**
58
69
  * Add surfaced scars to tracking (deduplicates by scar_id)
59
70
  * Called by session_start and recall when scars are surfaced.
@@ -20,6 +20,7 @@ let currentSession = null;
20
20
  export function setCurrentSession(context) {
21
21
  currentSession = {
22
22
  ...context,
23
+ recallCalled: false,
23
24
  surfacedScars: context.surfacedScars || [],
24
25
  confirmations: [],
25
26
  reflections: [],
@@ -59,6 +60,23 @@ export function getProject() {
59
60
  export function hasActiveIssue() {
60
61
  return !!(currentSession?.linearIssue);
61
62
  }
63
+ /**
64
+ * Mark that recall() was called this session (independent of whether it returned scars).
65
+ * Called by recall tool before any early return.
66
+ */
67
+ export function setRecallCalled() {
68
+ if (currentSession) {
69
+ currentSession.recallCalled = true;
70
+ console.error("[session-state] recall() marked as called");
71
+ }
72
+ }
73
+ /**
74
+ * Check if recall() was called this session.
75
+ * Used by enforcement to avoid false positives when recall returns 0 scars.
76
+ */
77
+ export function isRecallCalled() {
78
+ return currentSession?.recallCalled ?? false;
79
+ }
62
80
  /**
63
81
  * Add surfaced scars to tracking (deduplicates by scar_id)
64
82
  * Called by session_start and recall when scars are surfaced.
package/dist/tools/log.js CHANGED
@@ -75,7 +75,7 @@ export async function log(params) {
75
75
  try {
76
76
  const queryTimer = new Timer();
77
77
  const storage = getStorage();
78
- const filters = {};
78
+ const filters = { is_active: "eq.true" };
79
79
  if (typeFilter)
80
80
  filters.learning_type = typeFilter;
81
81
  if (severityFilter)
@@ -47,6 +47,7 @@ interface FormattedScar {
47
47
  applies_when: string[];
48
48
  source_issue?: string;
49
49
  similarity: number;
50
+ is_starter?: boolean;
50
51
  required_verification?: RequiredVerification;
51
52
  variant_info?: ScarWithVariant;
52
53
  why_this_matters?: string;
@@ -19,7 +19,7 @@ import { getProject } from "../services/session-state.js";
19
19
  import { getStorage } from "../services/storage.js";
20
20
  import { Timer, recordMetrics, buildPerformanceData, buildComponentPerformance, calculateContextBytes, } from "../services/metrics.js";
21
21
  import { getOrAssignVariant, formatVariantEnforcement, } from "../services/variant-assignment.js";
22
- import { addSurfacedScars, getCurrentSession } from "../services/session-state.js";
22
+ import { addSurfacedScars, getCurrentSession, setRecallCalled } from "../services/session-state.js";
23
23
  import { getAgentIdentity } from "../services/agent-detection.js";
24
24
  import { v4 as uuidv4 } from "uuid";
25
25
  import * as fs from "fs";
@@ -36,12 +36,18 @@ function formatResponse(scars, plan, dismissals) {
36
36
 
37
37
  No past lessons match this plan closely enough. Scars accumulate as you work — create learnings during session close to build institutional memory.`;
38
38
  }
39
+ // First-recall welcome: all results are starter scars
40
+ const allStarter = scars.length > 0 && scars.every((s) => s.is_starter === true);
39
41
  // Check if any scars have required_verification (blocking gates)
40
42
  const scarsWithVerification = scars.filter((s) => s.required_verification?.blocking);
41
43
  const lines = [
42
44
  formatNudgeHeader(scars.length),
43
45
  "",
44
46
  ];
47
+ if (allStarter) {
48
+ lines.push(`${dimText("This is your first recall — results will get more relevant as you add your own lessons from real experience.")}`);
49
+ lines.push("");
50
+ }
45
51
  // Display blocking verification requirements FIRST and prominently
46
52
  if (scarsWithVerification.length > 0) {
47
53
  lines.push(`${ANSI.yellow}VERIFICATION REQUIRED${ANSI.reset}`);
@@ -161,6 +167,9 @@ export async function recall(params) {
161
167
  const project = params.project || getProject() || "default";
162
168
  const matchCount = params.match_count || 3;
163
169
  const issueId = params.issue_id; // For variant assignment
170
+ // Mark recall as called BEFORE any search — prevents enforcement false positives
171
+ // when recall returns 0 matching scars
172
+ setRecallCalled();
164
173
  // Similarity threshold — suppress weak matches
165
174
  // Pro tier: 0.45 calibrated from UX audit (66% N_A rate at 0.35, APPLYING avg 0.55, N_A avg 0.51)
166
175
  // Free tier: 0.4 (BM25 scores are relative — top result always 1.0)
@@ -182,6 +191,7 @@ export async function recall(params) {
182
191
  counter_arguments: scar.counter_arguments || [],
183
192
  applies_when: [],
184
193
  similarity: scar.similarity || 0,
194
+ is_starter: scar.is_starter,
185
195
  }))
186
196
  // Filter below threshold
187
197
  .filter((scar) => scar.similarity >= similarityThreshold);
@@ -342,6 +352,7 @@ export async function recall(params) {
342
352
  applies_when: scar.applies_when || [],
343
353
  source_issue: scar.source_linear_issue,
344
354
  similarity: scar.similarity || 0,
355
+ is_starter: scar.is_starter,
345
356
  required_verification: scar.required_verification,
346
357
  variant_info: variantResults.get(scar.id),
347
358
  // LLM-cooperative enforcement fields
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "gitmem-mcp",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "mcpName": "io.github.gitmem-dev/gitmem",
5
- "description": "Institutional memory for AI coding agents. Memory that compounds.",
5
+ "description": "Persistent learning memory for AI coding agents. Memory that compounds.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
@@ -1,6 +1,6 @@
1
1
  [
2
2
  {
3
- "id": "00000000-0000-0000-0000-000000000002",
3
+ "id": "81fe2d44-24ae-4946-a063-d4ea3a12a71a",
4
4
  "learning_type": "scar",
5
5
  "title": "Done != Deployed != Verified Working",
6
6
  "description": "Code being 'done' (merged) doesn't mean it's deployed, and being deployed doesn't mean it's working correctly in production. The full loop is: merge → deploy → verify the feature works end-to-end in the target environment. Skipping verification leads to silent failures.",
@@ -18,7 +18,7 @@
18
18
  "created_at": "2026-01-01T00:00:00Z"
19
19
  },
20
20
  {
21
- "id": "00000000-0000-0000-0000-000000000004",
21
+ "id": "07c7f9d3-1336-4fef-81da-af7c97590ab9",
22
22
  "learning_type": "scar",
23
23
  "title": "Database Migration Without Rollback Plan",
24
24
  "description": "Running database migrations without a tested rollback plan risks data loss or extended downtime. Destructive migrations (dropping columns, changing types) are especially dangerous. Always write and test the down migration before running the up migration in production.",
@@ -36,7 +36,7 @@
36
36
  "created_at": "2026-01-01T00:00:00Z"
37
37
  },
38
38
  {
39
- "id": "00000000-0000-0000-0000-000000000010",
39
+ "id": "33996174-a7f4-42f5-81ac-f601e566cc5a",
40
40
  "learning_type": "scar",
41
41
  "title": "Silent Error Swallowing Hides Real Failures",
42
42
  "description": "Empty catch blocks and generic error handlers that log but don't surface errors lead to silent failures. The system appears to work while data is lost or corrupted. At minimum, log errors with enough context to diagnose the issue. Better: fail visibly so problems are caught early.",
@@ -52,5 +52,77 @@
52
52
  "project": "default",
53
53
  "source_date": "2026-01-01",
54
54
  "created_at": "2026-01-01T00:00:00Z"
55
+ },
56
+ {
57
+ "id": "726c03d4-8962-4b0f-89a2-ea1cd0b223aa",
58
+ "learning_type": "scar",
59
+ "title": "Untested Code Passes By Coincidence",
60
+ "description": "Code without tests may appear to work because the happy path succeeds, but untested edge cases silently fail. Tests that don't exist can't catch regressions. 'It works on my machine' is not a test — write assertions for the behavior you expect, especially boundary conditions and error paths.",
61
+ "severity": "high",
62
+ "scar_type": "process",
63
+ "is_starter": true,
64
+ "counter_arguments": [
65
+ "The code is simple enough that tests aren't needed — but simple code gets modified later and regressions appear without test coverage",
66
+ "Manual testing is sufficient for this — but manual tests don't run automatically and get skipped under time pressure"
67
+ ],
68
+ "keywords": ["testing", "coverage", "regression", "assertions", "edge-cases"],
69
+ "domain": ["testing", "quality"],
70
+ "project": "default",
71
+ "source_date": "2026-01-01",
72
+ "created_at": "2026-01-01T00:00:00Z"
73
+ },
74
+ {
75
+ "id": "8a376e29-dc8d-49a7-b840-28643086e90a",
76
+ "learning_type": "scar",
77
+ "title": "Environment Config Drift Between Local and Production",
78
+ "description": "Environment variables, feature flags, and configuration that differ between local dev and production cause bugs that only appear after deployment. The fix works locally but breaks in prod because of a missing env var, different API URL, or stricter security policy. Keep a canonical list of all config, validate it at startup, and test with production-like config.",
79
+ "severity": "high",
80
+ "scar_type": "operational",
81
+ "is_starter": true,
82
+ "counter_arguments": [
83
+ "We use the same .env file everywhere — but .env files drift silently and new variables get added locally without updating production",
84
+ "Our staging environment matches production — but staging configs still diverge over time without automated drift detection"
85
+ ],
86
+ "keywords": ["environment", "config", "env-vars", "production", "deployment", "drift"],
87
+ "domain": ["config", "deployment"],
88
+ "project": "default",
89
+ "source_date": "2026-01-01",
90
+ "created_at": "2026-01-01T00:00:00Z"
91
+ },
92
+ {
93
+ "id": "b6a9ac29-25ba-4667-9fed-0ada98dd588e",
94
+ "learning_type": "scar",
95
+ "title": "Changing Dependencies Without Checking Breaking Changes",
96
+ "description": "Upgrading or adding dependencies without reading the changelog introduces breaking changes that surface later as mysterious failures. Semver violations are common — a minor version bump can still break your code. Always read the changelog, check for breaking changes, and run your test suite after any dependency change.",
97
+ "severity": "medium",
98
+ "scar_type": "process",
99
+ "is_starter": true,
100
+ "counter_arguments": [
101
+ "It's just a patch version so it should be safe — but semver is a social contract, not a guarantee, and patch releases do contain breaking changes",
102
+ "Our lockfile protects us — but lockfiles only help if everyone uses them consistently and CI doesn't do fresh installs"
103
+ ],
104
+ "keywords": ["dependencies", "npm", "upgrade", "breaking-changes", "changelog", "semver"],
105
+ "domain": ["dependencies", "maintenance"],
106
+ "project": "default",
107
+ "source_date": "2026-01-01",
108
+ "created_at": "2026-01-01T00:00:00Z"
109
+ },
110
+ {
111
+ "id": "5db0ffc7-e6d8-48bc-aa23-fce7fd8eaadb",
112
+ "learning_type": "scar",
113
+ "title": "Fixing Symptoms Instead of Root Causes",
114
+ "description": "Adding workarounds, special cases, or retry logic to mask a bug treats the symptom while the root cause remains. The workaround becomes tech debt that makes the real fix harder to find later. Before writing a fix, trace the execution path to understand WHY the failure happens, not just WHERE it happens.",
115
+ "severity": "high",
116
+ "scar_type": "process",
117
+ "is_starter": true,
118
+ "counter_arguments": [
119
+ "We need a quick fix now and can investigate later — but 'later' rarely comes and the workaround becomes permanent",
120
+ "The root cause is in a system we don't control — but understanding the root cause still helps write a better workaround with clear documentation"
121
+ ],
122
+ "keywords": ["debugging", "root-cause", "workaround", "tech-debt", "symptoms"],
123
+ "domain": ["debugging", "engineering"],
124
+ "project": "default",
125
+ "source_date": "2026-01-01",
126
+ "created_at": "2026-01-01T00:00:00Z"
55
127
  }
56
128
  ]