@rigour-labs/core 6.5.2 → 6.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/context/automatic-index.d.ts +5 -0
- package/dist/context/automatic-index.js +24 -3
- package/dist/context/index-status.test.d.ts +1 -0
- package/dist/context/index-status.test.js +26 -0
- package/dist/gates/dedupe-failures.test.d.ts +1 -0
- package/dist/gates/dedupe-failures.test.js +12 -0
- package/dist/gates/deprecated-dependencies.d.ts +27 -0
- package/dist/gates/deprecated-dependencies.js +104 -0
- package/dist/gates/deprecated-dependencies.test.d.ts +1 -0
- package/dist/gates/deprecated-dependencies.test.js +55 -0
- package/dist/gates/runner.d.ts +7 -1
- package/dist/gates/runner.js +25 -11
- package/dist/gates/security-patterns-data.js +2 -1
- package/dist/gates/security-patterns-owasp.test.js +15 -0
- package/dist/gates/semantic-bugs.js +1 -1
- package/dist/gates/unindexed-reads/coverage.d.ts +15 -0
- package/dist/gates/unindexed-reads/coverage.js +24 -0
- package/dist/gates/unindexed-reads/index.d.ts +20 -0
- package/dist/gates/unindexed-reads/index.js +98 -0
- package/dist/gates/unindexed-reads/queries.d.ts +17 -0
- package/dist/gates/unindexed-reads/queries.js +147 -0
- package/dist/gates/unindexed-reads/queries.test.d.ts +1 -0
- package/dist/gates/unindexed-reads/queries.test.js +54 -0
- package/dist/gates/unindexed-reads/schema.d.ts +38 -0
- package/dist/gates/unindexed-reads/schema.js +256 -0
- package/dist/gates/unindexed-reads/schema.test.d.ts +1 -0
- package/dist/gates/unindexed-reads/schema.test.js +81 -0
- package/dist/gates/unindexed-reads/sql.d.ts +21 -0
- package/dist/gates/unindexed-reads/sql.js +131 -0
- package/dist/gates/unindexed-reads/unindexed-reads.test.d.ts +1 -0
- package/dist/gates/unindexed-reads/unindexed-reads.test.js +90 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/inference/brain-placeholder.test.d.ts +1 -0
- package/dist/inference/brain-placeholder.test.js +28 -0
- package/dist/inference/llama-engine-checksum.test.d.ts +1 -0
- package/dist/inference/llama-engine-checksum.test.js +27 -0
- package/dist/inference/llama-engine.d.ts +4 -1
- package/dist/inference/llama-engine.js +15 -8
- package/dist/memory/recall.d.ts +37 -0
- package/dist/memory/recall.js +93 -0
- package/dist/memory/recall.test.d.ts +1 -0
- package/dist/memory/recall.test.js +36 -0
- package/dist/pattern-index/assess.d.ts +16 -0
- package/dist/pattern-index/assess.js +37 -0
- package/dist/pattern-index/embeddings.d.ts +19 -0
- package/dist/pattern-index/embeddings.js +16 -0
- package/dist/pattern-index/index.d.ts +3 -2
- package/dist/pattern-index/index.js +4 -2
- package/dist/pattern-index/indexer.js +15 -7
- package/dist/pattern-index/matcher.d.ts +6 -0
- package/dist/pattern-index/matcher.js +13 -3
- package/dist/pattern-index/pattern-reuse.test.d.ts +1 -0
- package/dist/pattern-index/pattern-reuse.test.js +76 -0
- package/dist/pattern-index/staleness.d.ts +2 -15
- package/dist/pattern-index/staleness.js +1 -78
- package/dist/pattern-index/types.d.ts +2 -0
- package/dist/review/agent-fixes.d.ts +4 -0
- package/dist/review/agent-fixes.js +10 -7
- package/dist/review/agent-fixes.test.js +4 -4
- package/dist/review/changed-lines.d.ts +2 -1
- package/dist/review/changed-lines.js +12 -2
- package/dist/review/check-outcomes.d.ts +31 -0
- package/dist/review/check-outcomes.js +81 -0
- package/dist/review/check-outcomes.test.d.ts +1 -0
- package/dist/review/check-outcomes.test.js +56 -0
- package/dist/review/migration-order.d.ts +5 -0
- package/dist/review/migration-order.js +70 -0
- package/dist/review/migration-order.test.d.ts +1 -0
- package/dist/review/migration-order.test.js +62 -0
- package/dist/review/quiet.d.ts +2 -0
- package/dist/review/quiet.js +12 -3
- package/dist/review/review.d.ts +2 -0
- package/dist/review/review.js +7 -2
- package/dist/review/review.test.js +11 -0
- package/dist/semantic/ast.d.ts +1 -0
- package/dist/semantic/ast.js +4 -0
- package/dist/semantic/learn/compile.js +3 -1
- package/dist/semantic/learn/learn.js +40 -5
- package/dist/semantic/types.d.ts +2 -0
- package/dist/services/context-scope-summary.test.d.ts +1 -0
- package/dist/services/context-scope-summary.test.js +17 -0
- package/dist/services/context-telemetry-service.js +2 -1
- package/dist/storage/context-telemetry.js +33 -19
- package/dist/storage/db.js +52 -1
- package/dist/storage/findings.js +1 -1
- package/dist/storage/fix-lessons.d.ts +13 -0
- package/dist/storage/fix-lessons.js +84 -0
- package/dist/storage/fix-lessons.test.d.ts +1 -0
- package/dist/storage/fix-lessons.test.js +61 -0
- package/dist/storage/index.d.ts +2 -1
- package/dist/storage/index.js +2 -1
- package/dist/storage/lessons.d.ts +16 -2
- package/dist/storage/lessons.js +27 -61
- package/dist/storage/lessons.test.js +1 -15
- package/dist/storage/local-memory.js +39 -5
- package/dist/storage/local-memory.test.d.ts +1 -0
- package/dist/storage/local-memory.test.js +55 -0
- package/dist/storage/patterns.d.ts +1 -1
- package/dist/storage/patterns.js +1 -1
- package/dist/storage/share-memory.test.d.ts +1 -0
- package/dist/storage/share-memory.test.js +34 -0
- package/dist/storage/team-vector-store.js +2 -1
- package/dist/storage/telemetry-scope.test.d.ts +1 -0
- package/dist/storage/telemetry-scope.test.js +38 -0
- package/dist/templates/universal-config.js +11 -0
- package/dist/types/index.d.ts +132 -0
- package/dist/types/index.js +15 -0
- package/package.json +7 -7
|
@@ -6,6 +6,26 @@ import { randomUUID } from 'crypto';
|
|
|
6
6
|
import fs from 'fs-extra';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { openDatabase, isSQLiteAvailable } from './db.js';
|
|
9
|
+
import { getRepositoryId } from './lessons.js';
|
|
10
|
+
/** The repository a telemetry row belongs to, or null when no working directory was given. */
|
|
11
|
+
async function repositoryOf(cwd) {
|
|
12
|
+
return cwd ? getRepositoryId(cwd) : null;
|
|
13
|
+
}
|
|
14
|
+
/** WHERE clause for a telemetry read: the task when given, and this repository when cwd is given. */
|
|
15
|
+
async function scopedWhere(taskId, cwd) {
|
|
16
|
+
const clauses = [];
|
|
17
|
+
const params = [];
|
|
18
|
+
if (taskId) {
|
|
19
|
+
clauses.push('task_id = ?');
|
|
20
|
+
params.push(taskId);
|
|
21
|
+
}
|
|
22
|
+
const repositoryId = await repositoryOf(cwd);
|
|
23
|
+
if (repositoryId) {
|
|
24
|
+
clauses.push('repository_id = ?');
|
|
25
|
+
params.push(repositoryId);
|
|
26
|
+
}
|
|
27
|
+
return { where: clauses.length ? `WHERE ${clauses.join(' AND ')}` : '', params };
|
|
28
|
+
}
|
|
9
29
|
// Fallback JSON persistence path if SQLite is unavailable
|
|
10
30
|
function getFallbackDir(cwd) {
|
|
11
31
|
return path.join(cwd || process.cwd(), '.rigour');
|
|
@@ -27,8 +47,8 @@ export async function recordContextEvent(event, cwd) {
|
|
|
27
47
|
if (db) {
|
|
28
48
|
await db.run(`INSERT INTO context_events (
|
|
29
49
|
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);
|
|
50
|
+
candidate_tokens, returned_tokens, deduplicated_tokens, candidate_files, returned_files, created_at, repository_id
|
|
51
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, event.taskId || null, event.sessionId || null, event.agentId || null, event.toolName, event.queryHash || null, event.cacheStatus, candidateTokens, returnedTokens, deduplicatedTokens, candidateFiles, returnedFiles, timestamp, await repositoryOf(cwd));
|
|
32
52
|
await db.close();
|
|
33
53
|
return id;
|
|
34
54
|
}
|
|
@@ -56,8 +76,8 @@ export async function recordModelUsage(usage, cwd) {
|
|
|
56
76
|
if (db) {
|
|
57
77
|
await db.run(`INSERT INTO model_usage (
|
|
58
78
|
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);
|
|
79
|
+
input_tokens, output_tokens, cached_input_tokens, observed_cost_usd, source, created_at, repository_id
|
|
80
|
+
) 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, await repositoryOf(cwd));
|
|
61
81
|
await db.close();
|
|
62
82
|
return id;
|
|
63
83
|
}
|
|
@@ -234,12 +254,12 @@ export async function recordCheckpointMetric(metric, cwd) {
|
|
|
234
254
|
const db = await openDatabase();
|
|
235
255
|
if (db) {
|
|
236
256
|
await db.run(`INSERT INTO checkpoint_metrics (
|
|
237
|
-
checkpoint_id, task_id, agent_id, raw_state_tokens, checkpoint_tokens, replay_tokens_avoided, created_at
|
|
238
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
257
|
+
checkpoint_id, task_id, agent_id, raw_state_tokens, checkpoint_tokens, replay_tokens_avoided, created_at, repository_id
|
|
258
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
239
259
|
ON CONFLICT(checkpoint_id) DO UPDATE SET
|
|
240
260
|
raw_state_tokens=excluded.raw_state_tokens,
|
|
241
261
|
checkpoint_tokens=excluded.checkpoint_tokens,
|
|
242
|
-
replay_tokens_avoided=excluded.replay_tokens_avoided`, metric.checkpointId, metric.taskId, metric.agentId, metric.rawStateTokens, metric.checkpointTokens, metric.replayTokensAvoided, timestamp);
|
|
262
|
+
replay_tokens_avoided=excluded.replay_tokens_avoided`, metric.checkpointId, metric.taskId, metric.agentId, metric.rawStateTokens, metric.checkpointTokens, metric.replayTokensAvoided, timestamp, await repositoryOf(cwd));
|
|
243
263
|
await db.close();
|
|
244
264
|
return;
|
|
245
265
|
}
|
|
@@ -261,10 +281,8 @@ export async function getContextEvents(taskId, cwd) {
|
|
|
261
281
|
try {
|
|
262
282
|
const db = await openDatabase();
|
|
263
283
|
if (db) {
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
: `SELECT * FROM context_events ORDER BY created_at DESC`;
|
|
267
|
-
const params = taskId ? [taskId] : [];
|
|
284
|
+
const { where, params } = await scopedWhere(taskId, cwd);
|
|
285
|
+
const query = `SELECT * FROM context_events ${where} ORDER BY created_at DESC`;
|
|
268
286
|
const rows = await db.all(query, ...params);
|
|
269
287
|
await db.close();
|
|
270
288
|
return rows.map((r) => ({
|
|
@@ -303,10 +321,8 @@ export async function getModelUsages(taskId, cwd) {
|
|
|
303
321
|
try {
|
|
304
322
|
const db = await openDatabase();
|
|
305
323
|
if (db) {
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
: `SELECT * FROM model_usage ORDER BY created_at DESC`;
|
|
309
|
-
const params = taskId ? [taskId] : [];
|
|
324
|
+
const { where, params } = await scopedWhere(taskId, cwd);
|
|
325
|
+
const query = `SELECT * FROM model_usage ${where} ORDER BY created_at DESC`;
|
|
310
326
|
const rows = await db.all(query, ...params);
|
|
311
327
|
await db.close();
|
|
312
328
|
return rows.map((r) => ({
|
|
@@ -344,10 +360,8 @@ export async function getCheckpointMetrics(taskId, cwd) {
|
|
|
344
360
|
try {
|
|
345
361
|
const db = await openDatabase();
|
|
346
362
|
if (db) {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
: `SELECT * FROM checkpoint_metrics ORDER BY created_at DESC`;
|
|
350
|
-
const params = taskId ? [taskId] : [];
|
|
363
|
+
const { where, params } = await scopedWhere(taskId, cwd);
|
|
364
|
+
const query = `SELECT * FROM checkpoint_metrics ${where} ORDER BY created_at DESC`;
|
|
351
365
|
const rows = await db.all(query, ...params);
|
|
352
366
|
await db.close();
|
|
353
367
|
return rows.map((r) => ({
|
package/dist/storage/db.js
CHANGED
|
@@ -333,8 +333,11 @@ export async function openDatabase(dbPath) {
|
|
|
333
333
|
});
|
|
334
334
|
});
|
|
335
335
|
const db = wrapDatabase(raw);
|
|
336
|
+
// Wait for a lock instead of failing at once: a hook and an MCP server often open the database
|
|
337
|
+
// at the same moment, and without this the second open fails with SQLITE_BUSY.
|
|
338
|
+
await db.exec('PRAGMA busy_timeout = 5000');
|
|
336
339
|
// WAL mode for better concurrent read performance
|
|
337
|
-
await db
|
|
340
|
+
await enableWal(db);
|
|
338
341
|
await db.exec('PRAGMA foreign_keys = ON');
|
|
339
342
|
// Run schema creation + migrations
|
|
340
343
|
await db.exec(SCHEMA_SQL);
|
|
@@ -498,6 +501,54 @@ async function runMigrations(db) {
|
|
|
498
501
|
`);
|
|
499
502
|
await db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '7')");
|
|
500
503
|
}
|
|
504
|
+
if (current < 8) {
|
|
505
|
+
// Telemetry rows record their repository, so Studio shows this repository's numbers, not
|
|
506
|
+
// every repository's on the machine. Older rows stay NULL and are left out of per-repo views.
|
|
507
|
+
await addColumnIfMissing(db, 'context_events', 'repository_id', 'ALTER TABLE context_events ADD COLUMN repository_id TEXT');
|
|
508
|
+
await addColumnIfMissing(db, 'model_usage', 'repository_id', 'ALTER TABLE model_usage ADD COLUMN repository_id TEXT');
|
|
509
|
+
await addColumnIfMissing(db, 'checkpoint_metrics', 'repository_id', 'ALTER TABLE checkpoint_metrics ADD COLUMN repository_id TEXT');
|
|
510
|
+
await db.exec(`
|
|
511
|
+
CREATE INDEX IF NOT EXISTS idx_context_events_repo ON context_events(repository_id, created_at);
|
|
512
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_repo ON model_usage(repository_id, created_at);
|
|
513
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoint_metrics_repo ON checkpoint_metrics(repository_id, created_at);
|
|
514
|
+
`);
|
|
515
|
+
await db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '8')");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Switch to WAL. On a new database, two connections switching at once make the second fail with
|
|
520
|
+
* SQLITE_BUSY at once (busy_timeout does not cover a journal-mode change), so retry briefly; the
|
|
521
|
+
* mode is persistent, so once either succeeds, every later open finds WAL already set.
|
|
522
|
+
*/
|
|
523
|
+
async function enableWal(db, attempts = 10) {
|
|
524
|
+
for (let attempt = 1;; attempt++) {
|
|
525
|
+
try {
|
|
526
|
+
await db.exec('PRAGMA journal_mode = WAL');
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
if (!/SQLITE_BUSY/.test(String(error)) || attempt >= attempts)
|
|
531
|
+
throw error;
|
|
532
|
+
await new Promise(resolve => setTimeout(resolve, 25 * attempt));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Add a column unless it is there: a hook and an MCP server can open the database at once, and
|
|
538
|
+
* both may run the same migration; a second plain ALTER would fail with "duplicate column".
|
|
539
|
+
*/
|
|
540
|
+
async function addColumnIfMissing(db, table, column, alterSql) {
|
|
541
|
+
// The table is a bound parameter and the ALTER a literal written by the caller: nothing is interpolated.
|
|
542
|
+
const columns = await db.all('SELECT name FROM pragma_table_info(?)', table);
|
|
543
|
+
if (columns.some((c) => c.name === column))
|
|
544
|
+
return;
|
|
545
|
+
try {
|
|
546
|
+
await db.exec(alterSql);
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
if (!/duplicate column/i.test(String(error)))
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
501
552
|
}
|
|
502
553
|
/**
|
|
503
554
|
* Compact the database — prune old data, reclaim disk space.
|
package/dist/storage/findings.js
CHANGED
|
@@ -25,7 +25,7 @@ export async function getFindingsForScan(store, scanId) {
|
|
|
25
25
|
* Used by local memory to match known patterns against new scans.
|
|
26
26
|
*/
|
|
27
27
|
export async function getDeepFindings(store, repo, limit = 50) {
|
|
28
|
-
return store.all(`SELECT f
|
|
28
|
+
return store.all(`SELECT f.*, s.timestamp AS scan_time FROM findings f
|
|
29
29
|
JOIN scans s ON f.scan_id = s.id
|
|
30
30
|
WHERE s.repo = ? AND (f.source = 'llm' OR f.source = 'hybrid' OR f.confidence >= 0.7)
|
|
31
31
|
ORDER BY f.confidence DESC LIMIT ?`, repo, limit);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ResolvedFix } from '../review/agent-fixes.js';
|
|
2
|
+
/** Distinct files a kind of fix must reach before its lesson is validated. */
|
|
3
|
+
export declare const FILES_TO_VALIDATE = 2;
|
|
4
|
+
/** Groups fixes of one kind of defect: every lesson subject starts with it. */
|
|
5
|
+
export declare function fixLessonPrefix(fix: Pick<ResolvedFix, 'rule' | 'title'>): string;
|
|
6
|
+
/**
|
|
7
|
+
* What a lesson says, and what it is embedded and matched by: the kind of defect, then what was
|
|
8
|
+
* wrong in the first fix. The detail is what lets a task match it by meaning (a direct match
|
|
9
|
+
* scored 0.37 with it and 0.05 with the title alone).
|
|
10
|
+
*/
|
|
11
|
+
export declare function fixLessonSubject(fix: Pick<ResolvedFix, 'rule' | 'title' | 'details'>): string;
|
|
12
|
+
/** Record resolved fixes as lesson evidence. Returns the ids of lessons created or updated. */
|
|
13
|
+
export declare function recordFixLessons(cwd: string, fixes: ResolvedFix[]): Promise<string[]>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lessons from fixes: when an agent fixes a finding Rigour reported, that fix
|
|
3
|
+
* becomes evidence for a lesson of kind `fix`, one per kind of defect.
|
|
4
|
+
*
|
|
5
|
+
* A lesson starts as a personal candidate and is validated by evidence alone:
|
|
6
|
+
* fixes of the same kind landing in two different files make it a pattern
|
|
7
|
+
* rather than a one-off (a repeat fix in one file may be the same bug
|
|
8
|
+
* reintroduced). Validated lessons are what team knowledge search serves and
|
|
9
|
+
* what pgvector embeds; a person can still promote one to the whole team.
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
12
|
+
import { openDatabase } from './db.js';
|
|
13
|
+
import { decryptLocalPayload, encryptLocalPayload } from './local-encryption.js';
|
|
14
|
+
import { getRepositoryId, queueLessonSync, registerRepository, txSafeLesson } from './lessons.js';
|
|
15
|
+
import { loadTeamConfiguration } from './team-store.js';
|
|
16
|
+
/** Distinct files a kind of fix must reach before its lesson is validated. */
|
|
17
|
+
export const FILES_TO_VALIDATE = 2;
|
|
18
|
+
/** Example fixes kept per lesson; older ones still count through `files`. */
|
|
19
|
+
const MAX_EXAMPLES = 5;
|
|
20
|
+
/** Groups fixes of one kind of defect: every lesson subject starts with it. */
|
|
21
|
+
export function fixLessonPrefix(fix) {
|
|
22
|
+
return `Fixed before: ${fix.title?.trim() || fix.rule} (${fix.rule}).`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What a lesson says, and what it is embedded and matched by: the kind of defect, then what was
|
|
26
|
+
* wrong in the first fix. The detail is what lets a task match it by meaning (a direct match
|
|
27
|
+
* scored 0.37 with it and 0.05 with the title alone).
|
|
28
|
+
*/
|
|
29
|
+
export function fixLessonSubject(fix) {
|
|
30
|
+
const detail = fix.details?.trim().split(/(?<=[.!?])\s/)[0]?.slice(0, 240);
|
|
31
|
+
return detail ? `${fixLessonPrefix(fix)} ${detail}` : fixLessonPrefix(fix);
|
|
32
|
+
}
|
|
33
|
+
/** Record resolved fixes as lesson evidence. Returns the ids of lessons created or updated. */
|
|
34
|
+
export async function recordFixLessons(cwd, fixes) {
|
|
35
|
+
if (fixes.length === 0)
|
|
36
|
+
return [];
|
|
37
|
+
const db = await openDatabase();
|
|
38
|
+
if (!db)
|
|
39
|
+
return [];
|
|
40
|
+
try {
|
|
41
|
+
const repositoryId = await getRepositoryId(cwd);
|
|
42
|
+
await registerRepository(db, cwd, repositoryId);
|
|
43
|
+
const config = await loadTeamConfiguration();
|
|
44
|
+
const ids = [];
|
|
45
|
+
for (const fix of fixes)
|
|
46
|
+
ids.push(await recordOne(db, repositoryId, fix, config));
|
|
47
|
+
return [...new Set(ids)];
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await db.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function recordOne(db, repositoryId, fix, config) {
|
|
54
|
+
const subject = fixLessonSubject(fix);
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
const row = await db.get(`SELECT id, evidence_json FROM lessons WHERE repository_id = ? AND kind = 'fix' AND substr(subject, 1, ?) = ?`, repositoryId, fixLessonPrefix(fix).length, fixLessonPrefix(fix));
|
|
57
|
+
const prior = row ? await decryptLocalPayload(row.evidence_json) : { rule: fix.rule, files: [], examples: [] };
|
|
58
|
+
if (prior.examples.some(e => e.fixId === fix.id))
|
|
59
|
+
return row.id;
|
|
60
|
+
const evidence = {
|
|
61
|
+
rule: fix.rule,
|
|
62
|
+
files: [...new Set([...prior.files, fix.file])],
|
|
63
|
+
examples: [{ fixId: fix.id, file: fix.file, details: fix.details?.slice(0, 300), resolvedAt: fix.resolvedAt }, ...prior.examples].slice(0, MAX_EXAMPLES),
|
|
64
|
+
};
|
|
65
|
+
const validated = evidence.files.length >= FILES_TO_VALIDATE;
|
|
66
|
+
const confidence = validated ? 0.8 : 0.4;
|
|
67
|
+
const id = row?.id ?? `lesson-${randomUUID()}`;
|
|
68
|
+
if (row) {
|
|
69
|
+
await db.run(`UPDATE lessons SET state = CASE WHEN state IN ('promoted', 'rejected') THEN state ELSE ? END,
|
|
70
|
+
evidence_json = ?, confidence = MAX(confidence, ?), updated_at = ? WHERE id = ?`, validated ? 'validated' : 'candidate', await encryptLocalPayload(evidence), confidence, now, id);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
await db.run(`INSERT INTO lessons (
|
|
74
|
+
id, repository_id, actor_id, team_id, visibility, state, kind, subject,
|
|
75
|
+
evidence_json, confidence, source, created_at, updated_at
|
|
76
|
+
) VALUES (?, ?, ?, ?, 'personal', ?, 'fix', ?, ?, ?, 'agent-fix', ?, ?)`, id, repositoryId, config?.actorId || process.env.RIGOUR_ACTOR_ID || null, config?.teamId || process.env.RIGOUR_TEAM_ID || null, validated ? 'validated' : 'candidate', subject, await encryptLocalPayload(evidence), confidence, now, now);
|
|
77
|
+
}
|
|
78
|
+
if (config) {
|
|
79
|
+
const lesson = await txSafeLesson(db, id);
|
|
80
|
+
if (lesson)
|
|
81
|
+
await queueLessonSync(db, lesson, now);
|
|
82
|
+
}
|
|
83
|
+
return id;
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
/** An in-memory lessons table, enough for the reads and writes fix lessons make. */
|
|
3
|
+
const db = vi.hoisted(() => ({ rows: new Map(), reset() { this.rows = new Map(); } }));
|
|
4
|
+
vi.mock('./db.js', () => {
|
|
5
|
+
const store = {
|
|
6
|
+
run: async (sql, ...p) => {
|
|
7
|
+
if (sql.includes('INSERT INTO lessons'))
|
|
8
|
+
db.rows.set(p[0], { id: p[0], state: p[4], subject: p[5], evidence_json: p[6], confidence: p[7] });
|
|
9
|
+
if (sql.includes('UPDATE lessons')) {
|
|
10
|
+
const row = db.rows.get(p[4]);
|
|
11
|
+
if (!['promoted', 'rejected'].includes(row.state))
|
|
12
|
+
row.state = p[0];
|
|
13
|
+
Object.assign(row, { evidence_json: p[1], confidence: Math.max(row.confidence, p[2]) });
|
|
14
|
+
}
|
|
15
|
+
return { changes: 1, lastID: 0 };
|
|
16
|
+
},
|
|
17
|
+
get: async (sql, ...p) => (sql.includes("kind = 'fix'") ? [...db.rows.values()].find(r => r.subject.slice(0, p[1]) === p[2]) : undefined),
|
|
18
|
+
all: async () => [],
|
|
19
|
+
exec: async () => undefined,
|
|
20
|
+
close: async () => undefined,
|
|
21
|
+
};
|
|
22
|
+
return { openDatabase: async () => store };
|
|
23
|
+
});
|
|
24
|
+
vi.mock('./local-encryption.js', () => ({ encryptLocalPayload: async (v) => JSON.stringify(v), decryptLocalPayload: async (v) => JSON.parse(v) }));
|
|
25
|
+
vi.mock('./team-store.js', () => ({ loadTeamConfiguration: async () => null }));
|
|
26
|
+
const { recordFixLessons, fixLessonSubject } = await import('./fix-lessons.js');
|
|
27
|
+
const fix = (id, file) => ({
|
|
28
|
+
id, file, rule: 'semantic-bugs', title: 'Unbounded read without paging', details: 'Rows read without a limit.', before: 'a', after: 'b', resolvedAt: '2026-10-02T00:00:00Z',
|
|
29
|
+
});
|
|
30
|
+
const lessons = () => [...db.rows.values()];
|
|
31
|
+
beforeEach(() => db.reset());
|
|
32
|
+
describe('recordFixLessons', () => {
|
|
33
|
+
it('turns a fix into a candidate lesson named after the defect it removed', async () => {
|
|
34
|
+
await recordFixLessons(process.cwd(), [fix('f1', 'src/orders.ts')]);
|
|
35
|
+
expect(lessons().map(l => [l.subject, l.state])).toEqual([['Fixed before: Unbounded read without paging (semantic-bugs). Rows read without a limit.', 'candidate']]);
|
|
36
|
+
});
|
|
37
|
+
it('validates the lesson once fixes of that kind reach a second file, and counts the same fix once', async () => {
|
|
38
|
+
await recordFixLessons(process.cwd(), [fix('f1', 'src/orders.ts')]);
|
|
39
|
+
await recordFixLessons(process.cwd(), [fix('f1', 'src/orders.ts'), fix('f2', 'src/orders.ts')]);
|
|
40
|
+
expect(lessons()[0].state).toBe('candidate');
|
|
41
|
+
await recordFixLessons(process.cwd(), [fix('f3', 'src/invoices.ts')]);
|
|
42
|
+
expect(lessons()).toHaveLength(1);
|
|
43
|
+
expect(lessons()[0].state).toBe('validated');
|
|
44
|
+
expect(JSON.parse(lessons()[0].evidence_json).files).toEqual(['src/orders.ts', 'src/invoices.ts']);
|
|
45
|
+
});
|
|
46
|
+
it("never overrides a person's decision to promote or reject", async () => {
|
|
47
|
+
await recordFixLessons(process.cwd(), [fix('f1', 'src/orders.ts')]);
|
|
48
|
+
lessons()[0].state = 'rejected';
|
|
49
|
+
await recordFixLessons(process.cwd(), [fix('f2', 'src/invoices.ts')]);
|
|
50
|
+
expect(lessons()[0].state).toBe('rejected');
|
|
51
|
+
});
|
|
52
|
+
it('names a fix whose finding had no title after its rule', () => {
|
|
53
|
+
expect(fixLessonSubject({ rule: 'hallucinated-imports' })).toBe('Fixed before: hallucinated-imports (hallucinated-imports).');
|
|
54
|
+
});
|
|
55
|
+
it('groups fixes of one kind even when their details differ', async () => {
|
|
56
|
+
await recordFixLessons(process.cwd(), [fix('f1', 'src/orders.ts')]);
|
|
57
|
+
await recordFixLessons(process.cwd(), [{ ...fix('f2', 'src/invoices.ts'), details: 'Invoices read in full to count them.' }]);
|
|
58
|
+
expect(lessons()).toHaveLength(1);
|
|
59
|
+
expect(lessons()[0].state).toBe('validated');
|
|
60
|
+
});
|
|
61
|
+
});
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './loca
|
|
|
11
11
|
export type { ProjectStats } from './local-memory.js';
|
|
12
12
|
export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, listContextCacheRecords, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics } from './context-telemetry.js';
|
|
13
13
|
export type { ContextEvent, ModelUsage, ContextCacheRecord, CheckpointMetric } from './context-telemetry.js';
|
|
14
|
-
export {
|
|
14
|
+
export { recordFixLessons, fixLessonSubject, fixLessonPrefix, FILES_TO_VALIDATE } from './fix-lessons.js';
|
|
15
|
+
export { recordInteractionEvidence, countInteractionEvidence, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId, shareMemoryLesson } from './lessons.js';
|
|
15
16
|
export type { ApplicableLessons, InteractionEvidence, LessonRecord, LessonState, LessonVisibility } from './lessons.js';
|
|
16
17
|
export { queueLocalLessonsForTeam } from './team-import.js';
|
|
17
18
|
export type { LocalLessonImportOptions, LocalLessonImportResult } from './team-import.js';
|
package/dist/storage/index.js
CHANGED
|
@@ -8,7 +8,8 @@ export { insertFindings, getFindingsForScan, getDeepFindings } from './findings.
|
|
|
8
8
|
export { reinforcePattern, decayPatterns, getStrongPatterns, getPatterns, getHardRules } from './patterns.js';
|
|
9
9
|
export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './local-memory.js';
|
|
10
10
|
export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, listContextCacheRecords, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics } from './context-telemetry.js';
|
|
11
|
-
export {
|
|
11
|
+
export { recordFixLessons, fixLessonSubject, fixLessonPrefix, FILES_TO_VALIDATE } from './fix-lessons.js';
|
|
12
|
+
export { recordInteractionEvidence, countInteractionEvidence, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId, shareMemoryLesson } from './lessons.js';
|
|
12
13
|
export { queueLocalLessonsForTeam } from './team-import.js';
|
|
13
14
|
export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './team-store.js';
|
|
14
15
|
export { explainTeamConnectionError } from './team-diagnostics.js';
|
|
@@ -32,11 +32,22 @@ export interface InteractionEvidence {
|
|
|
32
32
|
/** A concise, evidence-backed proposition that could be reused beyond this call. */
|
|
33
33
|
reusableClaim?: string;
|
|
34
34
|
}
|
|
35
|
-
export declare function shouldValidateInteractionLesson(evidence: InteractionEvidence, verifiedSuccessfulOutcomes?: number): boolean;
|
|
36
35
|
export declare function recordInteractionEvidence(cwd: string, evidence: InteractionEvidence): Promise<string | null>;
|
|
37
36
|
export declare function countInteractionEvidence(cwd: string): Promise<number>;
|
|
38
37
|
export declare function getRepositoryId(cwd: string): Promise<string>;
|
|
39
|
-
export declare function
|
|
38
|
+
export declare function registerRepository(db: {
|
|
39
|
+
run(sql: string, ...params: unknown[]): Promise<unknown>;
|
|
40
|
+
}, cwd: string, repositoryId: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Share a memory with the team: a lesson of kind `memory` whose subject is the memory itself.
|
|
43
|
+
* It starts as a team candidate, so teammates' agents only receive it once a person promotes it
|
|
44
|
+
* (team knowledge search serves team lessons in the `promoted` state). Returns the lesson id, or
|
|
45
|
+
* null when the local store is unavailable.
|
|
46
|
+
*/
|
|
47
|
+
export declare function shareMemoryLesson(cwd: string, key: string, value: string): Promise<string | null>;
|
|
48
|
+
export declare function queueLessonSync(db: {
|
|
49
|
+
run(sql: string, ...params: unknown[]): Promise<unknown>;
|
|
50
|
+
}, lesson: LessonRecord, now: number): Promise<void>;
|
|
40
51
|
export declare function listLessons(cwd: string, limit?: number): Promise<LessonRecord[]>;
|
|
41
52
|
/**
|
|
42
53
|
* Returns the evidence visible to the current workspace for exploration. Cross-repository
|
|
@@ -58,3 +69,6 @@ export declare function transitionLesson(id: string, state: LessonState, options
|
|
|
58
69
|
visibility?: LessonVisibility;
|
|
59
70
|
queueSync?: boolean;
|
|
60
71
|
}): Promise<boolean>;
|
|
72
|
+
export declare function txSafeLesson(db: {
|
|
73
|
+
get(sql: string, ...params: unknown[]): Promise<any>;
|
|
74
|
+
}, id: string): Promise<LessonRecord | null>;
|
package/dist/storage/lessons.js
CHANGED
|
@@ -5,11 +5,6 @@ import { openDatabase } from './db.js';
|
|
|
5
5
|
import { encryptLocalPayload } from './local-encryption.js';
|
|
6
6
|
import { lessonRowToRecord as rowToLesson } from './lesson-record.js';
|
|
7
7
|
import { loadTeamConfiguration } from './team-store.js';
|
|
8
|
-
export function shouldValidateInteractionLesson(evidence, verifiedSuccessfulOutcomes = 0) {
|
|
9
|
-
return evidence.outcome === 'success'
|
|
10
|
-
&& Boolean(evidence.reusableClaim?.trim())
|
|
11
|
-
&& (evidence.deterministic === true || verifiedSuccessfulOutcomes >= 3);
|
|
12
|
-
}
|
|
13
8
|
export async function recordInteractionEvidence(cwd, evidence) {
|
|
14
9
|
const db = await openDatabase();
|
|
15
10
|
if (!db)
|
|
@@ -77,7 +72,7 @@ export async function getRepositoryId(cwd) {
|
|
|
77
72
|
}
|
|
78
73
|
return createHash('sha256').update(identity).digest('hex');
|
|
79
74
|
}
|
|
80
|
-
async function registerRepository(db, cwd, repositoryId) {
|
|
75
|
+
export async function registerRepository(db, cwd, repositoryId) {
|
|
81
76
|
let canonicalUri = path.resolve(cwd);
|
|
82
77
|
try {
|
|
83
78
|
const config = await readGitConfig(cwd);
|
|
@@ -93,65 +88,34 @@ async function registerRepository(db, cwd, repositoryId) {
|
|
|
93
88
|
ON CONFLICT(id) DO UPDATE SET canonical_uri=excluded.canonical_uri,
|
|
94
89
|
display_name=excluded.display_name, last_seen=excluded.last_seen`, repositoryId, canonicalUri, remoteName || path.basename(cwd), Date.now());
|
|
95
90
|
}
|
|
96
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Share a memory with the team: a lesson of kind `memory` whose subject is the memory itself.
|
|
93
|
+
* It starts as a team candidate, so teammates' agents only receive it once a person promotes it
|
|
94
|
+
* (team knowledge search serves team lessons in the `promoted` state). Returns the lesson id, or
|
|
95
|
+
* null when the local store is unavailable.
|
|
96
|
+
*/
|
|
97
|
+
export async function shareMemoryLesson(cwd, key, value) {
|
|
97
98
|
const db = await openDatabase();
|
|
98
99
|
if (!db)
|
|
99
100
|
return null;
|
|
100
101
|
try {
|
|
101
102
|
const repositoryId = await getRepositoryId(cwd);
|
|
102
103
|
await registerRepository(db, cwd, repositoryId);
|
|
103
|
-
const id = `lesson-${randomUUID()}`;
|
|
104
|
-
const now = Date.now();
|
|
105
|
-
const verifiedSuccess = shouldValidateInteractionLesson(evidence, evidence.verifiedOutcome ? 3 : 0);
|
|
106
|
-
const confidence = verifiedSuccess ? 0.8 : 0.3;
|
|
107
|
-
const state = confidence >= 0.8 ? 'validated' : 'candidate';
|
|
108
104
|
const config = await loadTeamConfiguration();
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const prior = await rowToLesson(priorRow);
|
|
116
|
-
const previousSuccesses = Number(prior.evidence.successfulOutcomes ?? 0);
|
|
117
|
-
const successfulOutcomes = previousSuccesses + (evidence.outcome === 'success' ? 1 : 0);
|
|
118
|
-
const previousVerified = Number(prior.evidence.verifiedSuccessfulOutcomes ?? 0);
|
|
119
|
-
const verifiedSuccessfulOutcomes = previousVerified + (evidence.verifiedOutcome && evidence.outcome === 'success' ? 1 : 0);
|
|
120
|
-
const repeatedValidation = shouldValidateInteractionLesson(evidence, verifiedSuccessfulOutcomes);
|
|
121
|
-
const mergedEvidence = {
|
|
122
|
-
...evidence,
|
|
123
|
-
observationCount: Number(prior.evidence.observationCount ?? 1) + 1,
|
|
124
|
-
successfulOutcomes,
|
|
125
|
-
verifiedSuccessfulOutcomes,
|
|
126
|
-
previousRequestId: prior.evidence.requestId,
|
|
127
|
-
};
|
|
128
|
-
const updated = {
|
|
129
|
-
...prior,
|
|
130
|
-
state: repeatedValidation ? 'validated' : 'candidate',
|
|
131
|
-
evidence: mergedEvidence,
|
|
132
|
-
confidence: repeatedValidation ? 0.8 : Math.min(0.7, prior.confidence + 0.15),
|
|
133
|
-
updatedAt: now,
|
|
134
|
-
};
|
|
135
|
-
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);
|
|
136
|
-
if (config)
|
|
137
|
-
await queueLessonSync(db, updated, now);
|
|
138
|
-
return prior.id;
|
|
139
|
-
}
|
|
140
|
-
const storedEvidence = {
|
|
141
|
-
...evidence,
|
|
142
|
-
observationCount: 1,
|
|
143
|
-
successfulOutcomes: evidence.outcome === 'success' ? 1 : 0,
|
|
144
|
-
verifiedSuccessfulOutcomes: evidence.verifiedOutcome && evidence.outcome === 'success' ? 1 : 0,
|
|
145
|
-
};
|
|
146
|
-
const encryptedEvidence = await encryptLocalPayload(storedEvidence);
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
const subject = `${key}: ${value}`;
|
|
107
|
+
const existing = await db.get(`SELECT id FROM lessons WHERE repository_id = ? AND kind = 'memory' AND visibility = 'team' AND subject = ?`, repositoryId, subject);
|
|
108
|
+
if (existing)
|
|
109
|
+
return String(existing.id);
|
|
110
|
+
const id = `lesson-${randomUUID()}`;
|
|
147
111
|
await db.run(`INSERT INTO lessons (
|
|
148
112
|
id, repository_id, actor_id, team_id, visibility, state, kind, subject,
|
|
149
113
|
evidence_json, confidence, source, created_at, updated_at
|
|
150
|
-
) VALUES (?, ?, ?, ?, '
|
|
114
|
+
) VALUES (?, ?, ?, ?, 'team', 'candidate', 'memory', ?, ?, 0.5, 'rigour_remember', ?, ?)`, id, repositoryId, config?.actorId || process.env.RIGOUR_ACTOR_ID || null, config?.teamId || process.env.RIGOUR_TEAM_ID || null, subject, await encryptLocalPayload({ key, value, sharedAt: now }), now, now);
|
|
151
115
|
if (config) {
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
154
|
-
await queueLessonSync(db,
|
|
116
|
+
const lesson = await txSafeLesson(db, id);
|
|
117
|
+
if (lesson)
|
|
118
|
+
await queueLessonSync(db, lesson, now);
|
|
155
119
|
}
|
|
156
120
|
return id;
|
|
157
121
|
}
|
|
@@ -159,7 +123,7 @@ export async function recordInteractionLesson(cwd, evidence) {
|
|
|
159
123
|
await db.close();
|
|
160
124
|
}
|
|
161
125
|
}
|
|
162
|
-
async function queueLessonSync(db, lesson, now) {
|
|
126
|
+
export async function queueLessonSync(db, lesson, now) {
|
|
163
127
|
await db.run(`INSERT INTO sync_outbox (id, operation, entity_type, entity_id, payload_json, created_at)
|
|
164
128
|
VALUES (?, 'upsert', 'lesson', ?, ?, ?)`, `outbox-${randomUUID()}`, lesson.id, await encryptLocalPayload(lesson), now);
|
|
165
129
|
}
|
|
@@ -178,7 +142,7 @@ export async function listLessons(cwd, limit = 100) {
|
|
|
178
142
|
await registerRepository(db, cwd, repositoryId);
|
|
179
143
|
const rows = await db.all(`SELECT lessons.*, repositories.display_name AS repository_name
|
|
180
144
|
FROM lessons LEFT JOIN repositories ON repositories.id = lessons.repository_id
|
|
181
|
-
WHERE repository_id = ?
|
|
145
|
+
WHERE repository_id = ? AND kind != 'interaction'
|
|
182
146
|
ORDER BY updated_at DESC LIMIT ?`, repositoryId, limit);
|
|
183
147
|
return await Promise.all(rows.map(rowToLesson));
|
|
184
148
|
}
|
|
@@ -207,14 +171,16 @@ export async function listKnowledgeLessons(cwd, limit = 500) {
|
|
|
207
171
|
const rows = config
|
|
208
172
|
? await db.all(`SELECT lessons.*, repositories.display_name AS repository_name
|
|
209
173
|
FROM lessons LEFT JOIN repositories ON repositories.id = lessons.repository_id
|
|
210
|
-
WHERE lessons.
|
|
174
|
+
WHERE lessons.kind != 'interaction'
|
|
175
|
+
AND (lessons.repository_id = ?
|
|
211
176
|
OR (lessons.visibility = 'personal' AND lessons.actor_id = ?)
|
|
212
|
-
OR (lessons.visibility = 'team' AND lessons.team_id = ? AND lessons.state = 'promoted')
|
|
177
|
+
OR (lessons.visibility = 'team' AND lessons.team_id = ? AND lessons.state = 'promoted'))
|
|
213
178
|
ORDER BY lessons.updated_at DESC LIMIT ?`, repositoryId, config.actorId, config.teamId, limit)
|
|
214
179
|
: await db.all(`SELECT lessons.*, repositories.display_name AS repository_name
|
|
215
180
|
FROM lessons LEFT JOIN repositories ON repositories.id = lessons.repository_id
|
|
216
|
-
WHERE lessons.
|
|
217
|
-
|
|
181
|
+
WHERE lessons.kind != 'interaction'
|
|
182
|
+
AND (lessons.repository_id = ?
|
|
183
|
+
OR (lessons.visibility = 'personal' AND lessons.actor_id IS NULL))
|
|
218
184
|
ORDER BY lessons.updated_at DESC LIMIT ?`, repositoryId, limit);
|
|
219
185
|
return await Promise.all(rows.map(rowToLesson));
|
|
220
186
|
}
|
|
@@ -298,7 +264,7 @@ export async function transitionLesson(id, state, options = {}) {
|
|
|
298
264
|
await db.close();
|
|
299
265
|
}
|
|
300
266
|
}
|
|
301
|
-
async function txSafeLesson(db, id) {
|
|
267
|
+
export async function txSafeLesson(db, id) {
|
|
302
268
|
const row = await db.get('SELECT * FROM lessons WHERE id = ?', id);
|
|
303
269
|
return row ? rowToLesson(row) : null;
|
|
304
270
|
}
|
|
@@ -4,7 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { openDatabase } from './db.js';
|
|
6
6
|
import { decryptLocalPayload, encryptLocalPayload } from './local-encryption.js';
|
|
7
|
-
import { getRepositoryId
|
|
7
|
+
import { getRepositoryId } from './lessons.js';
|
|
8
8
|
import { queueLocalLessonsForTeam } from './team-import.js';
|
|
9
9
|
const roots = [];
|
|
10
10
|
afterEach(async () => {
|
|
@@ -40,20 +40,6 @@ describe('getRepositoryId', () => {
|
|
|
40
40
|
expect(await getRepositoryId(worktree)).toHaveLength(64);
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
|
-
describe('interaction lesson validation', () => {
|
|
44
|
-
const evidence = { tool: 'rigour_context_scope', outcome: 'success', requestId: 'req-1' };
|
|
45
|
-
it('keeps repeated unverified interaction as evidence only', () => {
|
|
46
|
-
expect(shouldValidateInteractionLesson(evidence, 0)).toBe(false);
|
|
47
|
-
expect(shouldValidateInteractionLesson(evidence, 2)).toBe(false);
|
|
48
|
-
});
|
|
49
|
-
it('validates deterministic or repeatedly verified outcomes', () => {
|
|
50
|
-
expect(shouldValidateInteractionLesson({ ...evidence, deterministic: true, reusableClaim: 'Use scoped retrieval before edits.' }, 0)).toBe(true);
|
|
51
|
-
expect(shouldValidateInteractionLesson({ ...evidence, verifiedOutcome: true, reusableClaim: 'Run the focused test suite.' }, 3)).toBe(true);
|
|
52
|
-
});
|
|
53
|
-
it('does not turn a successful tool call into reusable knowledge', () => {
|
|
54
|
-
expect(shouldValidateInteractionLesson({ ...evidence, deterministic: true }, 3)).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
43
|
describe('local lesson team import', () => {
|
|
58
44
|
it('dry-runs, assigns legacy ownership, and queues each lesson version once', async () => {
|
|
59
45
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-team-import-'));
|