@rigour-labs/core 6.0.0 → 6.1.0-beta.1
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/automatic-index.d.ts +18 -0
- package/dist/context/automatic-index.js +142 -0
- package/dist/context/automatic-index.test.d.ts +1 -0
- package/dist/context/automatic-index.test.js +45 -0
- package/dist/context/cache-engine.d.ts +25 -0
- package/dist/context/dependency-graph.d.ts +13 -0
- package/dist/context/dependency-graph.js +97 -0
- package/dist/context/dependency-graph.test.d.ts +1 -0
- package/dist/context/dependency-graph.test.js +13 -0
- package/dist/context/index-bridge.js +21 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +7 -0
- package/dist/pattern-index/embeddings.js +10 -3
- package/dist/pattern-index/indexer.d.ts +2 -0
- package/dist/pattern-index/indexer.js +72 -0
- package/dist/services/agent-history.d.ts +76 -0
- package/dist/services/agent-history.js +201 -0
- package/dist/services/agent-history.test.d.ts +1 -0
- package/dist/services/agent-history.test.js +69 -0
- package/dist/services/context-telemetry-service.d.ts +21 -0
- package/dist/services/context-telemetry-service.js +36 -7
- package/dist/services/context-telemetry-service.test.js +13 -0
- package/dist/services/engineering-knowledge-graph.d.ts +55 -0
- package/dist/services/engineering-knowledge-graph.js +177 -0
- package/dist/services/engineering-knowledge-graph.test.d.ts +1 -0
- package/dist/services/engineering-knowledge-graph.test.js +59 -0
- package/dist/services/model-pricing.d.ts +2 -0
- package/dist/services/model-pricing.js +2 -0
- package/dist/services/system-health.d.ts +34 -0
- package/dist/services/system-health.js +53 -0
- package/dist/storage/db.js +130 -1
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/lessons.d.ts +60 -0
- package/dist/storage/lessons.js +322 -0
- package/dist/storage/lessons.test.d.ts +1 -0
- package/dist/storage/lessons.test.js +50 -0
- package/dist/storage/local-encryption.d.ts +2 -0
- package/dist/storage/local-encryption.js +41 -0
- package/dist/storage/team-store.d.ts +75 -0
- package/dist/storage/team-store.js +321 -0
- package/dist/storage/team-store.test.d.ts +1 -0
- package/dist/storage/team-store.test.js +11 -0
- package/dist/storage/team-vector-store.d.ts +25 -0
- package/dist/storage/team-vector-store.js +183 -0
- package/dist/storage/team-vector-store.test.d.ts +1 -0
- package/dist/storage/team-vector-store.test.js +42 -0
- package/package.json +8 -6
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { openDatabase } from './db.js';
|
|
5
|
+
import { decryptLocalPayload, encryptLocalPayload } from './local-encryption.js';
|
|
6
|
+
import { loadTeamConfiguration } from './team-store.js';
|
|
7
|
+
export function shouldValidateInteractionLesson(evidence, verifiedSuccessfulOutcomes = 0) {
|
|
8
|
+
return evidence.outcome === 'success'
|
|
9
|
+
&& Boolean(evidence.reusableClaim?.trim())
|
|
10
|
+
&& (evidence.deterministic === true || verifiedSuccessfulOutcomes >= 3);
|
|
11
|
+
}
|
|
12
|
+
export async function recordInteractionEvidence(cwd, evidence) {
|
|
13
|
+
const db = await openDatabase();
|
|
14
|
+
if (!db)
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
const repositoryId = await getRepositoryId(cwd);
|
|
18
|
+
await registerRepository(db, cwd, repositoryId);
|
|
19
|
+
const config = await loadTeamConfiguration();
|
|
20
|
+
const id = `observation-${randomUUID()}`;
|
|
21
|
+
await db.run(`INSERT INTO interaction_events (
|
|
22
|
+
id, repository_id, actor_id, task_id, session_id, request_id,
|
|
23
|
+
tool_name, phase, outcome, deterministic, evidence_json, created_at
|
|
24
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, repositoryId, config?.actorId || process.env.RIGOUR_ACTOR_ID || null, evidence.taskId || null, evidence.sessionId || null, evidence.requestId, evidence.tool, evidence.phase || 'response', evidence.outcome, evidence.deterministic ? 1 : 0, await encryptLocalPayload({ files: evidence.files || [], summary: evidence.summary }), Date.now());
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
await db.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function countInteractionEvidence(cwd) {
|
|
32
|
+
const db = await openDatabase();
|
|
33
|
+
if (!db)
|
|
34
|
+
return 0;
|
|
35
|
+
try {
|
|
36
|
+
const repositoryId = await getRepositoryId(cwd);
|
|
37
|
+
const row = await db.get('SELECT COUNT(*) AS count FROM interaction_events WHERE repository_id = ?', repositoryId);
|
|
38
|
+
return Number(row?.count || 0);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await db.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function normalizeRemote(remote) {
|
|
45
|
+
return remote.trim().replace(/^git@([^:]+):/, 'https://$1/').replace(/\.git$/, '').toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
async function readGitConfig(cwd) {
|
|
48
|
+
const dotGit = path.join(cwd, '.git');
|
|
49
|
+
const stat = await fs.stat(dotGit);
|
|
50
|
+
if (stat.isDirectory())
|
|
51
|
+
return fs.readFile(path.join(dotGit, 'config'), 'utf8');
|
|
52
|
+
const pointer = await fs.readFile(dotGit, 'utf8');
|
|
53
|
+
const match = pointer.match(/^gitdir:\s*(.+)$/m);
|
|
54
|
+
if (!match)
|
|
55
|
+
throw new Error('Invalid Git directory pointer.');
|
|
56
|
+
const gitDir = path.resolve(cwd, match[1].trim());
|
|
57
|
+
try {
|
|
58
|
+
return await fs.readFile(path.join(gitDir, 'config'), 'utf8');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Linked worktrees usually keep the shared remote configuration here.
|
|
62
|
+
const commonDir = (await fs.readFile(path.join(gitDir, 'commondir'), 'utf8')).trim();
|
|
63
|
+
return fs.readFile(path.resolve(gitDir, commonDir, 'config'), 'utf8');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function getRepositoryId(cwd) {
|
|
67
|
+
let identity = path.resolve(cwd);
|
|
68
|
+
try {
|
|
69
|
+
const config = await readGitConfig(cwd);
|
|
70
|
+
const remote = config.match(/\[remote\s+"origin"\][\s\S]*?url\s*=\s*([^\n]+)/)?.[1];
|
|
71
|
+
if (remote)
|
|
72
|
+
identity = normalizeRemote(remote);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Non-git workspaces retain a stable path-derived local identity.
|
|
76
|
+
}
|
|
77
|
+
return createHash('sha256').update(identity).digest('hex');
|
|
78
|
+
}
|
|
79
|
+
async function registerRepository(db, cwd, repositoryId) {
|
|
80
|
+
let canonicalUri = path.resolve(cwd);
|
|
81
|
+
try {
|
|
82
|
+
const config = await readGitConfig(cwd);
|
|
83
|
+
const remote = config.match(/\[remote\s+"origin"\][\s\S]*?url\s*=\s*([^\n]+)/)?.[1];
|
|
84
|
+
if (remote)
|
|
85
|
+
canonicalUri = normalizeRemote(remote);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Non-git workspaces use their resolved path as the canonical local identity.
|
|
89
|
+
}
|
|
90
|
+
const remoteName = canonicalUri.split('/').filter(Boolean).at(-1);
|
|
91
|
+
await db.run(`INSERT INTO repositories (id, canonical_uri, display_name, last_seen) VALUES (?, ?, ?, ?)
|
|
92
|
+
ON CONFLICT(id) DO UPDATE SET canonical_uri=excluded.canonical_uri,
|
|
93
|
+
display_name=excluded.display_name, last_seen=excluded.last_seen`, repositoryId, canonicalUri, remoteName || path.basename(cwd), Date.now());
|
|
94
|
+
}
|
|
95
|
+
async function rowToLesson(row) {
|
|
96
|
+
return {
|
|
97
|
+
id: String(row.id),
|
|
98
|
+
repositoryId: String(row.repository_id),
|
|
99
|
+
actorId: row.actor_id ? String(row.actor_id) : undefined,
|
|
100
|
+
teamId: row.team_id ? String(row.team_id) : undefined,
|
|
101
|
+
visibility: row.visibility,
|
|
102
|
+
state: row.state,
|
|
103
|
+
kind: String(row.kind),
|
|
104
|
+
subject: String(row.subject),
|
|
105
|
+
evidence: await decryptLocalPayload(String(row.evidence_json)),
|
|
106
|
+
confidence: Number(row.confidence),
|
|
107
|
+
source: String(row.source),
|
|
108
|
+
supersedesId: row.supersedes_id ? String(row.supersedes_id) : undefined,
|
|
109
|
+
createdAt: Number(row.created_at),
|
|
110
|
+
updatedAt: Number(row.updated_at),
|
|
111
|
+
repositoryName: row.repository_name ? String(row.repository_name) : undefined,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export async function recordInteractionLesson(cwd, evidence) {
|
|
115
|
+
const db = await openDatabase();
|
|
116
|
+
if (!db)
|
|
117
|
+
return null;
|
|
118
|
+
try {
|
|
119
|
+
const repositoryId = await getRepositoryId(cwd);
|
|
120
|
+
await registerRepository(db, cwd, repositoryId);
|
|
121
|
+
const id = `lesson-${randomUUID()}`;
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
const verifiedSuccess = shouldValidateInteractionLesson(evidence, evidence.verifiedOutcome ? 3 : 0);
|
|
124
|
+
const confidence = verifiedSuccess ? 0.8 : 0.3;
|
|
125
|
+
const state = confidence >= 0.8 ? 'validated' : 'candidate';
|
|
126
|
+
const config = await loadTeamConfiguration();
|
|
127
|
+
const actorId = config?.actorId || process.env.RIGOUR_ACTOR_ID || null;
|
|
128
|
+
const priorRow = await db.get(`SELECT * FROM lessons
|
|
129
|
+
WHERE repository_id = ? AND actor_id IS ? AND visibility = 'personal'
|
|
130
|
+
AND state = 'candidate' AND kind = 'interaction' AND subject = ?
|
|
131
|
+
ORDER BY updated_at DESC LIMIT 1`, repositoryId, actorId, evidence.reusableClaim?.trim() || `interaction:${evidence.tool}`);
|
|
132
|
+
if (priorRow) {
|
|
133
|
+
const prior = await rowToLesson(priorRow);
|
|
134
|
+
const previousSuccesses = Number(prior.evidence.successfulOutcomes ?? 0);
|
|
135
|
+
const successfulOutcomes = previousSuccesses + (evidence.outcome === 'success' ? 1 : 0);
|
|
136
|
+
const previousVerified = Number(prior.evidence.verifiedSuccessfulOutcomes ?? 0);
|
|
137
|
+
const verifiedSuccessfulOutcomes = previousVerified + (evidence.verifiedOutcome && evidence.outcome === 'success' ? 1 : 0);
|
|
138
|
+
const repeatedValidation = shouldValidateInteractionLesson(evidence, verifiedSuccessfulOutcomes);
|
|
139
|
+
const mergedEvidence = {
|
|
140
|
+
...evidence,
|
|
141
|
+
observationCount: Number(prior.evidence.observationCount ?? 1) + 1,
|
|
142
|
+
successfulOutcomes,
|
|
143
|
+
verifiedSuccessfulOutcomes,
|
|
144
|
+
previousRequestId: prior.evidence.requestId,
|
|
145
|
+
};
|
|
146
|
+
const updated = {
|
|
147
|
+
...prior,
|
|
148
|
+
state: repeatedValidation ? 'validated' : 'candidate',
|
|
149
|
+
evidence: mergedEvidence,
|
|
150
|
+
confidence: repeatedValidation ? 0.8 : Math.min(0.7, prior.confidence + 0.15),
|
|
151
|
+
updatedAt: now,
|
|
152
|
+
};
|
|
153
|
+
await db.run('UPDATE lessons SET state = ?, evidence_json = ?, confidence = ?, updated_at = ? WHERE id = ?', updated.state, await encryptLocalPayload(updated.evidence), updated.confidence, updated.updatedAt, updated.id);
|
|
154
|
+
if (config)
|
|
155
|
+
await queueLessonSync(db, updated, now);
|
|
156
|
+
return prior.id;
|
|
157
|
+
}
|
|
158
|
+
const storedEvidence = {
|
|
159
|
+
...evidence,
|
|
160
|
+
observationCount: 1,
|
|
161
|
+
successfulOutcomes: evidence.outcome === 'success' ? 1 : 0,
|
|
162
|
+
verifiedSuccessfulOutcomes: evidence.verifiedOutcome && evidence.outcome === 'success' ? 1 : 0,
|
|
163
|
+
};
|
|
164
|
+
const encryptedEvidence = await encryptLocalPayload(storedEvidence);
|
|
165
|
+
await db.run(`INSERT INTO lessons (
|
|
166
|
+
id, repository_id, actor_id, team_id, visibility, state, kind, subject,
|
|
167
|
+
evidence_json, confidence, source, created_at, updated_at
|
|
168
|
+
) VALUES (?, ?, ?, ?, 'personal', ?, 'interaction', ?, ?, ?, 'mcp', ?, ?)`, id, repositoryId, actorId, config?.teamId || process.env.RIGOUR_TEAM_ID || null, state, evidence.reusableClaim?.trim() || `interaction:${evidence.tool}`, encryptedEvidence, confidence, now, now);
|
|
169
|
+
if (config) {
|
|
170
|
+
const personal = await txSafeLesson(db, id);
|
|
171
|
+
if (personal)
|
|
172
|
+
await queueLessonSync(db, personal, now);
|
|
173
|
+
}
|
|
174
|
+
return id;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
await db.close();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function queueLessonSync(db, lesson, now) {
|
|
181
|
+
await db.run(`INSERT INTO sync_outbox (id, operation, entity_type, entity_id, payload_json, created_at)
|
|
182
|
+
VALUES (?, 'upsert', 'lesson', ?, ?, ?)`, `outbox-${randomUUID()}`, lesson.id, await encryptLocalPayload(lesson), now);
|
|
183
|
+
}
|
|
184
|
+
export async function listLessons(cwd, limit = 100) {
|
|
185
|
+
let db;
|
|
186
|
+
try {
|
|
187
|
+
db = await openDatabase();
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
if (!db)
|
|
193
|
+
return [];
|
|
194
|
+
try {
|
|
195
|
+
const repositoryId = await getRepositoryId(cwd);
|
|
196
|
+
await registerRepository(db, cwd, repositoryId);
|
|
197
|
+
const rows = await db.all(`SELECT lessons.*, repositories.display_name AS repository_name
|
|
198
|
+
FROM lessons LEFT JOIN repositories ON repositories.id = lessons.repository_id
|
|
199
|
+
WHERE repository_id = ?
|
|
200
|
+
ORDER BY updated_at DESC LIMIT ?`, repositoryId, limit);
|
|
201
|
+
return await Promise.all(rows.map(rowToLesson));
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
await db.close();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Returns the evidence visible to the current workspace for exploration. Cross-repository
|
|
209
|
+
* records are candidates only; getApplicableLessons remains the enforcement boundary.
|
|
210
|
+
*/
|
|
211
|
+
export async function listKnowledgeLessons(cwd, limit = 500) {
|
|
212
|
+
let db;
|
|
213
|
+
try {
|
|
214
|
+
db = await openDatabase();
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
if (!db)
|
|
220
|
+
return [];
|
|
221
|
+
try {
|
|
222
|
+
const repositoryId = await getRepositoryId(cwd);
|
|
223
|
+
await registerRepository(db, cwd, repositoryId);
|
|
224
|
+
const config = await loadTeamConfiguration();
|
|
225
|
+
const rows = config
|
|
226
|
+
? await db.all(`SELECT lessons.*, repositories.display_name AS repository_name
|
|
227
|
+
FROM lessons LEFT JOIN repositories ON repositories.id = lessons.repository_id
|
|
228
|
+
WHERE lessons.repository_id = ?
|
|
229
|
+
OR (lessons.visibility = 'personal' AND lessons.actor_id = ?)
|
|
230
|
+
OR (lessons.visibility = 'team' AND lessons.team_id = ? AND lessons.state = 'promoted')
|
|
231
|
+
ORDER BY lessons.updated_at DESC LIMIT ?`, repositoryId, config.actorId, config.teamId, limit)
|
|
232
|
+
: await db.all(`SELECT lessons.*, repositories.display_name AS repository_name
|
|
233
|
+
FROM lessons LEFT JOIN repositories ON repositories.id = lessons.repository_id
|
|
234
|
+
WHERE lessons.repository_id = ?
|
|
235
|
+
OR (lessons.visibility = 'personal' AND lessons.actor_id IS NULL)
|
|
236
|
+
ORDER BY lessons.updated_at DESC LIMIT ?`, repositoryId, limit);
|
|
237
|
+
return await Promise.all(rows.map(rowToLesson));
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
await db.close();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
export async function getApplicableLessons(cwd, actorId, teamId) {
|
|
244
|
+
const repositoryId = await getRepositoryId(cwd);
|
|
245
|
+
const all = await listKnowledgeLessons(cwd, 500);
|
|
246
|
+
const applicable = all
|
|
247
|
+
.filter(lesson => lesson.state === 'validated' || lesson.state === 'promoted')
|
|
248
|
+
.filter(lesson => lesson.visibility === 'team' || !actorId || !lesson.actorId || lesson.actorId === actorId)
|
|
249
|
+
.filter(lesson => !teamId || !lesson.teamId || lesson.teamId === teamId)
|
|
250
|
+
.map(lesson => {
|
|
251
|
+
const priority = lesson.repositoryId === repositoryId ? 1 : lesson.visibility === 'personal' ? 2 : 3;
|
|
252
|
+
return { ...lesson, priority, provenance: `${lesson.visibility}:${lesson.source}:${lesson.id}` };
|
|
253
|
+
})
|
|
254
|
+
.sort((a, b) => a.priority - b.priority || b.confidence - a.confidence);
|
|
255
|
+
const bySubject = new Map();
|
|
256
|
+
for (const lesson of applicable)
|
|
257
|
+
bySubject.set(lesson.subject, [...(bySubject.get(lesson.subject) ?? []), lesson]);
|
|
258
|
+
const conflicts = [...bySubject.entries()]
|
|
259
|
+
.filter(([, lessons]) => new Set(lessons.map(lesson => JSON.stringify(lesson.evidence))).size > 1)
|
|
260
|
+
.map(([subject, lessons]) => ({ subject, lessonIds: lessons.map(lesson => lesson.id) }));
|
|
261
|
+
return { lessons: applicable, conflicts };
|
|
262
|
+
}
|
|
263
|
+
export async function transitionLesson(id, state, options = {}) {
|
|
264
|
+
const db = await openDatabase();
|
|
265
|
+
if (!db)
|
|
266
|
+
return false;
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
try {
|
|
269
|
+
return await db.transaction(async (tx) => {
|
|
270
|
+
const existing = await tx.get('SELECT * FROM lessons WHERE id = ?', id);
|
|
271
|
+
if (!existing)
|
|
272
|
+
return false;
|
|
273
|
+
const visibility = options.visibility ?? existing.visibility;
|
|
274
|
+
if (state === 'promoted' && visibility === 'team') {
|
|
275
|
+
const teamConfig = await loadTeamConfiguration();
|
|
276
|
+
if (!teamConfig) {
|
|
277
|
+
throw new Error('Configure PostgreSQL team mode before publishing a team lesson.');
|
|
278
|
+
}
|
|
279
|
+
const source = await rowToLesson(existing);
|
|
280
|
+
const publishedId = `lesson-${randomUUID()}`;
|
|
281
|
+
const published = {
|
|
282
|
+
...source,
|
|
283
|
+
id: publishedId,
|
|
284
|
+
actorId: source.actorId || teamConfig.actorId,
|
|
285
|
+
teamId: teamConfig.teamId,
|
|
286
|
+
visibility: 'team',
|
|
287
|
+
state: 'promoted',
|
|
288
|
+
source: `published:${source.id}`,
|
|
289
|
+
createdAt: now,
|
|
290
|
+
updatedAt: now,
|
|
291
|
+
};
|
|
292
|
+
await tx.run(`INSERT INTO lessons (
|
|
293
|
+
id, repository_id, actor_id, team_id, visibility, state, kind, subject,
|
|
294
|
+
evidence_json, confidence, source, supersedes_id, created_at, updated_at
|
|
295
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, published.id, published.repositoryId, published.actorId || null, published.teamId || null, published.visibility, published.state, published.kind, published.subject, await encryptLocalPayload(published.evidence), published.confidence, published.source, published.supersedesId || null, published.createdAt, published.updatedAt);
|
|
296
|
+
if (options.queueSync) {
|
|
297
|
+
await tx.run(`INSERT INTO sync_outbox (id, operation, entity_type, entity_id, payload_json, created_at)
|
|
298
|
+
VALUES (?, 'upsert', 'lesson', ?, ?, ?)`, `outbox-${randomUUID()}`, published.id, await encryptLocalPayload(published), now);
|
|
299
|
+
}
|
|
300
|
+
await tx.run("DELETE FROM context_cache WHERE cache_type = 'semantic'");
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
const result = await tx.run('UPDATE lessons SET state = ?, visibility = ?, updated_at = ? WHERE id = ?', state, visibility, now, id);
|
|
304
|
+
if (result.changes > 0)
|
|
305
|
+
await tx.run("DELETE FROM context_cache WHERE cache_type = 'semantic'");
|
|
306
|
+
if (result.changes > 0 && options.queueSync && visibility === 'team') {
|
|
307
|
+
const payload = { ...await rowToLesson(existing), state, visibility, updatedAt: now };
|
|
308
|
+
await tx.run(`INSERT INTO sync_outbox (
|
|
309
|
+
id, operation, entity_type, entity_id, payload_json, created_at
|
|
310
|
+
) VALUES (?, 'upsert', 'lesson', ?, ?, ?)`, `outbox-${randomUUID()}`, id, await encryptLocalPayload(payload), now);
|
|
311
|
+
}
|
|
312
|
+
return result.changes > 0;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
await db.close();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function txSafeLesson(db, id) {
|
|
320
|
+
const row = await db.get('SELECT * FROM lessons WHERE id = ?', id);
|
|
321
|
+
return row ? rowToLesson(row) : null;
|
|
322
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { getRepositoryId, shouldValidateInteractionLesson } from './lessons.js';
|
|
6
|
+
const roots = [];
|
|
7
|
+
afterEach(async () => { await Promise.all(roots.splice(0).map((root) => fs.remove(root))); });
|
|
8
|
+
describe('getRepositoryId', () => {
|
|
9
|
+
it('uses normalized origin identity instead of a basename', async () => {
|
|
10
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-repo-id-'));
|
|
11
|
+
roots.push(root);
|
|
12
|
+
await fs.ensureDir(path.join(root, '.git'));
|
|
13
|
+
await fs.writeFile(path.join(root, '.git', 'config'), '[remote "origin"]\nurl = git@github.com:Rigour-Labs/Rigour.git\n');
|
|
14
|
+
const first = await getRepositoryId(root);
|
|
15
|
+
const second = await getRepositoryId(path.join(root, '..', path.basename(root)));
|
|
16
|
+
expect(first).toBe(second);
|
|
17
|
+
expect(first).toHaveLength(64);
|
|
18
|
+
});
|
|
19
|
+
it('uses the shared origin identity from a linked worktree', async () => {
|
|
20
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-worktree-id-'));
|
|
21
|
+
roots.push(root);
|
|
22
|
+
const common = path.join(root, 'common');
|
|
23
|
+
const linkedGit = path.join(common, 'worktrees', 'feature');
|
|
24
|
+
const worktree = path.join(root, 'checkout');
|
|
25
|
+
await fs.ensureDir(linkedGit);
|
|
26
|
+
await fs.ensureDir(worktree);
|
|
27
|
+
await fs.writeFile(path.join(worktree, '.git'), `gitdir: ${linkedGit}\n`);
|
|
28
|
+
await fs.writeFile(path.join(linkedGit, 'commondir'), '../..\n');
|
|
29
|
+
await fs.writeFile(path.join(common, 'config'), '[remote "origin"]\nurl = https://github.com/rigour-labs/rigour.git\n');
|
|
30
|
+
const regular = path.join(root, 'regular');
|
|
31
|
+
await fs.ensureDir(path.join(regular, '.git'));
|
|
32
|
+
await fs.writeFile(path.join(regular, '.git', 'config'), '[remote "origin"]\nurl = git@github.com:Rigour-Labs/Rigour.git\n');
|
|
33
|
+
expect(await getRepositoryId(worktree)).toBe(await getRepositoryId(regular));
|
|
34
|
+
expect(await getRepositoryId(worktree)).toHaveLength(64);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('interaction lesson validation', () => {
|
|
38
|
+
const evidence = { tool: 'rigour_context_scope', outcome: 'success', requestId: 'req-1' };
|
|
39
|
+
it('keeps repeated unverified interaction as evidence only', () => {
|
|
40
|
+
expect(shouldValidateInteractionLesson(evidence, 0)).toBe(false);
|
|
41
|
+
expect(shouldValidateInteractionLesson(evidence, 2)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
it('validates deterministic or repeatedly verified outcomes', () => {
|
|
44
|
+
expect(shouldValidateInteractionLesson({ ...evidence, deterministic: true, reusableClaim: 'Use scoped retrieval before edits.' }, 0)).toBe(true);
|
|
45
|
+
expect(shouldValidateInteractionLesson({ ...evidence, verifiedOutcome: true, reusableClaim: 'Run the focused test suite.' }, 3)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
it('does not turn a successful tool call into reusable knowledge', () => {
|
|
48
|
+
expect(shouldValidateInteractionLesson({ ...evidence, deterministic: true }, 3)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
const KEY_PATH = path.join(os.homedir(), '.rigour', 'team-cache.key');
|
|
6
|
+
async function getKey() {
|
|
7
|
+
const configured = process.env.RIGOUR_LOCAL_CACHE_KEY;
|
|
8
|
+
if (configured) {
|
|
9
|
+
const key = Buffer.from(configured, 'base64');
|
|
10
|
+
if (key.length !== 32)
|
|
11
|
+
throw new Error('RIGOUR_LOCAL_CACHE_KEY must be a base64-encoded 32-byte key.');
|
|
12
|
+
return key;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const key = Buffer.from((await fs.readFile(KEY_PATH, 'utf8')).trim(), 'base64');
|
|
16
|
+
if (key.length === 32)
|
|
17
|
+
return key;
|
|
18
|
+
}
|
|
19
|
+
catch { /* create below */ }
|
|
20
|
+
const key = randomBytes(32);
|
|
21
|
+
await fs.ensureDir(path.dirname(KEY_PATH));
|
|
22
|
+
await fs.writeFile(KEY_PATH, key.toString('base64'), { mode: 0o600 });
|
|
23
|
+
await fs.chmod(KEY_PATH, 0o600);
|
|
24
|
+
return key;
|
|
25
|
+
}
|
|
26
|
+
export async function encryptLocalPayload(value) {
|
|
27
|
+
const key = await getKey();
|
|
28
|
+
const iv = randomBytes(12);
|
|
29
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
30
|
+
const ciphertext = Buffer.concat([cipher.update(JSON.stringify(value), 'utf8'), cipher.final()]);
|
|
31
|
+
return `enc:v1:${iv.toString('base64')}:${cipher.getAuthTag().toString('base64')}:${ciphertext.toString('base64')}`;
|
|
32
|
+
}
|
|
33
|
+
export async function decryptLocalPayload(value) {
|
|
34
|
+
if (!value.startsWith('enc:v1:'))
|
|
35
|
+
return JSON.parse(value);
|
|
36
|
+
const [, , ivValue, tagValue, ciphertextValue] = value.split(':');
|
|
37
|
+
const decipher = createDecipheriv('aes-256-gcm', await getKey(), Buffer.from(ivValue, 'base64'));
|
|
38
|
+
decipher.setAuthTag(Buffer.from(tagValue, 'base64'));
|
|
39
|
+
const plaintext = Buffer.concat([decipher.update(Buffer.from(ciphertextValue, 'base64')), decipher.final()]);
|
|
40
|
+
return JSON.parse(plaintext.toString('utf8'));
|
|
41
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface TeamConfiguration {
|
|
2
|
+
organizationId: string;
|
|
3
|
+
teamId: string;
|
|
4
|
+
actorId: string;
|
|
5
|
+
databaseUrl?: string;
|
|
6
|
+
semantic?: TeamSemanticConfiguration;
|
|
7
|
+
}
|
|
8
|
+
export interface TeamSemanticConfiguration {
|
|
9
|
+
provider: 'pgvector';
|
|
10
|
+
model: 'Xenova/all-MiniLM-L6-v2';
|
|
11
|
+
dimensions: 384;
|
|
12
|
+
}
|
|
13
|
+
export interface TeamModeStatus {
|
|
14
|
+
mode: 'local' | 'team';
|
|
15
|
+
connectivity: 'local' | 'online' | 'offline';
|
|
16
|
+
organizationId?: string;
|
|
17
|
+
teamId?: string;
|
|
18
|
+
actorId?: string;
|
|
19
|
+
queuedChanges: number;
|
|
20
|
+
message: string;
|
|
21
|
+
semantic?: {
|
|
22
|
+
provider: 'pgvector';
|
|
23
|
+
status: 'ready' | 'degraded';
|
|
24
|
+
extensionVersion?: string;
|
|
25
|
+
model: string;
|
|
26
|
+
dimensions: number;
|
|
27
|
+
indexedLessons?: number;
|
|
28
|
+
missingLessons?: number;
|
|
29
|
+
message?: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface TeamDoctorResult extends TeamModeStatus {
|
|
33
|
+
schemaVersion?: number;
|
|
34
|
+
databaseRole?: string;
|
|
35
|
+
permissions?: string[];
|
|
36
|
+
}
|
|
37
|
+
export interface SemanticKnowledgeCandidate {
|
|
38
|
+
lessonId: string;
|
|
39
|
+
repositoryId: string;
|
|
40
|
+
subject: string;
|
|
41
|
+
visibility: string;
|
|
42
|
+
state: string;
|
|
43
|
+
confidence: number;
|
|
44
|
+
similarity: number;
|
|
45
|
+
provenance: string;
|
|
46
|
+
}
|
|
47
|
+
export interface SemanticKnowledgeResult {
|
|
48
|
+
status: 'disabled' | 'ready' | 'degraded';
|
|
49
|
+
provider?: 'pgvector';
|
|
50
|
+
model?: string;
|
|
51
|
+
candidates: SemanticKnowledgeCandidate[];
|
|
52
|
+
message?: string;
|
|
53
|
+
}
|
|
54
|
+
export declare const TEAM_CONFIG_PATH: string;
|
|
55
|
+
export declare function validateTeamDatabaseUrl(databaseUrl: string): void;
|
|
56
|
+
export declare function loadTeamConfiguration(): Promise<TeamConfiguration | null>;
|
|
57
|
+
export declare function saveTeamConfiguration(config: TeamConfiguration): Promise<void>;
|
|
58
|
+
export declare function initializeTeamSchema(databaseUrl: string, options?: {
|
|
59
|
+
pgvector?: boolean;
|
|
60
|
+
}): Promise<void>;
|
|
61
|
+
export declare function getTeamModeStatus(): Promise<TeamModeStatus>;
|
|
62
|
+
export declare function doctorTeamConnection(config?: TeamConfiguration): Promise<TeamDoctorResult>;
|
|
63
|
+
export declare function syncTeamOutbox(options?: {
|
|
64
|
+
dryRun?: boolean;
|
|
65
|
+
}): Promise<{
|
|
66
|
+
pending: number;
|
|
67
|
+
synced: number;
|
|
68
|
+
pulled: number;
|
|
69
|
+
}>;
|
|
70
|
+
export declare function backfillTeamEmbeddings(limit?: number): Promise<{
|
|
71
|
+
eligible: number;
|
|
72
|
+
embedded: number;
|
|
73
|
+
skipped: number;
|
|
74
|
+
}>;
|
|
75
|
+
export declare function searchTeamKnowledge(query: string, limit?: number): Promise<SemanticKnowledgeResult>;
|