@gmickel/gno 1.18.0 → 1.20.0
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/README.md +14 -7
- package/assets/skill/SKILL.md +54 -12
- package/assets/skill/mcp-reference.md +7 -2
- package/assets/skill/recipes/citation-and-provenance.md +32 -9
- package/package.json +2 -1
- package/spec/AGENTS.md +83 -0
- package/spec/CLAUDE.md +83 -0
- package/spec/bench-fixture.schema.json +137 -0
- package/spec/cli.md +2919 -0
- package/spec/db/schema.sql +442 -0
- package/spec/evals-agentic.md +592 -0
- package/spec/evals.md +1106 -0
- package/spec/mcp.md +2279 -0
- package/spec/output-schemas/activation-verification.schema.json +515 -0
- package/spec/output-schemas/ask.schema.json +564 -0
- package/spec/output-schemas/backlinks.schema.json +131 -0
- package/spec/output-schemas/bench-result.schema.json +120 -0
- package/spec/output-schemas/capture-receipt.schema.json +143 -0
- package/spec/output-schemas/claim-verification.schema.json +291 -0
- package/spec/output-schemas/collection-list.schema.json +45 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +726 -0
- package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
- package/spec/output-schemas/context-list.schema.json +21 -0
- package/spec/output-schemas/doctor.schema.json +313 -0
- package/spec/output-schemas/error.schema.json +30 -0
- package/spec/output-schemas/expansion.schema.json +37 -0
- package/spec/output-schemas/get.schema.json +140 -0
- package/spec/output-schemas/graph-query.schema.json +99 -0
- package/spec/output-schemas/graph.schema.json +371 -0
- package/spec/output-schemas/links-list.schema.json +186 -0
- package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
- package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
- package/spec/output-schemas/mcp-http-error.schema.json +30 -0
- package/spec/output-schemas/mcp-job-list.schema.json +58 -0
- package/spec/output-schemas/mcp-job-status.schema.json +224 -0
- package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
- package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
- package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
- package/spec/output-schemas/models-list.schema.json +93 -0
- package/spec/output-schemas/multi-get.schema.json +103 -0
- package/spec/output-schemas/process-status.schema.json +119 -0
- package/spec/output-schemas/query-diagnose.schema.json +123 -0
- package/spec/output-schemas/resident-status.schema.json +154 -0
- package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
- package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
- package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
- package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
- package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
- package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
- package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
- package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
- package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
- package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
- package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
- package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
- package/spec/output-schemas/search-result.schema.json +154 -0
- package/spec/output-schemas/search-results.schema.json +338 -0
- package/spec/output-schemas/similar.schema.json +84 -0
- package/spec/output-schemas/status.schema.json +676 -0
- package/spec/output-schemas/tags-list.schema.json +48 -0
- package/src/app/context-runtime-contract.ts +10 -5
- package/src/app/context-runtime-input.ts +29 -1
- package/src/app/context-runtime-types.ts +7 -0
- package/src/app/context-runtime.ts +20 -2
- package/src/app/context-surface.ts +4 -0
- package/src/app/verified-ask.ts +291 -0
- package/src/cli/commands/ask-format.ts +255 -0
- package/src/cli/commands/ask.ts +144 -183
- package/src/cli/commands/context-build.ts +56 -9
- package/src/cli/commands/get.ts +64 -3
- package/src/cli/commands/query.ts +62 -23
- package/src/cli/commands/replay.ts +140 -0
- package/src/cli/commands/search.ts +48 -3
- package/src/cli/commands/shared.ts +3 -1
- package/src/cli/commands/trace.ts +200 -0
- package/src/cli/commands/vsearch.ts +75 -53
- package/src/cli/program.ts +287 -1
- package/src/config/index.ts +9 -0
- package/src/config/retrieval-traces.ts +56 -0
- package/src/config/types.ts +4 -0
- package/src/core/context-budget.ts +6 -0
- package/src/core/context-capsule-retrieval-schema.ts +4 -0
- package/src/core/context-capsule-schema.ts +17 -0
- package/src/core/context-capsule-validation.ts +3 -2
- package/src/core/context-capsule.ts +18 -0
- package/src/core/context-compiler.ts +44 -25
- package/src/core/context-evidence.ts +6 -0
- package/src/core/retrieval-qrels.ts +405 -0
- package/src/core/retrieval-replay-candidate.ts +368 -0
- package/src/core/retrieval-replay-types.ts +109 -0
- package/src/core/retrieval-replay-validation.ts +89 -0
- package/src/core/retrieval-replay.ts +441 -0
- package/src/core/retrieval-trace-evidence-origin.ts +178 -0
- package/src/core/retrieval-trace-export.ts +113 -0
- package/src/core/retrieval-trace-filter-normalization.ts +27 -0
- package/src/core/retrieval-trace-filters.ts +19 -0
- package/src/core/retrieval-trace-management-helpers.ts +247 -0
- package/src/core/retrieval-trace-management-types.ts +132 -0
- package/src/core/retrieval-trace-management.ts +422 -0
- package/src/core/retrieval-trace-request.ts +141 -0
- package/src/core/retrieval-trace-session.ts +507 -0
- package/src/core/retrieval-trace.ts +472 -0
- package/src/llm/errors.ts +10 -1
- package/src/llm/httpGeneration.ts +11 -1
- package/src/llm/nodeLlamaCpp/generation.ts +54 -10
- package/src/llm/types.ts +6 -0
- package/src/mcp/tools/ask.ts +228 -0
- package/src/mcp/tools/context.ts +87 -15
- package/src/mcp/tools/get.ts +35 -1
- package/src/mcp/tools/index.ts +83 -0
- package/src/mcp/tools/query.ts +95 -64
- package/src/mcp/tools/search.ts +36 -13
- package/src/mcp/tools/trace.ts +143 -0
- package/src/mcp/tools/vsearch.ts +71 -38
- package/src/pipeline/answer.ts +167 -26
- package/src/pipeline/claim-verification-schema.ts +235 -0
- package/src/pipeline/claim-verification.ts +487 -0
- package/src/pipeline/claim-verifier.ts +474 -0
- package/src/pipeline/graph-retrieval.ts +15 -1
- package/src/pipeline/hybrid.ts +151 -43
- package/src/pipeline/search.ts +36 -3
- package/src/pipeline/trace-metadata.ts +47 -0
- package/src/pipeline/types.ts +68 -0
- package/src/pipeline/vsearch.ts +101 -38
- package/src/sdk/client.ts +415 -73
- package/src/sdk/documents.ts +48 -1
- package/src/sdk/index.ts +17 -0
- package/src/sdk/types.ts +28 -0
- package/src/serve/context-capsule.ts +67 -8
- package/src/serve/public/app.tsx +12 -1
- package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/workspace-tabs.ts +2 -0
- package/src/serve/public/pages/Ask.tsx +42 -4
- package/src/serve/public/pages/Dashboard.tsx +10 -0
- package/src/serve/public/pages/TraceHistory.tsx +478 -0
- package/src/serve/public/pages/trace-history-detail.tsx +224 -0
- package/src/serve/retrieval-trace.ts +28 -0
- package/src/serve/routes/api.ts +508 -68
- package/src/serve/routes/traces.ts +156 -0
- package/src/serve/server.ts +87 -2
- package/src/store/index.ts +31 -0
- package/src/store/migrations/014-retrieval-traces.ts +303 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/retrieval-trace-codec.ts +384 -0
- package/src/store/sqlite/adapter.ts +153 -1
- package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
- package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
- package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
- package/src/store/sqlite/retrieval-trace-store.ts +515 -0
- package/src/store/types.ts +297 -0
- package/src/store/vector/sqlite-vec.ts +76 -1
- package/src/store/vector/types.ts +1 -1
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/** SQLite primitives used by local retrieval-trace management surfaces. */
|
|
2
|
+
|
|
3
|
+
import type { Database } from "bun:sqlite";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
RetrievalTraceAppendResult,
|
|
7
|
+
RetrievalTraceBoundedBundle,
|
|
8
|
+
RetrievalTraceExportBundle,
|
|
9
|
+
RetrievalTraceExportManifestInput,
|
|
10
|
+
RetrievalTraceExportManifestRow,
|
|
11
|
+
StoreResult,
|
|
12
|
+
} from "../types";
|
|
13
|
+
|
|
14
|
+
import { ok } from "../types";
|
|
15
|
+
import {
|
|
16
|
+
type DbRetrievalTraceEventRow,
|
|
17
|
+
type DbRetrievalTraceExportRow,
|
|
18
|
+
type DbRetrievalTraceJudgmentRow,
|
|
19
|
+
type DbRetrievalTraceRow,
|
|
20
|
+
type DbRetrievalTraceRunRow,
|
|
21
|
+
mapEventRow,
|
|
22
|
+
mapExportRow,
|
|
23
|
+
mapJudgmentRow,
|
|
24
|
+
mapRunRow,
|
|
25
|
+
mapTraceRow,
|
|
26
|
+
RetrievalTraceConflictError,
|
|
27
|
+
traceReadError,
|
|
28
|
+
traceWriteError,
|
|
29
|
+
validateTraceId,
|
|
30
|
+
} from "./retrieval-trace-rows";
|
|
31
|
+
import { getTrace } from "./retrieval-trace-store";
|
|
32
|
+
|
|
33
|
+
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
34
|
+
const REDACTION_SECRET_KEY = "retrieval_trace_redaction_secret_v1";
|
|
35
|
+
|
|
36
|
+
const randomSecret = (): string => {
|
|
37
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
38
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(
|
|
39
|
+
""
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const countForTrace = (db: Database, table: string, traceId: string): number =>
|
|
44
|
+
db
|
|
45
|
+
.query<{ count: number }, [string]>(
|
|
46
|
+
`SELECT COUNT(*) AS count FROM ${table} WHERE trace_id = ?`
|
|
47
|
+
)
|
|
48
|
+
.get(traceId)?.count ?? 0;
|
|
49
|
+
|
|
50
|
+
export const getBoundedTrace = (
|
|
51
|
+
db: Database,
|
|
52
|
+
traceId: string,
|
|
53
|
+
detailLimit: number
|
|
54
|
+
): StoreResult<RetrievalTraceBoundedBundle | null> => {
|
|
55
|
+
try {
|
|
56
|
+
validateTraceId(traceId, "traceId");
|
|
57
|
+
if (
|
|
58
|
+
!Number.isSafeInteger(detailLimit) ||
|
|
59
|
+
detailLimit < 1 ||
|
|
60
|
+
detailLimit > 10_000
|
|
61
|
+
) {
|
|
62
|
+
throw new RangeError("Trace detail limit must be from 1 to 10000");
|
|
63
|
+
}
|
|
64
|
+
const transaction = db.transaction(
|
|
65
|
+
(): RetrievalTraceBoundedBundle | null => {
|
|
66
|
+
const trace = db
|
|
67
|
+
.query<DbRetrievalTraceRow, [string]>(
|
|
68
|
+
"SELECT * FROM retrieval_traces WHERE trace_id = ?"
|
|
69
|
+
)
|
|
70
|
+
.get(traceId);
|
|
71
|
+
if (!trace) return null;
|
|
72
|
+
const runs = db
|
|
73
|
+
.query<DbRetrievalTraceRunRow, [string, number]>(
|
|
74
|
+
`SELECT * FROM retrieval_trace_runs WHERE trace_id = ?
|
|
75
|
+
ORDER BY created_at_ms, run_id LIMIT ?`
|
|
76
|
+
)
|
|
77
|
+
.all(traceId, detailLimit);
|
|
78
|
+
const events = db
|
|
79
|
+
.query<DbRetrievalTraceEventRow, [string, number]>(
|
|
80
|
+
`SELECT * FROM retrieval_trace_events WHERE trace_id = ?
|
|
81
|
+
ORDER BY created_at_ms, event_id LIMIT ?`
|
|
82
|
+
)
|
|
83
|
+
.all(traceId, detailLimit);
|
|
84
|
+
const judgments = db
|
|
85
|
+
.query<DbRetrievalTraceJudgmentRow, [string, number]>(
|
|
86
|
+
`SELECT * FROM retrieval_trace_judgments WHERE trace_id = ?
|
|
87
|
+
ORDER BY created_at_ms, judgment_id LIMIT ?`
|
|
88
|
+
)
|
|
89
|
+
.all(traceId, detailLimit);
|
|
90
|
+
const exports = db
|
|
91
|
+
.query<DbRetrievalTraceExportRow, [string, number]>(
|
|
92
|
+
`SELECT x.*, et.trace_id FROM retrieval_trace_exports x
|
|
93
|
+
JOIN retrieval_trace_export_traces et USING (export_id)
|
|
94
|
+
WHERE et.trace_id = ?
|
|
95
|
+
ORDER BY x.created_at_ms, x.export_id LIMIT ?`
|
|
96
|
+
)
|
|
97
|
+
.all(traceId, detailLimit);
|
|
98
|
+
return {
|
|
99
|
+
bundle: {
|
|
100
|
+
trace: mapTraceRow(trace),
|
|
101
|
+
runs: runs.map(mapRunRow),
|
|
102
|
+
events: events.map(mapEventRow),
|
|
103
|
+
judgments: judgments.map(mapJudgmentRow),
|
|
104
|
+
exports: exports.map(mapExportRow),
|
|
105
|
+
},
|
|
106
|
+
totals: {
|
|
107
|
+
runs: countForTrace(db, "retrieval_trace_runs", traceId),
|
|
108
|
+
events: countForTrace(db, "retrieval_trace_events", traceId),
|
|
109
|
+
judgments: countForTrace(db, "retrieval_trace_judgments", traceId),
|
|
110
|
+
exports: countForTrace(
|
|
111
|
+
db,
|
|
112
|
+
"retrieval_trace_export_traces",
|
|
113
|
+
traceId
|
|
114
|
+
),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
return ok(transaction());
|
|
120
|
+
} catch (cause) {
|
|
121
|
+
return traceReadError(cause, "Failed to get bounded retrieval trace");
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const normalizeManifest = (
|
|
126
|
+
input: RetrievalTraceExportManifestInput
|
|
127
|
+
): RetrievalTraceExportManifestInput => {
|
|
128
|
+
validateTraceId(input.exportId, "exportId");
|
|
129
|
+
if (!["agentic-receipt", "qrels"].includes(input.format)) {
|
|
130
|
+
throw new RangeError("Invalid retrieval trace export format");
|
|
131
|
+
}
|
|
132
|
+
if (!SHA256_PATTERN.test(input.artifactHash)) {
|
|
133
|
+
throw new RangeError("artifactHash must be a lowercase SHA-256 digest");
|
|
134
|
+
}
|
|
135
|
+
if (!Number.isSafeInteger(input.createdAtMs) || input.createdAtMs < 0) {
|
|
136
|
+
throw new RangeError("createdAtMs must be epoch milliseconds");
|
|
137
|
+
}
|
|
138
|
+
if (input.traceIds.length < 1 || input.traceIds.length > 10_000) {
|
|
139
|
+
throw new RangeError("Export manifests require from 1 to 10000 traces");
|
|
140
|
+
}
|
|
141
|
+
const traceIds = [...new Set(input.traceIds)].sort();
|
|
142
|
+
for (const traceId of traceIds) validateTraceId(traceId, "traceId");
|
|
143
|
+
return { ...input, traceIds };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const readManifest = (
|
|
147
|
+
db: Database,
|
|
148
|
+
exportId: string
|
|
149
|
+
): RetrievalTraceExportManifestRow | null => {
|
|
150
|
+
const row = db
|
|
151
|
+
.query<
|
|
152
|
+
{
|
|
153
|
+
export_id: string;
|
|
154
|
+
format: RetrievalTraceExportManifestRow["format"];
|
|
155
|
+
artifact_hash: string;
|
|
156
|
+
created_at_ms: number;
|
|
157
|
+
},
|
|
158
|
+
[string]
|
|
159
|
+
>(
|
|
160
|
+
`SELECT export_id, format, artifact_hash, created_at_ms
|
|
161
|
+
FROM retrieval_trace_exports WHERE export_id = ?`
|
|
162
|
+
)
|
|
163
|
+
.get(exportId);
|
|
164
|
+
if (!row) return null;
|
|
165
|
+
const traceIds = db
|
|
166
|
+
.query<{ trace_id: string }, [string]>(
|
|
167
|
+
`SELECT trace_id FROM retrieval_trace_export_traces
|
|
168
|
+
WHERE export_id = ? ORDER BY trace_id`
|
|
169
|
+
)
|
|
170
|
+
.all(exportId)
|
|
171
|
+
.map(({ trace_id }) => trace_id);
|
|
172
|
+
return {
|
|
173
|
+
exportId: row.export_id,
|
|
174
|
+
traceIds,
|
|
175
|
+
format: row.format,
|
|
176
|
+
artifactHash: row.artifact_hash,
|
|
177
|
+
createdAtMs: row.created_at_ms,
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const getOrCreateRedactionSecret = (
|
|
182
|
+
db: Database
|
|
183
|
+
): StoreResult<string> => {
|
|
184
|
+
try {
|
|
185
|
+
const candidate = randomSecret();
|
|
186
|
+
const transaction = db.transaction((): string => {
|
|
187
|
+
db.run(
|
|
188
|
+
`INSERT OR IGNORE INTO schema_meta (key, value, updated_at)
|
|
189
|
+
VALUES (?, ?, datetime('now'))`,
|
|
190
|
+
[REDACTION_SECRET_KEY, candidate]
|
|
191
|
+
);
|
|
192
|
+
const stored = db
|
|
193
|
+
.query<{ value: string }, [string]>(
|
|
194
|
+
"SELECT value FROM schema_meta WHERE key = ?"
|
|
195
|
+
)
|
|
196
|
+
.get(REDACTION_SECRET_KEY)?.value;
|
|
197
|
+
if (!stored || !SHA256_PATTERN.test(stored)) {
|
|
198
|
+
throw new RetrievalTraceConflictError(
|
|
199
|
+
"Invalid retrieval trace redaction secret"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return stored;
|
|
203
|
+
});
|
|
204
|
+
return ok(transaction());
|
|
205
|
+
} catch (cause) {
|
|
206
|
+
return traceWriteError(
|
|
207
|
+
cause,
|
|
208
|
+
"Failed to load retrieval trace redaction secret"
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const appendExportManifest = (
|
|
214
|
+
db: Database,
|
|
215
|
+
input: RetrievalTraceExportManifestInput
|
|
216
|
+
): StoreResult<RetrievalTraceAppendResult> => {
|
|
217
|
+
try {
|
|
218
|
+
const manifest = normalizeManifest(input);
|
|
219
|
+
const transaction = db.transaction((): RetrievalTraceAppendResult => {
|
|
220
|
+
const hashOwner = db
|
|
221
|
+
.query<{ export_id: string }, [string, string]>(
|
|
222
|
+
`SELECT export_id FROM retrieval_trace_exports
|
|
223
|
+
WHERE format = ? AND artifact_hash = ?`
|
|
224
|
+
)
|
|
225
|
+
.get(manifest.format, manifest.artifactHash)?.export_id;
|
|
226
|
+
if (hashOwner && hashOwner !== manifest.exportId) {
|
|
227
|
+
throw new RetrievalTraceConflictError(
|
|
228
|
+
"Export artifact already belongs to a different manifest"
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
for (const traceId of manifest.traceIds) {
|
|
232
|
+
const trace = db
|
|
233
|
+
.query<{ status: string }, [string]>(
|
|
234
|
+
"SELECT status FROM retrieval_traces WHERE trace_id = ?"
|
|
235
|
+
)
|
|
236
|
+
.get(traceId);
|
|
237
|
+
if (!trace) {
|
|
238
|
+
throw new RetrievalTraceConflictError(
|
|
239
|
+
"Export manifest references a missing retrieval trace"
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (trace.status === "open") {
|
|
243
|
+
throw new RetrievalTraceConflictError(
|
|
244
|
+
"Open retrieval traces cannot be exported"
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const inserted = db.run(
|
|
249
|
+
`INSERT OR IGNORE INTO retrieval_trace_exports
|
|
250
|
+
(export_id, format, artifact_hash, created_at_ms) VALUES (?, ?, ?, ?)`,
|
|
251
|
+
[
|
|
252
|
+
manifest.exportId,
|
|
253
|
+
manifest.format,
|
|
254
|
+
manifest.artifactHash,
|
|
255
|
+
manifest.createdAtMs,
|
|
256
|
+
]
|
|
257
|
+
);
|
|
258
|
+
const existing = readManifest(db, manifest.exportId);
|
|
259
|
+
if (
|
|
260
|
+
!existing ||
|
|
261
|
+
existing.format !== manifest.format ||
|
|
262
|
+
existing.artifactHash !== manifest.artifactHash
|
|
263
|
+
) {
|
|
264
|
+
throw new RetrievalTraceConflictError(
|
|
265
|
+
"Export ID already exists with different content"
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
for (const traceId of manifest.traceIds) {
|
|
269
|
+
db.run(
|
|
270
|
+
`INSERT OR IGNORE INTO retrieval_trace_export_traces
|
|
271
|
+
(export_id, trace_id) VALUES (?, ?)`,
|
|
272
|
+
[manifest.exportId, traceId]
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const stored = readManifest(db, manifest.exportId);
|
|
276
|
+
if (
|
|
277
|
+
!stored ||
|
|
278
|
+
stored.traceIds.length !== manifest.traceIds.length ||
|
|
279
|
+
stored.traceIds.some(
|
|
280
|
+
(traceId, index) => traceId !== manifest.traceIds[index]
|
|
281
|
+
)
|
|
282
|
+
) {
|
|
283
|
+
throw new RetrievalTraceConflictError(
|
|
284
|
+
"Export manifest membership conflicts with stored content"
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return inserted.changes > 0 ? "inserted" : "duplicate";
|
|
288
|
+
});
|
|
289
|
+
return ok(transaction());
|
|
290
|
+
} catch (cause) {
|
|
291
|
+
return traceWriteError(
|
|
292
|
+
cause,
|
|
293
|
+
"Failed to append retrieval trace export manifest"
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export const getExportManifest = (
|
|
299
|
+
db: Database,
|
|
300
|
+
exportId: string
|
|
301
|
+
): StoreResult<RetrievalTraceExportManifestRow | null> => {
|
|
302
|
+
try {
|
|
303
|
+
validateTraceId(exportId, "exportId");
|
|
304
|
+
return ok(readManifest(db, exportId));
|
|
305
|
+
} catch (cause) {
|
|
306
|
+
return traceReadError(cause, "Failed to get retrieval trace export");
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export const getExportBundle = (
|
|
311
|
+
db: Database,
|
|
312
|
+
exportId: string
|
|
313
|
+
): StoreResult<RetrievalTraceExportBundle | null> => {
|
|
314
|
+
try {
|
|
315
|
+
validateTraceId(exportId, "exportId");
|
|
316
|
+
const transaction = db.transaction(
|
|
317
|
+
(): StoreResult<RetrievalTraceExportBundle | null> => {
|
|
318
|
+
const manifest = readManifest(db, exportId);
|
|
319
|
+
if (!manifest) return ok(null);
|
|
320
|
+
const traces = [];
|
|
321
|
+
for (const traceId of manifest.traceIds) {
|
|
322
|
+
const trace = getTrace(db, traceId);
|
|
323
|
+
if (!trace.ok) return trace;
|
|
324
|
+
if (!trace.value) {
|
|
325
|
+
throw new RetrievalTraceConflictError(
|
|
326
|
+
"Export manifest references a missing retrieval trace"
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
traces.push(trace.value);
|
|
330
|
+
}
|
|
331
|
+
return ok({ manifest, traces });
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
return transaction();
|
|
335
|
+
} catch (cause) {
|
|
336
|
+
return traceReadError(
|
|
337
|
+
cause,
|
|
338
|
+
"Failed to get complete retrieval trace export"
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/** Secure deletion, full purge, and deterministic trace retention for SQLite. */
|
|
2
|
+
|
|
3
|
+
import type { Database } from "bun:sqlite";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
RetrievalTraceDeleteCounts,
|
|
7
|
+
RetrievalTracePurgeResult,
|
|
8
|
+
RetrievalTraceRetentionPolicy,
|
|
9
|
+
RetrievalTraceRetentionResult,
|
|
10
|
+
StoreResult,
|
|
11
|
+
} from "../types";
|
|
12
|
+
|
|
13
|
+
import { ok } from "../types";
|
|
14
|
+
import {
|
|
15
|
+
countTraceContent,
|
|
16
|
+
traceWriteError,
|
|
17
|
+
validateTraceId,
|
|
18
|
+
} from "./retrieval-trace-rows";
|
|
19
|
+
|
|
20
|
+
const DAY_MS = 86_400_000;
|
|
21
|
+
const compareCodeUnits = (left: string, right: string): number =>
|
|
22
|
+
left < right ? -1 : left > right ? 1 : 0;
|
|
23
|
+
|
|
24
|
+
interface StorageRow {
|
|
25
|
+
trace_id: string;
|
|
26
|
+
created_at_ms: number;
|
|
27
|
+
expires_at_ms: number;
|
|
28
|
+
total_bytes: number;
|
|
29
|
+
record_count: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const storageRows = (db: Database): StorageRow[] =>
|
|
33
|
+
db
|
|
34
|
+
.query<StorageRow, []>(
|
|
35
|
+
`SELECT
|
|
36
|
+
t.trace_id,
|
|
37
|
+
t.created_at_ms,
|
|
38
|
+
t.expires_at_ms,
|
|
39
|
+
(
|
|
40
|
+
length(CAST(t.trace_id AS BLOB))
|
|
41
|
+
+ length(CAST(t.schema_version AS BLOB))
|
|
42
|
+
+ length(CAST(t.redaction_mode AS BLOB))
|
|
43
|
+
+ length(CAST(COALESCE(t.query_text, '') AS BLOB))
|
|
44
|
+
+ length(CAST(COALESCE(t.query_digest, '') AS BLOB))
|
|
45
|
+
+ length(CAST(t.query_shape_json AS BLOB))
|
|
46
|
+
+ length(CAST(COALESCE(t.goal_text, '') AS BLOB))
|
|
47
|
+
+ length(CAST(COALESCE(t.goal_digest, '') AS BLOB))
|
|
48
|
+
+ length(CAST(t.goal_shape_json AS BLOB))
|
|
49
|
+
+ length(CAST(t.filters_json AS BLOB))
|
|
50
|
+
+ length(CAST(t.pipeline_fingerprint AS BLOB))
|
|
51
|
+
+ length(CAST(t.model_fingerprint AS BLOB))
|
|
52
|
+
+ length(CAST(t.config_fingerprint AS BLOB))
|
|
53
|
+
+ length(CAST(t.index_fingerprint AS BLOB))
|
|
54
|
+
+ length(CAST(t.status AS BLOB))
|
|
55
|
+
+ length(CAST(t.creation_digest AS BLOB))
|
|
56
|
+
+ 40
|
|
57
|
+
)
|
|
58
|
+
+ COALESCE((
|
|
59
|
+
SELECT SUM(
|
|
60
|
+
length(CAST(r.run_id AS BLOB))
|
|
61
|
+
+ length(CAST(r.trace_id AS BLOB))
|
|
62
|
+
+ length(CAST(r.idempotency_key AS BLOB))
|
|
63
|
+
+ length(CAST(r.kind AS BLOB))
|
|
64
|
+
+ r.payload_bytes
|
|
65
|
+
+ length(CAST(r.canonical_digest AS BLOB))
|
|
66
|
+
+ 8
|
|
67
|
+
) FROM retrieval_trace_runs r
|
|
68
|
+
WHERE r.trace_id = t.trace_id
|
|
69
|
+
), 0)
|
|
70
|
+
+ COALESCE((
|
|
71
|
+
SELECT SUM(
|
|
72
|
+
length(CAST(e.event_id AS BLOB))
|
|
73
|
+
+ length(CAST(e.trace_id AS BLOB))
|
|
74
|
+
+ length(CAST(COALESCE(e.run_id, '') AS BLOB))
|
|
75
|
+
+ length(CAST(e.idempotency_key AS BLOB))
|
|
76
|
+
+ length(CAST(e.kind AS BLOB))
|
|
77
|
+
+ e.payload_bytes
|
|
78
|
+
+ length(CAST(e.canonical_digest AS BLOB))
|
|
79
|
+
+ 8
|
|
80
|
+
) FROM retrieval_trace_events e
|
|
81
|
+
WHERE e.trace_id = t.trace_id
|
|
82
|
+
), 0)
|
|
83
|
+
+ COALESCE((
|
|
84
|
+
SELECT SUM(
|
|
85
|
+
length(CAST(j.judgment_id AS BLOB))
|
|
86
|
+
+ length(CAST(j.trace_id AS BLOB))
|
|
87
|
+
+ length(CAST(COALESCE(j.run_id, '') AS BLOB))
|
|
88
|
+
+ length(CAST(j.idempotency_key AS BLOB))
|
|
89
|
+
+ length(CAST(j.label AS BLOB))
|
|
90
|
+
+ length(CAST(j.target_kind AS BLOB))
|
|
91
|
+
+ length(CAST(j.target_ref AS BLOB))
|
|
92
|
+
+ j.target_bytes
|
|
93
|
+
+ length(CAST(j.canonical_digest AS BLOB))
|
|
94
|
+
+ 8
|
|
95
|
+
) FROM retrieval_trace_judgments j
|
|
96
|
+
WHERE j.trace_id = t.trace_id
|
|
97
|
+
), 0)
|
|
98
|
+
+ COALESCE((
|
|
99
|
+
SELECT SUM(
|
|
100
|
+
length(CAST(x.export_id AS BLOB))
|
|
101
|
+
+ length(CAST(et.trace_id AS BLOB))
|
|
102
|
+
+ length(CAST(x.format AS BLOB))
|
|
103
|
+
+ length(CAST(x.artifact_hash AS BLOB))
|
|
104
|
+
+ 8
|
|
105
|
+
)
|
|
106
|
+
FROM retrieval_trace_export_traces et
|
|
107
|
+
JOIN retrieval_trace_exports x USING (export_id)
|
|
108
|
+
WHERE et.trace_id = t.trace_id
|
|
109
|
+
), 0) AS total_bytes,
|
|
110
|
+
(
|
|
111
|
+
SELECT COUNT(*) FROM retrieval_trace_runs r
|
|
112
|
+
WHERE r.trace_id = t.trace_id
|
|
113
|
+
) + (
|
|
114
|
+
SELECT COUNT(*) FROM retrieval_trace_events e
|
|
115
|
+
WHERE e.trace_id = t.trace_id
|
|
116
|
+
) + (
|
|
117
|
+
SELECT COUNT(*) FROM retrieval_trace_judgments j
|
|
118
|
+
WHERE j.trace_id = t.trace_id
|
|
119
|
+
) + (
|
|
120
|
+
SELECT COUNT(*) FROM retrieval_trace_export_traces et
|
|
121
|
+
WHERE et.trace_id = t.trace_id
|
|
122
|
+
) AS record_count
|
|
123
|
+
FROM retrieval_traces t
|
|
124
|
+
ORDER BY t.created_at_ms ASC, t.trace_id ASC`
|
|
125
|
+
)
|
|
126
|
+
.all();
|
|
127
|
+
|
|
128
|
+
const validatePolicy = (
|
|
129
|
+
policy: RetrievalTraceRetentionPolicy,
|
|
130
|
+
nowMs: number
|
|
131
|
+
): void => {
|
|
132
|
+
if (
|
|
133
|
+
!Number.isSafeInteger(nowMs) ||
|
|
134
|
+
nowMs < 0 ||
|
|
135
|
+
!Number.isSafeInteger(policy.maxAgeDays) ||
|
|
136
|
+
policy.maxAgeDays < 1 ||
|
|
137
|
+
policy.maxAgeDays > 3650 ||
|
|
138
|
+
!Number.isSafeInteger(policy.maxTraces) ||
|
|
139
|
+
policy.maxTraces < 1 ||
|
|
140
|
+
policy.maxTraces > 1_000_000 ||
|
|
141
|
+
!Number.isSafeInteger(policy.maxRecordsPerTrace) ||
|
|
142
|
+
policy.maxRecordsPerTrace < 1 ||
|
|
143
|
+
policy.maxRecordsPerTrace > 100_000 ||
|
|
144
|
+
!Number.isSafeInteger(policy.maxBytes) ||
|
|
145
|
+
policy.maxBytes < 65_536 ||
|
|
146
|
+
policy.maxBytes > 1024 * 1024 * 1024
|
|
147
|
+
) {
|
|
148
|
+
throw new RangeError("Invalid retrieval trace retention policy");
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const subtractCounts = (
|
|
153
|
+
before: RetrievalTraceDeleteCounts,
|
|
154
|
+
after: RetrievalTraceDeleteCounts
|
|
155
|
+
): RetrievalTraceDeleteCounts => ({
|
|
156
|
+
traces: before.traces - after.traces,
|
|
157
|
+
runs: before.runs - after.runs,
|
|
158
|
+
events: before.events - after.events,
|
|
159
|
+
judgments: before.judgments - after.judgments,
|
|
160
|
+
exports: before.exports - after.exports,
|
|
161
|
+
exportLinks: before.exportLinks - after.exportLinks,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const readSecureDelete = (db: Database): number =>
|
|
165
|
+
db.query<{ secure_delete: number }, []>("PRAGMA secure_delete").get()
|
|
166
|
+
?.secure_delete ?? 0;
|
|
167
|
+
|
|
168
|
+
const restoreSecureDelete = (db: Database, prior: number): void => {
|
|
169
|
+
const mode = prior === 0 ? "OFF" : prior === 2 ? "FAST" : "ON";
|
|
170
|
+
db.exec(`PRAGMA secure_delete = ${mode}`);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const deleteTrace = (
|
|
174
|
+
db: Database,
|
|
175
|
+
traceId: string
|
|
176
|
+
): StoreResult<RetrievalTraceDeleteCounts> => {
|
|
177
|
+
const prior = readSecureDelete(db);
|
|
178
|
+
try {
|
|
179
|
+
validateTraceId(traceId, "traceId");
|
|
180
|
+
db.exec("PRAGMA secure_delete = ON");
|
|
181
|
+
const transaction = db.transaction(() => {
|
|
182
|
+
const counts = countTraceContent(db, traceId);
|
|
183
|
+
db.run("DELETE FROM retrieval_traces WHERE trace_id = ?", [traceId]);
|
|
184
|
+
return counts;
|
|
185
|
+
});
|
|
186
|
+
return ok(transaction());
|
|
187
|
+
} catch (cause) {
|
|
188
|
+
return traceWriteError(cause, "Failed to delete retrieval trace");
|
|
189
|
+
} finally {
|
|
190
|
+
restoreSecureDelete(db, prior);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const purgeTraces = (
|
|
195
|
+
db: Database
|
|
196
|
+
): StoreResult<RetrievalTracePurgeResult> => {
|
|
197
|
+
const prior = readSecureDelete(db);
|
|
198
|
+
try {
|
|
199
|
+
db.exec("PRAGMA secure_delete = ON");
|
|
200
|
+
const transaction = db.transaction(() => {
|
|
201
|
+
const counts = countTraceContent(db);
|
|
202
|
+
db.run("DELETE FROM retrieval_traces");
|
|
203
|
+
db.run(
|
|
204
|
+
`DELETE FROM retrieval_trace_exports WHERE NOT EXISTS (
|
|
205
|
+
SELECT 1 FROM retrieval_trace_export_traces et
|
|
206
|
+
WHERE et.export_id = retrieval_trace_exports.export_id
|
|
207
|
+
)`
|
|
208
|
+
);
|
|
209
|
+
return counts;
|
|
210
|
+
});
|
|
211
|
+
const counts = transaction();
|
|
212
|
+
let checkpoint:
|
|
213
|
+
| { busy: number; log: number; checkpointed: number }
|
|
214
|
+
| undefined;
|
|
215
|
+
try {
|
|
216
|
+
checkpoint =
|
|
217
|
+
db
|
|
218
|
+
.query<{ busy: number; log: number; checkpointed: number }, []>(
|
|
219
|
+
"PRAGMA wal_checkpoint(TRUNCATE)"
|
|
220
|
+
)
|
|
221
|
+
.get() ?? undefined;
|
|
222
|
+
} catch {
|
|
223
|
+
return ok({
|
|
224
|
+
...counts,
|
|
225
|
+
physicalCleanup: "failed",
|
|
226
|
+
checkpointedFrames: 0,
|
|
227
|
+
remainingWalFrames: -1,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const physicalCleanup =
|
|
231
|
+
checkpoint?.busy === 0 && (checkpoint.log === 0 || checkpoint.log === -1)
|
|
232
|
+
? "completed"
|
|
233
|
+
: "wal_busy";
|
|
234
|
+
return ok({
|
|
235
|
+
...counts,
|
|
236
|
+
physicalCleanup,
|
|
237
|
+
checkpointedFrames: checkpoint?.checkpointed ?? 0,
|
|
238
|
+
remainingWalFrames: checkpoint?.log ?? -1,
|
|
239
|
+
});
|
|
240
|
+
} catch (cause) {
|
|
241
|
+
return traceWriteError(cause, "Failed to purge retrieval traces");
|
|
242
|
+
} finally {
|
|
243
|
+
restoreSecureDelete(db, prior);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export const enforceRetention = (
|
|
248
|
+
db: Database,
|
|
249
|
+
policy: RetrievalTraceRetentionPolicy,
|
|
250
|
+
nowMs: number
|
|
251
|
+
): StoreResult<RetrievalTraceRetentionResult> => {
|
|
252
|
+
const prior = readSecureDelete(db);
|
|
253
|
+
try {
|
|
254
|
+
validatePolicy(policy, nowMs);
|
|
255
|
+
db.exec("PRAGMA secure_delete = ON");
|
|
256
|
+
db.exec("BEGIN IMMEDIATE");
|
|
257
|
+
try {
|
|
258
|
+
const before = countTraceContent(db);
|
|
259
|
+
const rows = storageRows(db);
|
|
260
|
+
const ageMs = policy.maxAgeDays * DAY_MS;
|
|
261
|
+
const byExpiry = [...rows].sort((left, right) => {
|
|
262
|
+
const leftExpiry = Math.min(
|
|
263
|
+
left.expires_at_ms,
|
|
264
|
+
left.created_at_ms + ageMs
|
|
265
|
+
);
|
|
266
|
+
const rightExpiry = Math.min(
|
|
267
|
+
right.expires_at_ms,
|
|
268
|
+
right.created_at_ms + ageMs
|
|
269
|
+
);
|
|
270
|
+
return (
|
|
271
|
+
leftExpiry - rightExpiry ||
|
|
272
|
+
left.created_at_ms - right.created_at_ms ||
|
|
273
|
+
compareCodeUnits(left.trace_id, right.trace_id)
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
const deleted = new Set<string>();
|
|
277
|
+
const deletedTraceIds: string[] = [];
|
|
278
|
+
let remainingTraces = rows.length;
|
|
279
|
+
let remainingBytes = rows.reduce(
|
|
280
|
+
(total, row) => total + row.total_bytes,
|
|
281
|
+
0
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
for (const row of byExpiry) {
|
|
285
|
+
const effectiveExpiry = Math.min(
|
|
286
|
+
row.expires_at_ms,
|
|
287
|
+
row.created_at_ms + ageMs
|
|
288
|
+
);
|
|
289
|
+
if (effectiveExpiry > nowMs) break;
|
|
290
|
+
db.run("DELETE FROM retrieval_traces WHERE trace_id = ?", [
|
|
291
|
+
row.trace_id,
|
|
292
|
+
]);
|
|
293
|
+
deleted.add(row.trace_id);
|
|
294
|
+
deletedTraceIds.push(row.trace_id);
|
|
295
|
+
remainingTraces -= 1;
|
|
296
|
+
remainingBytes -= row.total_bytes;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const row of rows) {
|
|
300
|
+
if (
|
|
301
|
+
deleted.has(row.trace_id) ||
|
|
302
|
+
row.record_count <= policy.maxRecordsPerTrace
|
|
303
|
+
) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
db.run("DELETE FROM retrieval_traces WHERE trace_id = ?", [
|
|
307
|
+
row.trace_id,
|
|
308
|
+
]);
|
|
309
|
+
deleted.add(row.trace_id);
|
|
310
|
+
deletedTraceIds.push(row.trace_id);
|
|
311
|
+
remainingTraces -= 1;
|
|
312
|
+
remainingBytes -= row.total_bytes;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const row of rows) {
|
|
316
|
+
if (
|
|
317
|
+
remainingTraces <= policy.maxTraces &&
|
|
318
|
+
remainingBytes <= policy.maxBytes
|
|
319
|
+
) {
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
if (deleted.has(row.trace_id)) continue;
|
|
323
|
+
db.run("DELETE FROM retrieval_traces WHERE trace_id = ?", [
|
|
324
|
+
row.trace_id,
|
|
325
|
+
]);
|
|
326
|
+
deleted.add(row.trace_id);
|
|
327
|
+
deletedTraceIds.push(row.trace_id);
|
|
328
|
+
remainingTraces -= 1;
|
|
329
|
+
remainingBytes -= row.total_bytes;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const after = countTraceContent(db);
|
|
333
|
+
db.exec("COMMIT");
|
|
334
|
+
return ok({
|
|
335
|
+
deleted: subtractCounts(before, after),
|
|
336
|
+
deletedTraceIds,
|
|
337
|
+
remainingTraces,
|
|
338
|
+
remainingBytes,
|
|
339
|
+
});
|
|
340
|
+
} catch (cause) {
|
|
341
|
+
db.exec("ROLLBACK");
|
|
342
|
+
throw cause;
|
|
343
|
+
}
|
|
344
|
+
} catch (cause) {
|
|
345
|
+
return traceWriteError(cause, "Failed to enforce trace retention");
|
|
346
|
+
} finally {
|
|
347
|
+
restoreSecureDelete(db, prior);
|
|
348
|
+
}
|
|
349
|
+
};
|