@rigour-labs/core 6.2.1 → 6.2.2

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/index.d.ts CHANGED
@@ -24,8 +24,8 @@ export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, rese
24
24
  export type { RigourDB, CompactResult } from './storage/index.js';
25
25
  export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './storage/index.js';
26
26
  export type { ProjectStats } from './storage/index.js';
27
- export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId } from './storage/index.js';
28
- export type { ApplicableLessons, InteractionEvidence, LessonRecord, LessonState, LessonVisibility } from './storage/index.js';
27
+ export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId, queueLocalLessonsForTeam } from './storage/index.js';
28
+ export type { ApplicableLessons, InteractionEvidence, LessonRecord, LessonState, LessonVisibility, LocalLessonImportOptions, LocalLessonImportResult } from './storage/index.js';
29
29
  export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './storage/index.js';
30
30
  export type { TeamConfiguration, TeamSemanticConfiguration, TeamModeStatus, TeamDoctorResult, SemanticKnowledgeCandidate, SemanticKnowledgeResult, } from './storage/index.js';
31
31
  export { generateTemporalDriftReport, formatDriftSummary } from './services/temporal-drift.js';
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ export { extractFacts, factsToPromptString } from './deep/fact-extractor.js';
24
24
  export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, resetDatabase, insertScan, insertFindings, getRecentScans, getScoreTrendFromDB, getTopIssues, reinforcePattern, getStrongPatterns } from './storage/index.js';
25
25
  // Local Project Memory (hybrid intelligence — SQLite-backed per-project learning)
26
26
  export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './storage/index.js';
27
- export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId } from './storage/index.js';
27
+ export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId, queueLocalLessonsForTeam } from './storage/index.js';
28
28
  export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './storage/index.js';
29
29
  // Temporal Drift Engine (v5 — cross-session trend analysis + per-provenance EWMA)
30
30
  export { generateTemporalDriftReport, formatDriftSummary } from './services/temporal-drift.js';
@@ -13,5 +13,7 @@ export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContext
13
13
  export type { ContextEvent, ModelUsage, ContextCacheRecord, CheckpointMetric } from './context-telemetry.js';
14
14
  export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId } from './lessons.js';
15
15
  export type { ApplicableLessons, InteractionEvidence, LessonRecord, LessonState, LessonVisibility } from './lessons.js';
16
+ export { queueLocalLessonsForTeam } from './team-import.js';
17
+ export type { LocalLessonImportOptions, LocalLessonImportResult } from './team-import.js';
16
18
  export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './team-store.js';
17
19
  export type { TeamConfiguration, TeamSemanticConfiguration, TeamModeStatus, TeamDoctorResult, SemanticKnowledgeCandidate, SemanticKnowledgeResult, } from './team-store.js';
@@ -9,4 +9,5 @@ export { reinforcePattern, decayPatterns, getStrongPatterns, getPatterns, getHar
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
11
  export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId } from './lessons.js';
12
+ export { queueLocalLessonsForTeam } from './team-import.js';
12
13
  export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './team-store.js';
@@ -0,0 +1,2 @@
1
+ import type { LessonRecord } from './lessons.js';
2
+ export declare function lessonRowToRecord(row: Record<string, unknown>): Promise<LessonRecord>;
@@ -0,0 +1,20 @@
1
+ import { decryptLocalPayload } from './local-encryption.js';
2
+ export async function lessonRowToRecord(row) {
3
+ return {
4
+ id: String(row.id),
5
+ repositoryId: String(row.repository_id),
6
+ actorId: row.actor_id ? String(row.actor_id) : undefined,
7
+ teamId: row.team_id ? String(row.team_id) : undefined,
8
+ visibility: row.visibility,
9
+ state: row.state,
10
+ kind: String(row.kind),
11
+ subject: String(row.subject),
12
+ evidence: await decryptLocalPayload(String(row.evidence_json)),
13
+ confidence: Number(row.confidence),
14
+ source: String(row.source),
15
+ supersedesId: row.supersedes_id ? String(row.supersedes_id) : undefined,
16
+ createdAt: Number(row.created_at),
17
+ updatedAt: Number(row.updated_at),
18
+ repositoryName: row.repository_name ? String(row.repository_name) : undefined,
19
+ };
20
+ }
@@ -2,7 +2,8 @@ import { createHash, randomUUID } from 'crypto';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import { openDatabase } from './db.js';
5
- import { decryptLocalPayload, encryptLocalPayload } from './local-encryption.js';
5
+ import { encryptLocalPayload } from './local-encryption.js';
6
+ import { lessonRowToRecord as rowToLesson } from './lesson-record.js';
6
7
  import { loadTeamConfiguration } from './team-store.js';
7
8
  export function shouldValidateInteractionLesson(evidence, verifiedSuccessfulOutcomes = 0) {
8
9
  return evidence.outcome === 'success'
@@ -92,25 +93,6 @@ async function registerRepository(db, cwd, repositoryId) {
92
93
  ON CONFLICT(id) DO UPDATE SET canonical_uri=excluded.canonical_uri,
93
94
  display_name=excluded.display_name, last_seen=excluded.last_seen`, repositoryId, canonicalUri, remoteName || path.basename(cwd), Date.now());
94
95
  }
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
96
  export async function recordInteractionLesson(cwd, evidence) {
115
97
  const db = await openDatabase();
116
98
  if (!db)
@@ -1,10 +1,16 @@
1
1
  import fs from 'fs-extra';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- import { afterEach, describe, expect, it } from 'vitest';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { openDatabase } from './db.js';
6
+ import { decryptLocalPayload, encryptLocalPayload } from './local-encryption.js';
5
7
  import { getRepositoryId, shouldValidateInteractionLesson } from './lessons.js';
8
+ import { queueLocalLessonsForTeam } from './team-import.js';
6
9
  const roots = [];
7
- afterEach(async () => { await Promise.all(roots.splice(0).map((root) => fs.remove(root))); });
10
+ afterEach(async () => {
11
+ vi.unstubAllEnvs();
12
+ await Promise.all(roots.splice(0).map((root) => fs.remove(root)));
13
+ });
8
14
  describe('getRepositoryId', () => {
9
15
  it('uses normalized origin identity instead of a basename', async () => {
10
16
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-repo-id-'));
@@ -48,3 +54,42 @@ describe('interaction lesson validation', () => {
48
54
  expect(shouldValidateInteractionLesson({ ...evidence, deterministic: true }, 3)).toBe(false);
49
55
  });
50
56
  });
57
+ describe('local lesson team import', () => {
58
+ it('dry-runs, assigns legacy ownership, and queues each lesson version once', async () => {
59
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-team-import-'));
60
+ roots.push(root);
61
+ await fs.ensureDir(path.join(root, '.git'));
62
+ await fs.writeFile(path.join(root, '.git', 'config'), '[remote "origin"]\nurl = https://github.com/acme/service.git\n');
63
+ vi.stubEnv('RIGOUR_ORGANIZATION_ID', 'acme');
64
+ vi.stubEnv('RIGOUR_TEAM_ID', 'platform');
65
+ vi.stubEnv('RIGOUR_ACTOR_ID', 'alice');
66
+ vi.stubEnv('RIGOUR_TEAM_DATABASE_URL', 'postgresql://alice@127.0.0.1/rigour');
67
+ vi.stubEnv('RIGOUR_LOCAL_CACHE_KEY', Buffer.alloc(32, 7).toString('base64'));
68
+ const repositoryId = await getRepositoryId(root);
69
+ const databasePath = path.join(root, 'rigour.db');
70
+ const db = await openDatabase(databasePath);
71
+ expect(db).not.toBeNull();
72
+ const now = Date.now();
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 (?, ?, NULL, NULL, 'personal', 'candidate', 'interaction', ?, ?, 0.3, 'mcp', ?, ?)`, 'lesson-legacy', repositoryId, 'Prefer scoped retrieval.', await encryptLocalPayload({ requestId: 'legacy-request' }), now, now);
77
+ await db.close();
78
+ const preview = await queueLocalLessonsForTeam([root], { databasePath, dryRun: true });
79
+ expect(preview).toMatchObject({ matched: 1, eligible: 1, wouldQueue: 1, queued: 0 });
80
+ const queued = await queueLocalLessonsForTeam([root], { databasePath });
81
+ expect(queued).toMatchObject({ matched: 1, eligible: 1, wouldQueue: 1, queued: 1 });
82
+ const existingOutbox = await openDatabase(databasePath);
83
+ await existingOutbox.run('UPDATE sync_outbox SET id = ? WHERE entity_id = ?', 'outbox-created-by-normal-learning', 'lesson-legacy');
84
+ await existingOutbox.close();
85
+ const repeated = await queueLocalLessonsForTeam([root], { databasePath });
86
+ expect(repeated).toMatchObject({ matched: 1, eligible: 1, wouldQueue: 0, queued: 0, alreadyQueued: 1 });
87
+ const verified = await openDatabase(databasePath);
88
+ const lesson = await verified.get('SELECT actor_id, team_id, visibility, state FROM lessons WHERE id = ?', 'lesson-legacy');
89
+ expect(lesson).toMatchObject({ actor_id: 'alice', team_id: 'platform', visibility: 'personal', state: 'candidate' });
90
+ const outbox = await verified.get('SELECT payload_json FROM sync_outbox WHERE entity_id = ?', 'lesson-legacy');
91
+ const payload = await decryptLocalPayload(outbox.payload_json);
92
+ expect(payload).toMatchObject({ id: 'lesson-legacy', actorId: 'alice', teamId: 'platform', visibility: 'personal', state: 'candidate' });
93
+ await verified.close();
94
+ });
95
+ });
@@ -0,0 +1,17 @@
1
+ export interface LocalLessonImportResult {
2
+ dryRun: boolean;
3
+ repositoryIds: string[];
4
+ matched: number;
5
+ eligible: number;
6
+ queued: number;
7
+ wouldQueue: number;
8
+ alreadyQueued: number;
9
+ skippedOwnership: number;
10
+ skippedVisibility: number;
11
+ }
12
+ export interface LocalLessonImportOptions {
13
+ dryRun?: boolean;
14
+ databasePath?: string;
15
+ }
16
+ /** Queue pre-team-mode personal lessons without publishing or promoting them. */
17
+ export declare function queueLocalLessonsForTeam(repositoryPaths: string[], options?: LocalLessonImportOptions): Promise<LocalLessonImportResult>;
@@ -0,0 +1,97 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { openDatabase } from './db.js';
3
+ import { decryptLocalPayload, encryptLocalPayload } from './local-encryption.js';
4
+ import { lessonRowToRecord } from './lesson-record.js';
5
+ import { getRepositoryId } from './lessons.js';
6
+ import { loadTeamConfiguration } from './team-store.js';
7
+ function createResult(repositoryIds, dryRun) {
8
+ return {
9
+ dryRun,
10
+ repositoryIds,
11
+ matched: 0,
12
+ eligible: 0,
13
+ queued: 0,
14
+ wouldQueue: 0,
15
+ alreadyQueued: 0,
16
+ skippedOwnership: 0,
17
+ skippedVisibility: 0,
18
+ };
19
+ }
20
+ function importOutboxId(lesson, config) {
21
+ const identity = [lesson.id, lesson.updatedAt, config.organizationId, config.teamId, config.actorId].join('\0');
22
+ return `outbox-import-${createHash('sha256').update(identity).digest('hex')}`;
23
+ }
24
+ async function hasQueuedLessonVersion(db, lesson) {
25
+ const rows = await db.all(`SELECT payload_json FROM sync_outbox
26
+ WHERE entity_type = 'lesson' AND entity_id = ?`, lesson.id);
27
+ for (const row of rows) {
28
+ try {
29
+ const payload = await decryptLocalPayload(String(row.payload_json));
30
+ if (payload.id === lesson.id
31
+ && payload.updatedAt === lesson.updatedAt
32
+ && payload.actorId === lesson.actorId
33
+ && payload.teamId === lesson.teamId
34
+ && payload.visibility === lesson.visibility)
35
+ return true;
36
+ }
37
+ catch {
38
+ // Ignore unreadable legacy entries and queue a recoverable replacement.
39
+ }
40
+ }
41
+ return false;
42
+ }
43
+ async function queueLesson(db, row, config, result) {
44
+ const lesson = await lessonRowToRecord(row);
45
+ const normalized = { ...lesson, actorId: config.actorId, teamId: config.teamId, visibility: 'personal' };
46
+ const outboxId = importOutboxId(normalized, config);
47
+ const existing = await hasQueuedLessonVersion(db, normalized);
48
+ result.eligible++;
49
+ if (existing)
50
+ result.alreadyQueued++;
51
+ else
52
+ result.wouldQueue++;
53
+ if (result.dryRun)
54
+ return;
55
+ await db.run('UPDATE lessons SET actor_id = ?, team_id = ? WHERE id = ?', config.actorId, config.teamId, lesson.id);
56
+ if (existing)
57
+ return;
58
+ await db.run(`INSERT INTO sync_outbox (id, operation, entity_type, entity_id, payload_json, created_at)
59
+ VALUES (?, 'upsert', 'lesson', ?, ?, ?)`, outboxId, normalized.id, await encryptLocalPayload(normalized), Date.now());
60
+ result.queued++;
61
+ }
62
+ async function queueEligibleRows(db, rows, config, result) {
63
+ for (const row of rows) {
64
+ if (row.visibility !== 'personal') {
65
+ result.skippedVisibility++;
66
+ }
67
+ else if (row.actor_id && row.actor_id !== config.actorId) {
68
+ result.skippedOwnership++;
69
+ }
70
+ else {
71
+ await queueLesson(db, row, config, result);
72
+ }
73
+ }
74
+ }
75
+ /** Queue pre-team-mode personal lessons without publishing or promoting them. */
76
+ export async function queueLocalLessonsForTeam(repositoryPaths, options = {}) {
77
+ if (repositoryPaths.length === 0)
78
+ throw new Error('At least one repository path is required.');
79
+ const config = await loadTeamConfiguration();
80
+ if (!config)
81
+ throw new Error('Configure PostgreSQL team mode before importing local lessons.');
82
+ const repositoryIds = [...new Set(await Promise.all(repositoryPaths.map(getRepositoryId)))];
83
+ const db = await openDatabase(options.databasePath);
84
+ if (!db)
85
+ throw new Error('SQLite local cache is unavailable.');
86
+ const result = createResult(repositoryIds, Boolean(options.dryRun));
87
+ try {
88
+ const placeholders = repositoryIds.map(() => '?').join(', ');
89
+ const rows = await db.all(`SELECT * FROM lessons WHERE repository_id IN (${placeholders}) ORDER BY updated_at ASC`, ...repositoryIds);
90
+ result.matched = rows.length;
91
+ await db.transaction(tx => queueEligibleRows(tx, rows, config, result));
92
+ return result;
93
+ }
94
+ finally {
95
+ await db.close();
96
+ }
97
+ }
@@ -12,8 +12,9 @@ interface EmbeddableLesson {
12
12
  kind: string;
13
13
  subject: string;
14
14
  source: string;
15
+ state?: string;
15
16
  }
