codetrap 0.1.3 → 0.1.5

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.
@@ -0,0 +1,99 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import { posix, win32 } from "node:path";
3
+
4
+ export type ScopePathFlavor = "posix" | "win32";
5
+
6
+ export type ScopePathFs = {
7
+ exists(path: string): boolean;
8
+ realpath(path: string): string;
9
+ };
10
+
11
+ export type ScopePathResolverOptions = {
12
+ fs?: ScopePathFs;
13
+ platform?: NodeJS.Platform;
14
+ };
15
+
16
+ const nodeFs: ScopePathFs = {
17
+ exists: existsSync,
18
+ realpath: realpathSync,
19
+ };
20
+
21
+ export class ScopePathResolver {
22
+ private readonly fs: ScopePathFs;
23
+ private readonly platform: NodeJS.Platform;
24
+
25
+ constructor(options: ScopePathResolverOptions = {}) {
26
+ this.fs = options.fs ?? nodeFs;
27
+ this.platform = options.platform ?? process.platform;
28
+ }
29
+
30
+ resolve(path: string, relatedPath = path): string {
31
+ return this.resolveWithFlavor(path, this.flavorFor(path, relatedPath));
32
+ }
33
+
34
+ canonical(path: string, relatedPath = path): string {
35
+ const flavor = this.flavorFor(path, relatedPath);
36
+ const resolved = this.resolveWithFlavor(path, flavor);
37
+ const real = this.fs.exists(resolved) ? this.fs.realpath(resolved) : resolved;
38
+ const normalized = pathApi(flavor).normalize(real);
39
+ return flavor === "win32" ? normalized.toLowerCase() : normalized;
40
+ }
41
+
42
+ same(left: string, right: string): boolean {
43
+ const flavor = this.flavorFor(left, right);
44
+ return this.canonicalWithFlavor(left, flavor) === this.canonicalWithFlavor(right, flavor);
45
+ }
46
+
47
+ join(base: string, ...segments: string[]): string {
48
+ return pathApi(this.flavorFor(base)).join(base, ...segments);
49
+ }
50
+
51
+ exists(path: string): boolean {
52
+ return this.fs.exists(path);
53
+ }
54
+
55
+ dirname(path: string): string {
56
+ return pathApi(this.flavorFor(path)).dirname(path);
57
+ }
58
+
59
+ flavorFor(...paths: string[]): ScopePathFlavor {
60
+ if (this.platform === "win32") return "win32";
61
+ return paths.some((path) => isWindowsAbsolutePath(path) || isMsysAbsolutePath(path)) ? "win32" : "posix";
62
+ }
63
+
64
+ private canonicalWithFlavor(path: string, flavor: ScopePathFlavor): string {
65
+ const resolved = this.resolveWithFlavor(path, flavor);
66
+ const real = this.fs.exists(resolved) ? this.fs.realpath(resolved) : resolved;
67
+ const normalized = pathApi(flavor).normalize(real);
68
+ return flavor === "win32" ? normalized.toLowerCase() : normalized;
69
+ }
70
+
71
+ private resolveWithFlavor(path: string, flavor: ScopePathFlavor): string {
72
+ return pathApi(flavor).resolve(flavor === "win32" ? msysToWindowsPath(path) : path);
73
+ }
74
+ }
75
+
76
+ export const defaultScopePathResolver = new ScopePathResolver();
77
+
78
+ export function resolveScopePath(path: string, relatedPath = path): string {
79
+ return defaultScopePathResolver.resolve(path, relatedPath);
80
+ }
81
+
82
+ function pathApi(flavor: ScopePathFlavor): typeof posix {
83
+ return flavor === "win32" ? win32 : posix;
84
+ }
85
+
86
+ function msysToWindowsPath(path: string): string {
87
+ const match = path.match(/^\/([a-zA-Z])(?:\/(.*))?$/);
88
+ if (!match) return path;
89
+ const [, drive, rest = ""] = match;
90
+ return `${drive.toUpperCase()}:\\${rest.replaceAll("/", "\\")}`;
91
+ }
92
+
93
+ function isWindowsAbsolutePath(path: string): boolean {
94
+ return /^[a-zA-Z]:[\\/]/.test(path) || /^\\\\/.test(path);
95
+ }
96
+
97
+ function isMsysAbsolutePath(path: string): boolean {
98
+ return /^\/[a-zA-Z](?:\/|$)/.test(path);
99
+ }
package/src/lib/scope.ts CHANGED
@@ -1,32 +1,37 @@
1
1
  import { existsSync, mkdirSync } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
