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
+7 -9
View File
@@ -351,8 +351,7 @@ async function main() {
'createNode',
{
title: 'Add RA-H node',
description: 'Create a new node. Always search first (queryNodes) to avoid duplicates. Title: max 160 chars, clear and descriptive. Description is REQUIRED and must be explicit about what the thing is and why it matters for contextual grounding. Use "link" ONLY for external content (URL, video, article) — omit for synthesis/ideas derived from existing nodes. "content" = your notes/analysis. "chunk" = verbatim source text. Assign 1-5 dimensions — call queryDimensions first to use existing ones.',
// Note: MCP schema uses "content" for external API compat; mapped to "notes" internally
description: 'Create a new node. Always search first (queryNodes) to avoid duplicates. Title: max 160 chars, clear and descriptive. Description is REQUIRED and must be explicit about what the thing is and why it matters for contextual grounding. Use "link" ONLY for external content (URL, video, article) — omit for synthesis/ideas derived from existing nodes. "source" = verbatim or canonical content for embedding. Legacy "content" and "chunk" are mapped to source for compatibility. Assign 1-5 dimensions — call queryDimensions first to use existing ones.',
inputSchema: addNodeInputSchema
},
async ({ title, content, source, link, description, dimensions, metadata, chunk }) => {
@@ -392,8 +391,7 @@ async function main() {
'queryNodes',
{
title: 'Search RA-H nodes',
description: 'Search nodes by keyword across title, description, and content fields. Multi-word queries find nodes containing all words (not exact phrases). Returns up to 25 results (default 10). Call before creating nodes to check for duplicates. Optionally filter by dimensions. NOT for searching source documents (transcripts, articles) — use searchContentEmbeddings for that.',
// Note: searches across title, description, and notes columns
description: 'Search nodes by keyword across title, description, and source fields. Multi-word queries find nodes containing all words (not exact phrases). Returns up to 25 results (default 10). Call before creating nodes to check for duplicates. Optionally filter by dimensions. NOT for searching source documents (transcripts, articles) — use searchContentEmbeddings for that.',
inputSchema: searchNodesInputSchema
},
async ({ query: searchQuery, limit = 10, dimensions, created_after, created_before, event_after, event_before }) => {
@@ -560,13 +558,13 @@ async function main() {
for (const id of uniqueIds) {
const node = nodeService.getNodeById(id);
if (node) {
const rawChunk = node.source ?? node.chunk ?? null;
const rawChunk = node.source ?? null;
const chunkTruncated = rawChunk ? rawChunk.length > CHUNK_LIMIT : false;
nodes.push({
id: node.id,
title: node.title,
source: node.source ?? node.notes ?? null,
source: node.source ?? null,
description: node.description ?? null,
link: node.link ?? null,
chunk: chunkTruncated ? rawChunk.substring(0, CHUNK_LIMIT) : rawChunk,
@@ -595,7 +593,7 @@ async function main() {
'updateNode',
{
title: 'Update RA-H node',
description: 'Update an existing node. Description is REQUIRED on every update and must explicitly state WHAT this thing is + WHY it matters for contextual grounding. Content is APPENDED to existing notes (not replaced). Dimensions are REPLACED entirely with the new array. Title, description, and link are overwritten. Call getNodesById first to verify current state before updating.',
description: 'Update an existing node. Description is REQUIRED on every update and must explicitly state WHAT this thing is + WHY it matters for contextual grounding. Source content lives in "source". Legacy "content" and "chunk" are mapped to source for compatibility. Dimensions are REPLACED entirely with the new array. Title, description, and link are overwritten. Call getNodesById first to verify current state before updating.',
inputSchema: updateNodeInputSchema
},
async ({ id, updates }) => {
@@ -621,7 +619,7 @@ async function main() {
delete mappedUpdates.content;
delete mappedUpdates.chunk;
const node = nodeService.updateNode(id, mappedUpdates, { appendNotes: false });
const node = nodeService.updateNode(id, mappedUpdates);
return {
content: [{ type: 'text', text: `Updated node #${id}` }],
@@ -916,7 +914,7 @@ async function main() {
const tableCheck = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chunks'").get();
if (!tableCheck) {
return {
content: [{ type: 'text', text: 'No chunks table found. Source content has not been chunked yet. Use getNodesById to read the raw chunk field instead.' }],
content: [{ type: 'text', text: 'No chunks table found. Source content has not been chunked yet. Use getNodesById to read the raw source text instead.' }],
structuredContent: { count: 0, chunks: [], note: 'chunks table does not exist' }
};
}
@@ -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.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.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
@@ -70,7 +70,7 @@ function getNodes(filters = {}) {
*/
function getNodeById(id) {
const sql = `
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.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
@@ -164,8 +164,7 @@ function createNode(nodeData) {
* Source-first update path.
*/
function updateNode(id, updates, options = {}) {
const { appendNotes = true } = options;
const { title, description, source, notes, link, event_date, dimensions, chunk, metadata } = updates;
const { title, description, source, link, event_date, dimensions, metadata } = updates;
const now = new Date().toISOString();
const db = getDb();
@@ -190,12 +189,6 @@ function updateNode(id, updates, options = {}) {
if (source !== undefined) {
setFields.push('source = ?');
params.push(source);
} else if (notes !== undefined) {
const nextSource = appendNotes && existing.source
? `${existing.source}\n\n${notes}`
: notes;
setFields.push('source = ?');
params.push(nextSource);
}
if (link !== undefined) {
setFields.push('link = ?');
@@ -205,10 +198,6 @@ function updateNode(id, updates, options = {}) {
setFields.push('event_date = ?');
params.push(event_date);
}
if (chunk !== undefined && source === undefined) {
setFields.push('source = ?');
params.push(chunk);
}
if (metadata !== undefined) {
setFields.push('metadata = ?');
params.push(JSON.stringify(metadata));
@@ -50,13 +50,12 @@ function initDatabase() {
id INTEGER PRIMARY KEY,
title TEXT,
description TEXT,
notes TEXT,
source TEXT,
link TEXT,
event_date TEXT,
created_at TEXT,
updated_at TEXT,
metadata TEXT,
chunk TEXT,
embedding BLOB,
embedding_updated_at TEXT,
embedding_text TEXT,
@@ -113,6 +112,38 @@ function initDatabase() {
db.pragma('cache_size = 5000');
db.pragma('busy_timeout = 5000');
const nodeCols = db.prepare('PRAGMA table_info(nodes)').all().map(c => c.name);
if (!nodeCols.includes('source')) {
db.exec('ALTER TABLE nodes ADD COLUMN source TEXT;');
}
if (nodeCols.includes('content')) {
db.exec(`
UPDATE nodes
SET source = content
WHERE (source IS NULL OR LENGTH(TRIM(source)) = 0)
AND content IS NOT NULL
AND LENGTH(TRIM(content)) > 0;
`);
}
if (nodeCols.includes('notes')) {
db.exec(`
UPDATE nodes
SET source = notes
WHERE (source IS NULL OR LENGTH(TRIM(source)) = 0)
AND notes IS NOT NULL
AND LENGTH(TRIM(notes)) > 0;
`);
}
if (nodeCols.includes('chunk')) {
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;
`);
}
const edgeCols = db.prepare('PRAGMA table_info(edges)').all().map(c => c.name);
if (!edgeCols.includes('explanation')) {
db.exec('ALTER TABLE edges ADD COLUMN explanation TEXT;');