feat: final schema pass — sync 9 schema changes from main repo

- Rename nodes.content → nodes.notes across all services, tools, API routes, and UI
- Add dimensions.icon column support (GET/POST routes, types)
- Add nodes.event_date column (create/update flows, types)
- Drop nodes.type and nodes.is_pinned references
- Drop edges.user_feedback references
- Drop chat_memory_state table (replaced with DROP IF EXISTS in migration)
- Update schema guide files (system/schema.md)
- Add runtime migration block in sqlite-client.ts for existing databases
- Fix MCP tool schemas (external API keeps 'content' param, maps to 'notes' internally)
- Update standalone MCP server (nodeService.js, sqlite-client.js, index.js)
- Update HTTP MCP server (server.js, stdio-server.js)
- Update all extraction tools (paper, website, youtube)
- Update all UI components (FocusPanel, ThreePanelLayout, GridView, ListView, FolderViewOverlay)
- TypeScript: 0 errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2026-02-15 12:53:52 +11:00
co-authored by Claude Opus 4.6
parent 1d323de11a
commit 86f43c9975
36 changed files with 303 additions and 229 deletions
@@ -14,13 +14,12 @@ immutable: true
| id | INTEGER | Primary key, auto-increment |
| title | TEXT | Required |
| description | TEXT | AI-generated grounding context (~1 sentence) |
| content | TEXT | User's notes/thoughts (not source content) |
| notes | TEXT | User's notes/thoughts (not source content) |
| chunk | TEXT | Full verbatim source content |
| chunk_status | TEXT | 'pending', 'chunked', 'failed' |
| link | TEXT | External URL (only for nodes representing external content) |
| type | TEXT | Nullable (reserved for future use) |
| event_date | TEXT | When the content happened (ISO date) — distinct from created_at |
| metadata | TEXT | JSON blob (map_position, transcript_length, etc.) |
| is_pinned | INTEGER | Legacy — use hub node queries instead |
| created_at | TEXT | ISO timestamp |
| updated_at | TEXT | ISO timestamp |
@@ -40,6 +39,7 @@ immutable: true
|--------|------|-------|
| name | TEXT | Primary key |
| description | TEXT | Purpose description |
| icon | TEXT | Emoji or icon identifier for UI display |
| is_priority | INTEGER | 1 = priority dimension (auto-assigns to new nodes) |
| updated_at | TEXT | ISO timestamp |
@@ -60,9 +60,17 @@ immutable: true
| embedding_type | TEXT | Embedding model used |
| metadata | TEXT | JSON blob |
### voice_usage (daily tracking)
| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER | Primary key |
| date | TEXT | ISO date (UNIQUE) |
| minutes_used | REAL | Minutes consumed |
| updated_at | TEXT | ISO timestamp |
### FTS Tables
- `chunks_fts` — full-text search on chunk text
- `nodes_fts` — full-text search on node title + content
- `nodes_fts` — full-text search on node title + notes
## Common Query Patterns
+15 -9
View File
@@ -310,7 +310,7 @@ async function main() {
const node = nodeService.createNode({
title: title.trim(),
content: content?.trim(),
notes: content?.trim(),
link: link?.trim(),
description: description?.trim(),
dimensions: normalizedDimensions,
@@ -359,7 +359,7 @@ async function main() {
WITH fts_matches AS (
SELECT rowid, rank FROM nodes_fts WHERE nodes_fts MATCH ? LIMIT 100
)
SELECT n.id, n.title, n.description, n.content, n.link, n.updated_at,
SELECT n.id, n.title, n.description, n.notes, n.link, n.updated_at,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
FROM fts_matches fm
@@ -378,7 +378,7 @@ async function main() {
WITH fts_matches AS (
SELECT rowid, rank FROM nodes_fts WHERE nodes_fts MATCH ? LIMIT ?
)
SELECT n.id, n.title, n.description, n.content, n.link, n.updated_at,
SELECT n.id, n.title, n.description, n.notes, n.link, n.updated_at,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
FROM fts_matches fm
@@ -392,7 +392,7 @@ async function main() {
nodes = rows.map(row => ({
id: row.id,
title: row.title,
content: row.content ?? null,
notes: row.notes ?? null,
description: row.description ?? null,
link: row.link ?? null,
dimensions: JSON.parse(row.dimensions_json || '[]'),
@@ -410,7 +410,7 @@ async function main() {
const words = trimmedQuery.split(/\s+/).filter(w => w.length > 0);
let sql = `
SELECT n.id, n.title, n.description, n.content, n.link, n.updated_at,
SELECT n.id, n.title, n.description, n.notes, n.link, n.updated_at,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
FROM nodes n
@@ -419,7 +419,7 @@ async function main() {
const params = [];
for (const word of words) {
sql += ` AND (n.title LIKE ? COLLATE NOCASE OR n.description LIKE ? COLLATE NOCASE OR n.content LIKE ? COLLATE NOCASE)`;
sql += ` AND (n.title LIKE ? COLLATE NOCASE OR n.description LIKE ? COLLATE NOCASE OR n.notes LIKE ? COLLATE NOCASE)`;
params.push(`%${word}%`, `%${word}%`, `%${word}%`);
}
@@ -439,7 +439,7 @@ async function main() {
nodes = rows.map(row => ({
id: row.id,
title: row.title,
content: row.content ?? null,
notes: row.notes ?? null,
description: row.description ?? null,
link: row.link ?? null,
dimensions: JSON.parse(row.dimensions_json || '[]'),
@@ -485,7 +485,7 @@ async function main() {
nodes.push({
id: node.id,
title: node.title,
content: node.content ?? null,
notes: node.notes ?? null,
description: node.description ?? null,
link: node.link ?? null,
chunk: chunkTruncated ? rawChunk.substring(0, CHUNK_LIMIT) : rawChunk,
@@ -520,7 +520,13 @@ async function main() {
throw new Error('At least one field must be provided in updates.');
}
const node = nodeService.updateNode(id, updates, { appendContent: true });
// Map external 'content' param to internal 'notes'
const mappedUpdates = { ...updates };
if (mappedUpdates.content !== undefined) {
mappedUpdates.notes = mappedUpdates.content;
delete mappedUpdates.content;
}
const node = nodeService.updateNode(id, mappedUpdates, { appendNotes: true });
return {
content: [{ type: 'text', text: `Updated node #${id}` }],
@@ -9,7 +9,7 @@ function getNodes(filters = {}) {
const { dimensions, search, limit = 100, offset = 0 } = filters;
let sql = `
SELECT n.id, n.title, n.description, n.content, n.link, n.type, n.metadata, n.chunk,
SELECT n.id, n.title, n.description, n.notes, n.link, n.event_date, n.metadata, n.chunk,
n.created_at, n.updated_at,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
@@ -30,7 +30,7 @@ function getNodes(filters = {}) {
// Text search
if (search) {
sql += ` AND (n.title LIKE ? COLLATE NOCASE OR n.description LIKE ? COLLATE NOCASE OR n.content LIKE ? COLLATE NOCASE)`;
sql += ` AND (n.title LIKE ? COLLATE NOCASE OR n.description LIKE ? COLLATE NOCASE OR n.notes LIKE ? COLLATE NOCASE)`;
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
}
@@ -70,7 +70,7 @@ function getNodes(filters = {}) {
*/
function getNodeById(id) {
const sql = `
SELECT n.id, n.title, n.description, n.content, n.link, n.type, n.metadata, n.chunk,
SELECT n.id, n.title, n.description, n.notes, n.link, n.event_date, n.metadata, n.chunk,
n.created_at, n.updated_at,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
@@ -97,7 +97,7 @@ function createNode(nodeData) {
const {
title,
description,
content,
notes,
link,
type,
dimensions = [],
@@ -110,7 +110,7 @@ function createNode(nodeData) {
const nodeId = transaction(() => {
const stmt = db.prepare(`
INSERT INTO nodes (title, description, content, link, type, metadata, chunk, created_at, updated_at)
INSERT INTO nodes (title, description, notes, link, type, metadata, chunk, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
@@ -149,8 +149,8 @@ function createNode(nodeData) {
* Note: content is APPENDED by default (MCP tool behavior), not replaced.
*/
function updateNode(id, updates, options = {}) {
const { appendContent = true } = options;
const { title, description, content, link, type, dimensions, chunk, metadata } = updates;
const { appendNotes = true } = options;
const { title, description, notes, link, type, dimensions, chunk, metadata } = updates;
const now = new Date().toISOString();
const db = getDb();
@@ -173,10 +173,10 @@ function updateNode(id, updates, options = {}) {
params.push(description);
}
if (content !== undefined) {
if (appendContent && existing.content) {
if (appendNotes && existing.notes) {
// Append to existing content
setFields.push('content = ?');
params.push(existing.content + '\n\n' + content);
params.push(existing.notes + '\n\n' + content);
} else {
setFields.push('content = ?');
params.push(content);
@@ -50,9 +50,9 @@ function initDatabase() {
id INTEGER PRIMARY KEY,
title TEXT,
description TEXT,
content TEXT,
notes TEXT,
link TEXT,
type TEXT,
event_date TEXT,
created_at TEXT,
updated_at TEXT,
metadata TEXT,
@@ -71,7 +71,7 @@ function initDatabase() {
source TEXT,
created_at TEXT,
context TEXT,
user_feedback INTEGER,
FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE,
FOREIGN KEY (to_node_id) REFERENCES nodes(id) ON DELETE CASCADE
);