codetrap 0.1.2 → 0.1.4

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.
Files changed (53) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +112 -33
  3. package/docs/installation.md +18 -10
  4. package/package.json +4 -1
  5. package/plugins/codetrap-agent/.codex-plugin/plugin.json +34 -0
  6. package/plugins/codetrap-agent/hooks/post-flight-capture.example.md +25 -0
  7. package/plugins/codetrap-agent/hooks/pre-edit.example.sh +10 -0
  8. package/plugins/codetrap-agent/hooks.json +11 -0
  9. package/plugins/codetrap-agent/skills/codetrap-capture/SKILL.md +19 -0
  10. package/plugins/codetrap-agent/skills/codetrap-check/SKILL.md +14 -0
  11. package/plugins/codetrap-agent/templates/AGENTS.codetrap.md +25 -0
  12. package/scripts/release-preflight.ts +55 -0
  13. package/skills/codetrap-add/SKILL.md +4 -1
  14. package/skills/codetrap-check/SKILL.md +24 -4
  15. package/skills/codetrap-search/SKILL.md +32 -12
  16. package/src/commands/command-result.ts +29 -0
  17. package/src/commands/router.ts +6 -400
  18. package/src/commands/workflow.ts +419 -0
  19. package/src/db/embedding-queries.ts +33 -0
  20. package/src/db/queries.ts +165 -48
  21. package/src/db/repository.ts +72 -15
  22. package/src/db/schema.ts +35 -0
  23. package/src/domain/trap.ts +38 -10
  24. package/src/index.ts +13 -1
  25. package/src/lib/command-requests.ts +133 -0
  26. package/src/lib/config.ts +102 -0
  27. package/src/lib/constants.ts +1 -1
  28. package/src/lib/doctor.ts +86 -0
  29. package/src/lib/embedding-health.ts +49 -0
  30. package/src/lib/embedding-index.ts +53 -0
  31. package/src/lib/format.ts +6 -2
  32. package/src/lib/output-json.ts +141 -0
  33. package/src/lib/scope-context.ts +118 -0
  34. package/src/lib/scope-maintenance.ts +71 -0
  35. package/src/lib/scope-migration.ts +315 -0
  36. package/src/lib/scope-path.ts +99 -0
  37. package/src/lib/scope.ts +16 -11
  38. package/src/lib/search-normalizer.ts +6 -0
  39. package/src/lib/search-policy.ts +365 -0
  40. package/src/lib/search-result-card.ts +2 -7
  41. package/src/lib/search-service.ts +67 -120
  42. package/src/lib/store.ts +129 -108
  43. package/src/lib/trap-archive.ts +9 -42
  44. package/src/lib/trap-codec.ts +113 -0
  45. package/src/lib/trap-json-fields.ts +12 -0
  46. package/src/lib/trap-lifecycle.ts +37 -0
  47. package/src/lib/trap-mutation-result.ts +36 -0
  48. package/src/lib/trap-operations.ts +30 -9
  49. package/src/lib/trap-scope-match.ts +112 -0
  50. package/src/lib/trap-search-document.ts +8 -1
  51. package/src/lib/trap-transfer.ts +88 -0
  52. package/src/mcp/server.ts +77 -72
  53. package/src/mcp/tools.ts +32 -5
package/src/db/queries.ts CHANGED
@@ -12,32 +12,37 @@ import {
12
12
  } from "../domain/trap";
13
13
  import { prepareFTSQuery } from "../lib/fts-query";
14
14
  import { normalizeQuery } from "../lib/search-normalizer";
15
- import { normalizeEvidenceForExport } from "../lib/trap-archive";
16
- import { encodeEvidenceRelatedFiles, encodeTrapTags } from "../lib/trap-json-fields";
15
+ import {
16
+ encodeTrapInsertFields,
17
+ mergeTrapUpdateForSearchText,
18
+ normalizeEvidenceForExport,
19
+ normalizeTrapForExport,
20
+ } from "../lib/trap-codec";
21
+ import { encodeEvidenceRelatedFiles, encodeTrapPathGlobs, encodeTrapTags } from "../lib/trap-json-fields";
17
22
  import { buildTrapSearchText, passageFieldsChanged, searchTextFieldsChanged } from "../lib/trap-search-document";
18
23
 
19
24
  export type TrapStatusFilter = TrapStatus | "all";
25
+ export type TrapRecordInsert = Omit<Trap, "id">;
20
26
 
