@rigour-labs/core 6.5.1 → 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.
Files changed (120) hide show
  1. package/README.md +1 -1
  2. package/dist/context/automatic-index.d.ts +5 -0
  3. package/dist/context/automatic-index.js +24 -3
  4. package/dist/context/index-status.test.d.ts +1 -0
  5. package/dist/context/index-status.test.js +26 -0
  6. package/dist/deep/fact-extractor.js +2 -1
  7. package/dist/deep/fact-extractor.test.js +2 -1
  8. package/dist/gates/ast.js +4 -2
  9. package/dist/gates/coverage.js +4 -2
  10. package/dist/gates/dedupe-failures.test.d.ts +1 -0
  11. package/dist/gates/dedupe-failures.test.js +12 -0
  12. package/dist/gates/deprecated-dependencies.d.ts +27 -0
  13. package/dist/gates/deprecated-dependencies.js +104 -0
  14. package/dist/gates/deprecated-dependencies.test.d.ts +1 -0
  15. package/dist/gates/deprecated-dependencies.test.js +55 -0
  16. package/dist/gates/runner.d.ts +7 -1
  17. package/dist/gates/runner.js +25 -11
  18. package/dist/gates/security-patterns-data.js +2 -1
  19. package/dist/gates/security-patterns-owasp.test.js +15 -0
  20. package/dist/gates/semantic-bugs.js +1 -1
  21. package/dist/gates/unindexed-reads/coverage.d.ts +15 -0
  22. package/dist/gates/unindexed-reads/coverage.js +24 -0
  23. package/dist/gates/unindexed-reads/index.d.ts +20 -0
  24. package/dist/gates/unindexed-reads/index.js +98 -0
  25. package/dist/gates/unindexed-reads/queries.d.ts +17 -0
  26. package/dist/gates/unindexed-reads/queries.js +147 -0
  27. package/dist/gates/unindexed-reads/queries.test.d.ts +1 -0
  28. package/dist/gates/unindexed-reads/queries.test.js +54 -0
  29. package/dist/gates/unindexed-reads/schema.d.ts +38 -0
  30. package/dist/gates/unindexed-reads/schema.js +256 -0
  31. package/dist/gates/unindexed-reads/schema.test.d.ts +1 -0
  32. package/dist/gates/unindexed-reads/schema.test.js +81 -0
  33. package/dist/gates/unindexed-reads/sql.d.ts +21 -0
  34. package/dist/gates/unindexed-reads/sql.js +131 -0
  35. package/dist/gates/unindexed-reads/unindexed-reads.test.d.ts +1 -0
  36. package/dist/gates/unindexed-reads/unindexed-reads.test.js +90 -0
  37. package/dist/index.d.ts +4 -1
  38. package/dist/index.js +4 -1
  39. package/dist/inference/brain-placeholder.test.d.ts +1 -0
  40. package/dist/inference/brain-placeholder.test.js +28 -0
  41. package/dist/inference/llama-engine-checksum.test.d.ts +1 -0
  42. package/dist/inference/llama-engine-checksum.test.js +27 -0
  43. package/dist/inference/llama-engine.d.ts +4 -1
  44. package/dist/inference/llama-engine.js +15 -8
  45. package/dist/memory/recall.d.ts +37 -0
  46. package/dist/memory/recall.js +93 -0
  47. package/dist/memory/recall.test.d.ts +1 -0
  48. package/dist/memory/recall.test.js +36 -0
  49. package/dist/pattern-index/assess.d.ts +16 -0
  50. package/dist/pattern-index/assess.js +37 -0
  51. package/dist/pattern-index/embeddings.d.ts +19 -0
  52. package/dist/pattern-index/embeddings.js +16 -0
  53. package/dist/pattern-index/index.d.ts +3 -2
  54. package/dist/pattern-index/index.js +4 -2
  55. package/dist/pattern-index/indexer.js +19 -9
  56. package/dist/pattern-index/matcher.d.ts +6 -0
  57. package/dist/pattern-index/matcher.js +13 -3
  58. package/dist/pattern-index/pattern-reuse.test.d.ts +1 -0
  59. package/dist/pattern-index/pattern-reuse.test.js +76 -0
  60. package/dist/pattern-index/staleness.d.ts +2 -15
  61. package/dist/pattern-index/staleness.js +1 -78
  62. package/dist/pattern-index/types.d.ts +2 -0
  63. package/dist/review/agent-fixes.d.ts +4 -0
  64. package/dist/review/agent-fixes.js +10 -7
  65. package/dist/review/agent-fixes.test.js +4 -4
  66. package/dist/review/changed-lines.d.ts +2 -1
  67. package/dist/review/changed-lines.js +12 -2
  68. package/dist/review/check-outcomes.d.ts +31 -0
  69. package/dist/review/check-outcomes.js +81 -0
  70. package/dist/review/check-outcomes.test.d.ts +1 -0
  71. package/dist/review/check-outcomes.test.js +56 -0
  72. package/dist/review/migration-order.d.ts +5 -0
  73. package/dist/review/migration-order.js +70 -0
  74. package/dist/review/migration-order.test.d.ts +1 -0
  75. package/dist/review/migration-order.test.js +62 -0
  76. package/dist/review/quiet.d.ts +2 -0
  77. package/dist/review/quiet.js +12 -3
  78. package/dist/review/review.d.ts +4 -0
  79. package/dist/review/review.js +17 -5
  80. package/dist/review/review.test.js +36 -1
  81. package/dist/semantic/ast.d.ts +1 -0
  82. package/dist/semantic/ast.js +4 -0
  83. package/dist/semantic/learn/compile.js +3 -1
  84. package/dist/semantic/learn/learn.js +40 -5
  85. package/dist/semantic/types.d.ts +2 -0
  86. package/dist/services/context-scope-summary.test.d.ts +1 -0
  87. package/dist/services/context-scope-summary.test.js +17 -0
  88. package/dist/services/context-telemetry-service.js +2 -1
  89. package/dist/storage/context-telemetry.js +33 -19
  90. package/dist/storage/db.js +52 -1
  91. package/dist/storage/findings.js +1 -1
  92. package/dist/storage/fix-lessons.d.ts +13 -0
  93. package/dist/storage/fix-lessons.js +84 -0
  94. package/dist/storage/fix-lessons.test.d.ts +1 -0
  95. package/dist/storage/fix-lessons.test.js +61 -0
  96. package/dist/storage/index.d.ts +2 -1
  97. package/dist/storage/index.js +2 -1
  98. package/dist/storage/lessons.d.ts +16 -2
  99. package/dist/storage/lessons.js +27 -61
  100. package/dist/storage/lessons.test.js +1 -15
  101. package/dist/storage/local-memory.js +39 -5
  102. package/dist/storage/local-memory.test.d.ts +1 -0
  103. package/dist/storage/local-memory.test.js +55 -0
  104. package/dist/storage/patterns.d.ts +1 -1
  105. package/dist/storage/patterns.js +1 -1
  106. package/dist/storage/share-memory.test.d.ts +1 -0
  107. package/dist/storage/share-memory.test.js +34 -0
  108. package/dist/storage/team-vector-store.js +2 -1
  109. package/dist/storage/telemetry-scope.test.d.ts +1 -0
  110. package/dist/storage/telemetry-scope.test.js +38 -0
  111. package/dist/templates/universal-config.js +11 -0
  112. package/dist/types/index.d.ts +132 -0
  113. package/dist/types/index.js +15 -0
  114. package/dist/utils/glob-paths.d.ts +4 -0
  115. package/dist/utils/glob-paths.js +26 -0
  116. package/dist/utils/glob-paths.test.d.ts +1 -0
  117. package/dist/utils/glob-paths.test.js +40 -0
  118. package/dist/utils/scanner.js +4 -2
  119. package/dist/utils/scanner.test.js +2 -1
  120. package/package.json +7 -7