16
- export declare const TEAM_VECTOR_SCHEMA = "\nCREATE EXTENSION IF NOT EXISTS vector;\nCREATE TABLE IF NOT EXISTS rigour.lesson_embeddings (\n lesson_id TEXT PRIMARY KEY REFERENCES rigour.lessons(id) ON DELETE CASCADE,\n organization_id TEXT NOT NULL,\n team_id TEXT NOT NULL,\n repository_id TEXT NOT NULL,\n actor_id TEXT NOT NULL,\n model TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n embedding vector(384) NOT NULL,\n updated_at BIGINT NOT NULL\n);\nALTER TABLE rigour.lesson_embeddings ENABLE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS lesson_embeddings_read ON rigour.lesson_embeddings;\nCREATE POLICY lesson_embeddings_read ON rigour.lesson_embeddings FOR SELECT USING (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n JOIN rigour.lessons lesson ON lesson.id = lesson_embeddings.lesson_id\n WHERE membership.db_role = current_user\n AND membership.organization_id = lesson_embeddings.organization_id\n AND membership.team_id = lesson_embeddings.team_id\n AND ((lesson.visibility = 'team' AND lesson.state = 'promoted')\n OR membership.actor_id = lesson.actor_id)\n )\n);\nDROP POLICY IF EXISTS lesson_embeddings_write ON rigour.lesson_embeddings;\nCREATE POLICY lesson_embeddings_write ON rigour.lesson_embeddings FOR ALL USING (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lesson_embeddings.organization_id\n AND membership.team_id = lesson_embeddings.team_id\n AND membership.actor_id = lesson_embeddings.actor_id\n )\n) WITH CHECK (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lesson_embeddings.organization_id\n AND membership.team_id = lesson_embeddings.team_id\n AND membership.actor_id = lesson_embeddings.actor_id\n )\n);\nCREATE INDEX IF NOT EXISTS lesson_embeddings_scope\n ON rigour.lesson_embeddings (organization_id, team_id, model);\nCREATE INDEX IF NOT EXISTS lesson_embeddings_hnsw\n ON rigour.lesson_embeddings USING hnsw (embedding vector_cosine_ops);\n";
17
+ export declare const TEAM_VECTOR_SCHEMA = "\nCREATE EXTENSION IF NOT EXISTS vector;\nCREATE TABLE IF NOT EXISTS rigour.lesson_embeddings (\n lesson_id TEXT PRIMARY KEY REFERENCES rigour.lessons(id) ON DELETE CASCADE,\n organization_id TEXT NOT NULL,\n team_id TEXT NOT NULL,\n repository_id TEXT NOT NULL,\n actor_id TEXT NOT NULL,\n model TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n embedding vector(384) NOT NULL,\n updated_at BIGINT NOT NULL\n);\nALTER TABLE rigour.lesson_embeddings ENABLE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS lesson_embeddings_read ON rigour.lesson_embeddings;\nCREATE POLICY lesson_embeddings_read ON rigour.lesson_embeddings FOR SELECT USING (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n JOIN rigour.lessons lesson ON lesson.id = lesson_embeddings.lesson_id\n WHERE membership.db_role = current_user\n AND membership.organization_id = lesson_embeddings.organization_id\n AND membership.team_id = lesson_embeddings.team_id\n AND ((lesson.visibility = 'team' AND lesson.state = 'promoted')\n OR membership.actor_id = lesson.actor_id)\n )\n);\nDROP POLICY IF EXISTS lesson_embeddings_write ON rigour.lesson_embeddings;\nCREATE POLICY lesson_embeddings_write ON rigour.lesson_embeddings FOR ALL USING (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lesson_embeddings.organization_id\n AND membership.team_id = lesson_embeddings.team_id\n AND membership.actor_id = lesson_embeddings.actor_id\n )\n) WITH CHECK (\n EXISTS (\n SELECT 1 FROM rigour.memberships membership\n WHERE membership.db_role = current_user\n AND membership.organization_id = lesson_embeddings.organization_id\n AND membership.team_id = lesson_embeddings.team_id\n AND membership.actor_id = lesson_embeddings.actor_id\n )\n);\nCREATE INDEX IF NOT EXISTS lesson_embeddings_scope\n ON rigour.lesson_embeddings (organization_id, team_id, model);\nCREATE INDEX IF NOT EXISTS lesson_embeddings_hnsw\n ON rigour.lesson_embeddings USING hnsw (embedding vector_cosine_ops);\nDELETE FROM rigour.lesson_embeddings embedding\nUSING rigour.lessons lesson\nWHERE lesson.id = embedding.lesson_id\n AND lesson.state NOT IN ('validated', 'promoted');\n";
17
18
  export declare function getTeamSemanticHealth(pool: PgPool, config: TeamConfiguration): Promise<NonNullable<TeamModeStatus['semantic']>>;
