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.
- package/.agents/plugins/marketplace.json +20 -0
- package/README.md +112 -33
- package/docs/installation.md +18 -10
- package/package.json +4 -1
- package/plugins/codetrap-agent/.codex-plugin/plugin.json +34 -0
- package/plugins/codetrap-agent/hooks/post-flight-capture.example.md +25 -0
- package/plugins/codetrap-agent/hooks/pre-edit.example.sh +10 -0
- package/plugins/codetrap-agent/hooks.json +11 -0
- package/plugins/codetrap-agent/skills/codetrap-capture/SKILL.md +19 -0
- package/plugins/codetrap-agent/skills/codetrap-check/SKILL.md +14 -0
- package/plugins/codetrap-agent/templates/AGENTS.codetrap.md +25 -0
- package/scripts/release-preflight.ts +55 -0
- package/skills/codetrap-add/SKILL.md +4 -1
- package/skills/codetrap-check/SKILL.md +24 -4
- package/skills/codetrap-search/SKILL.md +32 -12
- package/src/commands/command-result.ts +29 -0
- package/src/commands/router.ts +6 -400
- package/src/commands/workflow.ts +419 -0
- package/src/db/embedding-queries.ts +33 -0
- package/src/db/queries.ts +165 -48
- package/src/db/repository.ts +72 -15
- package/src/db/schema.ts +35 -0
- package/src/domain/trap.ts +38 -10
- package/src/index.ts +13 -1
- package/src/lib/command-requests.ts +133 -0
- package/src/lib/config.ts +102 -0
- package/src/lib/constants.ts +1 -1
- package/src/lib/doctor.ts +86 -0
- package/src/lib/embedding-health.ts +49 -0
- package/src/lib/embedding-index.ts +53 -0
- package/src/lib/format.ts +6 -2
- package/src/lib/output-json.ts +141 -0
- package/src/lib/scope-context.ts +118 -0
- package/src/lib/scope-maintenance.ts +71 -0
- package/src/lib/scope-migration.ts +315 -0
- package/src/lib/scope-path.ts +99 -0
- package/src/lib/scope.ts +16 -11
- package/src/lib/search-normalizer.ts +6 -0
- package/src/lib/search-policy.ts +365 -0
- package/src/lib/search-result-card.ts +2 -7
- package/src/lib/search-service.ts +67 -120
- package/src/lib/store.ts +129 -108
- package/src/lib/trap-archive.ts +9 -42
- package/src/lib/trap-codec.ts +113 -0
- package/src/lib/trap-json-fields.ts +12 -0
- package/src/lib/trap-lifecycle.ts +37 -0
- package/src/lib/trap-mutation-result.ts +36 -0
- package/src/lib/trap-operations.ts +30 -9
- package/src/lib/trap-scope-match.ts +112 -0
- package/src/lib/trap-search-document.ts +8 -1
- package/src/lib/trap-transfer.ts +88 -0
- package/src/mcp/server.ts +77 -72
- 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 {
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
}
|
package/src/db/repository.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
97
|
+
return archiveTrapLifecycle(this.lifecycleAdapter(), id);
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
supersede(id: number, supersededById: number, stateKey?: string): boolean {
|
|
83
|
-
return
|
|
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
|
|
149
|
+
return this.embeddingIndex.get(trapId);
|
|
104
150
|
}
|
|
105
151
|
|
|
106
152
|
upsertEmbedding(record: StoredEmbedding): void {
|
|
107
|
-
|
|
153
|
+
this.embeddingIndex.save(record);
|
|
108
154
|
}
|
|
109
155
|
|
|
110
156
|
deleteEmbedding(trapId: number): void {
|
|
111
|
-
|
|
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
|
|
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) =>
|
|
178
|
+
countEmbeddable: (countOpts) => this.embeddingIndex.countEmbeddable(countOpts),
|
|
133
179
|
trapsNeedingEmbeddings: (config, jobOpts) =>
|
|
134
|
-
|
|
135
|
-
saveEmbedding: (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 {
|
package/src/domain/trap.ts
CHANGED
|
@@ -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?:
|
|
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
|
-
|
|
71
|
-
|
|
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
|
|
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
|
}
|