21
27
  export function insertTrap(db: Database, input: TrapInput): number {
22
- const tags = encodeTrapTags(input.tags);
23
- const searchText = buildTrapSearchText({ ...input, tags });
28
+ const fields = encodeTrapInsertFields(input);
24
29
  const stmt = db.prepare(`
25
30
  INSERT INTO traps (
26
31
  title, category, tags, scope, context, mistake, fix, search_text,
27
32
  before_code, after_code, severity, state_key, status, supersedes_id,
28
- valid_from, valid_until, project_path
33
+ valid_from, valid_until, project_path, path_globs, module, owner
29
34
  )
30
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), ?, ?)
35
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), ?, ?, ?, ?, ?)
31
36
  `);
32
37
  const result = stmt.run(
33
38
  input.title,
34
39
  input.category,
35
- tags,
40
+ fields.tags,
36
41
  input.scope,
37
42
  input.context,
38
43
  input.mistake,
39
44
  input.fix,
40
- searchText,
45
+ fields.search_text,
41
46
  input.before_code ?? null,
42
47
  input.after_code ?? null,
43
48
  input.severity ?? DEFAULT_SEVERITY,
@@ -45,7 +50,10 @@ export function insertTrap(db: Database, input: TrapInput): number {
45
50
  DEFAULT_TRAP_STATUS,
46
51
  null,
47
52
  null,
48
- input.project_path ?? null
53
+ input.project_path ?? null,
54
+ fields.path_globs,
55
+ normalizeOptionalText(input.module),
56
+ normalizeOptionalText(input.owner)
49
57
  );
50
58
  return Number(result.lastInsertRowid);
51
59
  }
@@ -53,7 +61,7 @@ export function insertTrap(db: Database, input: TrapInput): number {
53
61
  export function searchTraps(
54
62
  db: Database,
55
63
  query: string,
56
- opts: { category?: string; scope?: string; limit?: number; status?: TrapStatusFilter } = {}
64
+ opts: { category?: string; scope?: string; limit?: number; status?: TrapStatusFilter; module?: string; owner?: string } = {}
57
65
  ): TrapSearchResult[] {
58
66
  const prepared = prepareFTSQuery(normalizeQuery(query));
59
67
  if (!prepared) return [];
@@ -86,7 +94,7 @@ export function getTrap(db: Database, id: number): Trap | null {
86
94
 
87
95
  export function listTraps(
88
96
  db: Database,
89
- opts: { category?: string; scope?: string; limit?: number; offset?: number; status?: TrapStatusFilter } = {}
97
+ opts: { category?: string; scope?: string; limit?: number; offset?: number; status?: TrapStatusFilter; module?: string; owner?: string } = {}
90
98
  ): Trap[] {
91
99
  const conditions: string[] = [];
92
100
  const params: SQLQueryBindings[] = [];
@@ -108,24 +116,24 @@ export function updateTrap(db: Database, id: number, input: TrapUpdate): boolean
108
116
  const current = searchTextFieldsChanged(input) || passageFieldsChanged(input) ? getTrap(db, id) : null;
109
117
 
110
118
  for (const key of TRAP_UPDATE_FIELDS) {
111
- if (key === "tags") continue;
119
+ if (key === "tags" || key === "path_globs") continue;
112
120
  const value = input[key];
113
121
  if (value !== undefined) {
114
122
  updates.push(`${key} = ?`);
115
- params.push(value ?? null);
123
+ params.push(key === "module" || key === "owner" ? normalizeOptionalText(value) : value ?? null);
116
124
  }
117
125
  }
118
126
  if (input.tags !== undefined) {
119
127
  updates.push("tags = ?");
120
128
  params.push(encodeTrapTags(input.tags));
121
129
  }
130
+ if (input.path_globs !== undefined) {
131
+ updates.push("path_globs = ?");
132
+ params.push(encodeTrapPathGlobs(input.path_globs));
133
+ }
122
134
 
123
135
  if (current && searchTextFieldsChanged(input)) {
124
- const merged = {
125
- ...current,
126
- ...input,
127
- tags: input.tags !== undefined ? encodeTrapTags(input.tags) : current.tags,
128
- };
136
+ const merged = mergeTrapUpdateForSearchText(current, input);
129
137
  updates.push("search_text = ?");
130
138
  params.push(buildTrapSearchText(merged));
131
139
  }
@@ -172,6 +180,10 @@ export function listTrapEvidence(db: Database, trapId: number): TrapEvidence[] {
172
180
  }
173
181
 
174
182
  export function archiveTrap(db: Database, id: number): boolean {
183
+ return markTrapArchived(db, id);
184
+ }
185
+
186
+ export function markTrapArchived(db: Database, id: number): boolean {
175
187
  const result = db
176
188
  .prepare(
177
189
  `
@@ -200,33 +212,43 @@ export function supersedeTrap(
200
212
 
201
213
  const key = stateKey ?? oldTrap.state_key ?? newTrap.state_key ?? `trap:${id}`;
202
214
  const tx = db.transaction(() => {
203
- db.prepare(
204
- `
205
- UPDATE traps
206
- SET status = 'superseded',
207
- state_key = ?,
208
- valid_until = COALESCE(valid_until, datetime('now')),
209
- updated_at = datetime('now')
210
- WHERE id = ?
211
- `
212
- ).run(key, id);
213
- db.prepare(
214
- `
215
- UPDATE traps
216
- SET status = 'active',
217
- state_key = ?,
218
- supersedes_id = ?,
219
- valid_from = COALESCE(valid_from, datetime('now')),
220
- valid_until = NULL,
221
- updated_at = datetime('now')
222
- WHERE id = ?
223
- `
224
- ).run(key, id, supersededById);
215
+ markTrapSuperseded(db, id, key);
216
+ markTrapSuperseding(db, supersededById, id, key);
225
217
  });
226
218
  tx();
227
219
  return true;
228
220
  }
229
221
 
222
+ export function markTrapSuperseded(db: Database, id: number, stateKey: string): boolean {
223
+ const result = db.prepare(supersedeTrapSql).run(stateKey, id);
224
+ return result.changes > 0;
225
+ }
226
+
227
+ export function markTrapSuperseding(db: Database, id: number, supersedesId: number, stateKey: string): boolean {
228
+ const result = db.prepare(supersedingTrapSql).run(stateKey, supersedesId, id);
229
+ return result.changes > 0;
230
+ }
231
+
232
+ const supersedeTrapSql = `
233
+ UPDATE traps
234
+ SET status = 'superseded',
235
+ state_key = ?,
236
+ valid_until = COALESCE(valid_until, datetime('now')),
237
+ updated_at = datetime('now')
238
+ WHERE id = ?
239
+ `;
240
+
241
+ const supersedingTrapSql = `
242
+ UPDATE traps
243
+ SET status = 'active',
244
+ state_key = ?,
245
+ supersedes_id = ?,
246
+ valid_from = COALESCE(valid_from, datetime('now')),
247
+ valid_until = NULL,
248
+ updated_at = datetime('now')
249
+ WHERE id = ?
250
+ `;
251
+
230
252
  export function incrementHitCount(db: Database, id: number): void {
231
253
  db.prepare("UPDATE traps SET hit_count = hit_count + 1, updated_at = datetime('now') WHERE id = ?").run(id);
232
254
  }
@@ -237,18 +259,22 @@ export function getTopTraps(db: Database, scope: string, limit = 20): Trap[] {
237
259
  .all(scope, limit) as Trap[];
238
260
  }
239
261
 
240
- export function getStats(db: Database): {
262
+ export function getStats(db: Database, opts: { scope?: string; status?: TrapStatusFilter } = {}): {
241
263
  total: number;
242
264
  byCategory: Record<string, number>;
243
265
  bySeverity: Record<string, number>;
244
266
  } {
245
- const total = (db.query("SELECT COUNT(*) as c FROM traps").get() as { c: number }).c;
267
+ const conditions: string[] = [];
268
+ const params: SQLQueryBindings[] = [];
269
+ addTrapFilters(conditions, params, opts);
270
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
271
+ const total = (db.query(`SELECT COUNT(*) as c FROM traps ${where}`).get(...params) as { c: number }).c;
246
272
  const byCategory = db
247
- .query("SELECT category, COUNT(*) as c FROM traps GROUP BY category")
248
- .all() as { category: string; c: number }[];
273
+ .query(`SELECT category, COUNT(*) as c FROM traps ${where} GROUP BY category`)
274
+ .all(...params) as { category: string; c: number }[];
249
275
  const bySeverity = db
250
- .query("SELECT severity, COUNT(*) as c FROM traps GROUP BY severity")
251
- .all() as { severity: string; c: number }[];
276
+ .query(`SELECT severity, COUNT(*) as c FROM traps ${where} GROUP BY severity`)
277
+ .all(...params) as { severity: string; c: number }[];
252
278
 
253
279
  return {
254
280
  total,
@@ -260,11 +286,79 @@ export function getStats(db: Database): {
260
286
  export function exportTraps(db: Database): TrapExportRecord[] {
261
287
  const traps = db.query("SELECT * FROM traps").all() as Trap[];
262
288
  return traps.map((trap) => ({
263
- ...trap,
289
+ ...normalizeTrapForExport(trap),
290
+ evidence: listTrapEvidence(db, trap.id).map(normalizeEvidenceForExport),
291
+ }));
292
+ }
293
+
294
+ export function exportProjectTrapsByPath(db: Database, projectPath: string): TrapExportRecord[] {
295
+ const traps = db
296
+ .query("SELECT * FROM traps WHERE scope = 'project' AND project_path = ? ORDER BY id")
297
+ .all(projectPath) as Trap[];
298
+ return traps.map((trap) => ({
299
+ ...normalizeTrapForExport(trap),
264
300
  evidence: listTrapEvidence(db, trap.id).map(normalizeEvidenceForExport),
265
301
  }));
266
302
  }
267
303
 
304
+ export function insertTrapRecord(db: Database, record: TrapRecordInsert): number {
305
+ const stmt = db.prepare(`
306
+ INSERT INTO traps (
307
+ title, category, tags, scope, context, mistake, fix, search_text,
308
+ before_code, after_code, severity, state_key, status, supersedes_id,
309
+ valid_from, valid_until, project_path, path_globs, module, owner,
310
+ hit_count, created_at, updated_at
311
+ )
312
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
313
+ `);
314
+ const result = stmt.run(
315
+ record.title,
316
+ record.category,
317
+ record.tags,
318
+ record.scope,
319
+ record.context,
320
+ record.mistake,
321
+ record.fix,
322
+ record.search_text,
323
+ record.before_code,
324
+ record.after_code,
325
+ record.severity,
326
+ record.state_key,
327
+ record.status,
328
+ record.supersedes_id,
329
+ record.valid_from,
330
+ record.valid_until,
331
+ record.project_path,
332
+ record.path_globs,
333
+ normalizeOptionalText(record.module),
334
+ normalizeOptionalText(record.owner),
335
+ record.hit_count,
336
+ record.created_at,
337
+ record.updated_at
338
+ );
339
+ return Number(result.lastInsertRowid);
340
+ }
341
+
342
+ export function updateTrapSupersedesId(db: Database, id: number, supersedesId: number): boolean {
343
+ const result = db.prepare("UPDATE traps SET supersedes_id = ? WHERE id = ?").run(supersedesId, id);
344
+ return result.changes > 0;
345
+ }
346
+
347
+ export function deleteTrapsByIds(db: Database, ids: number[]): number {
348
+ if (ids.length === 0) return 0;
349
+ let deleted = 0;
350
+ const stmt = db.prepare("DELETE FROM traps WHERE id = ?");
351
+ const tx = db.transaction(() => {
352
+ for (const id of ids) {
353
+ if (!getTrap(db, id)) continue;
354
+ stmt.run(id);
355
+ deleted++;
356
+ }
357
+ });
358
+ tx();
359
+ return deleted;
360
+ }
361
+
268
362
  export function countTraps(db: Database, opts: { scope?: string; category?: string; status?: TrapStatusFilter } = {}): number {
269
363
  const conditions: string[] = [];
270
364
  const params: SQLQueryBindings[] = [];
@@ -274,10 +368,17 @@ export function countTraps(db: Database, opts: { scope?: string; category?: stri
274
368
  return row.c;
275
369
  }
276
370
 
371
+ export function countProjectTrapsByPath(db: Database, projectPath: string): number {
372
+ const row = db
373
+ .query("SELECT COUNT(*) as c FROM traps WHERE scope = 'project' AND project_path = ?")
374
+ .get(projectPath) as { c: number };
375
+ return row.c;
376
+ }
377
+
277
378
  function addTrapFilters(
278
379
  conditions: string[],
279
380
  params: SQLQueryBindings[],
280
- opts: { category?: string; scope?: string; status?: TrapStatusFilter },
381
+ opts: { category?: string; scope?: string; status?: TrapStatusFilter; module?: string; owner?: string },
281
382
  alias?: string
282
383
  ): void {
283
384
  const prefix = alias ? `${alias}.` : "";
@@ -293,4 +394,20 @@ function addTrapFilters(
293
394
  conditions.push(`${prefix}status = ?`);
294
395
  params.push(opts.status ?? DEFAULT_TRAP_STATUS);
295
396
  }
397
+ const module = normalizeOptionalText(opts.module);
398
+ if (module) {
399
+ conditions.push(`(${prefix}module IS NULL OR ${prefix}module = '' OR ${prefix}module = ?)`);
400
+ params.push(module);
401
+ }
402
+ const owner = normalizeOptionalText(opts.owner);
403
+ if (owner) {
404
+ conditions.push(`(${prefix}owner IS NULL OR ${prefix}owner = '' OR ${prefix}owner = ?)`);
405
+ params.push(owner);
406
+ }
407
+ }
408
+
409
+ function normalizeOptionalText(value: unknown): string | null {
410
+ if (typeof value !== "string") return value == null ? null : String(value);
411
+ const trimmed = value.trim();
412
+ return trimmed === "" ? null : trimmed;
296
413
  }
@@ -8,7 +8,6 @@ import type {
8
8
  TrapSearchResult,
9
9
  TrapUpdate,
10
10
  } from "../domain/trap";
11
- import * as embeddingQueries from "./embedding-queries";
12
11
  import { SearchService, type SearchOptions } from "../lib/search-service";
13
12
  import {
14
13
  type EmbeddingConfig,
@@ -19,17 +18,25 @@ import { runEmbeddingJob } from "../lib/embedding-job";
19
18
  import { passageFieldsChanged } from "../lib/trap-search-document";
20
19
  import * as queries from "./queries";
21
20
  import type { TrapStatus } from "../lib/constants";
21
+ import { TrapSearchPolicy } from "../lib/search-policy";
22
+ import { DatabaseEmbeddingIndex } from "../lib/embedding-index";
23
+ import { archiveTrapLifecycle, supersedeTrapLifecycle } from "../lib/trap-lifecycle";
22
24
 
23
25
  export type TrapStats = ReturnType<typeof queries.getStats>;
26
+ export type EmbeddingStateCounts = ReturnType<DatabaseEmbeddingIndex["stateCounts"]>;
27
+ export type TrapRecordInsert = queries.TrapRecordInsert;
24
28
 
25
29
  export class TrapRepository {
26
30
  private readonly searchService: SearchService;
31
+ private readonly searchPolicy = new TrapSearchPolicy();
32
+ private readonly embeddingIndex: DatabaseEmbeddingIndex;
27
33
 
28
34
  constructor(
29
35
  private readonly db: Database,
30
36
  private readonly embedder?: EmbeddingProvider
31
37
  ) {
32
38
  this.searchService = new SearchService(db, embedder);
39
+ this.embeddingIndex = new DatabaseEmbeddingIndex(db);
33
40
  }
34
41
 
35
42
  add(input: TrapInput): number {
@@ -54,14 +61,25 @@ export class TrapRepository {
54
61
  };
55
62
  }
56
63
 
57
- list(opts: { category?: string; scope?: string; limit?: number; offset?: number; status?: TrapStatus | "all" } = {}): Trap[] {
58
- return queries.listTraps(this.db, opts);
64
+ list(opts: { category?: string; scope?: string; limit?: number; offset?: number; status?: TrapStatus | "all"; path?: string; module?: string; owner?: string } = {}): Trap[] {
65
+ const limit = opts.limit ?? 50;
66
+ const queryLimit = opts.path ? Math.max(limit * 5, 250) : limit;
67
+ return queries
68
+ .listTraps(this.db, { ...opts, limit: queryLimit })
69
+ .filter((trap) => this.searchPolicy.matchesTrap(trap, opts))
70
+ .slice(0, limit);
71
+ }
72
+
73
+ listMisScoped(expectedScope: string): Trap[] {
74
+ return queries
75
+ .listTraps(this.db, { status: "all", limit: 100000 })
76
+ .filter((trap) => trap.scope !== expectedScope);
59
77
  }
60
78
 
61
79
  update(id: number, input: TrapUpdate): boolean {
62
80
  const success = queries.updateTrap(this.db, id, input);
63
81
  if (success && passageFieldsChanged(input)) {
64
- embeddingQueries.deleteEmbedding(this.db, id);
82
+ this.embeddingIndex.delete(id);
65
83
  }
66
84
  return success;
67
85
  }
@@ -76,11 +94,11 @@ export class TrapRepository {
76
94
  }
77
95
 
78
96
  archive(id: number): boolean {
79
- return queries.archiveTrap(this.db, id);
97
+ return archiveTrapLifecycle(this.lifecycleAdapter(), id);
80
98
  }
81
99
 
82
100
  supersede(id: number, supersededById: number, stateKey?: string): boolean {
83
- return queries.supersedeTrap(this.db, id, supersededById, stateKey);
101
+ return supersedeTrapLifecycle(this.lifecycleAdapter(), id, supersededById, stateKey);
84
102
  }
85
103
 
86
104
  hit(id: number): void {
@@ -91,31 +109,59 @@ export class TrapRepository {
91
109
  return queries.getTopTraps(this.db, scope, limit);
92
110
  }
93
111
 
94
- stats(): TrapStats {
95
- return queries.getStats(this.db);
112
+ stats(opts: { scope?: string; status?: TrapStatus | "all" } = {}): TrapStats {
113
+ return queries.getStats(this.db, opts);
114
+ }
115
+
116
+ embeddingStats(config: EmbeddingConfig | null, opts: { scope?: string; status?: TrapStatus | "all" } = {}): EmbeddingStateCounts {
117
+ return this.embeddingIndex.stateCounts(config, opts);
96
118
  }
97
119
 
98
120
  exportAll(): TrapExportRecord[] {
99
121
  return queries.exportTraps(this.db);
100
122
  }
101
123
 
124
+ exportProjectTrapsByPath(projectPath: string): TrapExportRecord[] {
125
+ return queries.exportProjectTrapsByPath(this.db, projectPath);
126
+ }
127
+
128
+ insertTrapRecord(record: TrapRecordInsert): number {
129
+ return queries.insertTrapRecord(this.db, record);
130
+ }
131
+
132
+ updateTrapSupersedesId(id: number, supersedesId: number): boolean {
133
+ return queries.updateTrapSupersedesId(this.db, id, supersedesId);
134
+ }
135
+
136
+ deleteTrapsByIds(ids: number[]): number {
137
+ return queries.deleteTrapsByIds(this.db, ids);
138
+ }
139
+
140
+ countProjectTrapsByPath(projectPath: string): number {
141
+ return queries.countProjectTrapsByPath(this.db, projectPath);
142
+ }
143
+
144
+ transaction<T>(callback: () => T): T {
145
+ return this.db.transaction(callback)();
146
+ }
147
+
102
148
  getEmbedding(trapId: number): StoredEmbedding | null {
103
- return embeddingQueries.getEmbedding(this.db, trapId);
149
+ return this.embeddingIndex.get(trapId);
104
150
  }
105
151
 
106
152
  upsertEmbedding(record: StoredEmbedding): void {
107
- embeddingQueries.upsertEmbedding(this.db, record);
153
+ this.embeddingIndex.save(record);
108
154
  }
109
155
 
110
156
  deleteEmbedding(trapId: number): void {
111
- embeddingQueries.deleteEmbedding(this.db, trapId);
157
+ this.embeddingIndex.delete(trapId);
112
158
  }
113
159
 
114
160
  getTrapsNeedingEmbeddings(
115
161
  config: EmbeddingConfig,
116
162
  opts: { scope?: string; category?: string; status?: TrapStatus | "all"; force?: boolean; limit?: number } = {}
117
163
  ): Trap[] {
118
- return embeddingQueries.getTrapsNeedingEmbeddings(this.db, config, opts);
164
+ return this.embeddingIndex.trapsNeedingEmbeddings(config, opts);
119
165
  }
120
166
 
121
167
  async ensureEmbeddings(opts: { scope?: string; category?: string; limit?: number; force?: boolean; batchSize?: number } = {}): Promise<{
@@ -129,13 +175,24 @@ export class TrapRepository {
129
175
 
130
176
  return runEmbeddingJob(
131
177
  {
132
- countEmbeddable: (countOpts) => embeddingQueries.countEmbeddableTraps(this.db, countOpts),
178
+ countEmbeddable: (countOpts) => this.embeddingIndex.countEmbeddable(countOpts),
133
179
  trapsNeedingEmbeddings: (config, jobOpts) =>
134
- embeddingQueries.getTrapsNeedingEmbeddings(this.db, config, jobOpts),
135
- saveEmbedding: (record) => embeddingQueries.upsertEmbedding(this.db, record),
180
+ this.embeddingIndex.trapsNeedingEmbeddings(config, jobOpts),
181
+ saveEmbedding: (record) => this.embeddingIndex.save(record),
136
182
  },
137
183
  this.embedder,
138
184
  opts
139
185
  );
140
186
  }
187
+
188
+ private lifecycleAdapter() {
189
+ return {
190
+ get: (id: number) => queries.getTrap(this.db, id),
191
+ transaction: <T>(callback: () => T) => this.transaction(callback),
192
+ markArchived: (id: number) => queries.markTrapArchived(this.db, id),
193
+ markSuperseded: (id: number, stateKey: string) => queries.markTrapSuperseded(this.db, id, stateKey),
194
+ markSuperseding: (id: number, supersedesId: number, stateKey: string) =>
195
+ queries.markTrapSuperseding(this.db, id, supersedesId, stateKey),
196
+ };
197
+ }
141
198
  }
package/src/db/schema.ts CHANGED
@@ -140,6 +140,41 @@ function applyMigrations(db: Database, from: number): void {
140
140
 
141
141
  db.prepare("UPDATE schema_version SET version = ?").run(4);
142
142
  }
143
+
144
+ if (from < 5) {
145
+ if (!columnExists(db, "traps", "path_globs")) {
146
+ db.exec("ALTER TABLE traps ADD COLUMN path_globs TEXT NOT NULL DEFAULT '[]'");
147
+ }
148
+ if (!columnExists(db, "traps", "module")) {
149
+ db.exec("ALTER TABLE traps ADD COLUMN module TEXT");
150
+ }
151
+ if (!columnExists(db, "traps", "owner")) {
152
+ db.exec("ALTER TABLE traps ADD COLUMN owner TEXT");
153
+ }
154
+
155
+ const rows = db
156
+ .query("SELECT id, title, context, mistake, fix, tags, before_code, after_code, path_globs, module, owner FROM traps")
157
+ .all() as {
158
+ id: number;
159
+ title: string;
160
+ context: string;
161
+ mistake: string;
162
+ fix: string;
163
+ tags: string;
164
+ before_code: string | null;
165
+ after_code: string | null;
166
+ path_globs: string;
167
+ module: string | null;
168
+ owner: string | null;
169
+ }[];
170
+
171
+ const update = db.prepare("UPDATE traps SET search_text = ? WHERE id = ?");
172
+ for (const row of rows) {
173
+ update.run(buildTrapSearchText(row), row.id);
174
+ }
175
+
176
+ db.prepare("UPDATE schema_version SET version = ?").run(5);
177
+ }
143
178
  }
144
179
 
145
180
  function columnExists(db: Database, table: string, column: string): boolean {
@@ -30,6 +30,9 @@ export interface Trap {
30
30
  valid_from: string;
31
31
  valid_until: string | null;
32
32
  project_path: string | null;
33
+ path_globs: string;
34
+ module: string | null;
35
+ owner: string | null;
33
36
  hit_count: number;
34
37
  created_at: string;
35
38
  updated_at: string;
@@ -47,6 +50,9 @@ export interface TrapInput {
47
50
  after_code?: string;
48
51
  severity?: string;
49
52
  project_path?: string | null;
53
+ path_globs?: string[];
54
+ module?: string | null;
55
+ owner?: string | null;
50
56
  }
51
57
 
52
58
  export interface TrapSearchResult {
@@ -54,7 +60,19 @@ export interface TrapSearchResult {
54
60
  rank: number;
55
61
  sources?: ("fts" | "semantic")[];
56
62
  score?: number;
57
- diagnostics?: { code: string; message: string }[];
63
+ diagnostics?: TrapSearchDiagnostic[];
64
+ ranking_signals?: RankingSignal[];
65
+ }
66
+
67
+ export interface TrapSearchDiagnostic {
68
+ code: string;
69
+ message: string;
70
+ }
71
+
72
+ export interface RankingSignal {
73
+ code: string;
74
+ weight: number;
75
+ detail?: string;
58
76
  }
59
77
 
60
78
  export interface TrapActionCard {
@@ -67,13 +85,8 @@ export interface TrapActionCard {
67
85
  severity: string;
68
86
  score: number | null;
69
87
  sources: ("fts" | "semantic")[];
70
- next_action: {
71
- details_tool: "get_trap";
72
- details_args: {
73
- id: number;
74
- scope: Scope;
75
- };
76
- };
88
+ diagnostics?: TrapSearchDiagnostic[];
89
+ ranking_signals?: RankingSignal[];
77
90
  }
78
91
 
79
92
  export type TrapUpdate = Partial<Omit<TrapInput, "scope" | "project_path">>;
@@ -101,7 +114,8 @@ export interface TrapExportEvidence extends Omit<TrapEvidence, "related_files">
101
114
  related_files: string[];
102
115
  }
103
116
 
104
- export interface TrapExportRecord extends Trap {
117
+ export interface TrapExportRecord extends Omit<Trap, "path_globs"> {
118
+ path_globs: string[];
105
119
  evidence: TrapExportEvidence[];
106
120
  }
107
121
 
@@ -110,11 +124,12 @@ export type TrapImportEvidence = Partial<Omit<TrapEvidence, "related_files">> &
110
124
  related_files?: string[] | string | null;
111
125
  };
112
126
 
113
- export type TrapImportRecord = Omit<TrapInput, "tags" | "before_code" | "after_code" | "project_path"> & {
127
+ export type TrapImportRecord = Omit<TrapInput, "tags" | "before_code" | "after_code" | "project_path" | "path_globs"> & {
114
128
  tags?: string[] | string | null;
115
129
  before_code?: string | null;
116
130
  after_code?: string | null;
117
131
  project_path?: string | null;
132
+ path_globs?: string[] | string | null;
118
133
  evidence?: TrapImportEvidence[];
119
134
  };
120
135
 
@@ -168,6 +183,9 @@ export const TRAP_UPDATE_FIELDS = [
168
183
  "before_code",
169
184
  "after_code",
170
185
  "severity",
186
+ "path_globs",
187
+ "module",
188
+ "owner",
171
189
  ] as const;
172
190
 
173
191
  export const TRAP_INPUT_SCHEMA_PROPERTIES = {
@@ -185,6 +203,13 @@ export const TRAP_INPUT_SCHEMA_PROPERTIES = {
185
203
  before_code: { type: "string", description: "Example of wrong code (optional)" },
186
204
  after_code: { type: "string", description: "Example of correct code (optional)" },
187
205
  severity: { type: "string", enum: [...SEVERITIES] as string[], description: "How severe is this pitfall?" },
206
+ path_globs: {
207
+ type: "array",
208
+ items: { type: "string" },
209
+ description: "Optional file globs where this trap applies, for example src/db/**",
210
+ },
211
+ module: { type: "string", description: "Optional module or subsystem where this trap applies" },
212
+ owner: { type: "string", description: "Optional team or owner for this trap" },
188
213
  } satisfies Record<keyof Omit<TrapInput, "project_path">, JsonSchemaProperty>;
189
214
 
190
215
  export function trapInputSchema() {
@@ -270,6 +295,9 @@ export function buildTrapInput(args: Record<string, any>): TrapInput {
270
295
  before_code: args.before_code,
271
296
  after_code: args.after_code,
272
297
  severity: args.severity ?? DEFAULT_SEVERITY,
298
+ path_globs: Array.isArray(args.path_globs) ? args.path_globs.map(String) : args.path_globs,
299
+ module: args.module,
300
+ owner: args.owner,
273
301
  };
274
302
  }
275
303
 
package/src/index.ts CHANGED
@@ -46,6 +46,9 @@ function showHelp(): void {
46
46
  console.log(" export Export traps as JSON");
47
47
  console.log(" import <file.json> Import traps from JSON");
48
48
  console.log(" stats Show statistics");
49
+ console.log(" doctor Diagnose scope, database, and embedding health");
50
+ console.log(" repair-scope Move mis-scoped project traps into the current project");
51
+ console.log(" migrate-project Move project traps between initialized projects");
49
52
  console.log(" serve Start MCP server (for Claude Code)");
50
53
  console.log("");
51
54
  console.log("Flags:");
@@ -53,6 +56,15 @@ function showHelp(): void {
53
56
  console.log(" --category <name> Filter by category");
54
57
  console.log(" --mode fts|semantic|hybrid Search mode");
55
58
  console.log(" --status active|superseded|archived|all Lifecycle filter");
59
+ console.log(" --path <file> Filter/boost traps scoped to a file path");
60
+ console.log(" --module <name> Filter/boost traps scoped to a module");
61
+ console.log(" --owner <name> Filter/boost traps scoped to an owner/team");
62
+ console.log(" --no-rerank Disable query-aware search reranking");
63
+ console.log(" --ranking-signals Include search ranking diagnostics in JSON cards");
56
64
  console.log(" --batch-size <n> Embedding generation batch size");
57
- console.log(" --json '{}' JSON input for add/edit");
65
+ console.log(" --json JSON output for search/show/list/stats/doctor; JSON input for add/edit");
66
+ console.log(" --output-json JSON output for add/edit when --json is used as input");
67
+ console.log(" --from-project-path <path> Source project path for scope repair/migration");
68
+ console.log(" --to-project-path <path> Destination project path for scope repair/migration");
69
+ console.log(" --dry-run|--apply Preview or apply scope repair/migration");
58
70
  }