fix: port search and dimension safety guardrails
- enforce canonical dimensions across app and MCP surfaces - prioritize title-first node search in UI and query tools - add regression tests for ranking and dimension validation
This commit is contained in:
@@ -66,6 +66,9 @@ function buildInstructions() {
|
||||
Proactively offer to save valuable information when insights, decisions, or references surface.
|
||||
Propose: "I'd add this as: [title] in [dimensions] — want me to?"
|
||||
Always search before creating to avoid duplicates.
|
||||
Use only existing dimensions returned by getContext or queryDimensions.
|
||||
Do not invent new dimensions from node titles, concepts, or phrasing.
|
||||
Only call createDimension when the user explicitly instructs you to create a new dimension.
|
||||
|
||||
## Available skills
|
||||
${skillIndex}
|
||||
@@ -81,7 +84,7 @@ const addNodeInputSchema = {
|
||||
source: z.string().max(50000).optional().describe('Full source text'),
|
||||
link: z.string().url().optional().describe('Source URL'),
|
||||
description: z.string().min(24).max(280).describe('REQUIRED. One-sentence summary: WHAT this is (explicit, concrete) + WHY it matters. No weak verbs (discusses, explores, examines). Example: "Podcast — Lex Fridman interviews Sam Altman on AGI timelines. First public comments since board drama."'),
|
||||
dimensions: z.array(z.string()).min(1).max(5).describe('1-5 categories. Call queryDimensions first to use existing ones.'),
|
||||
dimensions: z.array(z.string()).min(1).max(5).describe('1-5 existing categories. Call queryDimensions first to use existing ones. Do not invent new dimensions.'),
|
||||
metadata: z.record(z.any()).optional().describe('Additional metadata'),
|
||||
chunk: z.string().max(50000).optional().describe('Full source text')
|
||||
};
|
||||
@@ -351,7 +354,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. "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.',
|
||||
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 existing dimensions — call queryDimensions first to use existing ones. Do not invent new dimensions.',
|
||||
inputSchema: addNodeInputSchema
|
||||
},
|
||||
async ({ title, content, source, link, description, dimensions, metadata, chunk }) => {
|
||||
@@ -716,7 +719,7 @@ async function main() {
|
||||
'queryDimensions',
|
||||
{
|
||||
title: 'List RA-H dimensions',
|
||||
description: 'Get all dimensions with node counts. Call before creating nodes (to assign existing dimensions) or before creating new dimensions (to avoid duplicates). Shows priority/locked status.',
|
||||
description: 'Get all existing canonical dimensions with node counts. Call before creating nodes to assign existing dimensions. Only create a new dimension if the user explicitly instructs you to do so.',
|
||||
inputSchema: listDimensionsInputSchema
|
||||
},
|
||||
async () => {
|
||||
@@ -736,7 +739,7 @@ async function main() {
|
||||
'createDimension',
|
||||
{
|
||||
title: 'Create RA-H dimension',
|
||||
description: 'Create a new dimension/category. Use lowercase, singular form (e.g. "biology" not "Biology" or "biologies"). Set isPriority=true to lock it for automatic assignment to new nodes. Always include a description.',
|
||||
description: 'Create a new dimension/category only when the user explicitly instructs you to do so. Use lowercase, singular form (e.g. "biology" not "Biology" or "biologies"). Set isPriority=true to lock it for automatic assignment to new nodes. Always include a description.',
|
||||
inputSchema: createDimensionInputSchema
|
||||
},
|
||||
async ({ name, description, isPriority }) => {
|
||||
|
||||
@@ -2,6 +2,34 @@
|
||||
|
||||
const { query, transaction, getDb } = require('./sqlite-client');
|
||||
|
||||
function normalizeDimensionName(value) {
|
||||
return String(value || '').trim().replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
function getUnknownDimensions(dimensions) {
|
||||
if (!Array.isArray(dimensions) || dimensions.length === 0) return [];
|
||||
|
||||
const normalized = dimensions
|
||||
.map(normalizeDimensionName)
|
||||
.filter(Boolean);
|
||||
|
||||
if (normalized.length === 0) return [];
|
||||
|
||||
const placeholders = normalized.map(() => '?').join(', ');
|
||||
const rows = query(`SELECT name FROM dimensions WHERE name IN (${placeholders})`, normalized);
|
||||
const existing = new Set(rows.map(row => normalizeDimensionName(row.name)));
|
||||
|
||||
return normalized.filter(value => !existing.has(value));
|
||||
}
|
||||
|
||||
function formatUnknownDimensionsError(values) {
|
||||
if (values.length === 1) {
|
||||
return `Unknown dimension: "${values[0]}". Create it first or use an existing dimension.`;
|
||||
}
|
||||
|
||||
return `Unknown dimensions: ${values.map(value => `"${value}"`).join(', ')}. Create them first or use existing dimensions.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nodes with optional filtering.
|
||||
*/
|
||||
@@ -116,6 +144,10 @@ function createNode(nodeData) {
|
||||
} = nodeData;
|
||||
|
||||
const title = sanitizeTitle(rawTitle);
|
||||
const unknownDimensions = getUnknownDimensions(dimensions);
|
||||
if (unknownDimensions.length > 0) {
|
||||
throw new Error(formatUnknownDimensionsError(unknownDimensions));
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const db = getDb();
|
||||
@@ -174,6 +206,13 @@ function updateNode(id, updates, options = {}) {
|
||||
throw new Error(`Node with ID ${id} not found. Use rah_search_nodes to find nodes by keyword.`);
|
||||
}
|
||||
|
||||
if (Array.isArray(dimensions)) {
|
||||
const unknownDimensions = getUnknownDimensions(dimensions);
|
||||
if (unknownDimensions.length > 0) {
|
||||
throw new Error(formatUnknownDimensionsError(unknownDimensions));
|
||||
}
|
||||
}
|
||||
|
||||
transaction(() => {
|
||||
const setFields = [];
|
||||
const params = [];
|
||||
|
||||
Reference in New Issue
Block a user