refactor: align source-first schema surfaces

This commit is contained in:
“BeeRad”
2026-03-22 20:17:07 +11:00
parent 255377658b
commit b9af8dc385
21 changed files with 267 additions and 93 deletions
+2 -2
View File
@@ -291,7 +291,7 @@ export default function FocusPanel({
};
const startSourceEdit = () => {
setSourceEditValue(currentNode?.source || currentNode?.chunk || '');
setSourceEditValue(currentNode?.source || '');
setSourceEditMode(true);
};
@@ -598,7 +598,7 @@ export default function FocusPanel({
};
const renderSourceSection = () => {
const sourceContent = currentNode?.source || currentNode?.chunk || '';
const sourceContent = currentNode?.source || '';
return (
<section
@@ -235,7 +235,6 @@ export default function ThreePanelLayout() {
title: node.title,
link: node.link,
source: node.source,
notes: node.notes,
dimensions: node.dimensions,
created_at: node.created_at,
updated_at: node.updated_at,
+3 -3
View File
@@ -483,11 +483,11 @@ export default function DatabaseTableView({ onNodeClick, refreshToken = 0, toolb
</div>
</td>
{/* Notes */}
{/* Source */}
<td style={tdStyle()}>
<div style={truncCell}>
<span style={{ fontSize: '11px', color: 'var(--rah-text-soft)' }}>
{(node.source || node.notes) ? (node.source || node.notes || '').slice(0, 120) : '\u2014'}
{node.source ? node.source.slice(0, 120) : '\u2014'}
</span>
</div>
</td>
@@ -576,7 +576,7 @@ export default function DatabaseTableView({ onNodeClick, refreshToken = 0, toolb
<td style={tdStyle()}>
<div style={truncCell}>
<span style={{ fontSize: '10px', color: 'var(--rah-text-muted)' }}>
{(node.source || node.chunk) ? (node.source || node.chunk || '').slice(0, 100) : '\u2014'}
{node.source ? node.source.slice(0, 100) : '\u2014'}
</span>
</div>
</td>
+2 -2
View File
@@ -104,7 +104,7 @@ export default function GridView({ nodes, onNodeClick }: GridViewProps) {
</div>
{/* Description or Content Preview */}
{(node.description || node.source || node.notes) && (
{(node.description || node.source) && (
<div style={{
flex: 1,
fontSize: '11px',
@@ -116,7 +116,7 @@ export default function GridView({ nodes, onNodeClick }: GridViewProps) {
WebkitBoxOrient: 'vertical',
marginBottom: '10px'
}}>
{node.description || truncateContent(node.source || node.notes)}
{node.description || truncateContent(node.source)}
</div>
)}
+2 -2
View File
@@ -105,7 +105,7 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
</div>
{/* Description or Content Preview */}
{(node.description || node.source || node.notes) && (
{(node.description || node.source) && (
<div style={{
fontSize: '12px',
color: '#666',
@@ -116,7 +116,7 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
WebkitBoxOrient: 'vertical',
overflow: 'hidden'
}}>
{node.description || truncateContent(node.source || node.notes)}
{node.description || truncateContent(node.source)}
</div>
)}
+4 -4
View File
@@ -4,7 +4,7 @@ import { hasValidOpenAiKey } from '../storage/apiKeys';
export interface DescriptionInput {
title: string;
notes?: string;
source?: string;
link?: string;
metadata?: {
source?: string;
@@ -61,7 +61,7 @@ export async function generateDescription(input: DescriptionInput): Promise<stri
}
// Fast path: skip AI for very short inputs (likely just notes)
if (!input.notes && !input.link && input.title.length < 30) {
if (!input.source && !input.link && input.title.length < 30) {
console.log(`[DescriptionService] Short input, using fallback for: "${input.title}"`);
return generateFallbackDescription(input);
}
@@ -134,8 +134,8 @@ function buildDescriptionPrompt(input: DescriptionInput): string {
if (publisherHint) lines.push(`Publisher hint: ${publisherHint}`);
lines.push(`Likely user-authored: ${likelyUserAuthored ? 'yes' : 'no'}`);
const contentPreview = input.notes?.slice(0, 800) || '';
if (contentPreview) lines.push(`Notes: ${contentPreview}${input.notes && input.notes.length > 800 ? '...' : ''}`);
const contentPreview = input.source?.slice(0, 800) || '';
if (contentPreview) lines.push(`Source excerpt: ${contentPreview}${input.source && input.source.length > 800 ? '...' : ''}`);
return `Write a description for this knowledge node. Max 280 characters.
+6 -6
View File
@@ -138,7 +138,7 @@ export class NodeService {
// Use nodes_v view for array-like dimensions behavior (exclude embedding BLOB for performance)
let query = `
SELECT n.id, n.title, n.description, n.source, n.notes, n.link, n.event_date, n.metadata, n.chunk,
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
n.chunk_status, n.embedding_updated_at, n.embedding_text,
n.created_at, n.updated_at,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
@@ -252,7 +252,7 @@ export class NodeService {
private async getNodeByIdSQLite(id: number): Promise<Node | null> {
const sqlite = getSQLiteClient();
const query = `
SELECT n.id, n.title, n.description, n.source, n.notes, n.link, n.event_date, n.metadata, n.chunk,
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
n.chunk_status, n.embedding_updated_at, n.embedding_text,
n.created_at, n.updated_at,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
@@ -589,7 +589,7 @@ export class NodeService {
WHERE nodes_fts MATCH ?
LIMIT ?
)
SELECT n.id, n.title, n.description, n.source, n.notes, n.link, n.event_date, n.metadata, n.chunk,
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
n.chunk_status, n.embedding_updated_at, n.embedding_text,
n.created_at, n.updated_at,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
@@ -618,7 +618,7 @@ export class NodeService {
const words = search.split(/\s+/).filter(Boolean);
const { clauses, params } = this.buildNodeFilterClauses(filters);
let query = `
SELECT n.id, n.title, n.description, n.source, n.notes, n.link, n.event_date, n.metadata, n.chunk,
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
n.chunk_status, n.embedding_updated_at, n.embedding_text,
n.created_at, n.updated_at,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
@@ -663,7 +663,7 @@ export class NodeService {
const { clauses, params } = this.buildNodeFilterClauses(filters);
let query = `
SELECT n.id, n.title, n.description, n.source, n.notes, n.link, n.event_date, n.metadata, n.chunk,
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
n.chunk_status, n.embedding_updated_at, n.embedding_text,
n.created_at, n.updated_at,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
@@ -737,7 +737,7 @@ export class NodeService {
ORDER BY distance
LIMIT ?
)
SELECT n.id, n.title, n.description, n.source, n.notes, n.link, n.event_date, n.metadata, n.chunk,
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
n.chunk_status, n.embedding_updated_at, n.embedding_text,
n.created_at, n.updated_at,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
+32 -24
View File
@@ -616,19 +616,11 @@ class SQLiteClient {
}
}
// 10) Final schema pass migrations (content→notes, event_date, dimensions.icon, drop dead columns)
// 10) Final schema pass migrations (source canonicalization, event_date, dimensions.icon, drop dead columns)
try {
let nodeCols2 = this.db.prepare('PRAGMA table_info(nodes)').all() as Array<{ name: string }>;
let nodeColNames = nodeCols2.map(c => c.name);
// Rename content → notes (additive first)
if (nodeColNames.includes('content') && !nodeColNames.includes('notes')) {
console.log('Migrating nodes.content → nodes.notes...');
this.db.exec('ALTER TABLE nodes RENAME COLUMN content TO notes;');
nodeCols2 = this.db.prepare('PRAGMA table_info(nodes)').all() as Array<{ name: string }>;
nodeColNames = nodeCols2.map(c => c.name);
}
if (!nodeColNames.includes('source')) {
console.log('Adding nodes.source column...');
this.db.exec('ALTER TABLE nodes ADD COLUMN source TEXT;');
@@ -637,22 +629,38 @@ class SQLiteClient {
}
if (nodeColNames.includes('source')) {
this.db.exec(`
UPDATE nodes
SET source = chunk
WHERE (source IS NULL OR LENGTH(TRIM(source)) = 0)
AND chunk IS NOT NULL
AND LENGTH(TRIM(chunk)) > 0;
`);
if (nodeColNames.includes('content')) {
this.db.exec(`
UPDATE nodes
SET source = content,
chunk_status = 'not_chunked'
WHERE (source IS NULL OR LENGTH(TRIM(source)) = 0)
AND content IS NOT NULL
AND LENGTH(TRIM(content)) > 0;
`);
}
this.db.exec(`
UPDATE nodes
SET source = notes,
chunk_status = 'not_chunked'
WHERE (source IS NULL OR LENGTH(TRIM(source)) = 0)
AND notes IS NOT NULL
AND LENGTH(TRIM(notes)) > 0;
`);
if (nodeColNames.includes('notes')) {
this.db.exec(`
UPDATE nodes
SET source = notes,
chunk_status = 'not_chunked'
WHERE (source IS NULL OR LENGTH(TRIM(source)) = 0)
AND notes IS NOT NULL
AND LENGTH(TRIM(notes)) > 0;
`);
}
if (nodeColNames.includes('chunk')) {
this.db.exec(`
UPDATE nodes
SET source = chunk,
chunk_status = 'not_chunked'
WHERE (source IS NULL OR LENGTH(TRIM(source)) = 0)
AND chunk IS NOT NULL
AND LENGTH(TRIM(chunk)) > 0;
`);
}
this.db.exec(`
UPDATE nodes
+6 -7
View File
@@ -19,7 +19,6 @@ interface NodeRecord {
id: number;
title: string;
source: string | null;
notes: string | null;
description: string | null;
dimensions_json: string;
embedding?: Buffer | null;
@@ -63,7 +62,7 @@ export class NodeEmbedder {
const prompt = `Analyze this content and provide 2-3 key insights or themes in a concise paragraph (max 100 words):
Title: ${node.title}
Source: ${node.source || node.notes || 'No source'}
Source: ${node.source || 'No source'}
Dimensions: ${dimensionsText}
Focus on the main concepts, key relationships, and practical implications.`;
@@ -111,13 +110,13 @@ Focus on the main concepts, key relationships, and practical implications.`;
// Create base embedding text
let embeddingText = formatEmbeddingText(
node.title,
node.source || node.notes || '',
node.source || '',
dimensions,
node.description
);
// Add AI analysis if source exists
const sourceText = node.source || node.notes || '';
const sourceText = node.source || '';
if (sourceText.trim().length > 0) {
const analysis = await this.analyzeNodeWithAI(node);
if (analysis) {
@@ -178,7 +177,7 @@ Focus on the main concepts, key relationships, and practical implications.`;
if (nodeId) {
// Single node
query = `
SELECT n.id, n.title, n.source, n.notes, n.description,
SELECT n.id, n.title, n.source, n.description,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json,
n.embedding, n.embedding_updated_at
@@ -189,7 +188,7 @@ Focus on the main concepts, key relationships, and practical implications.`;
} else if (forceReEmbed) {
// All nodes
query = `
SELECT n.id, n.title, n.source, n.notes, n.description,
SELECT n.id, n.title, n.source, n.description,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json,
n.embedding, n.embedding_updated_at
@@ -199,7 +198,7 @@ Focus on the main concepts, key relationships, and practical implications.`;
} else {
// Only nodes without embeddings
query = `
SELECT n.id, n.title, n.source, n.notes, n.description,
SELECT n.id, n.title, n.source, n.description,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json,
n.embedding, n.embedding_updated_at
+2 -2
View File
@@ -48,8 +48,8 @@ export const queryDimensionNodesTool = tool({
event_date: node.event_date ?? null,
};
if (includeContent && (node.source || node.notes)) {
const previewSource = node.source || node.notes || '';
if (includeContent && node.source) {
const previewSource = node.source;
// Truncate to ~100 chars
formatted.sourcePreview = previewSource.length > 100
? previewSource.substring(0, 100) + '...'