@rigour-labs/core 6.0.0 → 6.1.0-beta.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.
Files changed (48) hide show
  1. package/dist/context/automatic-index.d.ts +18 -0
  2. package/dist/context/automatic-index.js +142 -0
  3. package/dist/context/automatic-index.test.d.ts +1 -0
  4. package/dist/context/automatic-index.test.js +45 -0
  5. package/dist/context/cache-engine.d.ts +25 -0
  6. package/dist/context/dependency-graph.d.ts +13 -0
  7. package/dist/context/dependency-graph.js +97 -0
  8. package/dist/context/dependency-graph.test.d.ts +1 -0
  9. package/dist/context/dependency-graph.test.js +13 -0
  10. package/dist/context/index-bridge.js +21 -2
  11. package/dist/index.d.ts +9 -0
  12. package/dist/index.js +7 -0
  13. package/dist/pattern-index/embeddings.js +10 -3
  14. package/dist/pattern-index/indexer.d.ts +2 -0
  15. package/dist/pattern-index/indexer.js +72 -0
  16. package/dist/services/agent-history.d.ts +76 -0
  17. package/dist/services/agent-history.js +201 -0
  18. package/dist/services/agent-history.test.d.ts +1 -0
  19. package/dist/services/agent-history.test.js +69 -0
  20. package/dist/services/context-telemetry-service.d.ts +21 -0
  21. package/dist/services/context-telemetry-service.js +36 -7
  22. package/dist/services/context-telemetry-service.test.js +13 -0
  23. package/dist/services/engineering-knowledge-graph.d.ts +55 -0
  24. package/dist/services/engineering-knowledge-graph.js +177 -0
  25. package/dist/services/engineering-knowledge-graph.test.d.ts +1 -0
  26. package/dist/services/engineering-knowledge-graph.test.js +59 -0
  27. package/dist/services/model-pricing.d.ts +2 -0
  28. package/dist/services/model-pricing.js +2 -0
  29. package/dist/services/system-health.d.ts +34 -0
  30. package/dist/services/system-health.js +53 -0
  31. package/dist/storage/db.js +130 -1
  32. package/dist/storage/index.d.ts +4 -0
  33. package/dist/storage/index.js +2 -0
  34. package/dist/storage/lessons.d.ts +60 -0
  35. package/dist/storage/lessons.js +322 -0
  36. package/dist/storage/lessons.test.d.ts +1 -0
  37. package/dist/storage/lessons.test.js +50 -0
  38. package/dist/storage/local-encryption.d.ts +2 -0
  39. package/dist/storage/local-encryption.js +41 -0
  40. package/dist/storage/team-store.d.ts +75 -0
  41. package/dist/storage/team-store.js +321 -0
  42. package/dist/storage/team-store.test.d.ts +1 -0
  43. package/dist/storage/team-store.test.js +11 -0
  44. package/dist/storage/team-vector-store.d.ts +25 -0
  45. package/dist/storage/team-vector-store.js +183 -0
  46. package/dist/storage/team-vector-store.test.d.ts +1 -0
  47. package/dist/storage/team-vector-store.test.js +42 -0
  48. package/package.json +8 -6
