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
package/src/lib/store.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { openGlobal, openProject } from "../db/connection";
2
- import { TrapRepository, type TrapStats } from "../db/repository";
1
+ import type { TrapStats } from "../db/repository";
3
2
  import {
4
3
  type Trap,
5
4
  type TrapDetails,
@@ -11,9 +10,15 @@ import {
11
10
  type TrapUpdate,
12
11
  } from "../domain/trap";
13
12
  import type { SearchMode, TrapStatus } from "./constants";
14
- import { createDefaultEmbeddingProvider, type EmbeddingProvider } from "./embedder";
15
- import { findProjectRoot } from "./scope";
13
+ import { createDefaultEmbeddingProvider, embeddingConfig, type EmbeddingConfig, type EmbeddingProvider } from "./embedder";
14
+ import { summarizeEmbeddingState, type EmbeddingStateSummary, type EmbeddingStatsResult } from "./embedding-health";
15
+ import { normalizeScope, ScopedRepositoryContext, type ScopedRepository } from "./scope-context";
16
16
  import { importTrapArchive } from "./trap-archive";
17
+ import {
18
+ resolveScopedMutation,
19
+ type AddTrapEvidenceResult,
20
+ type TrapMutationResult,
21
+ } from "./trap-mutation-result";
17
22
 
18
23
  export {
19
24
  type TrapInput,
@@ -26,47 +31,51 @@ export {
26
31
  type TrapStats,
27
32
  };
28
33
 
29
- type Scope = "project" | "global";
34
+ export type { EmbeddingStateSummary };
35
+ export type TrapEmbeddingStats = EmbeddingStatsResult;
30
36
 
31
37
  export class TrapStore {
32
- private projectRoot: string | null;
33
- private globalRepository?: TrapRepository;
34
- private projectRepository?: TrapRepository;
38
+ private readonly scopes: ScopedRepositoryContext;
35
39
  private readonly embedder?: EmbeddingProvider;
36
40
 
37
41
  constructor(cwd: string, embedder: EmbeddingProvider | undefined = createDefaultEmbeddingProvider()) {
38
- this.projectRoot = findProjectRoot(cwd);
39
42
  this.embedder = embedder;
43
+ this.scopes = new ScopedRepositoryContext(cwd, embedder);
40
44
  }
41
45
 
42
46
  add(input: TrapInput): { id: number; scope: string } {
43
47
  const scope = normalizeScope(input.scope);
44
- if (scope === "project" && !this.projectRoot) {
48
+ if (scope === "project" && !this.scopes.hasProject()) {
45
49
  throw new Error("Not in a project. Run 'codetrap init' first, or use --scope global.");
46
50
  }
47
51
 
48
- const id = this.repositoryFor(scope).add({
52
+ const id = this.scopes.repositoryFor(scope).add({
49
53
  ...input,
50
54
  scope,
51
- project_path: scope === "project" ? this.projectRoot : null,
55
+ project_path: scope === "project" ? this.scopes.projectRoot() : null,
52
56
  });
53
57
  return { id, scope };
54
58
  }
55
59
 
56
60
  async search(
57
61
  query: string,
58
- opts: { category?: string; scope?: string; limit?: number; mode?: SearchMode; status?: TrapStatus | "all" } = {}
62
+ opts: { category?: string; scope?: string; limit?: number; mode?: SearchMode; status?: TrapStatus | "all"; path?: string; module?: string; owner?: string; rerank?: boolean; includeRankingSignals?: boolean } = {}
59
63
  ): Promise<{ results: TrapSearchResult[]; scope: string }[]> {
60
64
  const out: { results: TrapSearchResult[]; scope: string }[] = [];
61
65
  const limit = opts.limit ?? 20;
62
66
 
63
- for (const scoped of this.repositoriesForRead(opts.scope)) {
67
+ for (const scoped of this.scopes.repositoriesForRead(opts.scope)) {
64
68
  const results = await scoped.repository.search(query, {
65
69
  category: opts.category,
66
70
  scope: scoped.scope,
67
71
  limit,
68
72
  mode: opts.mode ?? "hybrid",
69
73
  status: opts.status,
74
+ path: opts.path,
75
+ module: opts.module,
76
+ owner: opts.owner,
77
+ rerank: opts.rerank,
78
+ includeRankingSignals: opts.includeRankingSignals,
70
79
  });
71
80
  if (results.length > 0) out.push({ results, scope: scoped.scope });
72
81
  }
@@ -75,7 +84,7 @@ export class TrapStore {
75
84
  }
76
85
 
77
86
  get(id: number, scope?: string): { trap: Trap; scope: string } | null {
78
- for (const scoped of this.repositoriesForRead(scope)) {
87
+ for (const scoped of this.scopes.repositoriesForRead(scope)) {
79
88
  const trap = scoped.repository.get(id);
80
89
  if (trap) return { trap, scope: scoped.scope };
81
90
  }
@@ -83,17 +92,17 @@ export class TrapStore {
83
92
  }
84
93
 
85
94
  getDetails(id: number, scope?: string): TrapDetails | null {
86
- for (const scoped of this.repositoriesForRead(scope)) {
95
+ for (const scoped of this.scopes.repositoriesForRead(scope)) {
87
96
  const details = scoped.repository.getDetails(id, scoped.scope);
88
97
  if (details) return details;
89
98
  }
90
99
  return null;
91
100
  }
92
101
 
93
- list(opts: { category?: string; scope?: string; limit?: number; offset?: number; status?: TrapStatus | "all" } = {}): { traps: Trap[]; scope: string }[] {
102
+ list(opts: { category?: string; scope?: string; limit?: number; offset?: number; status?: TrapStatus | "all"; path?: string; module?: string; owner?: string } = {}): { traps: Trap[]; scope: string }[] {
94
103
  const out: { traps: Trap[]; scope: string }[] = [];
95
104
 
96
- for (const scoped of this.repositoriesForRead(opts.scope)) {
105
+ for (const scoped of this.scopes.repositoriesForRead(opts.scope)) {
97
106
  const traps = scoped.repository.list({ ...opts, scope: scoped.scope });
98
107
  if (traps.length > 0) out.push({ traps, scope: scoped.scope });
99
108
  }
@@ -101,36 +110,36 @@ export class TrapStore {
101
110
  return out;
102
111
  }
103
112
 
104
- update(id: number, input: TrapUpdate, scope?: string): { scope: string; success: boolean } {
105
- for (const scoped of this.repositoriesForWrite(scope)) {
106
- const success = scoped.repository.update(id, input);
107
- if (success || scope) return { scope: scoped.scope, success };
108
- }
109
- return { scope: "global", success: false };
113
+ update(id: number, input: TrapUpdate, scope?: string): TrapMutationResult {
114
+ return this.resolveMutation(scope, (scoped) => ({
115
+ success: scoped.repository.update(id, input),
116
+ }));
110
117
  }
111
118
 
112
- delete(id: number, scope?: string): { scope: string; success: boolean } {
113
- for (const scoped of this.repositoriesForWrite(scope)) {
114
- const success = scoped.repository.delete(id);
115
- if (success || scope) return { scope: scoped.scope, success };
116
- }
117
- return { scope: "global", success: false };
119
+ delete(id: number, scope?: string): TrapMutationResult {
120
+ return this.resolveMutation(scope, (scoped) => ({
121
+ success: scoped.repository.delete(id),
122
+ }));
118
123
  }
119
124
 
120
- addEvidence(id: number, input: TrapEvidenceInput, scope?: string): { scope: string; evidence_id: number | null; success: boolean } {
121
- for (const scoped of this.repositoriesForWrite(scope)) {
125
+ addEvidence(id: number, input: TrapEvidenceInput, scope?: string): AddTrapEvidenceResult {
126
+ return this.resolveMutation(
127
+ scope,
128
+ (scoped) => {
122
129
  const evidenceId = scoped.repository.addEvidence(id, input);
123
- if (evidenceId || scope) return { scope: scoped.scope, evidence_id: evidenceId, success: evidenceId !== null };
124
- }
125
- return { scope: "global", evidence_id: null, success: false };
130
+ return {
131
+ evidence_id: evidenceId,
132
+ success: evidenceId !== null,
133
+ };
134
+ },
135
+ () => ({ evidence_id: null, success: false })
136
+ );
126
137
  }
127
138
 
128
- archive(id: number, scope?: string): { scope: string; success: boolean } {
129
- for (const scoped of this.repositoriesForWrite(scope)) {
130
- const success = scoped.repository.archive(id);
131
- if (success || scope) return { scope: scoped.scope, success };
132
- }
133
- return { scope: "global", success: false };
139
+ archive(id: number, scope?: string): TrapMutationResult {
140
+ return this.resolveMutation(scope, (scoped) => ({
141
+ success: scoped.repository.archive(id),
142
+ }));
134
143
  }
135
144
 
136
145
  supersede(
@@ -138,16 +147,14 @@ export class TrapStore {
138
147
  supersededById: number,
139
148
  scope?: string,
140
149
  stateKey?: string
141
- ): { scope: string; success: boolean } {
142
- for (const scoped of this.repositoriesForWrite(scope)) {
143
- const success = scoped.repository.supersede(id, supersededById, stateKey);
144
- if (success || scope) return { scope: scoped.scope, success };
145
- }
146
- return { scope: "global", success: false };
150
+ ): TrapMutationResult {
151
+ return this.resolveMutation(scope, (scoped) => ({
152
+ success: scoped.repository.supersede(id, supersededById, stateKey),
153
+ }));
147
154
  }
148
155
 
149
156
  hit(id: number, scope?: string): void {
150
- for (const scoped of this.repositoriesForRead(scope)) {
157
+ for (const scoped of this.scopes.repositoriesForRead(scope)) {
151
158
  if (scope || scoped.repository.get(id)) {
152
159
  scoped.repository.hit(id);
153
160
  return;
@@ -157,17 +164,28 @@ export class TrapStore {
157
164
 
158
165
  topTraps(scope: string, limit = 20): Trap[] {
159
166
  const resolvedScope = normalizeScope(scope);
160
- return this.repositoryFor(resolvedScope).top(resolvedScope, limit);
167
+ return this.scopes.repositoryFor(resolvedScope).top(resolvedScope, limit);
161
168
  }
162
169
 
163
170
  stats(): { project: TrapStats | null; global: TrapStats } {
164
- const project = this.projectRoot ? this.projectRepo().stats() : null;
165
- return { project, global: this.globalRepo().stats() };
171
+ const project = this.scopes.repositoryEntry("project")?.repository.stats() ?? null;
172
+ return { project, global: this.scopes.repositoryFor("global").stats() };
173
+ }
174
+
175
+ embeddingStats(): TrapEmbeddingStats {
176
+ const config = this.embeddingConfig();
177
+ const project = this.scopes.repositoryEntry("project")
178
+ ? summarizeEmbeddingState(this.scopes.repositoryFor("project").embeddingStats(config), config)
179
+ : null;
180
+ return {
181
+ project,
182
+ global: summarizeEmbeddingState(this.scopes.repositoryFor("global").embeddingStats(config), config),
183
+ };
166
184
  }
167
185
 
168
186
  exportAll(opts: { scope?: string } = {}): TrapExportRecord[] {
169
187
  const traps: TrapExportRecord[] = [];
170
- for (const scoped of this.repositoriesForRead(opts.scope)) {
188
+ for (const scoped of this.scopes.repositoriesForRead(opts.scope)) {
171
189
  traps.push(...scoped.repository.exportAll());
172
190
  }
173
191
  return traps;
@@ -181,11 +199,23 @@ export class TrapStore {
181
199
  }
182
200
 
183
201
  hasProject(): boolean {
184
- return this.projectRoot !== null;
202
+ return this.scopes.hasProject();
185
203
  }
186
204
 
187
205
  getProjectRoot(): string | null {
188
- return this.projectRoot;
206
+ return this.scopes.projectRoot();
207
+ }
208
+
209
+ hasEmbeddingProvider(): boolean {
210
+ return this.embedder !== undefined;
211
+ }
212
+
213
+ embeddingConfig(): EmbeddingConfig | null {
214
+ return this.embedder ? embeddingConfig(this.embedder) : null;
215
+ }
216
+
217
+ forCwd(cwd: string): TrapStore {
218
+ return new TrapStore(cwd, this.embedder);
189
219
  }
190
220
 
191
221
  async ensureEmbeddings(opts: { scope?: string; category?: string; limit?: number; force?: boolean; batchSize?: number } = {}): Promise<{
@@ -199,7 +229,7 @@ export class TrapStore {
199
229
  let skipped = 0;
200
230
  let batches = 0;
201
231
 
202
- for (const scoped of this.repositoriesForRead(opts.scope)) {
232
+ for (const scoped of this.scopes.repositoriesForRead(opts.scope)) {
203
233
  const result = await scoped.repository.ensureEmbeddings({
204
234
  scope: scoped.scope,
205
235
  category: opts.category,
@@ -216,57 +246,16 @@ export class TrapStore {
216
246
  return { generated, skipped, batches, scopes };
217
247
  }
218
248
 
219
- private repositoriesForRead(scope?: string): { scope: Scope; repository: TrapRepository }[] {
220
- const resolvedScope = optionalScope(scope);
221
- if (resolvedScope) return this.repositoryEntry(resolvedScope) ? [this.repositoryEntry(resolvedScope)!] : [];
222
-
223
- const repositories: { scope: Scope; repository: TrapRepository }[] = [];
224
- const project = this.repositoryEntry("project");
225
- if (project) repositories.push(project);
226
- repositories.push({ scope: "global", repository: this.globalRepo() });
227
- return repositories;
228
- }
229
-
230
- private repositoriesForWrite(scope?: string): { scope: Scope; repository: TrapRepository }[] {
231
- return this.repositoriesForRead(scope);
249
+ private resolveMutation<TExtra extends object = {}>(
250
+ scope: string | undefined,
251
+ mutate: (scoped: ScopedRepository) => { success: boolean } & TExtra,
252
+ fallback: () => { success: false } & TExtra = () => ({ success: false } as { success: false } & TExtra)
253
+ ): { scope: "project" | "global"; success: boolean } & TExtra {
254
+ return resolveScopedMutation(
255
+ this.scopes.repositoriesForWrite(scope),
256
+ { explicitScope: scope !== undefined },
257
+ mutate,
258
+ fallback
259
+ );
232
260
  }
233
-
234
- private repositoryEntry(scope: Scope): { scope: Scope; repository: TrapRepository } | null {
235
- if (scope === "project") {
236
- return this.projectRoot ? { scope, repository: this.projectRepo() } : null;
237
- }
238
- return { scope, repository: this.globalRepo() };
239
- }
240
-
241
- private repositoryFor(scope: Scope): TrapRepository {
242
- if (scope === "project") return this.projectRepo();
243
- return this.globalRepo();
244
- }
245
-
246
- private projectRepo(): TrapRepository {
247
- if (!this.projectRoot) {
248
- throw new Error("Not in a project. Run 'codetrap init' first, or use --scope global.");
249
- }
250
- if (!this.projectRepository) {
251
- this.projectRepository = new TrapRepository(openProject(this.projectRoot), this.embedder);
252
- }
253
- return this.projectRepository;
254
- }
255
-
256
- private globalRepo(): TrapRepository {
257
- if (!this.globalRepository) {
258
- this.globalRepository = new TrapRepository(openGlobal(), this.embedder);
259
- }
260
- return this.globalRepository;
261
- }
262
- }
263
-
264
- function normalizeScope(scope: string): Scope {
265
- if (scope === "project" || scope === "global") return scope;
266
- throw new Error(`Invalid scope: ${scope}`);
267
- }
268
-
269
- function optionalScope(scope?: string): Scope | null {
270
- if (!scope) return null;
271
- return normalizeScope(scope);
272
261
  }
@@ -1,17 +1,17 @@
1
1
  import {
2
- buildTrapEvidenceInput,
3
- type TrapEvidence,
4
2
  type TrapEvidenceInput,
5
- type TrapExportEvidence,
6
3
  type TrapImportEvidence,
7
4
  type TrapImportRecord,
8
5
  type TrapInput,
9
6
  } from "../domain/trap";
10
7
  import {
11
- parseEvidenceRelatedFiles,
12
- parseOptionalEvidenceRelatedFiles,
13
- parseOptionalTrapTags,
14
- } from "./trap-json-fields";
8
+ importEvidenceToTrapInput,
9
+ importRecordToTrapInput,
10
+ normalizeEvidenceForExport,
11
+ normalizeTrapForExport,
12
+ } from "./trap-codec";
13
+
14
+ export { normalizeEvidenceForExport, normalizeTrapForExport };
15
15
 
16
16
  export interface TrapArchiveImportAdapter {
17
17
  add(input: TrapInput): { id: number; scope: string };
@@ -22,13 +22,6 @@ export interface TrapArchiveImportAdapter {
22
22
  ): { scope: string; evidence_id: number | null; success: boolean };
23
23
  }
24
24
 
25
- export function normalizeEvidenceForExport(evidence: TrapEvidence): TrapExportEvidence {
26
- return {
27
- ...evidence,
28
- related_files: parseEvidenceRelatedFiles(evidence.related_files),
29
- };
30
- }
31
-
32
25
  export function importTrapArchive(
33
26
  records: TrapImportRecord[],
34
27
  adapter: TrapArchiveImportAdapter
@@ -36,7 +29,7 @@ export function importTrapArchive(
36
29
  let count = 0;
37
30
  for (const record of records) {
38
31
  try {
39
- const result = adapter.add(toTrapInput(record));
32
+ const result = adapter.add(importRecordToTrapInput(record));
40
33
  importEvidenceRecords(record.evidence ?? [], result, adapter);
41
34
  count++;
42
35
  } catch {
@@ -55,7 +48,7 @@ function importEvidenceRecords(
55
48
  try {
56
49
  adapter.addEvidence(
57
50
  trap.id,
58
- buildTrapEvidenceInput(toEvidenceInput(evidence)),
51
+ importEvidenceToTrapInput(evidence),
59
52
  trap.scope
60
53
  );
61
54
  } catch {
@@ -63,29 +56,3 @@ function importEvidenceRecords(
63
56
  }
64
57
  }
65
58
  }
66
-
67
- function toTrapInput(record: TrapImportRecord): TrapInput {
68
- return {
69
- title: record.title,
70
- category: record.category,
71
- tags: parseOptionalTrapTags(record.tags),
72
- scope: record.scope,
73
- context: record.context,
74
- mistake: record.mistake,
75
- fix: record.fix,
76
- before_code: record.before_code ?? undefined,
77
- after_code: record.after_code ?? undefined,
78
- severity: record.severity ?? undefined,
79
- project_path: record.project_path ?? undefined,
80
- };
81
- }
82
-
83
- function toEvidenceInput(evidence: TrapImportEvidence): Record<string, unknown> {
84
- return {
85
- source_type: evidence.source_type,
86
- source_ref: evidence.source_ref ?? undefined,
87
- observed_at: evidence.observed_at ?? undefined,
88
- related_files: parseOptionalEvidenceRelatedFiles(evidence.related_files),
89
- note: evidence.note ?? undefined,
90
- };
91
- }
@@ -0,0 +1,113 @@
1
+ import type {
2
+ Trap,
3
+ TrapEvidence,
4
+ TrapEvidenceInput,
5
+ TrapExportEvidence,
6
+ TrapExportRecord,
7
+ TrapImportEvidence,
8
+ TrapImportRecord,
9
+ TrapInput,
10
+ TrapUpdate,
11
+ } from "../domain/trap";
12
+ import { buildTrapEvidenceInput } from "../domain/trap";
13
+ import {
14
+ parseEvidenceRelatedFiles,
15
+ parseOptionalEvidenceRelatedFiles,
16
+ parseOptionalTrapPathGlobs,
17
+ parseOptionalTrapTags,
18
+ parseTrapPathGlobs,
19
+ parseTrapTags,
20
+ encodeTrapPathGlobs,
21
+ encodeTrapTags,
22
+ } from "./trap-json-fields";
23
+ import { buildTrapSearchText } from "./trap-search-document";
24
+
25
+ export type JsonTrap = Omit<Trap, "tags" | "path_globs"> & {
26
+ tags: string[];
27
+ path_globs: string[];
28
+ };
29
+
30
+ export type JsonTrapEvidence = Omit<TrapEvidence, "related_files"> & {
31
+ related_files: string[];
32
+ };
33
+
34
+ export function toTrapJson(trap: Trap): JsonTrap {
35
+ return {
36
+ ...trap,
37
+ tags: parseTrapTags(trap.tags),
38
+ path_globs: parseTrapPathGlobs(trap.path_globs),
39
+ };
40
+ }
41
+
42
+ export function toTrapEvidenceJson(evidence: TrapEvidence): JsonTrapEvidence {
43
+ return {
44
+ ...evidence,
45
+ related_files: parseEvidenceRelatedFiles(evidence.related_files),
46
+ };
47
+ }
48
+
49
+ export function normalizeTrapForExport(trap: Trap): Omit<TrapExportRecord, "evidence"> {
50
+ return {
51
+ ...trap,
52
+ path_globs: parseTrapPathGlobs(trap.path_globs),
53
+ };
54
+ }
55
+
56
+ export function normalizeEvidenceForExport(evidence: TrapEvidence): TrapExportEvidence {
57
+ return {
58
+ ...evidence,
59
+ related_files: parseEvidenceRelatedFiles(evidence.related_files),
60
+ };
61
+ }
62
+
63
+ export function importRecordToTrapInput(record: TrapImportRecord): TrapInput {
64
+ return {
65
+ title: record.title,
66
+ category: record.category,
67
+ tags: parseOptionalTrapTags(record.tags),
68
+ scope: record.scope,
69
+ context: record.context,
70
+ mistake: record.mistake,
71
+ fix: record.fix,
72
+ before_code: record.before_code ?? undefined,
73
+ after_code: record.after_code ?? undefined,
74
+ severity: record.severity ?? undefined,
75
+ project_path: record.project_path ?? undefined,
76
+ path_globs: parseOptionalTrapPathGlobs(record.path_globs),
77
+ module: record.module ?? undefined,
78
+ owner: record.owner ?? undefined,
79
+ };
80
+ }
81
+
82
+ export function importEvidenceToTrapInput(evidence: TrapImportEvidence): TrapEvidenceInput {
83
+ return buildTrapEvidenceInput({
84
+ source_type: evidence.source_type,
85
+ source_ref: evidence.source_ref ?? undefined,
86
+ observed_at: evidence.observed_at ?? undefined,
87
+ related_files: parseOptionalEvidenceRelatedFiles(evidence.related_files),
88
+ note: evidence.note ?? undefined,
89
+ });
90
+ }
91
+
92
+ export function encodeTrapInsertFields(input: TrapInput): {
93
+ tags: string;
94
+ path_globs: string;
95
+ search_text: string;
96
+ } {
97
+ const tags = encodeTrapTags(input.tags);
98
+ const pathGlobs = encodeTrapPathGlobs(input.path_globs);
99
+ return {
100
+ tags,
101
+ path_globs: pathGlobs,
102
+ search_text: buildTrapSearchText({ ...input, tags, path_globs: pathGlobs }),
103
+ };
104
+ }
105
+
106
+ export function mergeTrapUpdateForSearchText(current: Trap, input: TrapUpdate): Trap {
107
+ return {
108
+ ...current,
109
+ ...input,
110
+ tags: input.tags !== undefined ? encodeTrapTags(input.tags) : current.tags,
111
+ path_globs: input.path_globs !== undefined ? encodeTrapPathGlobs(input.path_globs) : current.path_globs,
112
+ };
113
+ }
@@ -12,6 +12,18 @@ export function encodeTrapTags(value: TrapStringArrayInput): string {
12
12
  return JSON.stringify(parseTrapTags(value));
13
13
  }
14
14
 
15
+ export function parseTrapPathGlobs(value: TrapStringArrayInput): string[] {
16
+ return parseStringArrayField(value);
17
+ }
18
+
19
+ export function parseOptionalTrapPathGlobs(value: TrapStringArrayInput): string[] | undefined {
20
+ return emptyToUndefined(parseTrapPathGlobs(value));
21
+ }
22
+
23
+ export function encodeTrapPathGlobs(value: TrapStringArrayInput): string {
24
+ return JSON.stringify(parseTrapPathGlobs(value));
25
+ }
26
+
15
27
  export function parseEvidenceRelatedFiles(value: TrapStringArrayInput): string[] {
16
28
  return parseStringArrayField(value);
17
29
  }
@@ -0,0 +1,36 @@
1
+ import type { Scope } from "./constants";
2
+
3
+ export type TrapMutationResult = {
4
+ scope: Scope;
5
+ success: boolean;
6
+ };
7
+
8
+ export type AddTrapEvidenceResult = TrapMutationResult & {
9
+ evidence_id: number | null;
10
+ };
11
+
12
+ export type ScopedMutationTarget = {
13
+ scope: Scope;
14
+ };
15
+
16
+ export function resolveScopedMutation<TTarget extends ScopedMutationTarget, TExtra extends object>(
17
+ targets: TTarget[],
18
+ opts: { explicitScope: boolean; fallbackScope?: Scope },
19
+ mutate: (target: TTarget) => { success: boolean } & TExtra,
20
+ fallback: () => { success: false } & TExtra
21
+ ): ({ scope: Scope; success: boolean } & TExtra) {
22
+ for (const target of targets) {
23
+ const result = mutate(target);
24
+ if (result.success || opts.explicitScope) {
25
+ return { scope: target.scope, ...result };
26
+ }
27
+ }
28
+ return { scope: opts.fallbackScope ?? "global", ...fallback() };
29
+ }
30
+
31
+ export function mutationJsonPayload<T extends { success: boolean }>(
32
+ value: T,
33
+ error: string
34
+ ): T | (T & { error: string }) {
35
+ return value.success ? value : { ...value, error };
36
+ }
@@ -12,17 +12,14 @@ import {
12
12
  import type { TrapStore, TrapStats } from "./store";
13
13
  import type { SearchMode } from "./constants";
14
14
  import { toTrapActionCards } from "./search-result-card";
15
+ import type { AddTrapEvidenceResult, TrapMutationResult } from "./trap-mutation-result";
15
16
 
16
17
  export type TrapListGroup = { traps: Trap[]; scope: string };
17
18
  export type TrapSearchGroup = { results: TrapSearchResult[]; scope: string };
18
- export type TrapMutationResult = { scope: string; success: boolean };
19
- export type AddTrapEvidenceResult = {
20
- scope: string;
21
- evidence_id: number | null;
22
- success: boolean;
23
- };
19
+ export type { AddTrapEvidenceResult, TrapMutationResult };
24
20
 
25
21
  export type TrapStatsResult = { project: TrapStats | null; global: TrapStats };
22
+ export type EmbeddingStatsResult = ReturnType<TrapStore["embeddingStats"]>;
26
23
 
27
24
  export interface SearchTrapsArgs {
28
25
  query: string;
@@ -31,6 +28,11 @@ export interface SearchTrapsArgs {
31
28
  limit?: number;
32
29
  mode?: SearchMode;
33
30
  status?: string;
31
+ path?: string;
32
+ module?: string;
33
+ owner?: string;
34
+ rerank?: boolean;
35
+ includeRankingSignals?: boolean;
34
36
  }
35
37
 
36
38
  export interface ListTrapsArgs {
@@ -39,6 +41,9 @@ export interface ListTrapsArgs {
39
41
  limit?: number;
40
42
  offset?: number;
41
43
  status?: string;
44
+ path?: string;
45
+ module?: string;
46
+ owner?: string;
42
47
  }
43
48
 
44
49
  export class TrapOperations {
@@ -55,6 +60,11 @@ export class TrapOperations {
55
60
  limit: args.limit ?? 20,
56
61
  mode: args.mode,
57
62
  status: parseTrapStatus(args.status),
63
+ path: args.path,
64
+ module: args.module,
65
+ owner: args.owner,
66
+ rerank: args.rerank,
67
+ includeRankingSignals: args.includeRankingSignals,
58
68
  });
59
69
  }
60
70
 
@@ -77,9 +87,16 @@ export class TrapOperations {
77
87
  status: parseTrapStatus(args.status),
78
88
  limit: args.limit ?? 50,
79
89
  offset: args.offset,
90
+ path: args.path,
91
+ module: args.module,
92
+ owner: args.owner,
80
93
  });
81
94
  }
82
95
 
96
+ topTraps(scope: string, limit = 20): TrapListGroup[] {
97
+ return [{ traps: this.store.topTraps(scope, limit), scope }];
98
+ }
99
+
83
100
  updateTrap(
84
101
  id: number,
85
102
  args: Record<string, unknown>,
@@ -117,6 +134,10 @@ export class TrapOperations {
117
134
  return this.store.stats();
118
135
  }
119
136
 
137
+ getEmbeddingStats(): EmbeddingStatsResult {
138
+ return this.store.embeddingStats();
139
+ }
140
+
120
141
  exportTraps(scope?: string): ReturnType<TrapStore["exportAll"]> {
121
142
  return this.store.exportAll({ scope });
122
143
  }