Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
294
vendor/ruvector/npm/packages/ruvllm/src/intelligence.ts
vendored
Normal file
294
vendor/ruvector/npm/packages/ruvllm/src/intelligence.ts
vendored
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* External Intelligence Providers for SONA Learning (ADR-043)
|
||||
*
|
||||
* TypeScript bindings for the IntelligenceProvider trait, enabling
|
||||
* external systems to feed quality signals into RuvLLM's learning loops.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { IntelligenceLoader, FileSignalProvider, QualitySignal } from '@ruvector/ruvllm';
|
||||
*
|
||||
* const loader = new IntelligenceLoader();
|
||||
* loader.registerProvider(new FileSignalProvider('./signals.json'));
|
||||
*
|
||||
* const { signals, errors } = loader.loadAllSignals();
|
||||
* console.log(`Loaded ${signals.length} signals`);
|
||||
* ```
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/** Maximum signal file size (10 MiB) */
|
||||
const MAX_SIGNAL_FILE_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
/** Maximum number of signals per file */
|
||||
const MAX_SIGNALS_PER_FILE = 10_000;
|
||||
|
||||
/** Valid outcome values */
|
||||
const VALID_OUTCOMES = new Set(['success', 'partial_success', 'failure']);
|
||||
|
||||
/** Valid human verdict values */
|
||||
const VALID_VERDICTS = new Set(['approved', 'rejected']);
|
||||
|
||||
/**
|
||||
* A quality signal from an external system.
|
||||
*
|
||||
* Represents one completed task with quality assessment data
|
||||
* that can feed into SONA trajectories, the embedding classifier,
|
||||
* and model router calibration.
|
||||
*/
|
||||
export interface QualitySignal {
|
||||
/** Unique identifier for this signal */
|
||||
id: string;
|
||||
/** Human-readable task description (used for embedding generation) */
|
||||
taskDescription: string;
|
||||
/** Execution outcome */
|
||||
outcome: 'success' | 'partial_success' | 'failure';
|
||||
/** Composite quality score (0.0 - 1.0) */
|
||||
qualityScore: number;
|
||||
/** Optional human verdict */
|
||||
humanVerdict?: 'approved' | 'rejected';
|
||||
/** Optional structured quality factors for detailed analysis */
|
||||
qualityFactors?: QualityFactors;
|
||||
/** ISO 8601 timestamp of task completion */
|
||||
completedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Granular quality factor breakdown.
|
||||
*
|
||||
* Not all providers will have all factors. Undefined fields mean
|
||||
* "not assessed" (distinct from 0.0, which means "assessed as zero").
|
||||
*/
|
||||
export interface QualityFactors {
|
||||
acceptanceCriteriaMet?: number;
|
||||
testsPassing?: number;
|
||||
noRegressions?: number;
|
||||
lintClean?: number;
|
||||
typeCheckClean?: number;
|
||||
followsPatterns?: number;
|
||||
contextRelevance?: number;
|
||||
reasoningCoherence?: number;
|
||||
executionEfficiency?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quality weight overrides from a provider.
|
||||
*
|
||||
* Weights should sum to approximately 1.0.
|
||||
*/
|
||||
export interface ProviderQualityWeights {
|
||||
taskCompletion: number;
|
||||
codeQuality: number;
|
||||
process: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error from a single provider during batch loading.
|
||||
*/
|
||||
export interface ProviderError {
|
||||
providerName: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from a single provider during grouped loading.
|
||||
*/
|
||||
export interface ProviderResult {
|
||||
providerName: string;
|
||||
signals: QualitySignal[];
|
||||
weights?: ProviderQualityWeights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for external systems that supply quality signals to RuvLLM.
|
||||
*
|
||||
* Implement this interface and register with IntelligenceLoader.
|
||||
*/
|
||||
export interface IntelligenceProvider {
|
||||
/** Human-readable name for this provider */
|
||||
name(): string;
|
||||
/** Load quality signals from this provider's data source */
|
||||
loadSignals(): QualitySignal[];
|
||||
/** Optional quality weight overrides */
|
||||
qualityWeights?(): ProviderQualityWeights | undefined;
|
||||
}
|
||||
|
||||
function asOptionalNumber(val: unknown): number | undefined {
|
||||
if (val === undefined || val === null) return undefined;
|
||||
const n = Number(val);
|
||||
return Number.isFinite(n) && n >= 0 && n <= 1 ? n : undefined;
|
||||
}
|
||||
|
||||
function validateOutcome(val: unknown): QualitySignal['outcome'] {
|
||||
const s = String(val ?? 'failure');
|
||||
return VALID_OUTCOMES.has(s) ? s as QualitySignal['outcome'] : 'failure';
|
||||
}
|
||||
|
||||
function validateVerdict(val: unknown): QualitySignal['humanVerdict'] | undefined {
|
||||
if (val === undefined || val === null) return undefined;
|
||||
const s = String(val);
|
||||
return VALID_VERDICTS.has(s) ? s as QualitySignal['humanVerdict'] : undefined;
|
||||
}
|
||||
|
||||
function validateScore(val: unknown): number {
|
||||
const n = Number(val ?? 0);
|
||||
if (!Number.isFinite(n) || n < 0 || n > 1) return 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
function mapQualityFactors(raw: Record<string, unknown>): QualityFactors {
|
||||
return {
|
||||
acceptanceCriteriaMet: asOptionalNumber(raw.acceptance_criteria_met),
|
||||
testsPassing: asOptionalNumber(raw.tests_passing),
|
||||
noRegressions: asOptionalNumber(raw.no_regressions),
|
||||
lintClean: asOptionalNumber(raw.lint_clean),
|
||||
typeCheckClean: asOptionalNumber(raw.type_check_clean),
|
||||
followsPatterns: asOptionalNumber(raw.follows_patterns),
|
||||
contextRelevance: asOptionalNumber(raw.context_relevance),
|
||||
reasoningCoherence: asOptionalNumber(raw.reasoning_coherence),
|
||||
executionEfficiency: asOptionalNumber(raw.execution_efficiency),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Built-in file-based intelligence provider.
|
||||
*
|
||||
* Reads quality signals from a JSON file. This is the default provider
|
||||
* for non-Rust integrations that write signal files.
|
||||
*/
|
||||
export class FileSignalProvider implements IntelligenceProvider {
|
||||
private readonly filePath: string;
|
||||
|
||||
constructor(filePath: string) {
|
||||
this.filePath = path.resolve(filePath);
|
||||
}
|
||||
|
||||
name(): string {
|
||||
return 'file-signals';
|
||||
}
|
||||
|
||||
loadSignals(): QualitySignal[] {
|
||||
if (!fs.existsSync(this.filePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check file size before reading (prevent OOM)
|
||||
const stat = fs.statSync(this.filePath);
|
||||
if (stat.size > MAX_SIGNAL_FILE_SIZE) {
|
||||
throw new Error(
|
||||
`Signal file exceeds max size (${stat.size} bytes, limit ${MAX_SIGNAL_FILE_SIZE})`
|
||||
);
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(this.filePath, 'utf-8');
|
||||
const data: unknown = JSON.parse(raw);
|
||||
if (!Array.isArray(data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check signal count
|
||||
if (data.length > MAX_SIGNALS_PER_FILE) {
|
||||
throw new Error(
|
||||
`Signal file contains ${data.length} signals, max is ${MAX_SIGNALS_PER_FILE}`
|
||||
);
|
||||
}
|
||||
|
||||
return data.map((item: Record<string, unknown>) => {
|
||||
const qfRaw = (item.quality_factors ?? item.qualityFactors) as Record<string, unknown> | undefined;
|
||||
return {
|
||||
id: String(item.id ?? ''),
|
||||
taskDescription: String(item.task_description ?? item.taskDescription ?? ''),
|
||||
outcome: validateOutcome(item.outcome),
|
||||
qualityScore: validateScore(item.quality_score ?? item.qualityScore),
|
||||
humanVerdict: validateVerdict(item.human_verdict ?? item.humanVerdict),
|
||||
qualityFactors: qfRaw ? mapQualityFactors(qfRaw) : undefined,
|
||||
completedAt: String(item.completed_at ?? item.completedAt ?? new Date().toISOString()),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
qualityWeights(): ProviderQualityWeights | undefined {
|
||||
try {
|
||||
const weightsPath = path.join(path.dirname(this.filePath), 'quality-weights.json');
|
||||
if (!fs.existsSync(weightsPath)) return undefined;
|
||||
const raw = fs.readFileSync(weightsPath, 'utf-8');
|
||||
const data = JSON.parse(raw) as Record<string, unknown>;
|
||||
return {
|
||||
taskCompletion: Number(data.task_completion ?? data.taskCompletion ?? 0.5),
|
||||
codeQuality: Number(data.code_quality ?? data.codeQuality ?? 0.3),
|
||||
process: Number(data.process ?? 0.2),
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates quality signals from multiple registered providers.
|
||||
*
|
||||
* If no providers are registered, loadAllSignals returns empty arrays
|
||||
* with zero overhead.
|
||||
*/
|
||||
export class IntelligenceLoader {
|
||||
private providers: IntelligenceProvider[] = [];
|
||||
|
||||
/** Register an external intelligence provider */
|
||||
registerProvider(provider: IntelligenceProvider): void {
|
||||
this.providers.push(provider);
|
||||
}
|
||||
|
||||
/** Returns the number of registered providers */
|
||||
get providerCount(): number {
|
||||
return this.providers.length;
|
||||
}
|
||||
|
||||
/** Returns the names of all registered providers */
|
||||
get providerNames(): string[] {
|
||||
return this.providers.map(p => p.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load signals from all registered providers.
|
||||
*
|
||||
* Non-fatal: if a provider fails, its error is captured but
|
||||
* other providers continue loading.
|
||||
*/
|
||||
loadAllSignals(): { signals: QualitySignal[]; errors: ProviderError[] } {
|
||||
const signals: QualitySignal[] = [];
|
||||
const errors: ProviderError[] = [];
|
||||
|
||||
for (const provider of this.providers) {
|
||||
try {
|
||||
const providerSignals = provider.loadSignals();
|
||||
signals.push(...providerSignals);
|
||||
} catch (e) {
|
||||
errors.push({
|
||||
providerName: provider.name(),
|
||||
message: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { signals, errors };
|
||||
}
|
||||
|
||||
/** Load signals grouped by provider with weight overrides */
|
||||
loadGrouped(): ProviderResult[] {
|
||||
return this.providers.map(provider => {
|
||||
let providerSignals: QualitySignal[] = [];
|
||||
try {
|
||||
providerSignals = provider.loadSignals();
|
||||
} catch {
|
||||
// Non-fatal
|
||||
}
|
||||
return {
|
||||
providerName: provider.name(),
|
||||
signals: providerSignals,
|
||||
weights: provider.qualityWeights?.(),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user