@@ -0,0 +1,321 @@
1
+ import fs from 'fs-extra';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { openDatabase } from './db.js';
5
+ import { decryptLocalPayload, encryptLocalPayload } from './local-encryption.js';
6
+ import { TEAM_VECTOR_SCHEMA, backfillConfiguredTeamEmbeddings, getTeamSemanticHealth, searchConfiguredTeamKnowledge, upsertTeamLessonEmbedding, } from './team-vector-store.js';
7
+ const DEFAULT_SEMANTIC = {
8
+ provider: 'pgvector',
9
+ model: 'Xenova/all-MiniLM-L6-v2',
10
+ dimensions: 384,
11
+ };
12
+ export const TEAM_CONFIG_PATH = path.join(os.homedir(), '.rigour', 'team.json');
13
+ function nonEmpty(value) {
14
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
15
+ }
16
+ export function validateTeamDatabaseUrl(databaseUrl) {
17
+ const parsed = new URL(databaseUrl);
18
+ if (!['postgres:', 'postgresql:'].includes(parsed.protocol)) {
19
+ throw new Error('Team database URL must use postgres:// or postgresql://.');
20
+ }
21
+ const local = ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
22
+ const sslMode = parsed.searchParams.get('sslmode');
23
+ if (!local && sslMode !== 'require' && sslMode !== 'verify-full') {
24
+ throw new Error('Remote team databases require sslmode=require or sslmode=verify-full.');
25
+ }
26
+ }
27
+ export async function loadTeamConfiguration() {
28
+ let file = {};
29
+ try {
30
+ file = await fs.readJson(TEAM_CONFIG_PATH);
31
+ }
32
+ catch {
33
+ // Environment-only configuration is supported.
34
+ }
35
+ const organizationId = nonEmpty(process.env.RIGOUR_ORGANIZATION_ID) ?? nonEmpty(file.organizationId);
36
+ const teamId = nonEmpty(process.env.RIGOUR_TEAM_ID) ?? nonEmpty(file.teamId);
37
+ const actorId = nonEmpty(process.env.RIGOUR_ACTOR_ID) ?? nonEmpty(file.actorId);
38
+ const databaseUrl = nonEmpty(process.env.RIGOUR_TEAM_DATABASE_URL) ?? nonEmpty(file.databaseUrl);
39
+ if (!organizationId || !teamId || !actorId || !databaseUrl)
40
+ return null;
41
+ const envSemantic = process.env.RIGOUR_TEAM_SEMANTIC === 'pgvector' ? DEFAULT_SEMANTIC : undefined;
42
+ const semantic = envSemantic ?? (file.semantic?.provider === 'pgvector' ? DEFAULT_SEMANTIC : undefined);
43
+ return { organizationId, teamId, actorId, databaseUrl, semantic };
44
+ }
45
+ export async function saveTeamConfiguration(config) {
46
+ if (!config.databaseUrl)
47
+ throw new Error('A PostgreSQL database URL is required.');
48
+ validateTeamDatabaseUrl(config.databaseUrl);
49
+ for (const [name, value] of Object.entries({
50
+ organizationId: config.organizationId,
51
+ teamId: config.teamId,
52
+ actorId: config.actorId,
53
+ })) {
54
+ if (!nonEmpty(value))
55
+ throw new Error(`${name} is required.`);
56
+ }
57
+ await fs.ensureDir(path.dirname(TEAM_CONFIG_PATH));
58
+ await fs.writeJson(TEAM_CONFIG_PATH, config, { spaces: 2, mode: 0o600 });
59
+ await fs.chmod(TEAM_CONFIG_PATH, 0o600);
60
+ }
61
+ async function loadPg() {
62
+ try {
63
+ return await import('pg');
64
+ }
65
+ catch {
66
+ throw new Error('PostgreSQL support is unavailable. Install the optional "pg" dependency.');
67
+ }
68
+ }
69
+ async function queuedChanges() {
70
+ let db;
71
+ try {
72
+ db = await openDatabase();
73
+ }
74
+ catch {
75
+ return 0;
76
+ }
77
+ if (!db)
78
+ return 0;
79
+ try {
80
+ const row = await db.get('SELECT COUNT(*) AS count FROM sync_outbox WHERE synced_at IS NULL');
81
+ return Number(row?.count ?? 0);
82
+ }
83
+ finally {
84
+ await db.close();
85
+ }
86
+ }
87
+ const TEAM_SCHEMA = `
88
+ CREATE SCHEMA IF NOT EXISTS rigour;
89
+ CREATE TABLE IF NOT EXISTS rigour.meta (
90
+ key TEXT PRIMARY KEY,
91
+ value TEXT NOT NULL
92
+ );
93
+ CREATE TABLE IF NOT EXISTS rigour.memberships (
94
+ db_role NAME NOT NULL,
95
+ organization_id TEXT NOT NULL,
96
+ team_id TEXT NOT NULL,
97
+ actor_id TEXT NOT NULL,
98
+ role TEXT NOT NULL CHECK (role IN ('member', 'sme', 'owner')),
99
+ PRIMARY KEY (db_role, team_id)
100
+ );
101
+ CREATE TABLE IF NOT EXISTS rigour.lessons (
102
+ id TEXT PRIMARY KEY,
103
+ organization_id TEXT NOT NULL,
104
+ team_id TEXT NOT NULL,
105
+ repository_id TEXT NOT NULL,
106
+ actor_id TEXT NOT NULL,
107
+ visibility TEXT NOT NULL CHECK (visibility IN ('personal', 'team')),
108
+ state TEXT NOT NULL CHECK (state IN ('candidate', 'validated', 'promoted', 'rejected', 'superseded')),
109
+ kind TEXT NOT NULL,
110
+ subject TEXT NOT NULL,
111
+ evidence_json JSONB NOT NULL,
112
+ confidence DOUBLE PRECISION NOT NULL,
113
+ source TEXT NOT NULL,
114
+ supersedes_id TEXT,
115
+ created_at BIGINT NOT NULL,
116
+ updated_at BIGINT NOT NULL
117
+ );
118
+ ALTER TABLE rigour.lessons ENABLE ROW LEVEL SECURITY;
119
+ DROP POLICY IF EXISTS lessons_read ON rigour.lessons;
120
+ CREATE POLICY lessons_read ON rigour.lessons FOR SELECT USING (
121
+ EXISTS (
122
+ SELECT 1 FROM rigour.memberships membership
123
+ WHERE membership.db_role = current_user
124
+ AND membership.organization_id = lessons.organization_id
125
+ AND membership.team_id = lessons.team_id
126
+ AND ((lessons.visibility = 'team' AND lessons.state = 'promoted')
127
+ OR membership.actor_id = lessons.actor_id)
128
+ )
129
+ );
130
+ DROP POLICY IF EXISTS lessons_write ON rigour.lessons;
131
+ CREATE POLICY lessons_write ON rigour.lessons FOR ALL USING (
132
+ EXISTS (
133
+ SELECT 1 FROM rigour.memberships membership
134
+ WHERE membership.db_role = current_user
135
+ AND membership.organization_id = lessons.organization_id
136
+ AND membership.team_id = lessons.team_id
137
+ AND membership.actor_id = lessons.actor_id
138
+ AND (lessons.visibility = 'personal' OR membership.role IN ('sme', 'owner'))
139
+ )
140
+ ) WITH CHECK (
141
+ EXISTS (
142
+ SELECT 1 FROM rigour.memberships membership
143
+ WHERE membership.db_role = current_user
144
+ AND membership.organization_id = lessons.organization_id
145
+ AND membership.team_id = lessons.team_id
146
+ AND membership.actor_id = lessons.actor_id
147
+ AND (lessons.visibility = 'personal' OR membership.role IN ('sme', 'owner'))
148
+ )
149
+ );
150
+ INSERT INTO rigour.meta (key, value) VALUES ('schema_version', '1')
151
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
152
+ `;
153
+ export async function initializeTeamSchema(databaseUrl, options = {}) {
154
+ validateTeamDatabaseUrl(databaseUrl);
155
+ const { Pool } = await loadPg();
156
+ const pool = new Pool({ connectionString: databaseUrl });
157
+ try {
158
+ await pool.query(TEAM_SCHEMA);
159
+ if (options.pgvector)
160
+ await pool.query(TEAM_VECTOR_SCHEMA);
161
+ }
162
+ finally {
163
+ await pool.end();
164
+ }
165
+ }
166
+ export async function getTeamModeStatus() {
167
+ const config = await loadTeamConfiguration();
168
+ const queued = await queuedChanges();
169
+ if (!config?.databaseUrl) {
170
+ return { mode: 'local', connectivity: 'local', queuedChanges: queued, message: 'SQLite local-only mode' };
171
+ }
172
+ try {
173
+ const result = await doctorTeamConnection(config);
174
+ return result;
175
+ }
176
+ catch (error) {
177
+ return {
178
+ mode: 'team',
179
+ connectivity: 'offline',
180
+ organizationId: config.organizationId,
181
+ teamId: config.teamId,
182
+ actorId: config.actorId,
183
+ queuedChanges: queued,
184
+ message: `Offline — changes queued. ${error instanceof Error ? error.message : String(error)}`,
185
+ };
186
+ }
187
+ }
188
+ export async function doctorTeamConnection(config) {
189
+ const resolved = config ?? await loadTeamConfiguration();
190
+ if (!resolved?.databaseUrl) {
191
+ return { mode: 'local', connectivity: 'local', queuedChanges: await queuedChanges(), message: 'Team mode is not configured.' };
192
+ }
193
+ validateTeamDatabaseUrl(resolved.databaseUrl);
194
+ const { Pool } = await loadPg();
195
+ const pool = new Pool({ connectionString: resolved.databaseUrl });
196
+ try {
197
+ const result = await pool.query(`SELECT current_user AS database_role,
198
+ (SELECT value::integer FROM rigour.meta WHERE key = 'schema_version') AS schema_version,
199
+ role
200
+ FROM rigour.memberships
201
+ WHERE db_role = current_user AND team_id = $1 AND actor_id = $2 AND organization_id = $3`, [resolved.teamId, resolved.actorId, resolved.organizationId]);
202
+ if (result.rowCount !== 1)
203
+ throw new Error('Database role is not provisioned for this team.');
204
+ const schemaVersion = Number(result.rows[0].schema_version);
205
+ if (schemaVersion !== 1)
206
+ throw new Error(`Incompatible team schema version ${schemaVersion}; expected 1.`);
207
+ const semantic = resolved.semantic ? await getTeamSemanticHealth(pool, resolved) : undefined;
208
+ return {
209
+ mode: 'team',
210
+ connectivity: 'online',
211
+ organizationId: resolved.organizationId,
212
+ teamId: resolved.teamId,
213
+ actorId: resolved.actorId,
214
+ queuedChanges: await queuedChanges(),
215
+ schemaVersion,
216
+ databaseRole: String(result.rows[0].database_role),
217
+ permissions: [String(result.rows[0].role)],
218
+ semantic,
219
+ message: 'PostgreSQL team mode is healthy.',
220
+ };
221
+ }
222
+ finally {
223
+ await pool.end();
224
+ }
225
+ }
226
+ export async function syncTeamOutbox(options = {}) {
227
+ const config = await loadTeamConfiguration();
228
+ if (!config?.databaseUrl)
229
+ throw new Error('Team mode is not configured.');
230
+ const db = await openDatabase();
231
+ if (!db)
232
+ throw new Error('SQLite offline cache is unavailable.');
233
+ try {
234
+ const pending = await db.all(`SELECT * FROM sync_outbox WHERE synced_at IS NULL ORDER BY created_at ASC LIMIT 200`);
235
+ if (options.dryRun)
236
+ return { pending: pending.length, synced: 0, pulled: 0 };
237
+ const { Pool } = await loadPg();
238
+ const pool = new Pool({ connectionString: config.databaseUrl });
239
+ let synced = 0;
240
+ let pulled = 0;
241
+ try {
242
+ const membership = await pool.query(`SELECT 1 FROM rigour.memberships
243
+ WHERE db_role = current_user AND team_id = $1
244
+ AND actor_id = $2 AND organization_id = $3`, [config.teamId, config.actorId, config.organizationId]);
245
+ if (membership.rowCount !== 1)
246
+ throw new Error('Database role is not provisioned for this team.');
247
+ for (const item of pending) {
248
+ const lesson = await decryptLocalPayload(item.payload_json);
249
+ try {
250
+ const result = await pool.query(`INSERT INTO rigour.lessons (
251
+ id, organization_id, team_id, repository_id, actor_id, visibility, state,
252
+ kind, subject, evidence_json, confidence, source, supersedes_id, created_at, updated_at
253
+ )
254
+ SELECT $1, membership.organization_id, membership.team_id, $2, membership.actor_id,
255
+ $3, $4, $5, $6, $7::jsonb, $8, $9, $10, $11, $12
256
+ FROM rigour.memberships membership
257
+ WHERE membership.db_role = current_user AND membership.team_id = $13
258
+ AND membership.actor_id = $14 AND membership.organization_id = $15
259
+ ON CONFLICT (id) DO UPDATE SET
260
+ visibility = EXCLUDED.visibility,
261
+ state = EXCLUDED.state,
262
+ evidence_json = EXCLUDED.evidence_json,
263
+ confidence = EXCLUDED.confidence,
264
+ updated_at = EXCLUDED.updated_at
265
+ WHERE rigour.lessons.updated_at <= EXCLUDED.updated_at
266
+ RETURNING id`, [
267
+ lesson.id, lesson.repositoryId, lesson.visibility, lesson.state, lesson.kind,
268
+ lesson.subject, JSON.stringify(lesson.evidence), lesson.confidence, lesson.source,
269
+ lesson.supersedesId ?? null, lesson.createdAt, lesson.updatedAt, config.teamId,
270
+ config.actorId, config.organizationId,
271
+ ]);
272
+ if (config.semantic && result.rowCount > 0)
273
+ await upsertTeamLessonEmbedding(pool, config, lesson);
274
+ await db.run('UPDATE sync_outbox SET synced_at = ?, last_error = NULL WHERE id = ?', Date.now(), item.id);
275
+ synced++;
276
+ }
277
+ catch (error) {
278
+ await db.run('UPDATE sync_outbox SET attempts = attempts + 1, last_error = ? WHERE id = ?', String(error instanceof Error ? error.message : error).slice(0, 1_000), item.id);
279
+ throw error;
280
+ }
281
+ }
282
+ const remote = await pool.query(`SELECT * FROM rigour.lessons
283
+ WHERE team_id = $1 AND organization_id = $3
284
+ AND ((actor_id = $2) OR (visibility = 'team' AND state = 'promoted'))
285
+ ORDER BY updated_at ASC`, [config.teamId, config.actorId, config.organizationId]);
286
+ for (const row of remote.rows) {
287
+ const evidence = typeof row.evidence_json === 'string' ? JSON.parse(row.evidence_json) : row.evidence_json;
288
+ await db.run(`INSERT INTO lessons (
289
+ id, repository_id, actor_id, team_id, visibility, state, kind, subject,
290
+ evidence_json, confidence, source, supersedes_id, created_at, updated_at
291
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
292
+ ON CONFLICT(id) DO UPDATE SET
293
+ visibility=excluded.visibility, state=excluded.state, evidence_json=excluded.evidence_json,
294
+ confidence=excluded.confidence, source=excluded.source, supersedes_id=excluded.supersedes_id,
295
+ updated_at=excluded.updated_at`, row.id, row.repository_id, row.actor_id, row.team_id, row.visibility, row.state, row.kind, row.subject, await encryptLocalPayload(evidence), row.confidence, row.source, row.supersedes_id, Number(row.created_at), Number(row.updated_at));
296
+ pulled++;
297
+ }
298
+ if (pulled > 0)
299
+ await db.run("DELETE FROM context_cache WHERE cache_type = 'semantic'");
300
+ }
301
+ finally {
302
+ await pool.end();
303
+ }
304
+ return { pending: pending.length, synced, pulled };
305
+ }
306
+ finally {
307
+ await db.close();
308
+ }
309
+ }
310
+ export async function backfillTeamEmbeddings(limit = 200) {
311
+ const config = await loadTeamConfiguration();
312
+ if (!config)
313
+ throw new Error('pgvector team semantics is not configured.');
314
+ return backfillConfiguredTeamEmbeddings(config, limit);
315
+ }
316
+ export async function searchTeamKnowledge(query, limit = 8) {
317
+ const config = await loadTeamConfiguration();
318
+ if (!config)
319
+ return { status: 'disabled', candidates: [] };
320
+ return searchConfiguredTeamKnowledge(config, query, limit);
321
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { validateTeamDatabaseUrl } from './team-store.js';
3
+ describe('team database configuration', () => {
4
+ it('requires TLS for remote PostgreSQL URLs', () => {
5
+ expect(() => validateTeamDatabaseUrl('postgresql://user:pass@db.example.com/rigour')).toThrow(/sslmode/);
6
+ expect(() => validateTeamDatabaseUrl('postgresql://user:pass@db.example.com/rigour?sslmode=verify-full')).not.toThrow();
7
+ });
8
+ it('rejects non-PostgreSQL URLs', () => {
9
+ expect(() => validateTeamDatabaseUrl('mysql://localhost/rigour')).toThrow(/postgres/);
10
+ });
11
+ });
@@ -0,0 +1,25 @@
1
+ import type { SemanticKnowledgeResult, TeamConfiguration, TeamModeStatus } from './team-store.js';
2
+ type PgPool = {
3
+ query: (sql: string, params?: unknown[]) => Promise<any>;
4
+ end: () => Promise<void>;
5
+ };
6
+ interface EmbeddableLesson {
7
+ id: string;
8
+ repositoryId?: string;
9
+ repository_id?: string;
10
+ actorId?: string;
11
+ actor_id?: string;
12
+ kind: string;
13
+ subject: string;
14
+ source: string;
15
+ }
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 function getTeamSemanticHealth(pool: PgPool, config: TeamConfiguration): Promise<NonNullable<TeamModeStatus['semantic']>>;
18
+ export declare function upsertTeamLessonEmbedding(pool: PgPool, config: TeamConfiguration, lesson: EmbeddableLesson): Promise<boolean>;
19
+ export declare function backfillConfiguredTeamEmbeddings(config: TeamConfiguration, limit: number): Promise<{
20
+ eligible: number;
21
+ embedded: number;
22
+ skipped: number;
23
+ }>;
24
+ export declare function searchConfiguredTeamKnowledge(config: TeamConfiguration, query: string, limit: number): Promise<SemanticKnowledgeResult>;
25
+ export {};
@@ -0,0 +1,183 @@
1
+ import { createHash } from 'node:crypto';
2
+ export const TEAM_VECTOR_SCHEMA = `
3
+ CREATE EXTENSION IF NOT EXISTS vector;
4
+ CREATE TABLE IF NOT EXISTS rigour.lesson_embeddings (
5
+ lesson_id TEXT PRIMARY KEY REFERENCES rigour.lessons(id) ON DELETE CASCADE,
6
+ organization_id TEXT NOT NULL,
7
+ team_id TEXT NOT NULL,
8
+ repository_id TEXT NOT NULL,
9
+ actor_id TEXT NOT NULL,
10
+ model TEXT NOT NULL,
11
+ content_hash TEXT NOT NULL,
12
+ embedding vector(384) NOT NULL,
13
+ updated_at BIGINT NOT NULL
14
+ );
15
+ ALTER TABLE rigour.lesson_embeddings ENABLE ROW LEVEL SECURITY;
16
+ DROP POLICY IF EXISTS lesson_embeddings_read ON rigour.lesson_embeddings;
17
+ CREATE POLICY lesson_embeddings_read ON rigour.lesson_embeddings FOR SELECT USING (
18
+ EXISTS (
19
+ SELECT 1 FROM rigour.memberships membership
20
+ JOIN rigour.lessons lesson ON lesson.id = lesson_embeddings.lesson_id
21
+ WHERE membership.db_role = current_user
22
+ AND membership.organization_id = lesson_embeddings.organization_id
23
+ AND membership.team_id = lesson_embeddings.team_id
24
+ AND ((lesson.visibility = 'team' AND lesson.state = 'promoted')
25
+ OR membership.actor_id = lesson.actor_id)
26
+ )
27
+ );
28
+ DROP POLICY IF EXISTS lesson_embeddings_write ON rigour.lesson_embeddings;
29
+ CREATE POLICY lesson_embeddings_write ON rigour.lesson_embeddings FOR ALL USING (
30
+ EXISTS (
31
+ SELECT 1 FROM rigour.memberships membership
32
+ WHERE membership.db_role = current_user
33
+ AND membership.organization_id = lesson_embeddings.organization_id
34
+ AND membership.team_id = lesson_embeddings.team_id
35
+ AND membership.actor_id = lesson_embeddings.actor_id
36
+ )
37
+ ) WITH CHECK (
38
+ EXISTS (
39
+ SELECT 1 FROM rigour.memberships membership
40
+ WHERE membership.db_role = current_user
41
+ AND membership.organization_id = lesson_embeddings.organization_id
42
+ AND membership.team_id = lesson_embeddings.team_id
43
+ AND membership.actor_id = lesson_embeddings.actor_id
44
+ )
45
+ );
46
+ CREATE INDEX IF NOT EXISTS lesson_embeddings_scope
47
+ ON rigour.lesson_embeddings (organization_id, team_id, model);
48
+ CREATE INDEX IF NOT EXISTS lesson_embeddings_hnsw
49
+ ON rigour.lesson_embeddings USING hnsw (embedding vector_cosine_ops);
50
+ `;
51
+ async function createPool(databaseUrl) {
52
+ try {
53
+ const { Pool } = await import('pg');
54
+ return new Pool({ connectionString: databaseUrl });
55
+ }
56
+ catch {
57
+ throw new Error('PostgreSQL support is unavailable. Install the optional "pg" dependency.');
58
+ }
59
+ }
60
+ function vectorLiteral(values) {
61
+ if (!values.length || values.some(value => !Number.isFinite(value)))
62
+ throw new Error('Embedding contains invalid values.');
63
+ return `[${values.join(',')}]`;
64
+ }
65
+ export async function getTeamSemanticHealth(pool, config) {
66
+ if (!config.semantic)
67
+ throw new Error('pgvector team semantics is not configured.');
68
+ const result = await pool.query(`SELECT (SELECT extversion FROM pg_extension WHERE extname = 'vector') AS extversion,
69
+ to_regclass('rigour.lesson_embeddings') IS NOT NULL AS table_ready,
70
+ (SELECT COUNT(*)::integer FROM rigour.lesson_embeddings) AS indexed_lessons,
71
+ (SELECT COUNT(*)::integer
72
+ FROM rigour.lessons lesson
73
+ WHERE lesson.state IN ('validated', 'promoted')
74
+ AND NOT EXISTS (
75
+ SELECT 1 FROM rigour.lesson_embeddings embedding
76
+ WHERE embedding.lesson_id = lesson.id
77
+ )) AS missing_lessons`);
78
+ const row = result.rows[0];
79
+ if (!row?.extversion || !row?.table_ready) {
80
+ throw new Error('pgvector semantic storage is configured but the vector extension or lesson_embeddings table is missing.');
81
+ }
82
+ return {
83
+ provider: 'pgvector',
84
+ status: 'ready',
85
+ extensionVersion: String(row.extversion),
86
+ model: config.semantic.model,
87
+ dimensions: config.semantic.dimensions,
88
+ indexedLessons: Number(row.indexed_lessons ?? 0),
89
+ missingLessons: Number(row.missing_lessons ?? 0),
90
+ };
91
+ }
92
+ export async function upsertTeamLessonEmbedding(pool, config, lesson) {
93
+ if (!config.semantic)
94
+ return false;
95
+ const { generateEmbedding } = await import('../pattern-index/embeddings.js');
96
+ const content = `${lesson.kind}\n${lesson.subject}\n${lesson.source}`;
97
+ const embedding = await generateEmbedding(content);
98
+ if (embedding.length !== config.semantic.dimensions)
99
+ return false;
100
+ await pool.query(`INSERT INTO rigour.lesson_embeddings (
101
+ lesson_id, organization_id, team_id, repository_id, actor_id,
102
+ model, content_hash, embedding, updated_at
103
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::vector, $9)
104
+ ON CONFLICT (lesson_id) DO UPDATE SET
105
+ model=EXCLUDED.model, content_hash=EXCLUDED.content_hash,
106
+ embedding=EXCLUDED.embedding, updated_at=EXCLUDED.updated_at`, [lesson.id, config.organizationId, config.teamId, lesson.repositoryId ?? lesson.repository_id,
107
+ lesson.actorId ?? lesson.actor_id ?? config.actorId, config.semantic.model,
108
+ createHash('sha256').update(content).digest('hex'), vectorLiteral(embedding), Date.now()]);
109
+ return true;
110
+ }
111
+ export async function backfillConfiguredTeamEmbeddings(config, limit) {
112
+ if (!config.databaseUrl || !config.semantic)
113
+ throw new Error('pgvector team semantics is not configured.');
114
+ const pool = await createPool(config.databaseUrl);
115
+ try {
116
+ const lessons = await pool.query(`SELECT lesson.id, lesson.repository_id, lesson.actor_id, lesson.kind, lesson.subject, lesson.source
117
+ FROM rigour.lessons lesson
118
+ WHERE lesson.organization_id = $1 AND lesson.team_id = $2
119
+ AND lesson.actor_id = $3
120
+ AND lesson.state IN ('validated', 'promoted')
121
+ AND NOT EXISTS (
122
+ SELECT 1 FROM rigour.lesson_embeddings embedding
123
+ WHERE embedding.lesson_id = lesson.id AND embedding.model = $4
124
+ )
125
+ ORDER BY lesson.updated_at ASC LIMIT $5`, [config.organizationId, config.teamId, config.actorId, config.semantic.model, Math.max(1, Math.min(2_000, limit))]);
126
+ let embedded = 0;
127
+ for (const lesson of lessons.rows) {
128
+ if (await upsertTeamLessonEmbedding(pool, config, lesson))
129
+ embedded++;
130
+ }
131
+ return { eligible: lessons.rows.length, embedded, skipped: lessons.rows.length - embedded };
132
+ }
133
+ finally {
134
+ await pool.end();
135
+ }
136
+ }
137
+ export async function searchConfiguredTeamKnowledge(config, query, limit) {
138
+ if (!config.semantic || !config.databaseUrl)
139
+ return { status: 'disabled', candidates: [] };
140
+ try {
141
+ const { generateEmbedding } = await import('../pattern-index/embeddings.js');
142
+ const embedding = await generateEmbedding(query);
143
+ if (embedding.length !== config.semantic.dimensions) {
144
+ return {
145
+ status: 'degraded', provider: 'pgvector', model: config.semantic.model, candidates: [],
146
+ message: `Embedding model returned ${embedding.length} dimensions; expected ${config.semantic.dimensions}.`,
147
+ };
148
+ }
149
+ const pool = await createPool(config.databaseUrl);
150
+ try {
151
+ const result = await pool.query(`SELECT lesson.id, lesson.repository_id, lesson.subject, lesson.visibility,
152
+ lesson.state, lesson.confidence, lesson.source,
153
+ 1 - (embedding.embedding <=> $1::vector) AS similarity
154
+ FROM rigour.lesson_embeddings embedding
155
+ JOIN rigour.lessons lesson ON lesson.id = embedding.lesson_id
156
+ WHERE embedding.organization_id = $2 AND embedding.team_id = $3
157
+ AND embedding.model = $4
158
+ AND ((lesson.visibility = 'personal' AND lesson.actor_id = $7
159
+ AND lesson.state IN ('validated', 'promoted'))
160
+ OR (lesson.visibility = 'team' AND lesson.state = 'promoted'))
161
+ AND 1 - (embedding.embedding <=> $1::vector) >= $6
162
+ ORDER BY embedding.embedding <=> $1::vector LIMIT $5`, [vectorLiteral(embedding), config.organizationId, config.teamId, config.semantic.model,
163
+ Math.max(1, Math.min(25, limit)), 0.45, config.actorId]);
164
+ return {
165
+ status: 'ready', provider: 'pgvector', model: config.semantic.model,
166
+ candidates: result.rows.map((row) => ({
167
+ lessonId: String(row.id), repositoryId: String(row.repository_id), subject: String(row.subject),
168
+ visibility: String(row.visibility), state: String(row.state), confidence: Number(row.confidence),
169
+ similarity: Number(row.similarity), provenance: `${row.visibility}:${row.source}:${row.id}`,
170
+ })),
171
+ };
172
+ }
173
+ finally {
174
+ await pool.end();
175
+ }
176
+ }
177
+ catch (error) {
178
+ return {
179
+ status: 'degraded', provider: 'pgvector', model: config.semantic.model, candidates: [],
180
+ message: error instanceof Error ? error.message : String(error),
181
+ };
182
+ }
183
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getTeamSemanticHealth } from './team-vector-store.js';
3
+ const config = {
4
+ organizationId: 'acme',
5
+ teamId: 'platform',
6
+ actorId: 'alice',
7
+ databaseUrl: 'postgresql://localhost/rigour',
8
+ semantic: {
9
+ provider: 'pgvector',
10
+ model: 'Xenova/all-MiniLM-L6-v2',
11
+ dimensions: 384,
12
+ },
13
+ };
14
+ describe('pgvector team health', () => {
15
+ it('reports extension, model, and embedding coverage', async () => {
16
+ const pool = {
17
+ query: async () => ({ rows: [{
18
+ extversion: '0.8.1',
19
+ table_ready: true,
20
+ indexed_lessons: 12,
21
+ missing_lessons: 3,
22
+ }] }),
23
+ end: async () => undefined,
24
+ };
25
+ await expect(getTeamSemanticHealth(pool, config)).resolves.toEqual({
26
+ provider: 'pgvector',
27
+ status: 'ready',
28
+ extensionVersion: '0.8.1',
29
+ model: 'Xenova/all-MiniLM-L6-v2',
30
+ dimensions: 384,
31
+ indexedLessons: 12,
32
+ missingLessons: 3,
33
+ });
34
+ });
35
+ it('fails health checks when vector storage is incomplete', async () => {
36
+ const pool = {
37
+ query: async () => ({ rows: [{ extversion: null, table_ready: false }] }),
38
+ end: async () => undefined,
39
+ };
40
+ await expect(getTeamSemanticHealth(pool, config)).rejects.toThrow(/vector extension/);
41
+ });
42
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "6.0.0",
3
+ "version": "6.1.0-beta.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",
@@ -64,18 +64,20 @@
64
64
  "optionalDependencies": {
65
65
  "@anthropic-ai/sdk": "^0.30.1",
66
66
  "@xenova/transformers": "^2.17.2",
67
+ "pg": "^8.16.3",
67
68
  "sqlite3": "^5.1.7",
68
69
  "openai": "^4.104.0",
69
- "@rigour-labs/brain-darwin-arm64": "6.0.0",
70
- "@rigour-labs/brain-linux-arm64": "6.0.0",
71
- "@rigour-labs/brain-darwin-x64": "6.0.0",
72
- "@rigour-labs/brain-win-x64": "6.0.0",
73
- "@rigour-labs/brain-linux-x64": "6.0.0"
70
+ "@rigour-labs/brain-darwin-arm64": "6.1.0-beta.2",
71
+ "@rigour-labs/brain-win-x64": "6.1.0-beta.2",
72
+ "@rigour-labs/brain-linux-x64": "6.1.0-beta.2",
73
+ "@rigour-labs/brain-darwin-x64": "6.1.0-beta.2",
74
+ "@rigour-labs/brain-linux-arm64": "6.1.0-beta.2"
74
75
  },
75
76
  "devDependencies": {
76
77
  "@types/fs-extra": "^11.0.4",
77
78
  "@types/micromatch": "^4.0.10",
78
79
  "@types/node": "^25.0.3",
80
+ "@types/pg": "^8.15.5",
79
81
  "@types/semver": "^7.7.1"
80
82
  },
81
83
  "scripts": {