codetrap 0.1.2 → 0.1.3

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 (47) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +107 -32
  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 +466 -0
  19. package/src/db/embedding-queries.ts +33 -0
  20. package/src/db/queries.ts +119 -20
  21. package/src/db/repository.ts +39 -2
  22. package/src/db/schema.ts +35 -0
  23. package/src/domain/trap.ts +31 -2
  24. package/src/index.ts +13 -1
  25. package/src/lib/config.ts +102 -0
  26. package/src/lib/constants.ts +1 -1
  27. package/src/lib/doctor.ts +76 -0
  28. package/src/lib/embedding-health.ts +49 -0
  29. package/src/lib/format.ts +5 -1
  30. package/src/lib/output-json.ts +116 -0
  31. package/src/lib/scope-context.ts +116 -0
  32. package/src/lib/scope-migration.ts +360 -0
  33. package/src/lib/search-normalizer.ts +6 -0
  34. package/src/lib/search-policy.ts +276 -0
  35. package/src/lib/search-result-card.ts +1 -0
  36. package/src/lib/search-service.ts +36 -98
  37. package/src/lib/store.ts +96 -107
  38. package/src/lib/trap-archive.ts +9 -42
  39. package/src/lib/trap-codec.ts +113 -0
  40. package/src/lib/trap-json-fields.ts +12 -0
  41. package/src/lib/trap-mutation-result.ts +36 -0
  42. package/src/lib/trap-operations.ts +27 -6
  43. package/src/lib/trap-scope-match.ts +112 -0
  44. package/src/lib/trap-search-document.ts +8 -1
  45. package/src/lib/trap-transfer.ts +88 -0
  46. package/src/mcp/server.ts +75 -57
  47. package/src/mcp/tools.ts +32 -5
