sync: temporal-awareness from private repo

- Surface created_at, updated_at, event_date in queryNodes + queryDimensionNodes responses
- Add date range filters (createdAfter/Before, eventAfter/Before) to NodeFilters + nodeService.getNodes()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2026-02-15 14:34:55 +11:00
co-authored by Claude Opus 4.6
parent 0095641278
commit 9954792b1d
4 changed files with 54 additions and 14 deletions
+25 -6
View File
@@ -10,15 +10,16 @@ export class NodeService {
// PostgreSQL path removed in SQLite-only consolidation // PostgreSQL path removed in SQLite-only consolidation
private async getNodesSQLite(filters: NodeFilters = {}): Promise<Node[]> { private async getNodesSQLite(filters: NodeFilters = {}): Promise<Node[]> {
const { dimensions, search, limit = 100, offset = 0, sortBy, dimensionsMatch = 'any' } = filters; const { dimensions, search, limit = 100, offset = 0, sortBy, dimensionsMatch = 'any',
createdAfter, createdBefore, eventAfter, eventBefore } = filters;
const sqlite = getSQLiteClient(); const sqlite = getSQLiteClient();
// Use nodes_v view for array-like dimensions behavior (exclude embedding BLOB for performance) // Use nodes_v view for array-like dimensions behavior (exclude embedding BLOB for performance)
let query = ` let query = `
SELECT n.id, n.title, n.description, n.notes, n.link, n.event_date, n.metadata, n.chunk, SELECT n.id, n.title, n.description, n.notes, n.link, n.event_date, n.metadata, n.chunk,
n.chunk_status, n.embedding_updated_at, n.embedding_text, n.chunk_status, n.embedding_updated_at, n.embedding_text,
n.created_at, n.updated_at, n.created_at, n.updated_at,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension) COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json, FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json,
(SELECT COUNT(*) FROM edges WHERE from_node_id = n.id OR to_node_id = n.id) as edge_count (SELECT COUNT(*) FROM edges WHERE from_node_id = n.id OR to_node_id = n.id) as edge_count
FROM nodes n FROM nodes n
@@ -47,15 +48,33 @@ export class NodeService {
} }
} }
// Text search in title, description, and content (SQLite LIKE with COLLATE NOCASE) // Text search in title, description, and notes (SQLite LIKE with COLLATE NOCASE)
if (search) { if (search) {
query += ` AND (n.title LIKE ? COLLATE NOCASE OR n.description LIKE ? COLLATE NOCASE OR n.notes LIKE ? COLLATE NOCASE)`; query += ` AND (n.title LIKE ? COLLATE NOCASE OR n.description LIKE ? COLLATE NOCASE OR n.notes LIKE ? COLLATE NOCASE)`;
params.push(`%${search}%`, `%${search}%`, `%${search}%`); params.push(`%${search}%`, `%${search}%`, `%${search}%`);
} }
// Temporal filters
if (createdAfter) {
query += ` AND n.created_at >= ?`;
params.push(createdAfter);
}
if (createdBefore) {
query += ` AND n.created_at < ?`;
params.push(createdBefore);
}
if (eventAfter) {
query += ` AND n.event_date >= ?`;
params.push(eventAfter);
}
if (eventBefore) {
query += ` AND n.event_date < ?`;
params.push(eventBefore);
}
// Sorting logic // Sorting logic
if (search) { if (search) {
// For search queries, prioritize by relevance: exact title → starts with → contains in title → description → content // For search queries, prioritize by relevance: exact title → starts with → contains in title → description → notes
query += ` ORDER BY query += ` ORDER BY
CASE WHEN LOWER(n.title) = LOWER(?) THEN 1 ELSE 6 END, CASE WHEN LOWER(n.title) = LOWER(?) THEN 1 ELSE 6 END,
CASE WHEN LOWER(n.title) LIKE LOWER(?) THEN 2 ELSE 6 END, CASE WHEN LOWER(n.title) LIKE LOWER(?) THEN 2 ELSE 6 END,
@@ -68,7 +87,7 @@ export class NodeService {
`${search}%`, // Starts with search term `${search}%`, // Starts with search term
`%${search}%`, // Contains in title `%${search}%`, // Contains in title
`%${search}%`, // Contains in description `%${search}%`, // Contains in description
`%${search}%` // Contains in content `%${search}%` // Contains in notes
); );
} else if (sortBy === 'edges') { } else if (sortBy === 'edges') {
// Sort by edge count (most connected first) // Sort by edge count (most connected first)
+4 -1
View File
@@ -5,7 +5,7 @@ import { formatNodeForChat } from '../infrastructure/nodeFormatter';
import type { Node } from '@/types/database'; import type { Node } from '@/types/database';
export const queryDimensionNodesTool = tool({ export const queryDimensionNodesTool = tool({
description: 'Query all nodes within a specific dimension. Returns nodes sorted by edge count (most connected first).', description: 'Get nodes in a dimension, sorted by connection count.',
inputSchema: z.object({ inputSchema: z.object({
dimension: z.string().describe('The dimension name to query nodes from'), dimension: z.string().describe('The dimension name to query nodes from'),
limit: z.number().optional().default(20).describe('Maximum number of nodes to return (default: 20)'), limit: z.number().optional().default(20).describe('Maximum number of nodes to return (default: 20)'),
@@ -43,6 +43,9 @@ export const queryDimensionNodesTool = tool({
}), }),
edgeCount: node.edge_count || 0, edgeCount: node.edge_count || 0,
dimensions: node.dimensions || [], dimensions: node.dimensions || [],
created_at: node.created_at,
updated_at: node.updated_at,
event_date: node.event_date ?? null,
}; };
if (includeContent && node.notes) { if (includeContent && node.notes) {
+16 -2
View File
@@ -10,7 +10,11 @@ export const queryNodesTool = tool({
filters: z.object({ filters: z.object({
dimensions: z.array(z.string()).describe('Filter by dimensions (e.g., ["research", "ai", "technology"]). Replaces old type/stage filtering.').optional(), dimensions: z.array(z.string()).describe('Filter by dimensions (e.g., ["research", "ai", "technology"]). Replaces old type/stage filtering.').optional(),
search: z.string().describe('Search term to match against title or notes').optional(), search: z.string().describe('Search term to match against title or notes').optional(),
limit: z.number().min(1).max(50).default(10).describe('Maximum number of results to return') limit: z.number().min(1).max(50).default(10).describe('Maximum number of results to return'),
createdAfter: z.string().optional().describe('ISO date (YYYY-MM-DD). Only return nodes created on or after this date.'),
createdBefore: z.string().optional().describe('ISO date (YYYY-MM-DD). Only return nodes created before this date.'),
eventAfter: z.string().optional().describe('ISO date (YYYY-MM-DD). Only return nodes with event_date on or after this date.'),
eventBefore: z.string().optional().describe('ISO date (YYYY-MM-DD). Only return nodes with event_date before this date.'),
}).optional() }).optional()
}), }),
execute: async ({ filters = {} }) => { execute: async ({ filters = {} }) => {
@@ -47,6 +51,9 @@ export const queryNodesTool = tool({
id: node.id, id: node.id,
title: node.title, title: node.title,
dimensions: node.dimensions || [], dimensions: node.dimensions || [],
created_at: node.created_at,
updated_at: node.updated_at,
event_date: node.event_date ?? null,
formatted_display: formatted, formatted_display: formatted,
}], }],
count: 1, count: 1,
@@ -65,7 +72,11 @@ export const queryNodesTool = tool({
const nodesPromise: Promise<Node[] | undefined> = nodeService.getNodes({ const nodesPromise: Promise<Node[] | undefined> = nodeService.getNodes({
limit, limit,
dimensions: filters.dimensions, dimensions: filters.dimensions,
search: filters.search search: filters.search,
createdAfter: filters.createdAfter,
createdBefore: filters.createdBefore,
eventAfter: filters.eventAfter,
eventBefore: filters.eventBefore,
}); });
const nodes = await Promise.race<Node[] | undefined>([nodesPromise, timeoutPromise]); const nodes = await Promise.race<Node[] | undefined>([nodesPromise, timeoutPromise]);
@@ -85,6 +96,9 @@ export const queryNodesTool = tool({
id: node.id, id: node.id,
title: node.title, title: node.title,
dimensions: node.dimensions || [], dimensions: node.dimensions || [],
created_at: node.created_at,
updated_at: node.updated_at,
event_date: node.event_date ?? null,
formatted_display: formatted formatted_display: formatted
}; };
}); });
+9 -5
View File
@@ -3,19 +3,18 @@ export interface Node {
id: number; id: number;
title: string; title: string;
description?: string; description?: string;
notes?: string; // Consolidated content from description + abstract + notes notes?: string; // User's notes/thoughts about this node
link?: string; link?: string;
event_date?: string; event_date?: string | null; // When the thing actually happened (ISO 8601)
dimensions: string[]; // Flexible dimensions replacing type + stage + segment + tags dimensions: string[]; // Flexible dimensions replacing type + stage + segment + tags
embedding?: Buffer; // Node-level embedding (BLOB data) embedding?: Buffer; // Node-level embedding (BLOB data)
chunk?: string; chunk?: string;
metadata?: any; // Flexible metadata storage from extras + chunk_status + sub_type metadata?: any; // Flexible metadata storage
created_at: string; created_at: string;
updated_at: string; updated_at: string;
// Legacy pin flag (read-only, slated for removal)
edge_count?: number; // Derived count of edges, included in some queries edge_count?: number; // Derived count of edges, included in some queries
// Optional embedding fields (restored from migration) // Optional embedding fields
embedding_updated_at?: string; embedding_updated_at?: string;
embedding_text?: string; embedding_text?: string;
chunk_status?: 'not_chunked' | 'chunking' | 'chunked' | 'error' | null; chunk_status?: 'not_chunked' | 'chunking' | 'chunked' | 'error' | null;
@@ -72,6 +71,10 @@ export interface NodeFilters {
offset?: number; offset?: number;
sortBy?: 'updated' | 'edges' | 'created'; // Sort by updated_at, edge count, or created_at sortBy?: 'updated' | 'edges' | 'created'; // Sort by updated_at, edge count, or created_at
dimensionsMatch?: 'any' | 'all'; // 'any' = OR (default), 'all' = AND dimensionsMatch?: 'any' | 'all'; // 'any' = OR (default), 'all' = AND
createdAfter?: string; // ISO date (YYYY-MM-DD) — nodes created on or after
createdBefore?: string; // ISO date (YYYY-MM-DD) — nodes created before
eventAfter?: string; // ISO date (YYYY-MM-DD) — nodes with event_date on or after
eventBefore?: string; // ISO date (YYYY-MM-DD) — nodes with event_date before
} }
export interface ChunkData { export interface ChunkData {
@@ -118,6 +121,7 @@ export interface DatabaseError {
export interface Dimension { export interface Dimension {
name: string; name: string;
description?: string | null; description?: string | null;
icon?: string | null;
is_priority: boolean; is_priority: boolean;
updated_at: string; updated_at: string;
} }