Files
ra-h-os/src/services/database/dimensionService.ts
T
“BeeRad” 4c75df101f feat: sync runtime search and schema quality updates from app repo
- port retrieval, validation, and eval improvements relevant to os
- align prompts and dimensions with the flat single-agent model
- replace the old eval suite with the focused core scenarios

Generated with Codex
2026-03-15 14:55:45 +11:00

190 lines
5.6 KiB
TypeScript

import { getSQLiteClient } from './sqlite-client';
export interface Dimension {
name: string;
description: string | null;
is_priority: boolean;
updated_at: string;
}
export interface LockedDimension {
name: string;
description: string | null;
count: number;
}
export class DimensionService {
/**
* Legacy compatibility shim. Dimensions are now flat, so there is no locked subset.
*/
static async getLockedDimensions(): Promise<LockedDimension[]> {
return [];
}
/**
* Automatic special-dimension assignment has been removed. Callers must provide dimensions explicitly.
*/
static async assignDimensions(nodeData: {
title: string;
content?: string;
link?: string;
description?: string;
}): Promise<{ locked: string[]; keywords: string[] }> {
console.log(`[DimensionAssignment] Skipped for "${nodeData.title}" — flat dimensions require explicit assignment.`);
return { locked: [], keywords: [] };
}
/**
* Legacy method for backwards compatibility
* @deprecated Use assignDimensions() instead
*/
static async assignLockedDimensions(nodeData: {
title: string;
content?: string;
link?: string;
}): Promise<string[]> {
const result = await this.assignDimensions(nodeData);
return result.locked;
}
/**
* Update dimension description
*/
static async updateDimensionDescription(name: string, description: string): Promise<void> {
const sqlite = getSQLiteClient();
sqlite.query(`
INSERT INTO dimensions(name, description, is_priority, updated_at)
VALUES (?, ?, 0, CURRENT_TIMESTAMP)
ON CONFLICT(name) DO UPDATE SET
description = ?,
updated_at = CURRENT_TIMESTAMP
`, [name, description, description]);
}
/**
* Get dimension by name with description
*/
static async getDimensionByName(name: string): Promise<Dimension | null> {
const sqlite = getSQLiteClient();
const result = sqlite.query(`
SELECT name, description, is_priority, updated_at
FROM dimensions
WHERE name = ?
`, [name]);
if (result.rows.length === 0) {
return null;
}
const row = result.rows[0] as any;
return {
name: row.name,
description: row.description,
is_priority: Boolean(row.is_priority),
updated_at: row.updated_at
};
}
/**
* Legacy no-op prompt builder retained only for backward compatibility.
*/
private static buildAssignmentPrompt(
nodeData: { title: string; notes?: string; link?: string; description?: string },
lockedDimensions: LockedDimension[]
): string {
// Use description as primary context, content as fallback
let nodeContextSection: string;
if (nodeData.description) {
const notesPreview = nodeData.notes?.slice(0, 500) || '';
nodeContextSection = `DESCRIPTION: ${nodeData.description}
NOTES PREVIEW: ${notesPreview}${nodeData.notes && nodeData.notes.length > 500 ? '...' : ''}`;
} else {
const notesPreview = nodeData.notes?.slice(0, 2000) || '';
nodeContextSection = `NOTES: ${notesPreview}${nodeData.notes && nodeData.notes.length > 2000 ? '...' : ''}`;
}
// Include ALL locked dimensions, using fallback text for those without descriptions
const dimensionsList = lockedDimensions
.map(d => {
const description = d.description && d.description.trim().length > 0
? d.description
: '(none - infer from name)';
return `DIMENSION: "${d.name}"\nDESCRIPTION: ${description}`;
})
.join('\n---\n');
return `Dimensions are now flat categories with no locked subset.
=== NODE TO CATEGORIZE ===
Title: ${nodeData.title}
${nodeContextSection}
URL: ${nodeData.link || 'none'}
=== LOCKED DIMENSIONS ===
CRITICAL: Read each dimension's DESCRIPTION carefully.
The description defines what belongs in that dimension.
Only assign if the content CLEARLY matches the description.
If unsure, skip it — better to miss than assign incorrectly.
AVAILABLE DIMENSIONS:
${dimensionsList}
=== RESPONSE FORMAT ===
LOCKED:
[dimension names from the list above, one per line, or "none"]`;
}
/**
* Legacy no-op parser retained only for backward compatibility.
*/
private static parseAssignmentResponse(
response: string,
availableDimensions: LockedDimension[]
): { locked: string[]; keywords: string[] } {
const lockedDimensions: string[] = [];
// Extract LOCKED section
const lockedMatch = response.match(/LOCKED:\s*([\s\S]*?)$/i);
if (lockedMatch) {
const lockedLines = lockedMatch[1].trim().split('\n');
for (const line of lockedLines) {
const dimensionName = line.trim().toLowerCase();
if (dimensionName === 'none' || dimensionName === '') {
continue;
}
// Find matching dimension (case-insensitive)
const matchedDimension = availableDimensions.find(
d => d.name.toLowerCase() === dimensionName
);
if (matchedDimension && !lockedDimensions.includes(matchedDimension.name)) {
lockedDimensions.push(matchedDimension.name);
}
}
}
return { locked: lockedDimensions, keywords: [] };
}
/**
* Create or get a keyword dimension (unlocked)
*/
static async ensureKeywordDimension(keyword: string): Promise<void> {
const sqlite = getSQLiteClient();
// INSERT OR IGNORE - if dimension exists, do nothing
sqlite.query(`
INSERT OR IGNORE INTO dimensions(name, description, is_priority, updated_at)
VALUES (?, ?, 0, CURRENT_TIMESTAMP)
`, [keyword, null]);
}
}
export const dimensionService = new DimensionService();