@@ -0,0 +1,466 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { TrapStore } from "../lib/store";
3
+ import { formatTrapShort, formatTrapDetails, formatTrapActionCard } from "../lib/format";
4
+ import type { Trap } from "../domain/trap";
5
+ import { SEARCH_MODES, type SearchMode } from "../lib/constants";
6
+ import {
7
+ formatScopeMigrationText,
8
+ runScopeMigration,
9
+ type ScopeMigrationCommand,
10
+ } from "../lib/scope-migration";
11
+ import { TrapOperations } from "../lib/trap-operations";
12
+ import { buildDoctorReport, formatDoctorText } from "../lib/doctor";
13
+ import { searchDefaultsFromConfig } from "../lib/config";
14
+ import {
15
+ toCliSearchJson,
16
+ toListJson,
17
+ toStatsJson,
18
+ toTrapDetailsJson,
19
+ } from "../lib/output-json";
20
+ import {
21
+ errorResult,
22
+ jsonResult,
23
+ textResult,
24
+ type CommandResult,
25
+ } from "./command-result";
26
+ import { mutationJsonPayload } from "../lib/trap-mutation-result";
27
+
28
+ type ParsedArgs = {
29
+ opts: Record<string, string>;
30
+ positionals: string[];
31
+ };
32
+
33
+ export async function executeCommand(strip: string[], store: TrapStore): Promise<CommandResult> {
34
+ const sub = strip[0];
35
+ const args = strip.slice(1);
36
+ const operations = new TrapOperations(store);
37
+
38
+ switch (sub) {
39
+ case "add":
40
+ return cmdAdd(args, operations);
41
+ case "search":
42
+ return cmdSearch(args, operations);
43
+ case "list":
44
+ return cmdList(args, operations);
45
+ case "show":
46
+ return cmdShow(args, operations);
47
+ case "edit":
48
+ return cmdEdit(args, operations);
49
+ case "delete":
50
+ case "rm":
51
+ return cmdDelete(args, operations);
52
+ case "add_trap_evidence":
53
+ case "add-evidence":
54
+ return cmdAddTrapEvidence(args, operations);
55
+ case "archive_trap":
56
+ case "archive":
57
+ return cmdArchiveTrap(args, operations);
58
+ case "supersede_trap":
59
+ case "supersede":
60
+ return cmdSupersedeTrap(args, operations);
61
+ case "init":
62
+ return cmdInit(args, store);
63
+ case "export":
64
+ return cmdExport(args, operations);
65
+ case "import":
66
+ return cmdImport(args, operations);
67
+ case "stats":
68
+ return cmdStats(args, operations);
69
+ case "doctor":
70
+ return cmdDoctor(args, store, operations);
71
+ case "repair-scope":
72
+ return cmdScopeMigration("repair-scope", args, operations);
73
+ case "migrate-project":
74
+ return cmdScopeMigration("migrate-project", args, operations);
75
+ case "embed":
76
+ return cmdEmbed(args, store);
77
+ default:
78
+ return errorResult([
79
+ `Unknown command: ${sub}`,
80
+ "Commands: init, add, search, list, show, edit, delete, add_trap_evidence, archive_trap, supersede_trap, export, import, stats, doctor, repair-scope, migrate-project, embed",
81
+ ].join("\n"));
82
+ }
83
+ }
84
+
85
+ export function parseArgs(args: string[]): ParsedArgs {
86
+ const opts: Record<string, string> = {};
87
+ const positionals: string[] = [];
88
+ for (let i = 0; i < args.length; i++) {
89
+ if (args[i].startsWith("--")) {
90
+ const key = args[i].slice(2);
91
+ const val = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : "true";
92
+ opts[key] = val;
93
+ } else {
94
+ positionals.push(args[i]);
95
+ }
96
+ }
97
+ return { opts, positionals };
98
+ }
99
+
100
+ function cmdInit(_args: string[], store: TrapStore): CommandResult {
101
+ if (store.hasProject()) {
102
+ return textResult(`Already in a project: ${store.getProjectRoot()}`);
103
+ }
104
+ return textResult("Project initialized.");
105
+ }
106
+
107
+ function cmdAdd(args: string[], operations: TrapOperations): CommandResult {
108
+ const { opts, positionals } = parseArgs(args);
109
+ if (opts.json !== undefined) {
110
+ if (!opts.json || opts.json === "true") {
111
+ return errorResult("Error: --json requires a JSON string argument");
112
+ }
113
+ try {
114
+ const result = operations.addTrap(JSON.parse(opts.json));
115
+ return opts["output-json"] !== undefined
116
+ ? jsonResult(result)
117
+ : textResult(`Trap #${result.id} added to ${result.scope} scope.`);
118
+ } catch (error) {
119
+ return errorFrom(error);
120
+ }
121
+ }
122
+
123
+ if (positionals.length > 0) {
124
+ return textResult([
125
+ "Use --json mode for structured input.",
126
+ `Quick add: codetrap add --json '{"title":"${positionals.join(" ")}","category":"other","scope":"global","context":"...","mistake":"...","fix":"..."}'`,
127
+ ].join("\n"));
128
+ }
129
+
130
+ return textResult([
131
+ "Interactive mode not yet implemented. Use --json for now.",
132
+ 'Example: codetrap add --json \'{"title":"...","category":"convention","scope":"project","context":"...","mistake":"...","fix":"..."}\'',
133
+ ].join("\n"));
134
+ }
135
+
136
+ async function cmdSearch(args: string[], operations: TrapOperations): Promise<CommandResult> {
137
+ const { opts, positionals } = parseArgs(args);
138
+ const query = readQuery(positionals);
139
+ if (!query) {
140
+ return errorResult("Usage: codetrap search <query> [--category X] [--limit N] [--mode fts|semantic|hybrid] [--status active|superseded|archived|all] [--path file] [--module name] [--owner name] [--json]");
141
+ }
142
+
143
+ try {
144
+ const mode = opts.mode ? parseSearchMode(opts.mode) : undefined;
145
+ const defaults = searchDefaultsFromConfig();
146
+ const cards = await operations.searchTrapCards({
147
+ query,
148
+ category: opts.category,
149
+ scope: opts.scope ?? defaults.scope,
150
+ limit: opts.limit ? parseOptionalInt(opts.limit) : defaults.limit,
151
+ mode: mode ?? defaults.mode,
152
+ status: opts.status,
153
+ path: opts.path,
154
+ module: opts.module,
155
+ owner: opts.owner,
156
+ rerank: opts["no-rerank"] !== undefined ? false : defaults.rerank,
157
+ includeRankingSignals: opts["ranking-signals"] !== undefined,
158
+ });
159
+ if (opts.json !== undefined) return jsonResult(toCliSearchJson(cards));
160
+ return textResult(cards.length > 0 ? cards.map(formatTrapActionCard).join("\n\n") : "No traps found.");
161
+ } catch (error) {
162
+ return errorFrom(error);
163
+ }
164
+ }
165
+
166
+ function cmdList(args: string[], operations: TrapOperations): CommandResult {
167
+ const { opts } = parseArgs(args);
168
+ try {
169
+ const groups = operations.listTraps({
170
+ category: opts.category,
171
+ scope: opts.scope,
172
+ status: opts.status,
173
+ path: opts.path,
174
+ module: opts.module,
175
+ owner: opts.owner,
176
+ limit: parseOptionalInt(opts.limit, 50),
177
+ });
178
+ if (opts.json !== undefined) return jsonResult(toListJson(groups));
179
+
180
+ const lines = groups.flatMap((group) =>
181
+ group.traps.map((trap) => formatTrapShort(trap, group.scope))
182
+ );
183
+ return textResult(lines.length > 0 ? lines.join("\n") : "No traps found.");
184
+ } catch (error) {
185
+ return errorFrom(error);
186
+ }
187
+ }
188
+
189
+ function cmdShow(args: string[], operations: TrapOperations): CommandResult {
190
+ const { opts, positionals } = parseArgs(args);
191
+ const id = parseId(positionals[0], "Usage: codetrap show <id> [--scope project|global] [--json]");
192
+ if (typeof id !== "number") return id;
193
+
194
+ const result = operations.getTrapDetails(id, opts.scope);
195
+ if (!result) return errorResult(`Trap #${id} not found.`);
196
+
197
+ operations.hitTrap(id, result.scope);
198
+ return opts.json !== undefined
199
+ ? jsonResult(toTrapDetailsJson(result))
200
+ : textResult(formatTrapDetails(result));
201
+ }
202
+
203
+ function cmdEdit(args: string[], operations: TrapOperations): CommandResult {
204
+ const { opts, positionals } = parseArgs(args);
205
+ const id = parseId(positionals[0], "Usage: codetrap edit <id> --json '{\"title\":\"new title\"}' [--scope project|global]");
206
+ if (typeof id !== "number") return id;
207
+
208
+ if (!opts.json) {
209
+ return errorResult([
210
+ "Error: edit requires --json for now.",
211
+ "Example: codetrap edit 1 --json '{\"title\":\"new title\"}' [--scope project|global]",
212
+ ].join("\n"));
213
+ }
214
+
215
+ try {
216
+ const result = operations.updateTrap(id, JSON.parse(opts.json), opts.scope);
217
+ if (!result.success) return errorResult(`Trap #${id} not found or no fields changed.`);
218
+ return opts["output-json"] !== undefined
219
+ ? jsonResult({ id, ...result })
220
+ : textResult(`Trap #${id} updated in ${result.scope} scope.`);
221
+ } catch (error) {
222
+ return errorFrom(error);
223
+ }
224
+ }
225
+
226
+ function cmdDelete(args: string[], operations: TrapOperations): CommandResult {
227
+ const { opts, positionals } = parseArgs(args);
228
+ const id = parseId(positionals[0], "Usage: codetrap delete <id> [--scope project|global] [--json]");
229
+ if (typeof id !== "number") return id;
230
+
231
+ const result = operations.deleteTrap(id, opts.scope);
232
+ if (opts.json !== undefined) {
233
+ return mutationJsonResult({ id, ...result }, `Trap #${id} not found.`);
234
+ }
235
+ return result.success
236
+ ? textResult(`Trap #${id} deleted from ${result.scope} scope.`)
237
+ : errorResult(`Trap #${id} not found.`);
238
+ }
239
+
240
+ function cmdAddTrapEvidence(args: string[], operations: TrapOperations): CommandResult {
241
+ const { opts, positionals } = parseArgs(args);
242
+ const id = parseId(
243
+ positionals[0],
244
+ "Usage: codetrap add_trap_evidence <id> --source_type manual|conversation|commit|issue|test_failure [--scope project|global] [--source_ref X] [--related_files a,b] [--note X]"
245
+ );
246
+ if (typeof id !== "number") return id;
247
+
248
+ try {
249
+ const input = opts.json ? JSON.parse(opts.json) : {
250
+ source_type: opts.source_type ?? opts["source-type"],
251
+ source_ref: opts.source_ref ?? opts["source-ref"],
252
+ observed_at: opts.observed_at ?? opts["observed-at"],
253
+ related_files: parseCsv(opts.related_files ?? opts["related-files"]),
254
+ note: opts.note,
255
+ };
256
+ const result = operations.addTrapEvidence(id, input, opts.scope);
257
+ if (opts["output-json"] !== undefined) {
258
+ return mutationJsonResult({ id, ...result }, `Trap #${id} not found.`);
259
+ }
260
+ return result.success
261
+ ? textResult(`Evidence #${result.evidence_id} added to trap #${id} in ${result.scope} scope.`)
262
+ : errorResult(`Trap #${id} not found.`);
263
+ } catch (error) {
264
+ return errorFrom(error);
265
+ }
266
+ }
267
+
268
+ function cmdArchiveTrap(args: string[], operations: TrapOperations): CommandResult {
269
+ const { opts, positionals } = parseArgs(args);
270
+ const id = parseId(positionals[0], "Usage: codetrap archive_trap <id> [--scope project|global] [--json]");
271
+ if (typeof id !== "number") return id;
272
+
273
+ const result = operations.archiveTrap(id, opts.scope);
274
+ if (opts.json !== undefined) {
275
+ return mutationJsonResult({ id, ...result, status: result.success ? "archived" : undefined }, `Trap #${id} not found.`);
276
+ }
277
+ return result.success
278
+ ? textResult(`Trap #${id} archived in ${result.scope} scope.`)
279
+ : errorResult(`Trap #${id} not found.`);
280
+ }
281
+
282
+ function cmdSupersedeTrap(args: string[], operations: TrapOperations): CommandResult {
283
+ const { opts, positionals } = parseArgs(args);
284
+ if (positionals.length < 2) {
285
+ return errorResult("Usage: codetrap supersede_trap <old_id> <new_id> [--scope project|global] [--state_key key] [--json]");
286
+ }
287
+ const id = Number.parseInt(positionals[0], 10);
288
+ const supersededById = Number.parseInt(positionals[1], 10);
289
+ if (Number.isNaN(id) || Number.isNaN(supersededById)) {
290
+ return errorResult("Error: ids must be numbers");
291
+ }
292
+
293
+ const result = operations.supersedeTrap(id, supersededById, opts.scope, opts.state_key ?? opts["state-key"]);
294
+ if (opts.json !== undefined) {
295
+ return mutationJsonResult(
296
+ { id, superseded_by_id: supersededById, ...result },
297
+ `Trap #${id} or #${supersededById} not found in the same scope.`
298
+ );
299
+ }
300
+ return result.success
301
+ ? textResult(`Trap #${id} superseded by #${supersededById} in ${result.scope} scope.`)
302
+ : errorResult(`Trap #${id} or #${supersededById} not found in the same scope.`);
303
+ }
304
+
305
+ function cmdExport(args: string[], operations: TrapOperations): CommandResult {
306
+ const { opts } = parseArgs(args);
307
+ return jsonResult(operations.exportTraps(opts.scope));
308
+ }
309
+
310
+ function cmdImport(args: string[], operations: TrapOperations): CommandResult {
311
+ const { opts, positionals } = parseArgs(args);
312
+ if (positionals.length === 0) return errorResult("Usage: codetrap import <file.json>");
313
+
314
+ try {
315
+ const traps = JSON.parse(readFileSync(positionals[0], "utf-8"));
316
+ if (!Array.isArray(traps)) {
317
+ const message = "Error: JSON file must contain an array of traps";
318
+ return opts.json !== undefined ? jsonResult({ success: false, error: message }, 1) : errorResult(message);
319
+ }
320
+ const imported = operations.importTraps(traps);
321
+ return opts.json !== undefined
322
+ ? jsonResult({ imported, success: true })
323
+ : textResult(`Imported ${imported} traps.`);
324
+ } catch (error) {
325
+ if (opts.json !== undefined) {
326
+ return jsonResult({ success: false, error: errorMessage(error) }, 1);
327
+ }
328
+ return errorFrom(error);
329
+ }
330
+ }
331
+
332
+ function cmdStats(args: string[], operations: TrapOperations): CommandResult {
333
+ const { opts } = parseArgs(args);
334
+ const stats = operations.getStats();
335
+ const embeddingStats = operations.getEmbeddingStats();
336
+ return opts.json !== undefined
337
+ ? jsonResult(toStatsJson(stats, embeddingStats))
338
+ : textResult(formatStatsText(stats));
339
+ }
340
+
341
+ function cmdDoctor(args: string[], store: TrapStore, operations: TrapOperations): CommandResult {
342
+ const { opts } = parseArgs(args);
343
+ const report = buildDoctorReport(store, operations);
344
+ return opts.json !== undefined
345
+ ? jsonResult(report)
346
+ : textResult(formatDoctorText(report));
347
+ }
348
+
349
+ function cmdScopeMigration(
350
+ command: ScopeMigrationCommand,
351
+ args: string[],
352
+ operations: TrapOperations
353
+ ): CommandResult {
354
+ const { opts } = parseArgs(args);
355
+ if (opts.apply !== undefined && opts["dry-run"] !== undefined) {
356
+ return errorResult("Error: choose either --dry-run or --apply, not both.");
357
+ }
358
+ if (command === "migrate-project" && (!opts["from-project-path"] || !opts["to-project-path"])) {
359
+ return errorResult("Usage: codetrap migrate-project --from-project-path <path> --to-project-path <path> [--dry-run|--apply] [--json]");
360
+ }
361
+
362
+ try {
363
+ const result = runScopeMigration({
364
+ command,
365
+ fromProjectPath: opts["from-project-path"],
366
+ toProjectPath: opts["to-project-path"],
367
+ apply: opts.apply !== undefined,
368
+ cwd: process.cwd(),
369
+ });
370
+ return opts.json !== undefined
371
+ ? jsonResult(result)
372
+ : textResult(formatScopeMigrationText(result));
373
+ } catch (error) {
374
+ return errorFrom(error);
375
+ }
376
+ }
377
+
378
+ async function cmdEmbed(args: string[], store: TrapStore): Promise<CommandResult> {
379
+ const { opts } = parseArgs(args);
380
+ try {
381
+ const result = await store.ensureEmbeddings({
382
+ scope: opts.scope,
383
+ category: opts.category,
384
+ limit: opts.limit ? parseOptionalInt(opts.limit) : undefined,
385
+ force: opts.force === "true",
386
+ batchSize: opts["batch-size"] ? parseOptionalInt(opts["batch-size"]) : undefined,
387
+ });
388
+ return textResult([
389
+ ...result.scopes.map((scoped) =>
390
+ `[${scoped.scope}] embeddings generated: ${scoped.generated}, skipped: ${scoped.skipped}, batches: ${scoped.batches}`
391
+ ),
392
+ `Total generated: ${result.generated}, skipped: ${result.skipped}, batches: ${result.batches}`,
393
+ ].join("\n"));
394
+ } catch (error) {
395
+ return errorFrom(error);
396
+ }
397
+ }
398
+
399
+ function formatStatsText(stats: ReturnType<TrapOperations["getStats"]>): string {
400
+ const sections: string[] = [];
401
+ if (stats.project) {
402
+ sections.push("── Project ──", formatStatsBlock(stats.project));
403
+ }
404
+ sections.push("── Global ──", formatStatsBlock(stats.global));
405
+ return sections.join("\n");
406
+ }
407
+
408
+ function formatStatsBlock(stats: { total: number; byCategory: Record<string, number>; bySeverity: Record<string, number> }): string {
409
+ return [
410
+ ` Total: ${stats.total}`,
411
+ " By category:",
412
+ ...Object.entries(stats.byCategory).map(([category, count]) => ` ${category}: ${count}`),
413
+ " By severity:",
414
+ ...Object.entries(stats.bySeverity).map(([severity, count]) => ` ${severity}: ${count}`),
415
+ ].join("\n");
416
+ }
417
+
418
+ function parseSearchMode(mode: string): SearchMode {
419
+ if ((SEARCH_MODES as readonly string[]).includes(mode)) return mode as SearchMode;
420
+ throw new Error(`Invalid search mode: ${mode}. Expected one of: ${SEARCH_MODES.join(", ")}`);
421
+ }
422
+
423
+ function parseId(value: string | undefined, usage: string): number | CommandResult {
424
+ if (value === undefined) return errorResult(usage);
425
+ const id = Number.parseInt(value, 10);
426
+ return Number.isNaN(id) ? errorResult("Error: id must be a number") : id;
427
+ }
428
+
429
+ function parseOptionalInt(value: string | undefined, fallback?: number): number {
430
+ if (value === undefined) {
431
+ if (fallback === undefined) throw new Error("Missing numeric value.");
432
+ return fallback;
433
+ }
434
+ const parsed = Number.parseInt(value, 10);
435
+ if (Number.isNaN(parsed)) throw new Error(`Invalid number: ${value}`);
436
+ return parsed;
437
+ }
438
+
439
+ function readQuery(positionals: string[]): string {
440
+ if (positionals.length > 0) return positionals.join(" ").trim();
441
+ if (process.stdin.isTTY) return "";
442
+ return readFileSync(0, "utf-8").trim();
443
+ }
444
+
445
+ function parseCsv(value?: string): string[] | undefined {
446
+ if (!value) return undefined;
447
+ return value
448
+ .split(",")
449
+ .map((item) => item.trim())
450
+ .filter(Boolean);
451
+ }
452
+
453
+ function mutationJsonResult<T extends Record<string, unknown> & { success: boolean }>(
454
+ value: T,
455
+ error: string
456
+ ): CommandResult {
457
+ return jsonResult(mutationJsonPayload(value, error), value.success ? 0 : 1);
458
+ }
459
+
460
+ function errorFrom(error: unknown): CommandResult {
461
+ return errorResult(`Error: ${errorMessage(error)}`);
462
+ }
463
+
464
+ function errorMessage(error: unknown): string {
465
+ return error instanceof Error ? error.message : String(error);
466
+ }
@@ -8,6 +8,7 @@ import {
8
8
  type FreshEmbedding,
9
9
  type StoredEmbedding,
10
10
  } from "../lib/embedder";
