@rigour-labs/core 5.2.8 → 5.3.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/dist/context/cache-engine.d.ts +75 -0
- package/dist/context/cache-engine.js +155 -0
- package/dist/context/cache-engine.test.d.ts +1 -0
- package/dist/context/cache-engine.test.js +107 -0
- package/dist/context/context-session.d.ts +25 -0
- package/dist/context/context-session.js +78 -0
- package/dist/context/index-bridge.d.ts +24 -0
- package/dist/context/index-bridge.js +114 -0
- package/dist/hooks/dlp-confidence.d.ts +16 -0
- package/dist/hooks/dlp-confidence.js +302 -0
- package/dist/hooks/dlp-confidence.test.d.ts +1 -0
- package/dist/hooks/dlp-confidence.test.js +51 -0
- package/dist/hooks/dlp-feedback.d.ts +48 -0
- package/dist/hooks/dlp-feedback.js +174 -0
- package/dist/hooks/dlp-feedback.test.d.ts +1 -0
- package/dist/hooks/dlp-feedback.test.js +39 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/input-validator.d.ts +8 -0
- package/dist/hooks/input-validator.js +131 -12
- package/dist/hooks/input-validator.test.js +92 -34
- package/dist/index.d.ts +6 -1
- package/dist/index.js +7 -1
- package/dist/services/context-telemetry-service.d.ts +121 -0
- package/dist/services/context-telemetry-service.js +375 -0
- package/dist/services/context-telemetry-service.test.d.ts +1 -0
- package/dist/services/context-telemetry-service.test.js +130 -0
- package/dist/settings.d.ts +11 -0
- package/dist/settings.js +18 -0
- package/dist/storage/context-telemetry.d.ts +83 -0
- package/dist/storage/context-telemetry.js +313 -0
- package/dist/storage/context-telemetry.test.d.ts +1 -0
- package/dist/storage/context-telemetry.test.js +80 -0
- package/dist/storage/db.js +140 -2
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +1 -0
- package/package.json +82 -82
- package/LICENSE +0 -21
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export interface ContextEvent {
|
|
2
|
+
id?: string;
|
|
3
|
+
taskId?: string;
|
|
4
|
+
sessionId?: string;
|
|
5
|
+
agentId?: string;
|
|
6
|
+
toolName: string;
|
|
7
|
+
queryHash?: string;
|
|
8
|
+
cacheStatus: 'exact-hit' | 'semantic-hit' | 'partial-hit' | 'miss' | 'none';
|
|
9
|
+
candidateTokens: number;
|
|
10
|
+
returnedTokens: number;
|
|
11
|
+
deduplicatedTokens?: number;
|
|
12
|
+
candidateFiles?: number;
|
|
13
|
+
returnedFiles?: number;
|
|
14
|
+
createdAt?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ModelUsage {
|
|
17
|
+
id?: string;
|
|
18
|
+
taskId?: string;
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
agentId?: string;
|
|
21
|
+
provider?: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
inputTokens: number;
|
|
24
|
+
outputTokens: number;
|
|
25
|
+
cachedInputTokens?: number;
|
|
26
|
+
observedCostUsd?: number;
|
|
27
|
+
source: string;
|
|
28
|
+
createdAt?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ContextCacheRecord {
|
|
31
|
+
cacheKey: string;
|
|
32
|
+
cacheType: 'static' | 'component' | 'semantic' | 'checkpoint';
|
|
33
|
+
repo: string;
|
|
34
|
+
branch: string;
|
|
35
|
+
commitSha?: string;
|
|
36
|
+
dependencyFingerprint: string;
|
|
37
|
+
payloadJson: string;
|
|
38
|
+
payloadTokens: number;
|
|
39
|
+
hitCount?: number;
|
|
40
|
+
createdAt?: number;
|
|
41
|
+
lastHitAt?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface CheckpointMetric {
|
|
44
|
+
checkpointId: string;
|
|
45
|
+
taskId: string;
|
|
46
|
+
agentId: string;
|
|
47
|
+
rawStateTokens: number;
|
|
48
|
+
checkpointTokens: number;
|
|
49
|
+
replayTokensAvoided: number;
|
|
50
|
+
createdAt?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Log a context retrieval event.
|
|
54
|
+
*/
|
|
55
|
+
export declare function recordContextEvent(event: ContextEvent, cwd?: string): Promise<string>;
|
|
56
|
+
/**
|
|
57
|
+
* Log observed model usage (Cursor Admin API, CSV import, or manual).
|
|
58
|
+
*/
|
|
59
|
+
export declare function recordModelUsage(usage: ModelUsage, cwd?: string): Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Insert or update cache entry.
|
|
62
|
+
*/
|
|
63
|
+
export declare function setContextCacheRecord(record: ContextCacheRecord, cwd?: string): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Fetch cached context record by cache key. Increments hit count.
|
|
66
|
+
*/
|
|
67
|
+
export declare function getContextCacheRecord(cacheKey: string, cwd?: string): Promise<ContextCacheRecord | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Record subagent checkpoint metrics.
|
|
70
|
+
*/
|
|
71
|
+
export declare function recordCheckpointMetric(metric: CheckpointMetric, cwd?: string): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Helper to fetch all recorded context events.
|
|
74
|
+
*/
|
|
75
|
+
export declare function getContextEvents(taskId?: string, cwd?: string): Promise<ContextEvent[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Helper to fetch model usage records.
|
|
78
|
+
*/
|
|
79
|
+
export declare function getModelUsages(taskId?: string, cwd?: string): Promise<ModelUsage[]>;
|
|
80
|
+
/**
|
|
81
|
+
* Helper to fetch checkpoint metrics.
|
|
82
|
+
*/
|
|
83
|
+
export declare function getCheckpointMetrics(taskId?: string, cwd?: string): Promise<CheckpointMetric[]>;
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Telemetry & Avoided Token Database Access Layer
|
|
3
|
+
* Stores and queries context events, model usage, context caches, and checkpoint metrics.
|
|
4
|
+
*/
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { openDatabase, isSQLiteAvailable } from './db.js';
|
|
9
|
+
// Fallback JSON persistence path if SQLite is unavailable
|
|
10
|
+
function getFallbackDir(cwd) {
|
|
11
|
+
return path.join(cwd || process.cwd(), '.rigour');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Log a context retrieval event.
|
|
15
|
+
*/
|
|
16
|
+
export async function recordContextEvent(event, cwd) {
|
|
17
|
+
const id = event.id || `evt-${randomUUID()}`;
|
|
18
|
+
const timestamp = event.createdAt || Date.now();
|
|
19
|
+
const candidateTokens = event.candidateTokens || 0;
|
|
20
|
+
const returnedTokens = event.returnedTokens || 0;
|
|
21
|
+
const deduplicatedTokens = event.deduplicatedTokens || 0;
|
|
22
|
+
const candidateFiles = event.candidateFiles || 0;
|
|
23
|
+
const returnedFiles = event.returnedFiles || 0;
|
|
24
|
+
if (isSQLiteAvailable()) {
|
|
25
|
+
try {
|
|
26
|
+
const db = await openDatabase();
|
|
27
|
+
if (db) {
|
|
28
|
+
await db.run(`INSERT INTO context_events (
|
|
29
|
+
id, task_id, session_id, agent_id, tool_name, query_hash, cache_status,
|
|
30
|
+
candidate_tokens, returned_tokens, deduplicated_tokens, candidate_files, returned_files, created_at
|
|
31
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, event.taskId || null, event.sessionId || null, event.agentId || null, event.toolName, event.queryHash || null, event.cacheStatus, candidateTokens, returnedTokens, deduplicatedTokens, candidateFiles, returnedFiles, timestamp);
|
|
32
|
+
await db.close();
|
|
33
|
+
return id;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// fallback to JSON
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const fallbackDir = getFallbackDir(cwd);
|
|
41
|
+
const fallbackPath = path.join(fallbackDir, 'context-events.jsonl');
|
|
42
|
+
await fs.ensureDir(fallbackDir);
|
|
43
|
+
const record = { ...event, id, createdAt: timestamp };
|
|
44
|
+
await fs.appendFile(fallbackPath, JSON.stringify(record) + '\n');
|
|
45
|
+
return id;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Log observed model usage (Cursor Admin API, CSV import, or manual).
|
|
49
|
+
*/
|
|
50
|
+
export async function recordModelUsage(usage, cwd) {
|
|
51
|
+
const id = usage.id || `usage-${randomUUID()}`;
|
|
52
|
+
const timestamp = usage.createdAt || Date.now();
|
|
53
|
+
if (isSQLiteAvailable()) {
|
|
54
|
+
try {
|
|
55
|
+
const db = await openDatabase();
|
|
56
|
+
if (db) {
|
|
57
|
+
await db.run(`INSERT INTO model_usage (
|
|
58
|
+
id, task_id, session_id, agent_id, provider, model,
|
|
59
|
+
input_tokens, output_tokens, cached_input_tokens, observed_cost_usd, source, created_at
|
|
60
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, usage.taskId || null, usage.sessionId || null, usage.agentId || null, usage.provider || 'cursor', usage.model || 'claude-3-5-sonnet', usage.inputTokens, usage.outputTokens, usage.cachedInputTokens || 0, usage.observedCostUsd || 0, usage.source, timestamp);
|
|
61
|
+
await db.close();
|
|
62
|
+
return id;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// fallback to JSON
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const fallbackDir = getFallbackDir(cwd);
|
|
70
|
+
const fallbackPath = path.join(fallbackDir, 'model-usage.jsonl');
|
|
71
|
+
await fs.ensureDir(fallbackDir);
|
|
72
|
+
const record = { ...usage, id, createdAt: timestamp };
|
|
73
|
+
await fs.appendFile(fallbackPath, JSON.stringify(record) + '\n');
|
|
74
|
+
return id;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Insert or update cache entry.
|
|
78
|
+
*/
|
|
79
|
+
export async function setContextCacheRecord(record, cwd) {
|
|
80
|
+
const timestamp = record.createdAt || Date.now();
|
|
81
|
+
if (isSQLiteAvailable()) {
|
|
82
|
+
try {
|
|
83
|
+
const db = await openDatabase();
|
|
84
|
+
if (db) {
|
|
85
|
+
await db.run(`INSERT INTO context_cache (
|
|
86
|
+
cache_key, cache_type, repo, branch, commit_sha, dependency_fingerprint,
|
|
87
|
+
payload_json, payload_tokens, hit_count, created_at, last_hit_at
|
|
88
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
89
|
+
ON CONFLICT(cache_key) DO UPDATE SET
|
|
90
|
+
dependency_fingerprint=excluded.dependency_fingerprint,
|
|
91
|
+
payload_json=excluded.payload_json,
|
|
92
|
+
payload_tokens=excluded.payload_tokens,
|
|
93
|
+
created_at=excluded.created_at`, record.cacheKey, record.cacheType, record.repo, record.branch, record.commitSha || null, record.dependencyFingerprint, record.payloadJson, record.payloadTokens, record.hitCount || 0, timestamp, record.lastHitAt || null);
|
|
94
|
+
await db.close();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// fallback
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const fallbackDir = getFallbackDir(cwd);
|
|
103
|
+
const fallbackPath = path.join(fallbackDir, 'context-cache.json');
|
|
104
|
+
await fs.ensureDir(fallbackDir);
|
|
105
|
+
let cacheData = {};
|
|
106
|
+
if (await fs.pathExists(fallbackPath)) {
|
|
107
|
+
try {
|
|
108
|
+
cacheData = await fs.readJson(fallbackPath);
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
}
|
|
112
|
+
cacheData[record.cacheKey] = { ...record, createdAt: timestamp };
|
|
113
|
+
await fs.writeJson(fallbackPath, cacheData, { spaces: 2 });
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Fetch cached context record by cache key. Increments hit count.
|
|
117
|
+
*/
|
|
118
|
+
export async function getContextCacheRecord(cacheKey, cwd) {
|
|
119
|
+
if (isSQLiteAvailable()) {
|
|
120
|
+
try {
|
|
121
|
+
const db = await openDatabase();
|
|
122
|
+
if (db) {
|
|
123
|
+
const row = await db.get(`SELECT * FROM context_cache WHERE cache_key = ?`, cacheKey);
|
|
124
|
+
if (row) {
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
await db.run(`UPDATE context_cache SET hit_count = hit_count + 1, last_hit_at = ? WHERE cache_key = ?`, now, cacheKey);
|
|
127
|
+
await db.close();
|
|
128
|
+
return {
|
|
129
|
+
cacheKey: row.cache_key,
|
|
130
|
+
cacheType: row.cache_type,
|
|
131
|
+
repo: row.repo,
|
|
132
|
+
branch: row.branch,
|
|
133
|
+
commitSha: row.commit_sha,
|
|
134
|
+
dependencyFingerprint: row.dependency_fingerprint,
|
|
135
|
+
payloadJson: row.payload_json,
|
|
136
|
+
payloadTokens: row.payload_tokens,
|
|
137
|
+
hitCount: (row.hit_count || 0) + 1,
|
|
138
|
+
createdAt: row.created_at,
|
|
139
|
+
lastHitAt: now
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
await db.close();
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// fallback
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const fallbackPath = path.join(getFallbackDir(cwd), 'context-cache.json');
|
|
151
|
+
if (await fs.pathExists(fallbackPath)) {
|
|
152
|
+
try {
|
|
153
|
+
const cacheData = await fs.readJson(fallbackPath);
|
|
154
|
+
const entry = cacheData[cacheKey];
|
|
155
|
+
if (entry) {
|
|
156
|
+
entry.hitCount = (entry.hitCount || 0) + 1;
|
|
157
|
+
entry.lastHitAt = Date.now();
|
|
158
|
+
await fs.writeJson(fallbackPath, cacheData, { spaces: 2 });
|
|
159
|
+
return entry;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch { }
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Record subagent checkpoint metrics.
|
|
168
|
+
*/
|
|
169
|
+
export async function recordCheckpointMetric(metric, cwd) {
|
|
170
|
+
const timestamp = metric.createdAt || Date.now();
|
|
171
|
+
if (isSQLiteAvailable()) {
|
|
172
|
+
try {
|
|
173
|
+
const db = await openDatabase();
|
|
174
|
+
if (db) {
|
|
175
|
+
await db.run(`INSERT INTO checkpoint_metrics (
|
|
176
|
+
checkpoint_id, task_id, agent_id, raw_state_tokens, checkpoint_tokens, replay_tokens_avoided, created_at
|
|
177
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
178
|
+
ON CONFLICT(checkpoint_id) DO UPDATE SET
|
|
179
|
+
raw_state_tokens=excluded.raw_state_tokens,
|
|
180
|
+
checkpoint_tokens=excluded.checkpoint_tokens,
|
|
181
|
+
replay_tokens_avoided=excluded.replay_tokens_avoided`, metric.checkpointId, metric.taskId, metric.agentId, metric.rawStateTokens, metric.checkpointTokens, metric.replayTokensAvoided, timestamp);
|
|
182
|
+
await db.close();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// fallback
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const fallbackDir = getFallbackDir(cwd);
|
|
191
|
+
const fallbackPath = path.join(fallbackDir, 'checkpoint-metrics.jsonl');
|
|
192
|
+
await fs.ensureDir(fallbackDir);
|
|
193
|
+
await fs.appendFile(fallbackPath, JSON.stringify({ ...metric, createdAt: timestamp }) + '\n');
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Helper to fetch all recorded context events.
|
|
197
|
+
*/
|
|
198
|
+
export async function getContextEvents(taskId, cwd) {
|
|
199
|
+
if (isSQLiteAvailable()) {
|
|
200
|
+
try {
|
|
201
|
+
const db = await openDatabase();
|
|
202
|
+
if (db) {
|
|
203
|
+
const query = taskId
|
|
204
|
+
? `SELECT * FROM context_events WHERE task_id = ? ORDER BY created_at DESC`
|
|
205
|
+
: `SELECT * FROM context_events ORDER BY created_at DESC`;
|
|
206
|
+
const params = taskId ? [taskId] : [];
|
|
207
|
+
const rows = await db.all(query, ...params);
|
|
208
|
+
await db.close();
|
|
209
|
+
return rows.map((r) => ({
|
|
210
|
+
id: r.id,
|
|
211
|
+
taskId: r.task_id,
|
|
212
|
+
sessionId: r.session_id,
|
|
213
|
+
agentId: r.agent_id,
|
|
214
|
+
toolName: r.tool_name,
|
|
215
|
+
queryHash: r.query_hash,
|
|
216
|
+
cacheStatus: r.cache_status,
|
|
217
|
+
candidateTokens: r.candidate_tokens,
|
|
218
|
+
returnedTokens: r.returned_tokens,
|
|
219
|
+
deduplicatedTokens: r.deduplicated_tokens,
|
|
220
|
+
candidateFiles: r.candidate_files,
|
|
221
|
+
returnedFiles: r.returned_files,
|
|
222
|
+
createdAt: r.created_at
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// fallback
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const fallbackPath = path.join(getFallbackDir(cwd), 'context-events.jsonl');
|
|
231
|
+
if (!await fs.pathExists(fallbackPath))
|
|
232
|
+
return [];
|
|
233
|
+
const lines = (await fs.readFile(fallbackPath, 'utf8')).split('\n').filter(Boolean);
|
|
234
|
+
const events = lines.map(l => JSON.parse(l));
|
|
235
|
+
return taskId ? events.filter(e => e.taskId === taskId) : events;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Helper to fetch model usage records.
|
|
239
|
+
*/
|
|
240
|
+
export async function getModelUsages(taskId, cwd) {
|
|
241
|
+
if (isSQLiteAvailable()) {
|
|
242
|
+
try {
|
|
243
|
+
const db = await openDatabase();
|
|
244
|
+
if (db) {
|
|
245
|
+
const query = taskId
|
|
246
|
+
? `SELECT * FROM model_usage WHERE task_id = ? ORDER BY created_at DESC`
|
|
247
|
+
: `SELECT * FROM model_usage ORDER BY created_at DESC`;
|
|
248
|
+
const params = taskId ? [taskId] : [];
|
|
249
|
+
const rows = await db.all(query, ...params);
|
|
250
|
+
await db.close();
|
|
251
|
+
return rows.map((r) => ({
|
|
252
|
+
id: r.id,
|
|
253
|
+
taskId: r.task_id,
|
|
254
|
+
sessionId: r.session_id,
|
|
255
|
+
agentId: r.agent_id,
|
|
256
|
+
provider: r.provider,
|
|
257
|
+
model: r.model,
|
|
258
|
+
inputTokens: r.input_tokens,
|
|
259
|
+
outputTokens: r.output_tokens,
|
|
260
|
+
cachedInputTokens: r.cached_input_tokens,
|
|
261
|
+
observedCostUsd: r.observed_cost_usd,
|
|
262
|
+
source: r.source,
|
|
263
|
+
createdAt: r.created_at
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// fallback
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const fallbackPath = path.join(getFallbackDir(cwd), 'model-usage.jsonl');
|
|
272
|
+
if (!await fs.pathExists(fallbackPath))
|
|
273
|
+
return [];
|
|
274
|
+
const lines = (await fs.readFile(fallbackPath, 'utf8')).split('\n').filter(Boolean);
|
|
275
|
+
const usages = lines.map(l => JSON.parse(l));
|
|
276
|
+
return taskId ? usages.filter(u => u.taskId === taskId) : usages;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Helper to fetch checkpoint metrics.
|
|
280
|
+
*/
|
|
281
|
+
export async function getCheckpointMetrics(taskId, cwd) {
|
|
282
|
+
if (isSQLiteAvailable()) {
|
|
283
|
+
try {
|
|
284
|
+
const db = await openDatabase();
|
|
285
|
+
if (db) {
|
|
286
|
+
const query = taskId
|
|
287
|
+
? `SELECT * FROM checkpoint_metrics WHERE task_id = ? ORDER BY created_at DESC`
|
|
288
|
+
: `SELECT * FROM checkpoint_metrics ORDER BY created_at DESC`;
|
|
289
|
+
const params = taskId ? [taskId] : [];
|
|
290
|
+
const rows = await db.all(query, ...params);
|
|
291
|
+
await db.close();
|
|
292
|
+
return rows.map((r) => ({
|
|
293
|
+
checkpointId: r.checkpoint_id,
|
|
294
|
+
taskId: r.task_id,
|
|
295
|
+
agentId: r.agent_id,
|
|
296
|
+
rawStateTokens: r.raw_state_tokens,
|
|
297
|
+
checkpointTokens: r.checkpoint_tokens,
|
|
298
|
+
replayTokensAvoided: r.replay_tokens_avoided,
|
|
299
|
+
createdAt: r.created_at
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// fallback
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const fallbackPath = path.join(getFallbackDir(cwd), 'checkpoint-metrics.jsonl');
|
|
308
|
+
if (!await fs.pathExists(fallbackPath))
|
|
309
|
+
return [];
|
|
310
|
+
const lines = (await fs.readFile(fallbackPath, 'utf8')).split('\n').filter(Boolean);
|
|
311
|
+
const metrics = lines.map(l => JSON.parse(l));
|
|
312
|
+
return taskId ? metrics.filter(m => m.taskId === taskId) : metrics;
|
|
313
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
vi.mock('./db.js', () => ({
|
|
6
|
+
isSQLiteAvailable: () => false,
|
|
7
|
+
openDatabase: async () => null,
|
|
8
|
+
DB_PATH: '/tmp/rigour-test.db',
|
|
9
|
+
RIGOUR_DIR: '/tmp/.rigour',
|
|
10
|
+
}));
|
|
11
|
+
import { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics, } from './context-telemetry.js';
|
|
12
|
+
describe('context-telemetry storage', () => {
|
|
13
|
+
const testCwd = path.join(os.tmpdir(), `rigour-ctx-telemetry-${Date.now()}`);
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
await fs.ensureDir(path.join(testCwd, '.rigour'));
|
|
16
|
+
});
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
await fs.remove(testCwd);
|
|
19
|
+
});
|
|
20
|
+
it('records and retrieves context events via JSON fallback', async () => {
|
|
21
|
+
const id = await recordContextEvent({
|
|
22
|
+
taskId: 'task-1',
|
|
23
|
+
toolName: 'rigour_recall',
|
|
24
|
+
cacheStatus: 'semantic-hit',
|
|
25
|
+
candidateTokens: 4000,
|
|
26
|
+
returnedTokens: 800,
|
|
27
|
+
deduplicatedTokens: 1200,
|
|
28
|
+
}, testCwd);
|
|
29
|
+
expect(id).toMatch(/^evt-/);
|
|
30
|
+
const events = await getContextEvents('task-1', testCwd);
|
|
31
|
+
expect(events).toHaveLength(1);
|
|
32
|
+
expect(events[0].toolName).toBe('rigour_recall');
|
|
33
|
+
expect(events[0].candidateTokens).toBe(4000);
|
|
34
|
+
expect(events[0].returnedTokens).toBe(800);
|
|
35
|
+
});
|
|
36
|
+
it('records and retrieves model usage via JSON fallback', async () => {
|
|
37
|
+
await recordModelUsage({
|
|
38
|
+
taskId: 'cursor-imported',
|
|
39
|
+
provider: 'cursor',
|
|
40
|
+
model: 'claude-3-5-sonnet',
|
|
41
|
+
inputTokens: 12000,
|
|
42
|
+
outputTokens: 1500,
|
|
43
|
+
observedCostUsd: 1.25,
|
|
44
|
+
source: 'cursor-dashboard-csv',
|
|
45
|
+
}, testCwd);
|
|
46
|
+
const usages = await getModelUsages(undefined, testCwd);
|
|
47
|
+
expect(usages).toHaveLength(1);
|
|
48
|
+
expect(usages[0].inputTokens).toBe(12000);
|
|
49
|
+
expect(usages[0].source).toBe('cursor-dashboard-csv');
|
|
50
|
+
});
|
|
51
|
+
it('stores cache records and increments hit count', async () => {
|
|
52
|
+
await setContextCacheRecord({
|
|
53
|
+
cacheKey: 'static:repo:main:file.ts:abc123:v1',
|
|
54
|
+
cacheType: 'static',
|
|
55
|
+
repo: 'repo',
|
|
56
|
+
branch: 'main',
|
|
57
|
+
dependencyFingerprint: 'dep-1',
|
|
58
|
+
payloadJson: JSON.stringify({ exports: ['foo'] }),
|
|
59
|
+
payloadTokens: 42,
|
|
60
|
+
}, testCwd);
|
|
61
|
+
const first = await getContextCacheRecord('static:repo:main:file.ts:abc123:v1', testCwd);
|
|
62
|
+
const second = await getContextCacheRecord('static:repo:main:file.ts:abc123:v1', testCwd);
|
|
63
|
+
expect(first?.payloadTokens).toBe(42);
|
|
64
|
+
expect(second?.hitCount).toBe(2);
|
|
65
|
+
});
|
|
66
|
+
it('records and retrieves checkpoint metrics via JSON fallback', async () => {
|
|
67
|
+
await recordCheckpointMetric({
|
|
68
|
+
checkpointId: 'checkpoint:CTP-1:agent-a:implementation',
|
|
69
|
+
taskId: 'CTP-1',
|
|
70
|
+
agentId: 'agent-a',
|
|
71
|
+
rawStateTokens: 50000,
|
|
72
|
+
checkpointTokens: 400,
|
|
73
|
+
replayTokensAvoided: 49600,
|
|
74
|
+
}, testCwd);
|
|
75
|
+
const metrics = await getCheckpointMetrics('CTP-1', testCwd);
|
|
76
|
+
expect(metrics).toHaveLength(1);
|
|
77
|
+
expect(metrics[0].rawStateTokens).toBe(50000);
|
|
78
|
+
expect(metrics[0].replayTokensAvoided).toBe(49600);
|
|
79
|
+
});
|
|
80
|
+
});
|
package/dist/storage/db.js
CHANGED
|
@@ -45,7 +45,7 @@ function loadSqlite3() {
|
|
|
45
45
|
const RIGOUR_DIR = path.join(os.homedir(), '.rigour');
|
|
46
46
|
const DB_PATH = path.join(RIGOUR_DIR, 'rigour.db');
|
|
47
47
|
/** Current schema version — bump when adding migrations. */
|
|
48
|
-
const SCHEMA_VERSION =
|
|
48
|
+
const SCHEMA_VERSION = 4;
|
|
49
49
|
const SCHEMA_SQL = `
|
|
50
50
|
-- Schema version tracking
|
|
51
51
|
CREATE TABLE IF NOT EXISTS meta (
|
|
@@ -118,6 +118,65 @@ CREATE TABLE IF NOT EXISTS codebase (
|
|
|
118
118
|
last_indexed INTEGER NOT NULL
|
|
119
119
|
);
|
|
120
120
|
|
|
121
|
+
-- Rigour Telemetry: Avoided and observed context events
|
|
122
|
+
CREATE TABLE IF NOT EXISTS context_events (
|
|
123
|
+
id TEXT PRIMARY KEY,
|
|
124
|
+
task_id TEXT,
|
|
125
|
+
session_id TEXT,
|
|
126
|
+
agent_id TEXT,
|
|
127
|
+
tool_name TEXT NOT NULL,
|
|
128
|
+
query_hash TEXT,
|
|
129
|
+
cache_status TEXT NOT NULL,
|
|
130
|
+
candidate_tokens INTEGER NOT NULL DEFAULT 0,
|
|
131
|
+
returned_tokens INTEGER NOT NULL DEFAULT 0,
|
|
132
|
+
deduplicated_tokens INTEGER NOT NULL DEFAULT 0,
|
|
133
|
+
candidate_files INTEGER NOT NULL DEFAULT 0,
|
|
134
|
+
returned_files INTEGER NOT NULL DEFAULT 0,
|
|
135
|
+
created_at INTEGER NOT NULL
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
-- Cursor / Model Usage telemetry
|
|
139
|
+
CREATE TABLE IF NOT EXISTS model_usage (
|
|
140
|
+
id TEXT PRIMARY KEY,
|
|
141
|
+
task_id TEXT,
|
|
142
|
+
session_id TEXT,
|
|
143
|
+
agent_id TEXT,
|
|
144
|
+
provider TEXT,
|
|
145
|
+
model TEXT,
|
|
146
|
+
input_tokens INTEGER,
|
|
147
|
+
output_tokens INTEGER,
|
|
148
|
+
cached_input_tokens INTEGER,
|
|
149
|
+
observed_cost_usd REAL,
|
|
150
|
+
source TEXT NOT NULL,
|
|
151
|
+
created_at INTEGER NOT NULL
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
-- 4-Layer Context Cache
|
|
155
|
+
CREATE TABLE IF NOT EXISTS context_cache (
|
|
156
|
+
cache_key TEXT PRIMARY KEY,
|
|
157
|
+
cache_type TEXT NOT NULL,
|
|
158
|
+
repo TEXT NOT NULL,
|
|
159
|
+
branch TEXT NOT NULL,
|
|
160
|
+
commit_sha TEXT,
|
|
161
|
+
dependency_fingerprint TEXT NOT NULL,
|
|
162
|
+
payload_json TEXT NOT NULL,
|
|
163
|
+
payload_tokens INTEGER NOT NULL,
|
|
164
|
+
hit_count INTEGER NOT NULL DEFAULT 0,
|
|
165
|
+
created_at INTEGER NOT NULL,
|
|
166
|
+
last_hit_at INTEGER
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
-- Checkpoint compression metrics
|
|
170
|
+
CREATE TABLE IF NOT EXISTS checkpoint_metrics (
|
|
171
|
+
checkpoint_id TEXT PRIMARY KEY,
|
|
172
|
+
task_id TEXT NOT NULL,
|
|
173
|
+
agent_id TEXT NOT NULL,
|
|
174
|
+
raw_state_tokens INTEGER,
|
|
175
|
+
checkpoint_tokens INTEGER NOT NULL,
|
|
176
|
+
replay_tokens_avoided INTEGER,
|
|
177
|
+
created_at INTEGER NOT NULL
|
|
178
|
+
);
|
|
179
|
+
|
|
121
180
|
-- Indexes for performance
|
|
122
181
|
CREATE INDEX IF NOT EXISTS idx_scans_repo ON scans(repo);
|
|
123
182
|
CREATE INDEX IF NOT EXISTS idx_scans_timestamp ON scans(timestamp);
|
|
@@ -125,6 +184,14 @@ CREATE INDEX IF NOT EXISTS idx_findings_scan ON findings(scan_id);
|
|
|
125
184
|
CREATE INDEX IF NOT EXISTS idx_findings_category ON findings(category);
|
|
126
185
|
CREATE INDEX IF NOT EXISTS idx_patterns_repo ON patterns(repo);
|
|
127
186
|
CREATE INDEX IF NOT EXISTS idx_patterns_strength ON patterns(strength);
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_context_events_task ON context_events(task_id);
|
|
188
|
+
CREATE INDEX IF NOT EXISTS idx_context_events_session ON context_events(session_id);
|
|
189
|
+
CREATE INDEX IF NOT EXISTS idx_context_events_agent ON context_events(agent_id);
|
|
190
|
+
CREATE INDEX IF NOT EXISTS idx_context_events_created ON context_events(created_at);
|
|
191
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_task ON model_usage(task_id);
|
|
192
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_created ON model_usage(created_at);
|
|
193
|
+
CREATE INDEX IF NOT EXISTS idx_context_cache_type ON context_cache(cache_type);
|
|
194
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoint_metrics_task ON checkpoint_metrics(task_id);
|
|
128
195
|
`;
|
|
129
196
|
function wrapDatabase(raw) {
|
|
130
197
|
const db = {
|
|
@@ -230,7 +297,78 @@ async function runMigrations(db) {
|
|
|
230
297
|
`);
|
|
231
298
|
await db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '2')");
|
|
232
299
|
}
|
|
233
|
-
|
|
300
|
+
if (current < 3) {
|
|
301
|
+
await db.exec(`
|
|
302
|
+
CREATE TABLE IF NOT EXISTS context_events (
|
|
303
|
+
id TEXT PRIMARY KEY,
|
|
304
|
+
task_id TEXT,
|
|
305
|
+
session_id TEXT,
|
|
306
|
+
agent_id TEXT,
|
|
307
|
+
tool_name TEXT NOT NULL,
|
|
308
|
+
query_hash TEXT,
|
|
309
|
+
cache_status TEXT NOT NULL,
|
|
310
|
+
candidate_tokens INTEGER NOT NULL DEFAULT 0,
|
|
311
|
+
returned_tokens INTEGER NOT NULL DEFAULT 0,
|
|
312
|
+
deduplicated_tokens INTEGER NOT NULL DEFAULT 0,
|
|
313
|
+
candidate_files INTEGER NOT NULL DEFAULT 0,
|
|
314
|
+
returned_files INTEGER NOT NULL DEFAULT 0,
|
|
315
|
+
created_at INTEGER NOT NULL
|
|
316
|
+
);
|
|
317
|
+
CREATE TABLE IF NOT EXISTS model_usage (
|
|
318
|
+
id TEXT PRIMARY KEY,
|
|
319
|
+
task_id TEXT,
|
|
320
|
+
session_id TEXT,
|
|
321
|
+
agent_id TEXT,
|
|
322
|
+
provider TEXT,
|
|
323
|
+
model TEXT,
|
|
324
|
+
input_tokens INTEGER,
|
|
325
|
+
output_tokens INTEGER,
|
|
326
|
+
cached_input_tokens INTEGER,
|
|
327
|
+
observed_cost_usd REAL,
|
|
328
|
+
source TEXT NOT NULL,
|
|
329
|
+
created_at INTEGER NOT NULL
|
|
330
|
+
);
|
|
331
|
+
CREATE TABLE IF NOT EXISTS context_cache (
|
|
332
|
+
cache_key TEXT PRIMARY KEY,
|
|
333
|
+
cache_type TEXT NOT NULL,
|
|
334
|
+
repo TEXT NOT NULL,
|
|
335
|
+
branch TEXT NOT NULL,
|
|
336
|
+
commit_sha TEXT,
|
|
337
|
+
dependency_fingerprint TEXT NOT NULL,
|
|
338
|
+
payload_json TEXT NOT NULL,
|
|
339
|
+
payload_tokens INTEGER NOT NULL,
|
|
340
|
+
hit_count INTEGER NOT NULL DEFAULT 0,
|
|
341
|
+
created_at INTEGER NOT NULL,
|
|
342
|
+
last_hit_at INTEGER
|
|
343
|
+
);
|
|
344
|
+
CREATE TABLE IF NOT EXISTS checkpoint_metrics (
|
|
345
|
+
checkpoint_id TEXT PRIMARY KEY,
|
|
346
|
+
task_id TEXT NOT NULL,
|
|
347
|
+
agent_id TEXT NOT NULL,
|
|
348
|
+
raw_state_tokens INTEGER,
|
|
349
|
+
checkpoint_tokens INTEGER NOT NULL,
|
|
350
|
+
replay_tokens_avoided INTEGER,
|
|
351
|
+
created_at INTEGER NOT NULL
|
|
352
|
+
);
|
|
353
|
+
CREATE INDEX IF NOT EXISTS idx_context_events_task ON context_events(task_id);
|
|
354
|
+
CREATE INDEX IF NOT EXISTS idx_context_events_session ON context_events(session_id);
|
|
355
|
+
CREATE INDEX IF NOT EXISTS idx_context_events_agent ON context_events(agent_id);
|
|
356
|
+
CREATE INDEX IF NOT EXISTS idx_context_events_created ON context_events(created_at);
|
|
357
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_task ON model_usage(task_id);
|
|
358
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_created ON model_usage(created_at);
|
|
359
|
+
CREATE INDEX IF NOT EXISTS idx_context_cache_type ON context_cache(cache_type);
|
|
360
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoint_metrics_task ON checkpoint_metrics(task_id);
|
|
361
|
+
`);
|
|
362
|
+
await db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '3')");
|
|
363
|
+
}
|
|
364
|
+
if (current < 4) {
|
|
365
|
+
await db.exec(`
|
|
366
|
+
CREATE INDEX IF NOT EXISTS idx_context_events_session ON context_events(session_id);
|
|
367
|
+
CREATE INDEX IF NOT EXISTS idx_context_events_created ON context_events(created_at);
|
|
368
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_created ON model_usage(created_at);
|
|
369
|
+
`);
|
|
370
|
+
await db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '4')");
|
|
371
|
+
}
|
|
234
372
|
}
|
|
235
373
|
/**
|
|
236
374
|
* Compact the database — prune old data, reclaim disk space.
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -9,3 +9,5 @@ export { insertFindings, getFindingsForScan, getDeepFindings } from './findings.
|
|
|
9
9
|
export { reinforcePattern, decayPatterns, getStrongPatterns, getPatterns, getHardRules } from './patterns.js';
|
|
10
10
|
export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './local-memory.js';
|
|
11
11
|
export type { ProjectStats } from './local-memory.js';
|
|
12
|
+
export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics } from './context-telemetry.js';
|
|
13
|
+
export type { ContextEvent, ModelUsage, ContextCacheRecord, CheckpointMetric } from './context-telemetry.js';
|
package/dist/storage/index.js
CHANGED
|
@@ -7,3 +7,4 @@ export { insertScan, getRecentScans, getScoreTrendFromDB, getTopIssues } from '.
|
|
|
7
7
|
export { insertFindings, getFindingsForScan, getDeepFindings } from './findings.js';
|
|
8
8
|
export { reinforcePattern, decayPatterns, getStrongPatterns, getPatterns, getHardRules } from './patterns.js';
|
|
9
9
|
export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './local-memory.js';
|
|
10
|
+
export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics } from './context-telemetry.js';
|