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 { 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 { const result = await this.assignDimensions(nodeData); return result.locked; } /** * Update dimension description */ static async updateDimensionDescription(name: string, description: string): Promise { 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 { 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 { 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();