11
+ import type { EmbeddingStateCounts } from "../lib/embedding-health";
11
12
  import { embeddingIsFresh, passageHashForTrap } from "../lib/trap-search-document";
12
13
  import { countTraps, listTraps } from "./queries";
13
14
 
@@ -140,6 +141,38 @@ export function countEmbeddableTraps(
140
141
  return countTraps(db, opts);
141
142
  }
142
143
 
144
+ export function getEmbeddingStateCounts(
145
+ db: Database,
146
+ config: EmbeddingConfig | null,
147
+ opts: { scope?: string; category?: string; status?: TrapStatus | "all" } = {}
148
+ ): EmbeddingStateCounts {
149
+ const traps = listTraps(db, {
150
+ scope: opts.scope,
151
+ category: opts.category,
152
+ status: opts.status,
153
+ limit: 100000,
154
+ });
155
+ const counts: EmbeddingStateCounts = {
156
+ total: traps.length,
157
+ fresh: 0,
158
+ stale: 0,
159
+ missing: 0,
160
+ };
161
+
162
+ for (const trap of traps) {
163
+ const embedding = getEmbedding(db, trap.id);
164
+ if (!embedding) {
165
+ counts.missing++;
166
+ } else if (config && embeddingIsFresh(trap, embedding, config)) {
167
+ counts.fresh++;
168
+ } else {
169
+ counts.stale++;
170
+ }
171
+ }
172
+
173
+ return counts;
174
+ }
175
+
143
176
  function rowToStoredEmbedding(row: TrapEmbeddingRow): StoredEmbedding {
144
177
  return {
145
178
  trap_id: row.trap_id,