18
19
  export declare function upsertTeamLessonEmbedding(pool: PgPool, config: TeamConfiguration, lesson: EmbeddableLesson): Promise<boolean>;
19
20
  export declare function backfillConfiguredTeamEmbeddings(config: TeamConfiguration, limit: number): Promise<{
@@ -47,6 +47,10 @@ CREATE INDEX IF NOT EXISTS lesson_embeddings_scope
47
47
  ON rigour.lesson_embeddings (organization_id, team_id, model);
48
48
  CREATE INDEX IF NOT EXISTS lesson_embeddings_hnsw
49
49
  ON rigour.lesson_embeddings USING hnsw (embedding vector_cosine_ops);
50
+ DELETE FROM rigour.lesson_embeddings embedding
51
+ USING rigour.lessons lesson
52
+ WHERE lesson.id = embedding.lesson_id
53
+ AND lesson.state NOT IN ('validated', 'promoted');
50
54
  `;
51
55
  async function createPool(databaseUrl) {
52
56
  try {
@@ -67,14 +71,24 @@ export async function getTeamSemanticHealth(pool, config) {
67
71
  throw new Error('pgvector team semantics is not configured.');
68
72
  const result = await pool.query(`SELECT (SELECT extversion FROM pg_extension WHERE extname = 'vector') AS extversion,
69
73
  to_regclass('rigour.lesson_embeddings') IS NOT NULL AS table_ready,
70
- (SELECT COUNT(*)::integer FROM rigour.lesson_embeddings) AS indexed_lessons,
74
+ (SELECT COUNT(*)::integer
75
+ FROM rigour.lesson_embeddings embedding
76
+ JOIN rigour.lessons lesson ON lesson.id = embedding.lesson_id
77
+ WHERE embedding.organization_id = $1 AND embedding.team_id = $2
78
+ AND embedding.model = $3
79
+ AND lesson.state IN ('validated', 'promoted')
80
+ AND ((lesson.visibility = 'personal' AND lesson.actor_id = $4)
81
+ OR (lesson.visibility = 'team' AND lesson.state = 'promoted'))) AS indexed_lessons,
71
82
  (SELECT COUNT(*)::integer
72
83
  FROM rigour.lessons lesson
73
- WHERE lesson.state IN ('validated', 'promoted')
84
+ WHERE lesson.organization_id = $1 AND lesson.team_id = $2
85
+ AND lesson.state IN ('validated', 'promoted')
86
+ AND ((lesson.visibility = 'personal' AND lesson.actor_id = $4)
87
+ OR (lesson.visibility = 'team' AND lesson.state = 'promoted'))
74
88
  AND NOT EXISTS (
75
89
  SELECT 1 FROM rigour.lesson_embeddings embedding
76
- WHERE embedding.lesson_id = lesson.id
77
- )) AS missing_lessons`);
90
+ WHERE embedding.lesson_id = lesson.id AND embedding.model = $3
91
+ )) AS missing_lessons`, [config.organizationId, config.teamId, config.semantic.model, config.actorId]);
78
92
  const row = result.rows[0];
79
93
  if (!row?.extversion || !row?.table_ready) {
80
94
  throw new Error('pgvector semantic storage is configured but the vector extension or lesson_embeddings table is missing.');
@@ -92,6 +106,8 @@ export async function getTeamSemanticHealth(pool, config) {
92
106
  export async function upsertTeamLessonEmbedding(pool, config, lesson) {
93
107
  if (!config.semantic)
94
108
  return false;
109
+ if (!lesson.state || !['validated', 'promoted'].includes(lesson.state))
110
+ return false;
95
111
  const { generateEmbedding } = await import('../pattern-index/embeddings.js');
96
112
  const content = `${lesson.kind}\n${lesson.subject}\n${lesson.source}`;
97
113
  const embedding = await generateEmbedding(content);
@@ -113,7 +129,7 @@ export async function backfillConfiguredTeamEmbeddings(config, limit) {
113
129
  throw new Error('pgvector team semantics is not configured.');
114
130
  const pool = await createPool(config.databaseUrl);
115
131
  try {
116
- const lessons = await pool.query(`SELECT lesson.id, lesson.repository_id, lesson.actor_id, lesson.kind, lesson.subject, lesson.source
132
+ const lessons = await pool.query(`SELECT lesson.id, lesson.repository_id, lesson.actor_id, lesson.kind, lesson.subject, lesson.source, lesson.state
117
133
  FROM rigour.lessons lesson
118
134
  WHERE lesson.organization_id = $1 AND lesson.team_id = $2
119
135
  AND lesson.actor_id = $3
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { getTeamSemanticHealth } from './team-vector-store.js';
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { getTeamSemanticHealth, upsertTeamLessonEmbedding } from './team-vector-store.js';
3
3
  const config = {
4
4
  organizationId: 'acme',
5
5
  teamId: 'platform',
@@ -14,12 +14,12 @@ const config = {
14
14
  describe('pgvector team health', () => {
15
15
  it('reports extension, model, and embedding coverage', async () => {
16
16
  const pool = {
17
- query: async () => ({ rows: [{
17
+ query: vi.fn(async () => ({ rows: [{
18
18
  extversion: '0.8.1',
19
19
  table_ready: true,
20
20
  indexed_lessons: 12,
21
21
  missing_lessons: 3,
22
- }] }),
22
+ }] })),
23
23
  end: async () => undefined,
24
24
  };
25
25
  await expect(getTeamSemanticHealth(pool, config)).resolves.toEqual({
@@ -31,6 +31,7 @@ describe('pgvector team health', () => {
31
31
  indexedLessons: 12,
32
32
  missingLessons: 3,
33
33
  });
34
+ expect(pool.query).toHaveBeenCalledWith(expect.stringContaining("lesson.state IN ('validated', 'promoted')"), ['acme', 'platform', 'Xenova/all-MiniLM-L6-v2', 'alice']);
34
35
  });
35
36
  it('fails health checks when vector storage is incomplete', async () => {
36
37
  const pool = {
@@ -39,4 +40,17 @@ describe('pgvector team health', () => {
39
40
  };
40
41
  await expect(getTeamSemanticHealth(pool, config)).rejects.toThrow(/vector extension/);
41
42
  });
43
+ it('does not embed unvalidated candidate lessons', async () => {
44
+ const pool = { query: vi.fn(), end: async () => undefined };
45
+ await expect(upsertTeamLessonEmbedding(pool, config, {
46
+ id: 'candidate-1',
47
+ repositoryId: 'repo-1',
48
+ actorId: 'alice',
49
+ kind: 'interaction',
50
+ subject: 'Unverified suggestion',
51
+ source: 'mcp',
52
+ state: 'candidate',
53
+ })).resolves.toBe(false);
54
+ expect(pool.query).not.toHaveBeenCalled();
55
+ });
42
56
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "6.2.1",
3
+ "version": "6.2.2",
4
4
  "description": "AI-native quality gate engine with local Bayesian learning. AST analysis, drift detection, Fix Packet generation, and agent self-healing across TypeScript, JavaScript, Python, Go, Ruby, and C#.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rigour.run",
@@ -67,11 +67,11 @@
67
67
  "pg": "^8.16.3",
68
68
  "sqlite3": "^5.1.7",
69
69
  "openai": "^4.104.0",
70
- "@rigour-labs/brain-darwin-arm64": "6.2.1",
71
- "@rigour-labs/brain-darwin-x64": "6.2.1",
72
- "@rigour-labs/brain-linux-arm64": "6.2.1",
73
- "@rigour-labs/brain-linux-x64": "6.2.1",
74
- "@rigour-labs/brain-win-x64": "6.2.1"
70
+ "@rigour-labs/brain-darwin-x64": "6.2.2",
71
+ "@rigour-labs/brain-darwin-arm64": "6.2.2",
72
+ "@rigour-labs/brain-linux-arm64": "6.2.2",
73
+ "@rigour-labs/brain-win-x64": "6.2.2",
74
+ "@rigour-labs/brain-linux-x64": "6.2.2"
75
75
  },
76
76
  "devDependencies": {
77
77
  "@types/fs-extra": "^11.0.4",