@rigour-labs/core 6.2.0 → 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/firewall/trusted-control.d.ts +1 -0
- package/dist/firewall/trusted-control.js +19 -0
- package/dist/firewall/trusted-control.test.js +14 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/services/engineering-knowledge-graph.d.ts +15 -2
- package/dist/services/engineering-knowledge-graph.js +98 -1
- package/dist/services/engineering-knowledge-graph.test.js +17 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/lesson-record.d.ts +2 -0
- package/dist/storage/lesson-record.js +20 -0
- package/dist/storage/lessons.js +2 -20
- package/dist/storage/lessons.test.js +47 -2
- package/dist/storage/team-import.d.ts +17 -0
- package/dist/storage/team-import.js +97 -0
- package/dist/storage/team-vector-store.d.ts +2 -1
- package/dist/storage/team-vector-store.js +21 -5
- package/dist/storage/team-vector-store.test.js +18 -4
- package/package.json +6 -6
|
@@ -29,6 +29,7 @@ interface GrantInput {
|
|
|
29
29
|
parentCapabilityId?: string;
|
|
30
30
|
}
|
|
31
31
|
export declare function loadTrustedGrant(cwd: string, id: string, controlRoot?: string): Promise<CapabilityGrant | null>;
|
|
32
|
+
export declare function listTrustedGrants(cwd: string, limit?: number, controlRoot?: string): Promise<CapabilityGrant[]>;
|
|
32
33
|
export declare function issueTrustedGrant(cwd: string, input: GrantInput, controlRoot?: string): Promise<CapabilityGrant>;
|
|
33
34
|
export declare function consumeTrustedGrant(cwd: string, input: {
|
|
34
35
|
id: string;
|
|
@@ -117,6 +117,25 @@ export async function loadTrustedGrant(cwd, id, controlRoot) {
|
|
|
117
117
|
return null;
|
|
118
118
|
return fs.readJson(source);
|
|
119
119
|
}
|
|
120
|
+
export async function listTrustedGrants(cwd, limit = 100, controlRoot) {
|
|
121
|
+
const grantsDir = path.join(getTrustedControlDir(cwd, controlRoot), 'capabilities');
|
|
122
|
+
if (!await fs.pathExists(grantsDir))
|
|
123
|
+
return [];
|
|
124
|
+
const files = (await fs.readdir(grantsDir))
|
|
125
|
+
.filter((file) => /^[0-9a-f-]{36}\.json$/i.test(file));
|
|
126
|
+
const grants = await Promise.all(files.map(async (file) => {
|
|
127
|
+
try {
|
|
128
|
+
return await fs.readJson(path.join(grantsDir, file));
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}));
|
|
134
|
+
const requested = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 100;
|
|
135
|
+
return grants.filter((grant) => grant !== null)
|
|
136
|
+
.sort((a, b) => b.expiresAt - a.expiresAt)
|
|
137
|
+
.slice(0, requested);
|
|
138
|
+
}
|
|
120
139
|
function assertDelegationSubset(parent, input) {
|
|
121
140
|
if (parent.used)
|
|
122
141
|
throw new Error('Parent capability is already consumed');
|
|
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from 'vitest';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import { appendExecutionReceipt, consumeTrustedGrant, getTrustedControlDir, issueTrustedGrant, normalizeMcpAction, saveGatewayConfig, verifyExecutionReceiptChain, } from './index.js';
|
|
5
|
+
import { appendExecutionReceipt, consumeTrustedGrant, getTrustedControlDir, issueTrustedGrant, listTrustedGrants, normalizeMcpAction, saveGatewayConfig, verifyExecutionReceiptChain, } from './index.js';
|
|
6
6
|
const repositories = [];
|
|
7
7
|
const controlRoots = [];
|
|
8
8
|
async function repository() {
|
|
@@ -37,6 +37,19 @@ describe('trusted gateway control', () => {
|
|
|
37
37
|
expect(location.startsWith(cwd)).toBe(false);
|
|
38
38
|
expect(await fs.pathExists(location)).toBe(true);
|
|
39
39
|
});
|
|
40
|
+
it('lists valid grants without failing on malformed control files', async () => {
|
|
41
|
+
const cwd = await repository();
|
|
42
|
+
const root = await controlRoot();
|
|
43
|
+
const grant = await issueTrustedGrant(cwd, {
|
|
44
|
+
issuerId: 'owner', subjectId: 'agent-1', taskId: 'task-1',
|
|
45
|
+
action: 'mcp.call', resource: 'mcp://files/read', ttlMs: 60_000,
|
|
46
|
+
}, root);
|
|
47
|
+
const grantsDir = path.join(getTrustedControlDir(cwd, root), 'capabilities');
|
|
48
|
+
await fs.writeFile(path.join(grantsDir, 'not-a-grant.json'), '{broken');
|
|
49
|
+
await expect(listTrustedGrants(cwd, 10, root)).resolves.toEqual([
|
|
50
|
+
expect.objectContaining({ id: grant.id, resource: 'mcp://files/read' }),
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
40
53
|
it('consumes a capability exactly once', async () => {
|
|
41
54
|
const cwd = await repository();
|
|
42
55
|
const root = await controlRoot();
|
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';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { DependencyGraph } from '../context/dependency-graph.js';
|
|
2
|
+
import type { CapabilityGrant, ExecutionReceipt } from '../firewall/types.js';
|
|
2
3
|
import type { LessonRecord } from '../storage/lessons.js';
|
|
3
4
|
import type { AgentHistoryEvent, AgentRun } from './agent-history.js';
|
|
4
|
-
export type KnowledgeNodeType = 'repository' | 'file' | 'agent' | 'task' | 'run' | 'advice' | 'pattern' | 'memory' | 'lesson' | 'policy' | 'outcome';
|
|
5
|
+
export type KnowledgeNodeType = 'repository' | 'file' | 'agent' | 'task' | 'run' | 'gateway' | 'action' | 'capability' | 'advice' | 'pattern' | 'memory' | 'lesson' | 'policy' | 'outcome';
|
|
5
6
|
export interface KnowledgeNode {
|
|
6
7
|
id: string;
|
|
7
8
|
type: KnowledgeNodeType;
|
|
@@ -16,7 +17,7 @@ export interface KnowledgeEdge {
|
|
|
16
17
|
id: string;
|
|
17
18
|
from: string;
|
|
18
19
|
to: string;
|
|
19
|
-
type: 'contains' | 'imports' | 'performed' | 'belongs_to' | 'touched' | 'produced' | 'resulted_in' | 'defined_in' | 'protects' | 'remembered' | 'guided' | 'informed';
|
|
20
|
+
type: 'contains' | 'imports' | 'performed' | 'belongs_to' | 'touched' | 'produced' | 'resulted_in' | 'defined_in' | 'protects' | 'remembered' | 'guided' | 'informed' | 'routes_through' | 'governed_by' | 'authorized_by' | 'delegated_from';
|
|
20
21
|
}
|
|
21
22
|
export interface EngineeringKnowledgeGraph {
|
|
22
23
|
schemaVersion: 1;
|
|
@@ -49,6 +50,18 @@ interface GraphInput {
|
|
|
49
50
|
detail?: string;
|
|
50
51
|
source?: string;
|
|
51
52
|
}>;
|
|
53
|
+
gateway?: {
|
|
54
|
+
mode: 'observe' | 'enforce';
|
|
55
|
+
agentId: string;
|
|
56
|
+
taskId: string;
|
|
57
|
+
serverCount: number;
|
|
58
|
+
toolCount: number;
|
|
59
|
+
chainValid: boolean;
|
|
60
|
+
receiptCount: number;
|
|
61
|
+
chainReason?: string;
|
|
62
|
+
} | null;
|
|
63
|
+
receipts?: ExecutionReceipt[];
|
|
64
|
+
capabilities?: CapabilityGrant[];
|
|
52
65
|
maxFiles?: number;
|
|
53
66
|
}
|
|
54
67
|
export declare function buildEngineeringKnowledgeGraph(input: GraphInput): EngineeringKnowledgeGraph;
|
|
@@ -5,6 +5,100 @@ function shortPath(value) {
|
|
|
5
5
|
function shortLabel(value, limit = 72) {
|
|
6
6
|
return value.length > limit ? `${value.slice(0, limit - 1)}…` : value;
|
|
7
7
|
}
|
|
8
|
+
function addGatewayFoundation(input, repoId, addNode, addEdge) {
|
|
9
|
+
const gatewayId = 'gateway:rigour-mcp';
|
|
10
|
+
const policyId = 'policy:rigour-mcp-gateway';
|
|
11
|
+
const gateway = input.gateway;
|
|
12
|
+
addNode({
|
|
13
|
+
id: gatewayId,
|
|
14
|
+
type: 'gateway',
|
|
15
|
+
label: 'Trusted MCP gateway',
|
|
16
|
+
detail: `${gateway.serverCount} servers · ${gateway.toolCount} allowed tools`,
|
|
17
|
+
state: gateway.mode,
|
|
18
|
+
repositoryId: input.repository.id,
|
|
19
|
+
weight: 8,
|
|
20
|
+
evidence: { chainValid: gateway.chainValid, receiptCount: gateway.receiptCount, chainReason: gateway.chainReason },
|
|
21
|
+
});
|
|
22
|
+
addNode({ id: policyId, type: 'policy', label: 'MCP capability policy', detail: `${gateway.mode} mode`, state: gateway.mode, weight: 7 });
|
|
23
|
+
addEdge(gatewayId, policyId, 'governed_by');
|
|
24
|
+
addEdge(policyId, repoId, 'protects');
|
|
25
|
+
const agentId = `agent:${gateway.agentId}`;
|
|
26
|
+
const taskId = `task:${gateway.taskId}`;
|
|
27
|
+
addNode({ id: agentId, type: 'agent', label: gateway.agentId, weight: 5 });
|
|
28
|
+
addNode({ id: taskId, type: 'task', label: gateway.taskId, weight: 4 });
|
|
29
|
+
addEdge(agentId, taskId, 'performed');
|
|
30
|
+
return { gatewayId, policyId };
|
|
31
|
+
}
|
|
32
|
+
function addCapabilityEvidence(capabilities, gatewayId, writer) {
|
|
33
|
+
const { addNode, addEdge } = writer;
|
|
34
|
+
const selected = capabilities.slice(0, 100);
|
|
35
|
+
const selectedIds = new Set(selected.map(capability => capability.id));
|
|
36
|
+
for (const capability of selected) {
|
|
37
|
+
const capabilityId = `capability:${capability.id}`;
|
|
38
|
+
const state = capability.used ? 'consumed' : capability.expiresAt < Date.now() ? 'expired' : 'available';
|
|
39
|
+
addNode({
|
|
40
|
+
id: capabilityId,
|
|
41
|
+
type: 'capability',
|
|
42
|
+
label: `${capability.action} · ${shortLabel(capability.resource, 48)}`,
|
|
43
|
+
detail: `${capability.subjectId ?? capability.agentId ?? 'unknown actor'} · ${state}`,
|
|
44
|
+
state,
|
|
45
|
+
weight: 3,
|
|
46
|
+
evidence: { expiresAt: capability.expiresAt, issuerId: capability.issuerId, subjectId: capability.subjectId },
|
|
47
|
+
});
|
|
48
|
+
addEdge(capabilityId, gatewayId, 'authorized_by');
|
|
49
|
+
if (capability.parentCapabilityId) {
|
|
50
|
+
const parentId = `capability:${capability.parentCapabilityId}`;
|
|
51
|
+
if (!selectedIds.has(capability.parentCapabilityId)) {
|
|
52
|
+
addNode({ id: parentId, type: 'capability', label: 'Parent capability', detail: 'Referenced by delegated grant', state: 'referenced', weight: 2 });
|
|
53
|
+
}
|
|
54
|
+
addEdge(capabilityId, parentId, 'delegated_from');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function addReceiptEvidence(receipts, repositoryId, gatewayId, policyId, writer) {
|
|
59
|
+
const { addNode, addEdge } = writer;
|
|
60
|
+
for (const receipt of receipts.slice(-100)) {
|
|
61
|
+
const actionId = `action:${receipt.id}`;
|
|
62
|
+
const agentId = `agent:${receipt.action.actorId}`;
|
|
63
|
+
const taskId = `task:${receipt.action.taskId}`;
|
|
64
|
+
const outcomeId = `outcome:receipt:${receipt.id}`;
|
|
65
|
+
addNode({ id: agentId, type: 'agent', label: receipt.action.actorId, weight: 5 });
|
|
66
|
+
addNode({ id: taskId, type: 'task', label: receipt.action.taskId, weight: 4 });
|
|
67
|
+
addNode({
|
|
68
|
+
id: actionId,
|
|
69
|
+
type: 'action',
|
|
70
|
+
label: receipt.action.operation,
|
|
71
|
+
detail: `${receipt.action.resource} · ${receipt.mode}`,
|
|
72
|
+
state: receipt.simulatedDecision ? `would ${receipt.simulatedDecision}` : receipt.decision,
|
|
73
|
+
repositoryId,
|
|
74
|
+
weight: receipt.outcome === 'denied' ? 5 : 3,
|
|
75
|
+
evidence: {
|
|
76
|
+
receiptId: receipt.id,
|
|
77
|
+
outcome: receipt.outcome,
|
|
78
|
+
mode: receipt.mode,
|
|
79
|
+
decision: receipt.decision,
|
|
80
|
+
simulatedDecision: receipt.simulatedDecision,
|
|
81
|
+
createdAt: receipt.createdAt,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
addNode({ id: outcomeId, type: 'outcome', label: receipt.outcome, detail: receipt.reason, state: receipt.outcome, weight: 3 });
|
|
85
|
+
addEdge(agentId, actionId, 'performed');
|
|
86
|
+
addEdge(actionId, taskId, 'belongs_to');
|
|
87
|
+
addEdge(actionId, gatewayId, 'routes_through');
|
|
88
|
+
addEdge(actionId, policyId, 'governed_by');
|
|
89
|
+
addEdge(actionId, outcomeId, 'resulted_in');
|
|
90
|
+
if (receipt.capabilityId)
|
|
91
|
+
addEdge(actionId, `capability:${receipt.capabilityId}`, 'authorized_by');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function addGatewayEvidence(input, repoId, addNode, addEdge) {
|
|
95
|
+
if (!input.gateway)
|
|
96
|
+
return;
|
|
97
|
+
const { gatewayId, policyId } = addGatewayFoundation(input, repoId, addNode, addEdge);
|
|
98
|
+
const writer = { addNode, addEdge };
|
|
99
|
+
addCapabilityEvidence(input.capabilities ?? [], gatewayId, writer);
|
|
100
|
+
addReceiptEvidence(input.receipts ?? [], input.repository.id, gatewayId, policyId, writer);
|
|
101
|
+
}
|
|
8
102
|
export function buildEngineeringKnowledgeGraph(input) {
|
|
9
103
|
const nodes = new Map();
|
|
10
104
|
const edges = new Map();
|
|
@@ -100,6 +194,7 @@ export function buildEngineeringKnowledgeGraph(input) {
|
|
|
100
194
|
});
|
|
101
195
|
addEdge(repoId, memoryId, 'remembered');
|
|
102
196
|
}
|
|
197
|
+
addGatewayEvidence(input, repoId, addNode, addEdge);
|
|
103
198
|
for (const event of input.events) {
|
|
104
199
|
if (!event.guidance)
|
|
105
200
|
continue;
|
|
@@ -171,7 +266,9 @@ export function buildEngineeringKnowledgeGraph(input) {
|
|
|
171
266
|
|| input.runs.length > 100
|
|
172
267
|
|| input.lessons.length > 150
|
|
173
268
|
|| (input.patterns?.length ?? 0) > 120
|
|
174
|
-
|| (input.memories?.length ?? 0) > 100
|
|
269
|
+
|| (input.memories?.length ?? 0) > 100
|
|
270
|
+
|| (input.receipts?.length ?? 0) > 100
|
|
271
|
+
|| (input.capabilities?.length ?? 0) > 100,
|
|
175
272
|
counts,
|
|
176
273
|
};
|
|
177
274
|
}
|
|
@@ -56,4 +56,21 @@ describe('engineering knowledge graph', () => {
|
|
|
56
56
|
expect(graph.edges).toContainEqual(expect.objectContaining({ from: 'run:run-1', to: 'advice:run-1', type: 'guided' }));
|
|
57
57
|
expect(graph.edges).toContainEqual(expect.objectContaining({ from: 'pattern:pattern-1', to: 'advice:run-1', type: 'informed' }));
|
|
58
58
|
});
|
|
59
|
+
it('shows trusted gateway receipts, simulated denials and capability lineage', () => {
|
|
60
|
+
const graph = buildEngineeringKnowledgeGraph({
|
|
61
|
+
repository: { id: 'repo-a', name: 'payments' }, events: [], runs: [], lessons: [],
|
|
62
|
+
gateway: { mode: 'observe', agentId: 'agent-1', taskId: 'task-1', serverCount: 1, toolCount: 2, chainValid: true, receiptCount: 1 },
|
|
63
|
+
capabilities: [{ id: 'child', action: 'mcp.call', resource: 'mcp://github/get_issue', expiresAt: Date.now() + 1000, policyHash: 'hash', used: false, issuerId: 'owner', subjectId: 'agent-1', parentCapabilityId: 'parent' }],
|
|
64
|
+
receipts: [{
|
|
65
|
+
version: 1, id: 'receipt-1', repositoryId: 'repo-a', mode: 'observe', decision: 'allow', simulatedDecision: 'deny',
|
|
66
|
+
reason: 'would deny', policyHash: 'hash', outcome: 'forwarded', createdAt: '2026-01-01T00:00:00Z', digest: 'digest', signature: 'signature',
|
|
67
|
+
action: { version: 1, actorId: 'agent-1', taskId: 'task-1', channel: 'mcp', operation: 'github__get_issue', resource: 'mcp://github/get_issue', environment: 'local', sideEffect: 'read', reversibility: 'high', sensitivity: 'normal', blastRadius: 'single-resource' },
|
|
68
|
+
capabilityId: 'child',
|
|
69
|
+
}],
|
|
70
|
+
});
|
|
71
|
+
expect(graph.nodes).toContainEqual(expect.objectContaining({ type: 'gateway', evidence: expect.objectContaining({ chainValid: true }) }));
|
|
72
|
+
expect(graph.nodes).toContainEqual(expect.objectContaining({ type: 'action', state: 'would deny' }));
|
|
73
|
+
expect(graph.edges).toContainEqual(expect.objectContaining({ from: 'capability:child', to: 'capability:parent', type: 'delegated_from' }));
|
|
74
|
+
expect(graph.edges).toContainEqual(expect.objectContaining({ from: 'action:receipt-1', to: 'gateway:rigour-mcp', type: 'routes_through' }));
|
|
75
|
+
});
|
|
59
76
|
});
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/storage/index.js
CHANGED
|
@@ -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,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
|
+
}
|
package/dist/storage/lessons.js
CHANGED
|
@@ -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 {
|
|
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 () => {
|
|
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
|
|
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.
|
|
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.
|
|
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-
|
|
71
|
-
"@rigour-labs/brain-
|
|
72
|
-
"@rigour-labs/brain-
|
|
73
|
-
"@rigour-labs/brain-win-x64": "6.2.
|
|
74
|
-
"@rigour-labs/brain-linux-x64": "6.2.
|
|
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",
|