@@ -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
- export async function recordInteractionLesson(cwd, evidence) {
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 actorId = config?.actorId || process.env.RIGOUR_ACTOR_ID || null;
110
- const priorRow = await db.get(`SELECT * FROM lessons
111
- WHERE repository_id = ? AND actor_id IS ? AND visibility = 'personal'
112
- AND state = 'candidate' AND kind = 'interaction' AND subject = ?
113
- ORDER BY updated_at DESC LIMIT 1`, repositoryId, actorId, evidence.reusableClaim?.trim() || `interaction:${evidence.tool}`);
114
- if (priorRow) {
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 (?, ?, ?, ?, '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);
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 personal = await txSafeLesson(db, id);
153
- if (personal)
154
- await queueLessonSync(db, personal, now);
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.repository_id = ?
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.repository_id = ?
217
- OR (lessons.visibility = 'personal' AND lessons.actor_id IS NULL)
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, shouldValidateInteractionLesson } from './lessons.js';
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-'));
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Before deep scan: checks local SQLite for known patterns in this project.
5
5
  * After any scan: stores verified findings and reinforces patterns.
6
- * Result: Rigour gets smarter every time you use it, code never leaves the machine.
6
+ * Counts are per repository and stay on the machine; replayed findings are limited to files
7
+ * unchanged since the scan that found them.
7
8
  */
8
9
  import path from 'path';
9
10
  import { openDatabase } from './db.js';
@@ -11,6 +12,8 @@ import { insertScan, getRecentScans } from './scans.js';
11
12
  import { insertFindings, getDeepFindings } from './findings.js';
12
13
  import { reinforcePattern, getStrongPatterns, getHardRules, decayPatterns } from './patterns.js';
13
14
  import { Logger } from '../utils/logger.js';
15
+ import { getRepositoryId } from './lessons.js';
16
+ import fs from 'fs-extra';
14
17
  /** Minimum pattern strength to produce instant findings (skip LLM). */
15
18
  const INSTANT_MATCH_THRESHOLD = 0.7;
16
19
  /** Max local findings to inject per scan (avoid flooding). */
@@ -20,6 +23,14 @@ const MIN_REUSE_CONFIDENCE = 0.7;
20
23
  /**
21
24
  * Build a map of relative file path → known issues from past findings.
22
25
  */
26
+ async function fileModifiedAt(cwd, relPath) {
27
+ try {
28
+ return (await fs.stat(path.join(cwd, relPath))).mtimeMs;
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }
23
34
  function buildFileIssueMap(recentFindings) {
24
35
  const map = new Map();
25
36
  for (const f of recentFindings) {
@@ -40,7 +51,9 @@ export async function checkLocalPatterns(cwd, fileList) {
40
51
  const db = await openDatabase();
41
52
  if (!db)
42
53
  return [];
43
- const repoName = path.basename(cwd);
54
+ // The repository's identity (its remote, else its path), never the folder name: two
55
+ // checkouts called `api` are different projects.
56
+ const repoName = await getRepositoryId(cwd);
44
57
  const findings = [];
45
58
  try {
46
59
  const patterns = await getStrongPatterns(db, repoName, INSTANT_MATCH_THRESHOLD);
@@ -52,13 +65,23 @@ export async function checkLocalPatterns(cwd, fileList) {
52
65
  return [];
53
66
  const fileIssueMap = buildFileIssueMap(recentFindings);
54
67
  // fileList contains relative paths — match directly against DB (also relative)
68
+ const seen = new Set();
55
69
  for (const relPath of fileList) {
56
70
  const known = fileIssueMap.get(relPath);
57
71
  if (!known)
58
72
  continue;
73
+ const modifiedAt = await fileModifiedAt(cwd, relPath);
59
74
  for (const issue of known) {
60
75
  if ((issue.confidence ?? 0) < MIN_REUSE_CONFIDENCE)
61
76
  continue;
77
+ // A finding still holds only while its file is the one it was found in: an edit
78
+ // since that scan may be the fix, and replaying it would report fixed code.
79
+ if (modifiedAt === undefined || modifiedAt > Number(issue.scan_time))
80
+ continue;
81
+ const key = `${issue.line}:${issue.category}:${issue.description}`;
82
+ if (seen.has(`${relPath}:${key}`))
83
+ continue;
84
+ seen.add(`${relPath}:${key}`);
62
85
  const matchingPattern = patterns.find((p) => p.pattern === issue.category && p.strength >= INSTANT_MATCH_THRESHOLD);
63
86
  if (!matchingPattern)
64
87
  continue;
@@ -95,20 +118,29 @@ export async function persistAndReinforce(cwd, report, meta) {
95
118
  const db = await openDatabase();
96
119
  if (!db)
97
120
  return;
98
- const repoName = path.basename(cwd);
121
+ // The repository's identity (its remote, else its path), never the folder name: two
122
+ // checkouts called `api` are different projects.
123
+ const repoName = await getRepositoryId(cwd);
99
124
  try {
100
125
  const scanId = await insertScan(db, repoName, report, meta);
101
126
  if (report.failures.length > 0) {
102
127
  await insertFindings(db, scanId, report.failures);
103
128
  }
129
+ // One reinforcement per category per scan: five findings of one kind in one scan are
130
+ // one observation of that kind, not five.
131
+ const byCategory = new Map();
104
132
  for (const f of report.failures) {
105
133
  const category = f.category || f.id;
134
+ if (!byCategory.has(category))
135
+ byCategory.set(category, f);
136
+ }
137
+ for (const [category, f] of byCategory) {
106
138
  const source = f.source === 'llm' ? 'llm' : 'ast';
107
139
  await reinforcePattern(db, repoName, category, `${f.title}: ${f.details?.substring(0, 120)}`, source);
108
140
  }
109
141
  await decayPatterns(db, 30);
110
142
  Logger.info(`Local memory: stored ${report.failures.length} findings, ` +
111
- `reinforced ${report.failures.length} patterns for ${repoName}`);
143
+ `reinforced ${byCategory.size} patterns`);
112
144
  }
113
145
  catch (error) {
114
146
  Logger.warn(`Local memory persist failed: ${error}`);
@@ -124,7 +156,9 @@ export async function getProjectStats(cwd) {
124
156
  const db = await openDatabase();
125
157
  if (!db)
126
158
  return null;
127
- const repoName = path.basename(cwd);
159
+ // The repository's identity (its remote, else its path), never the folder name: two
160
+ // checkouts called `api` are different projects.
161
+ const repoName = await getRepositoryId(cwd);
128
162
  try {
129
163
  const scans = await getRecentScans(db, repoName, 100);
130
164
  const patterns = await getStrongPatterns(db, repoName, 0.3);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,55 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ /** An in-memory stand-in for the local SQLite store: records writes, answers the two reads under test. */
6
+ const db = vi.hoisted(() => ({
7
+ writes: [],
8
+ patterns: [],
9
+ findings: [],
10
+ reset() { this.writes = []; this.patterns = []; this.findings = []; },
11
+ }));
12
+ vi.mock('./db.js', () => {
13
+ const store = {
14
+ run: async (sql) => { db.writes.push(sql.trim().split(/\s+/).slice(0, 3).join(' ')); return { changes: 1, lastID: 0 }; },
15
+ get: async () => undefined,
16
+ all: async (sql) => (sql.includes('FROM patterns') ? db.patterns : sql.includes('FROM findings') ? db.findings : []),
17
+ exec: async () => undefined,
18
+ transaction: async (work) => work(store),
19
+ close: async () => undefined,
20
+ };
21
+ return { openDatabase: async () => store };
22
+ });
23
+ const { checkLocalPatterns, persistAndReinforce } = await import('./local-memory.js');
24
+ let repo;
25
+ beforeEach(() => {
26
+ db.reset();
27
+ repo = fs.mkdtempSync(path.join(os.tmpdir(), 'local-memory-'));
28
+ fs.mkdirSync(path.join(repo, 'src'));
29
+ });
30
+ afterEach(() => fs.rmSync(repo, { recursive: true, force: true }));
31
+ const failure = (id, line) => ({ id, title: id, details: 'x', severity: 'medium', provenance: 'traditional', files: ['src/a.ts'], line });
32
+ describe('persistAndReinforce', () => {
33
+ it('reinforces each category once per scan, however many findings it has', async () => {
34
+ await persistAndReinforce(repo, { status: 'FAIL', failures: [failure('content-check', 1), failure('content-check', 2), failure('content-check', 3), failure('ast', 4)], stats: {} });
35
+ expect(db.writes.filter(w => w.startsWith('INSERT INTO patterns'))).toHaveLength(2);
36
+ });
37
+ });
38
+ describe('checkLocalPatterns', () => {
39
+ const remembered = (scanTime) => ({ file: 'src/a.ts', line: 3, category: 'security', description: 'token logged', confidence: 0.9, scan_time: scanTime });
40
+ it('replays a remembered finding while its file is unchanged since that scan, once', async () => {
41
+ fs.writeFileSync(path.join(repo, 'src/a.ts'), 'log(token)\n');
42
+ const after = fs.statSync(path.join(repo, 'src/a.ts')).mtimeMs + 1000;
43
+ db.patterns = [{ pattern: 'security', strength: 0.9 }];
44
+ db.findings = [remembered(after), remembered(after)];
45
+ const findings = await checkLocalPatterns(repo, ['src/a.ts']);
46
+ expect(findings.map(f => f.description)).toEqual(['[Local Memory] token logged']);
47
+ });
48
+ it('never replays a finding for a file edited since, which may be the fix', async () => {
49
+ fs.writeFileSync(path.join(repo, 'src/a.ts'), 'log(redact(token))\n');
50
+ const before = fs.statSync(path.join(repo, 'src/a.ts')).mtimeMs - 1000;
51
+ db.patterns = [{ pattern: 'security', strength: 0.9 }];
52
+ db.findings = [remembered(before)];
53
+ expect(await checkLocalPatterns(repo, ['src/a.ts'])).toEqual([]);
54
+ });
55
+ });
@@ -30,6 +30,6 @@ export declare function getStrongPatterns(store: RigourDB, repo: string, thresho
30
30
  export declare function getPatterns(store: RigourDB, repo: string): Promise<PatternRecord[]>;
31
31
  /**
32
32
  * Get patterns promoted to hard rules (strength > 0.9).
33
- * These can be used as AST-level checks without LLM inference.
33
+ * Categories seen this often; shown by `rigour brain`. They are counts, not checks.
34
34
  */
35
35
  export declare function getHardRules(store: RigourDB, repo: string): Promise<PatternRecord[]>;
@@ -49,7 +49,7 @@ export async function getPatterns(store, repo) {
49
49
  }
50
50
  /**
51
51
  * Get patterns promoted to hard rules (strength > 0.9).
52
- * These can be used as AST-level checks without LLM inference.
52
+ * Categories seen this often; shown by `rigour brain`. They are counts, not checks.
53
53
  */
54
54
  export async function getHardRules(store, repo) {
55
55
  return store.all('SELECT * FROM patterns WHERE repo = ? AND strength >= 0.9 ORDER BY times_seen DESC', repo);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ const db = vi.hoisted(() => ({ rows: [], outbox: 0, reset() { this.rows = []; this.outbox = 0; } }));
3
+ vi.mock('./db.js', () => {
4
+ const store = {
5
+ run: async (sql, ...params) => {
6
+ if (sql.includes('INSERT INTO lessons'))
7
+ db.rows.push({ id: params[0], visibility: 'team', state: 'candidate', kind: 'memory', subject: params[4] });
8
+ if (sql.includes('INSERT INTO sync_outbox'))
9
+ db.outbox++;
10
+ return { changes: 1, lastID: 0 };
11
+ },
12
+ get: async (sql, ...params) => (sql.includes("kind = 'memory'") ? db.rows.find(r => r.subject === params[1]) : undefined),
13
+ all: async () => [],
14
+ exec: async () => undefined,
15
+ close: async () => undefined,
16
+ };
17
+ return { openDatabase: async () => store };
18
+ });
19
+ vi.mock('./local-encryption.js', () => ({ encryptLocalPayload: async (v) => JSON.stringify(v), decryptLocalPayload: async (v) => JSON.parse(v) }));
20
+ vi.mock('./team-store.js', () => ({ loadTeamConfiguration: async () => null }));
21
+ const { shareMemoryLesson } = await import('./lessons.js');
22
+ beforeEach(() => db.reset());
23
+ describe('shareMemoryLesson', () => {
24
+ it('stores a team candidate of kind memory, once per memory', async () => {
25
+ const first = await shareMemoryLesson(process.cwd(), 'retries', 'Use withRetry from lib/net.');
26
+ const again = await shareMemoryLesson(process.cwd(), 'retries', 'Use withRetry from lib/net.');
27
+ expect(first).toBe(again);
28
+ expect(db.rows).toEqual([{ id: first, visibility: 'team', state: 'candidate', kind: 'memory', subject: 'retries: Use withRetry from lib/net.' }]);
29
+ });
30
+ it('queues nothing for sync until team mode is configured', async () => {
31
+ await shareMemoryLesson(process.cwd(), 'naming', 'Hooks are named useX.');
32
+ expect(db.outbox).toBe(0);
33
+ });
34
+ });
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { SEMANTIC_FLOOR } from '../memory/recall.js';
2
3
  export const TEAM_VECTOR_SCHEMA = `
3
4
  CREATE EXTENSION IF NOT EXISTS vector;
4
5
  CREATE TABLE IF NOT EXISTS rigour.lesson_embeddings (
@@ -176,7 +177,7 @@ export async function searchConfiguredTeamKnowledge(config, query, limit) {
176
177
  OR (lesson.visibility = 'team' AND lesson.state = 'promoted'))
177
178
  AND 1 - (embedding.embedding <=> $1::vector) >= $6
178
179
  ORDER BY embedding.embedding <=> $1::vector LIMIT $5`, [vectorLiteral(embedding), config.organizationId, config.teamId, config.semantic.model,
179
- Math.max(1, Math.min(25, limit)), 0.45, config.actorId]);
180
+ Math.max(1, Math.min(25, limit)), SEMANTIC_FLOOR, config.actorId]);
180
181
  return {
181
182
  status: 'ready', provider: 'pgvector', model: config.semantic.model,
182
183
  candidates: result.rows.map((row) => ({
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterAll, describe, expect, it, vi } from 'vitest';
5
+ /** Every open goes to a scratch database, never ~/.rigour/rigour.db. */
6
+ const dbFile = vi.hoisted(() => require('path').join(require('os').tmpdir(), `rigour-telemetry-${process.pid}.db`));
7
+ vi.mock('./db.js', async (importOriginal) => {
8
+ const real = await importOriginal();
9
+ return { ...real, openDatabase: (p) => real.openDatabase(p ?? dbFile) };
10
+ });
11
+ const { openDatabase, isSQLiteAvailable } = await import('./db.js');
12
+ const { getCheckpointMetrics, getContextEvents, recordCheckpointMetric, recordContextEvent } = await import('./context-telemetry.js');
13
+ const repoA = fs.mkdtempSync(path.join(os.tmpdir(), 'telemetry-a-'));
14
+ const repoB = fs.mkdtempSync(path.join(os.tmpdir(), 'telemetry-b-'));
15
+ afterAll(() => {
16
+ for (const p of [dbFile, `${dbFile}-wal`, `${dbFile}-shm`])
17
+ fs.rmSync(p, { force: true });
18
+ for (const dir of [repoA, repoB])
19
+ fs.rmSync(dir, { recursive: true, force: true });
20
+ });
21
+ describe.runIf(isSQLiteAvailable())('telemetry scoped to its repository', () => {
22
+ it('migrates idempotently when two processes open the database at once', async () => {
23
+ const [a, b] = await Promise.all([openDatabase(), openDatabase()]);
24
+ const columns = (await a.all('PRAGMA table_info(checkpoint_metrics)')).map((c) => c.name);
25
+ expect(columns).toContain('repository_id');
26
+ await a.close();
27
+ await b.close();
28
+ });
29
+ it("shows a repository its own checkpoints and context events, never another's", async () => {
30
+ await recordCheckpointMetric({ checkpointId: 'cp-a', taskId: 't', agentId: 'agent', checkpointTokens: 10, rawStateTokens: 0, replayTokensAvoided: 0 }, repoA);
31
+ await recordCheckpointMetric({ checkpointId: 'cp-b', taskId: 't', agentId: 'agent', checkpointTokens: 10, rawStateTokens: 0, replayTokensAvoided: 0 }, repoB);
32
+ await recordContextEvent({ toolName: 'rigour_recall', cacheStatus: 'miss', candidateTokens: 0, returnedTokens: 0 }, repoA);
33
+ expect((await getCheckpointMetrics(undefined, repoA)).map(m => m.checkpointId)).toEqual(['cp-a']);
34
+ expect((await getCheckpointMetrics(undefined, repoB)).map(m => m.checkpointId)).toEqual(['cp-b']);
35
+ expect(await getContextEvents(undefined, repoB)).toEqual([]);
36
+ expect((await getContextEvents(undefined, repoA)).map(e => e.toolName)).toEqual(['rigour_recall']);
37
+ });
38
+ });
@@ -234,6 +234,17 @@ export const UNIVERSAL_CONFIG = {
234
234
  track_branches: false,
235
235
  track_returns: false,
236
236
  },
237
+ unindexed_reads: {
238
+ enabled: false,
239
+ migrations: [],
240
+ },
241
+ deprecated_dependencies: {
242
+ enabled: false,
243
+ },
244
+ migration_order: {
245
+ enabled: false,
246
+ dirs: ['**/supabase/migrations'],
247
+ },
237
248
  semantic_bugs: {
238
249
  enabled: true,
239
250
  },