3
2
  import { homedir } from "node:os";
4
3
  import { CODETRAP_DIR, TRAPS_DB_FILE } from "./constants";
4
+ import { defaultScopePathResolver, resolveScopePath, ScopePathResolver } from "./scope-path";
5
+
6
+ export { resolveScopePath, ScopePathResolver } from "./scope-path";
5
7
 
6
8
  export function getGlobalDir(): string {
7
- const dir = join(homedir(), CODETRAP_DIR);
9
+ const dir = defaultScopePathResolver.join(homedir(), CODETRAP_DIR);
8
10
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
9
11
  return dir;
10
12
  }
11
13
 
12
14
  export function getGlobalDB(): string {
13
- return join(getGlobalDir(), TRAPS_DB_FILE);
15
+ return defaultScopePathResolver.join(getGlobalDir(), TRAPS_DB_FILE);
14
16
  }
15
17
 
16
- export function findProjectRoot(cwd: string, homeDir = homedir()): string | null {
17
- let dir = resolve(cwd);
18
- const home = resolve(homeDir);
18
+ export function findProjectRoot(
19
+ cwd: string,
20
+ homeDir = homedir(),
21
+ resolver: ScopePathResolver = defaultScopePathResolver
22
+ ): string | null {
23
+ let dir = resolver.resolve(cwd, homeDir);
19
24
  while (true) {
20
- if (dir === home) return null;
21
- if (existsSync(join(dir, CODETRAP_DIR))) return dir;
22
- const parent = dirname(dir);
25
+ if (resolver.same(dir, homeDir)) return null;
26
+ if (resolver.exists(resolver.join(dir, CODETRAP_DIR))) return dir;
27
+ const parent = resolver.dirname(dir);
23
28
  if (parent === dir) return null;
24
29
  dir = parent;
25
30
  }
26
31
  }
27
32
 
28
33
  export function getProjectDB(root: string): string {
29
- const dir = join(root, CODETRAP_DIR);
34
+ const dir = defaultScopePathResolver.join(root, CODETRAP_DIR);
30
35
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
31
- return join(dir, TRAPS_DB_FILE);
36
+ return defaultScopePathResolver.join(dir, TRAPS_DB_FILE);
32
37
  }
@@ -1,4 +1,5 @@
1
1
  import type { RankingSignal, Trap, TrapSearchResult } from "../domain/trap";
2
+ import type { TrapStatus } from "./constants";
2
3
  import { EmbeddingProviderUnavailableError } from "./embedder";
3
4
  import { parseTrapPathGlobs, parseTrapTags } from "./trap-json-fields";
4
5
  import {
@@ -8,11 +9,39 @@ import {
8
9
  } from "./trap-scope-match";
9
10
 
10
11
  export interface SearchPolicyOptions extends ApplicabilityFilter {
12
+ category?: string;
13
+ scope?: string;
14
+ status?: TrapStatus | "all";
11
15
  limit?: number;
12
16
  rerank?: boolean;
13
17
  includeRankingSignals?: boolean;
14
18
  }
15
19
 
20
+ export type SearchStorageFilter = {
21
+ category?: string;
22
+ scope?: string;
23
+ status?: TrapStatus | "all";
24
+ module?: string;
25
+ owner?: string;
26
+ limit?: number;
27
+ };
28
+
29
+ export type SemanticStorageFilter = {
30
+ category?: string;
31
+ scope?: string;
32
+ status?: TrapStatus | "all";
33
+ };
34
+
35
+ export type SearchRetrievalSource = "fts" | "semantic";
36
+
37
+ export interface SearchRetrievalPlan {
38
+ resultLimit: number;
39
+ candidateLimit: number;
40
+ ftsStorageFilter: SearchStorageFilter & { limit: number };
41
+ semanticStorageFilter: SemanticStorageFilter;
42
+ applicabilityFilter: ApplicabilityFilter;
43
+ }
44
+
16
45
  export interface RankingConfig {
17
46
  rrfK: number;
18
47
  semanticMinScore: number;
@@ -48,6 +77,33 @@ export const DEFAULT_RANKING_CONFIG: RankingConfig = {
48
77
  export class TrapSearchPolicy {
49
78
  constructor(private readonly ranking: RankingConfig = DEFAULT_RANKING_CONFIG) {}
50
79
 
80
+ plan(opts: SearchPolicyOptions, defaultLimit: number): SearchRetrievalPlan {
81
+ const resultLimit = opts.limit ?? defaultLimit;
82
+ const candidateLimit = this.candidateLimit(opts, resultLimit);
83
+ return {
84
+ resultLimit,
85
+ candidateLimit,
86
+ ftsStorageFilter: {
87
+ category: opts.category,
88
+ scope: opts.scope,
89
+ status: opts.status,
90
+ module: opts.module,
91
+ owner: opts.owner,
92
+ limit: candidateLimit,
93
+ },
94
+ semanticStorageFilter: {
95
+ category: opts.category,
96
+ scope: opts.scope,
97
+ status: opts.status,
98
+ },
99
+ applicabilityFilter: {
100
+ path: opts.path,
101
+ module: opts.module,
102
+ owner: opts.owner,
103
+ },
104
+ };
105
+ }
106
+
51
107
  candidateLimit(opts: SearchPolicyOptions, resultLimit: number): number {
52
108
  return shouldOverfetch(opts) ? Math.max(resultLimit * 5, 50) : resultLimit;
53
109
  }
@@ -68,6 +124,30 @@ export class TrapSearchPolicy {
68
124
  return traps.filter((trap) => this.matchesTrap(trap, filter));
69
125
  }
70
126
 
127
+ prepareRetrievedResults(
128
+ results: TrapSearchResult[],
129
+ source: SearchRetrievalSource,
130
+ plan: SearchRetrievalPlan
131
+ ): TrapSearchResult[] {
132
+ const applicable = this.filterResults(results, plan.applicabilityFilter);
133
+ if (source === "semantic") {
134
+ return applicable
135
+ .filter((result) => (result.score ?? 0) >= this.semanticMinScore())
136
+ .sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
137
+ .slice(0, plan.candidateLimit);
138
+ }
139
+ return applicable.slice(0, plan.candidateLimit);
140
+ }
141
+
142
+ finalizeResults(
143
+ results: TrapSearchResult[],
144
+ query: string,
145
+ opts: SearchPolicyOptions,
146
+ plan: SearchRetrievalPlan
147
+ ): TrapSearchResult[] {
148
+ return this.rankResults(results, query, opts, plan.resultLimit);
149
+ }
150
+
71
151
  rankResults(
72
152
  results: TrapSearchResult[],
73
153
  query: string,
@@ -95,12 +175,21 @@ export class TrapSearchPolicy {
95
175
  score: applyLengthNormalization(result.score, result.trap, this.ranking),
96
176
  rank: applyLengthNormalization(result.score, result.trap, this.ranking),
97
177
  }))
98
- .sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
99
- .slice(0, limit);
178
+ .sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
100
179
 
101
180
  return this.rankResults(fused, query, opts, limit);
102
181
  }
103
182
 
183
+ fuseAndFinalize(
184
+ ftsResults: TrapSearchResult[],
185
+ semanticResults: TrapSearchResult[],
186
+ query: string,
187
+ opts: SearchPolicyOptions,
188
+ plan: SearchRetrievalPlan
189
+ ): TrapSearchResult[] {
190
+ return this.fuse(ftsResults, semanticResults, query, opts, plan.resultLimit);
191
+ }
192
+
104
193
  withDiagnostics(
105
194
  results: TrapSearchResult[],
106
195
  diagnostic: { code: string; message: string }
@@ -15,14 +15,8 @@ export function toTrapActionCard(result: TrapSearchResult, scope: Scope): TrapAc
15
15
  severity: trap.severity,
16
16
  score: result.score ?? null,
17
17
  sources: result.sources ?? [],
18
+ ...(result.diagnostics ? { diagnostics: result.diagnostics } : {}),
18
19
  ...(result.ranking_signals ? { ranking_signals: result.ranking_signals } : {}),
19
- next_action: {
20
- details_tool: "get_trap",
21
- details_args: {
22
- id: trap.id,
23
- scope,
24
- },
25
- },
26
20
  };
27
21
  }
28
22
 
@@ -1,5 +1,4 @@
1
1
  import type { Database } from "bun:sqlite";
2
- import * as embeddingQueries from "../db/embedding-queries";
3
2
  import * as queries from "../db/queries";
4
3
  import type { TrapSearchResult } from "../domain/trap";
5
4
  import type { SearchMode, TrapStatus } from "./constants";
@@ -13,7 +12,9 @@ import {
13
12
  DEFAULT_RANKING_CONFIG,
14
13
  TrapSearchPolicy,
15
14
  type RankingConfig,
15
+ type SearchRetrievalPlan,
16
16
  } from "./search-policy";
17
+ import { DatabaseEmbeddingIndex } from "./embedding-index";
17
18
 
18
19
  export interface SearchOptions {
19
20
  category?: string;
@@ -32,6 +33,7 @@ const DEFAULT_LIMIT = 20;
32
33
 
33
34
  export class SearchService {
34
35
  private readonly policy: TrapSearchPolicy;
36
+ private readonly embeddingIndex: DatabaseEmbeddingIndex;
35
37
 
36
38
  constructor(
37
39
  private readonly db: Database,
@@ -39,6 +41,7 @@ export class SearchService {
39
41
  ranking: RankingConfig = DEFAULT_RANKING_CONFIG
40
42
  ) {
41
43
  this.policy = new TrapSearchPolicy(ranking);
44
+ this.embeddingIndex = new DatabaseEmbeddingIndex(db);
42
45
  }
43
46
 
44
47
  async search(query: string, opts: SearchOptions = {}): Promise<TrapSearchResult[]> {
@@ -58,20 +61,51 @@ export class SearchService {
58
61
  }
59
62
 
60
63
  ftsSearch(query: string, opts: SearchOptions = {}): TrapSearchResult[] {
61
- const limit = opts.limit ?? DEFAULT_LIMIT;
62
- const searchLimit = this.policy.candidateLimit(opts, limit);
64
+ const plan = this.policy.plan(opts, DEFAULT_LIMIT);
65
+ return this.policy.finalizeResults(this.retrieveFtsCandidates(query, plan), query, opts, plan);
66
+ }
67
+
68
+ async semanticSearch(query: string, opts: SearchOptions = {}): Promise<TrapSearchResult[]> {
69
+ const plan = this.policy.plan(opts, DEFAULT_LIMIT);
70
+ return this.policy.finalizeResults(await this.retrieveSemanticCandidates(query, plan), query, opts, plan);
71
+ }
72
+
73
+ async hybridSearch(query: string, opts: SearchOptions = {}): Promise<TrapSearchResult[]> {
74
+ const plan = this.policy.plan(opts, DEFAULT_LIMIT);
75
+ const ftsCandidates = this.retrieveFtsCandidates(query, plan);
76
+
77
+ try {
78
+ const semanticCandidates = await this.retrieveSemanticCandidates(query, plan);
79
+ if (semanticCandidates.length === 0) {
80
+ return this.policy.withDiagnostics(this.policy.finalizeResults(ftsCandidates, query, opts, plan), {
81
+ code: "semantic_no_candidates",
82
+ message: "Hybrid search used FTS results because no fresh semantic candidates passed the score threshold.",
83
+ });
84
+ }
85
+ return this.policy.fuseAndFinalize(ftsCandidates, semanticCandidates, query, opts, plan);
86
+ } catch (error) {
87
+ return this.policy.withDiagnostics(
88
+ this.policy.finalizeResults(ftsCandidates, query, opts, plan),
89
+ this.policy.semanticDiagnostic(error)
90
+ );
91
+ }
92
+ }
93
+
94
+ private retrieveFtsCandidates(query: string, plan: SearchRetrievalPlan): TrapSearchResult[] {
63
95
  const candidates = queries
64
- .searchTraps(this.db, query, { ...opts, limit: searchLimit })
65
- .filter((result) => this.policy.matchesTrap(result.trap, opts))
96
+ .searchTraps(this.db, query, plan.ftsStorageFilter)
66
97
  .map((result) => ({
67
98
  ...result,
68
99
  sources: ["fts"] as ("fts")[],
69
100
  score: ftsScore(result.rank),
70
101
  }));
71
- return this.policy.rankResults(candidates, query, opts, limit);
102
+ return this.policy.prepareRetrievedResults(candidates, "fts", plan);
72
103
  }
73
104
 
74
- async semanticSearch(query: string, opts: SearchOptions = {}): Promise<TrapSearchResult[]> {
105
+ private async retrieveSemanticCandidates(
106
+ query: string,
107
+ plan: SearchRetrievalPlan
108
+ ): Promise<TrapSearchResult[]> {
75
109
  if (!this.embedder) {
76
110
  throw new EmbeddingProviderUnavailableError();
77
111
  }
@@ -80,11 +114,7 @@ export class SearchService {
80
114
  if (!queryEmbedding) return [];
81
115
 
82
116
  const config = embeddingConfig(this.embedder);
83
- const candidates = embeddingQueries.getAllFreshEmbeddings(this.db, config, {
84
- category: opts.category,
85
- scope: opts.scope,
86
- status: opts.status,
87
- });
117
+ const candidates = this.embeddingIndex.freshEmbeddings(config, plan.semanticStorageFilter);
88
118
 
89
119
  const results = candidates
90
120
  .map(({ trap, embedding }) => {
@@ -96,29 +126,8 @@ export class SearchService {
96
126
  score,
97
127
  };
98
128
  })
99
- .filter((result) => this.policy.matchesTrap(result.trap, opts))
100
- .filter((result) => (result.score ?? 0) >= this.policy.semanticMinScore())
101
- .sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
102
129
  .map((result) => result as TrapSearchResult);
103
- return this.policy.rankResults(results, query, opts, opts.limit ?? DEFAULT_LIMIT);
104
- }
105
-
106
- async hybridSearch(query: string, opts: SearchOptions = {}): Promise<TrapSearchResult[]> {
107
- const limit = opts.limit ?? DEFAULT_LIMIT;
108
- const ftsResults = this.ftsSearch(query, { ...opts, limit });
109
-
110
- try {
111
- const semanticResults = await this.semanticSearch(query, { ...opts, limit });
112
- if (semanticResults.length === 0) {
113
- return this.policy.withDiagnostics(ftsResults, {
114
- code: "semantic_no_candidates",
115
- message: "Hybrid search used FTS results because no fresh semantic candidates passed the score threshold.",
116
- });
117
- }
118
- return this.policy.fuse(ftsResults, semanticResults, query, opts, limit);
119
- } catch (error) {
120
- return this.policy.withDiagnostics(ftsResults, this.policy.semanticDiagnostic(error));
121
- }
130
+ return this.policy.prepareRetrievedResults(results, "semantic", plan);
122
131
  }
123
132
  }
124
133
 
package/src/lib/store.ts CHANGED
@@ -167,19 +167,51 @@ export class TrapStore {
167
167
  return this.scopes.repositoryFor(resolvedScope).top(resolvedScope, limit);
168
168
  }
169
169
 
170
- stats(): { project: TrapStats | null; global: TrapStats } {
171
- const project = this.scopes.repositoryEntry("project")?.repository.stats() ?? null;
172
- return { project, global: this.scopes.repositoryFor("global").stats() };
170
+ stats(opts: { scope?: string } = {}): { project: TrapStats | null; global: TrapStats | null } {
171
+ const scope = opts.scope ? normalizeScope(opts.scope) : null;
172
+ const project = scope === "global"
173
+ ? null
174
+ : this.scopes.repositoryEntry("project")?.repository.stats({ scope: "project" }) ?? null;
175
+ const global = scope === "project"
176
+ ? null
177
+ : this.scopes.repositoryFor("global").stats({ scope: "global" });
178
+ return { project, global };
173
179
  }
174
180
 
175
- embeddingStats(): TrapEmbeddingStats {
181
+ embeddingStats(opts: { scope?: string } = {}): TrapEmbeddingStats {
182
+ const scope = opts.scope ? normalizeScope(opts.scope) : null;
176
183
  const config = this.embeddingConfig();
177
- const project = this.scopes.repositoryEntry("project")
178
- ? summarizeEmbeddingState(this.scopes.repositoryFor("project").embeddingStats(config), config)
184
+ const project = scope === "global"
185
+ ? null
186
+ : this.scopes.repositoryEntry("project")
187
+ ? summarizeEmbeddingState(this.scopes.repositoryFor("project").embeddingStats(config, { scope: "project" }), config)
179
188
  : null;
189
+ const global = scope === "project"
190
+ ? null
191
+ : summarizeEmbeddingState(this.scopes.repositoryFor("global").embeddingStats(config, { scope: "global" }), config);
180
192
  return {
181
193
  project,
182
- global: summarizeEmbeddingState(this.scopes.repositoryFor("global").embeddingStats(config), config),
194
+ global,
195
+ };
196
+ }
197
+
198
+ diagnostics(): {
199
+ mis_scoped_traps: {
200
+ global_db_project_traps: Pick<Trap, "id" | "title" | "scope" | "project_path" | "status">[];
201
+ };
202
+ } {
203
+ return {
204
+ mis_scoped_traps: {
205
+ global_db_project_traps: this.scopes.repositoryFor("global")
206
+ .listMisScoped("global")
207
+ .map((trap) => ({
208
+ id: trap.id,
209
+ title: trap.title,
210
+ scope: trap.scope,
211
+ project_path: trap.project_path,
212
+ status: trap.status,
213
+ })),
214
+ },
183
215
  };
184
216
  }
185
217
 
@@ -0,0 +1,37 @@
1
+ import type { Trap } from "../domain/trap";
2
+
3
+ export type TrapLifecycleAdapter = {
4
+ get(id: number): Trap | null;
5
+ transaction<T>(callback: () => T): T;
6
+ markArchived(id: number): boolean;
7
+ markSuperseded(id: number, stateKey: string): boolean;
8
+ markSuperseding(id: number, supersedesId: number, stateKey: string): boolean;
9
+ };
10
+
11
+ export function archiveTrapLifecycle(adapter: TrapLifecycleAdapter, id: number): boolean {
12
+ return adapter.markArchived(id);
13
+ }
14
+
15
+ export function supersedeTrapLifecycle(
16
+ adapter: TrapLifecycleAdapter,
17
+ id: number,
18
+ supersededById: number,
19
+ stateKey?: string
20
+ ): boolean {
21
+ if (id === supersededById) return false;
22
+
23
+ const oldTrap = adapter.get(id);
24
+ const newTrap = adapter.get(supersededById);
25
+ if (!oldTrap || !newTrap) return false;
26
+
27
+ const key = lifecycleStateKey(oldTrap, newTrap, stateKey);
28
+ return adapter.transaction(() => {
29
+ adapter.markSuperseded(id, key);
30
+ adapter.markSuperseding(supersededById, id, key);
31
+ return true;
32
+ });
33
+ }
34
+
35
+ export function lifecycleStateKey(oldTrap: Trap, newTrap: Trap, requested?: string): string {
36
+ return requested ?? oldTrap.state_key ?? newTrap.state_key ?? `trap:${oldTrap.id}`;
37
+ }
@@ -18,7 +18,7 @@ export type TrapListGroup = { traps: Trap[]; scope: string };
18
18
  export type TrapSearchGroup = { results: TrapSearchResult[]; scope: string };
19
19
  export type { AddTrapEvidenceResult, TrapMutationResult };
20
20
 
21
- export type TrapStatsResult = { project: TrapStats | null; global: TrapStats };
21
+ export type TrapStatsResult = { project: TrapStats | null; global: TrapStats | null };
22
22
  export type EmbeddingStatsResult = ReturnType<TrapStore["embeddingStats"]>;
23
23
 
24
24
  export interface SearchTrapsArgs {
@@ -130,12 +130,12 @@ export class TrapOperations {
130
130
  return this.store.supersede(id, supersededById, scope, stateKey);
131
131
  }
132
132
 
133
- getStats(): TrapStatsResult {
134
- return this.store.stats();
133
+ getStats(scope?: string): TrapStatsResult {
134
+ return this.store.stats({ scope });
135
135
  }
136
136
 
137
- getEmbeddingStats(): EmbeddingStatsResult {
138
- return this.store.embeddingStats();
137
+ getEmbeddingStats(scope?: string): EmbeddingStatsResult {
138
+ return this.store.embeddingStats({ scope });
139
139
  }
140
140
 
141
141
  exportTraps(scope?: string): ReturnType<TrapStore["exportAll"]> {
package/src/mcp/server.ts CHANGED
@@ -21,6 +21,11 @@ import {
21
21
  toTrapDetailsJson,
22
22
  } from "../lib/output-json";
23
23
  import { storeForScopeContext } from "../lib/scope-context";
24
+ import {
25
+ listRequestFromArgs,
26
+ searchRequestFromArgs,
27
+ statsRequestFromArgs,
28
+ } from "../lib/command-requests";
24
29
 
25
30
  type ToolArgs = Record<string, any>;
26
31
 
@@ -30,19 +35,8 @@ export async function handleToolCall(store: TrapStore, name: string, args: ToolA
30
35
  try {
31
36
  switch (name) {
32
37
  case "search_traps": {
33
- const cards = await operations.searchTrapCards({
34
- query: args.query,
35
- scope: args.scope,
36
- category: args.category,
37
- mode: args.mode,
38
- limit: args.limit ?? 20,
39
- status: args.status,
40
- path: args.path,
41
- module: args.module,
42
- owner: args.owner,
43
- rerank: args.rerank,
44
- includeRankingSignals: args.ranking_signals,
45
- });
38
+ const defaults = { mode: "hybrid" as const, limit: args.limit ?? 20, rerank: true };
39
+ const cards = await operations.searchTrapCards(searchRequestFromArgs(args.query, args, defaults));
46
40
  return toMcpTextJson(toMcpSearchJson(cards));
47
41
  }
48
42
 
@@ -60,15 +54,7 @@ export async function handleToolCall(store: TrapStore, name: string, args: ToolA
60
54
  }
61
55
 
62
56
  case "list_traps": {
63
- const groups = operations.listTraps({
64
- scope: args.scope,
65
- category: args.category,
66
- status: args.status,
67
- limit: args.limit ?? 50,
68
- path: args.path,
69
- module: args.module,
70
- owner: args.owner,
71
- });
57
+ const groups = operations.listTraps(listRequestFromArgs(args));
72
58
  return toMcpTextJson(toListJson(groups));
73
59
  }
74
60
 
@@ -98,8 +84,9 @@ export async function handleToolCall(store: TrapStore, name: string, args: ToolA
98
84
  }
99
85
 
100
86
  case "get_stats": {
101
- const stats = operations.getStats();
102
- return toMcpTextJson(toStatsJson(stats, operations.getEmbeddingStats()));
87
+ const request = statsRequestFromArgs(args);
88
+ const stats = operations.getStats(request.scope);
89
+ return toMcpTextJson(toStatsJson(stats, operations.getEmbeddingStats(request.scope)));
103
90
  }
104
91
 
105
92
  default: