codetrap 0.1.1 → 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.
- package/.agents/plugins/marketplace.json +20 -0
- package/README.md +121 -38
- 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 +466 -0
- package/src/db/embedding-queries.ts +33 -0
- package/src/db/queries.ts +119 -20
- package/src/db/repository.ts +39 -2
- package/src/db/schema.ts +35 -0
- package/src/domain/trap.ts +31 -2
- package/src/index.ts +13 -1
- package/src/lib/config.ts +102 -0
- package/src/lib/constants.ts +1 -1
- package/src/lib/doctor.ts +76 -0
- package/src/lib/embedding-health.ts +49 -0
- package/src/lib/format.ts +5 -1
- package/src/lib/output-json.ts +116 -0
- package/src/lib/scope-context.ts +116 -0
- package/src/lib/scope-migration.ts +360 -0
- package/src/lib/scope.ts +6 -4
- package/src/lib/search-normalizer.ts +6 -0
- package/src/lib/search-policy.ts +276 -0
- package/src/lib/search-result-card.ts +1 -0
- package/src/lib/search-service.ts +36 -98
- package/src/lib/store.ts +96 -107
- 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-mutation-result.ts +36 -0
- package/src/lib/trap-operations.ts +27 -6
- 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 +75 -57
- package/src/mcp/tools.ts +32 -5
package/src/lib/store.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
|
34
|
+
export type { EmbeddingStateSummary };
|
|
35
|
+
export type TrapEmbeddingStats = EmbeddingStatsResult;
|
|
30
36
|
|
|
31
37
|
export class TrapStore {
|
|
32
|
-
private
|
|
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.
|
|
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):
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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):
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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):
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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):
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
):
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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.
|
|
165
|
-
return { project, global: this.
|
|
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.
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
}
|
package/src/lib/trap-archive.ts
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
}
|