- 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
190 lines
5.6 KiB
TypeScript
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();
|