feat: demote context startup priority
- cap contexts to 10 per account - align MCP and onboarding context guidance - keep contexts optional unless obviously relevant
This commit is contained in:
@@ -13,9 +13,9 @@ const SERVER_INFO = {
|
||||
|
||||
const instructions = [
|
||||
'RA-H is a personal knowledge graph — local-first, vendor-neutral.',
|
||||
'Core concepts: contexts (optional soft scopes), nodes (knowledge units), and edges (connections with explanations).',
|
||||
'Always call rah_get_context first to orient yourself — it returns contexts, hub nodes, stats, and available guides.',
|
||||
'Use contexts only when they are explicit and helpful. Do not expect automatic context assignment.',
|
||||
'Core concepts: contexts (optional soft scopes, max 10), nodes (knowledge units), and edges (connections with explanations).',
|
||||
'Always call rah_get_context first to orient yourself — it returns high-level graph state, contexts, hub nodes, stats, and available guides.',
|
||||
'Use contexts only when one obvious existing context is explicit and helpful. If unsure or if none exist, leave context empty. Do not expect automatic context assignment.',
|
||||
'Search before creating: use rah_search_nodes to check if content already exists.',
|
||||
'Every edge needs an explanation: why does this connection exist?',
|
||||
].join(' ');
|
||||
@@ -52,7 +52,7 @@ function createServer(request: NextRequest): McpServer {
|
||||
'rah_add_node',
|
||||
{
|
||||
title: 'Add RA-H node',
|
||||
description: 'Create a new node in the local RA-H knowledge base. Set context explicitly when clear and useful; otherwise leave it empty.',
|
||||
description: 'Create a new node in the local RA-H knowledge base. Set context only when one obvious existing context clearly fits; otherwise leave it empty.',
|
||||
inputSchema: {
|
||||
title: z.string().min(1).max(160),
|
||||
content: z.string().max(20000).optional(),
|
||||
@@ -132,7 +132,7 @@ function createServer(request: NextRequest): McpServer {
|
||||
'rah_query_contexts',
|
||||
{
|
||||
title: 'Query RA-H contexts',
|
||||
description: 'List or inspect contexts, the soft organizational scope layer for the graph.',
|
||||
description: 'List or inspect optional contexts. Use this only when a context is already obviously relevant or the user asks for it.',
|
||||
inputSchema: {
|
||||
contextId: z.number().int().positive().optional(),
|
||||
name: z.string().optional(),
|
||||
@@ -507,7 +507,7 @@ function createServer(request: NextRequest): McpServer {
|
||||
'rah_get_context',
|
||||
{
|
||||
title: 'Get RA-H context',
|
||||
description: 'Get orientation context: contexts, hub nodes, stats, and available guides.',
|
||||
description: 'Get orientation context: high-level graph state, optional contexts, hub nodes, stats, and available guides.',
|
||||
inputSchema: {},
|
||||
},
|
||||
async () => {
|
||||
@@ -536,7 +536,7 @@ function createServer(request: NextRequest): McpServer {
|
||||
const guides = Array.isArray(guidesPayload.data) ? guidesPayload.data.map((guide: any) => guide.name) : [];
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Knowledge graph: ${contexts.length} contexts, ${hubNodes.length} hub nodes, ${guides.length} guides available.` }],
|
||||
content: [{ type: 'text', text: `Knowledge graph: ${contexts.length} optional contexts, ${hubNodes.length} hub nodes, ${guides.length} guides available.` }],
|
||||
structuredContent: {
|
||||
stats: {
|
||||
nodeCount: countPayload.total ?? 0,
|
||||
|
||||
@@ -59,7 +59,7 @@ function buildInstructions() {
|
||||
return `Today's date: ${now}. RA-H is the user's personal knowledge graph — local SQLite, fully on-device.
|
||||
|
||||
## Quick start
|
||||
1. Call getContext for orientation (stats, contexts, anchors/hubs).
|
||||
1. Call getContext for orientation (high-level graph state, optional contexts, anchors/hubs).
|
||||
2. For simple tasks, tool descriptions have everything you need.
|
||||
3. For complex tasks, call readSkill("db-operations").
|
||||
|
||||
@@ -282,11 +282,11 @@ async function main() {
|
||||
// First-run welcome message
|
||||
if (context.stats.nodeCount === 0) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Empty knowledge graph. This is a fresh start! Suggest adding the first node about something the user is working on or interested in.' }],
|
||||
content: [{ type: 'text', text: 'Empty knowledge graph. This is a fresh start. Ask what matters right now and help create the first useful node. Contexts are optional and can wait until one is obviously helpful.' }],
|
||||
structuredContent: {
|
||||
...context,
|
||||
welcome: true,
|
||||
suggestion: 'Ask the user what they\'re working on or interested in, then create the first node.'
|
||||
suggestion: 'Ask what matters right now, create the first useful node, and leave contexts empty unless one is an obvious fit.'
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -305,7 +305,7 @@ async function main() {
|
||||
'createNode',
|
||||
{
|
||||
title: 'Add RA-H node',
|
||||
description: 'Create a new node. Always search first (queryNodes) to avoid duplicates. Set context explicitly when clear and useful. Title: max 160 chars, clear and descriptive. Description is strongly recommended and should explicitly describe what the thing is and any surrounding context available, but the write will never be blocked over description quality. 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.',
|
||||
description: 'Create a new node. Always search first (queryNodes) to avoid duplicates. Set context only when one obvious existing context clearly fits; otherwise leave it empty. Title: max 160 chars, clear and descriptive. Description is strongly recommended and should explicitly describe what the thing is and any surrounding context available, but the write will never be blocked over description quality. 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.',
|
||||
inputSchema: addNodeInputSchema
|
||||
},
|
||||
async ({ title, content, source, link, description, context_id, context_name, metadata, chunk }) => {
|
||||
@@ -564,7 +564,7 @@ async function main() {
|
||||
'queryContexts',
|
||||
{
|
||||
title: 'List RA-H contexts',
|
||||
description: 'List or inspect contexts, the soft organizational layer for the graph. Use this before assigning or filtering by context.',
|
||||
description: 'List or inspect optional contexts. Use this only when a context is already obviously relevant or the user asks for it.',
|
||||
inputSchema: queryContextsInputSchema
|
||||
},
|
||||
async ({ contextId, name, search, limit = 50, includeNodes = false }) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const { getDb } = require('./sqlite-client');
|
||||
const MAX_CONTEXTS_PER_ACCOUNT = 10;
|
||||
|
||||
function mapContext(row) {
|
||||
if (!row) return null;
|
||||
@@ -57,6 +58,10 @@ function createContext({ name, description = null, icon = null }) {
|
||||
if (!trimmedName) {
|
||||
throw new Error('Context name is required.');
|
||||
}
|
||||
const existingCount = Number(db.prepare('SELECT COUNT(*) AS count FROM contexts').get()?.count ?? 0);
|
||||
if (existingCount >= MAX_CONTEXTS_PER_ACCOUNT) {
|
||||
throw new Error(`Context limit reached. Maximum ${MAX_CONTEXTS_PER_ACCOUNT} contexts are allowed per account.`);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const info = db.prepare(`
|
||||
@@ -126,6 +131,7 @@ function resolveContextId(input = {}) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MAX_CONTEXTS_PER_ACCOUNT,
|
||||
listContexts,
|
||||
getContextById,
|
||||
getContextByName,
|
||||
|
||||
@@ -10,7 +10,7 @@ description: "Use for graph read, write, connect, classify, or traverse operatio
|
||||
1. Search before create to avoid duplicates.
|
||||
2. Always try to include a natural description that clearly says what the thing is and any surrounding context available. But description quality is guidance only; RA-H should never block or rewrite a write because of description quality.
|
||||
3. Use event dates when known (when it happened, not when saved).
|
||||
4. Apply contexts only when they are explicit and helpful. One node gets at most one context. If explicit context is missing on create, leave it empty instead of guessing.
|
||||
4. Apply context only when it is an obvious match to one of the user's existing contexts and genuinely helpful. One node gets at most one context. If explicit context is missing on create, leave it empty instead of guessing.
|
||||
5. Do not rely on dimensions. Node quality comes from title, description, source, metadata, and strong edges.
|
||||
5. Create edges when relationships are meaningful; edge explanations should read as a sentence.
|
||||
6. For user-authored ideas, notes, or dictated thoughts, preserve the user's wording in `source` as fully as possible with only minimal cleanup.
|
||||
@@ -22,7 +22,7 @@ description: "Use for graph read, write, connect, classify, or traverse operatio
|
||||
- `source`: full verbatim or canonical content of the node (transcript, article text, book passage, user's thoughts). This is what gets chunked and embedded for semantic search.
|
||||
- For idea capture from chat, the `source` should usually be the raw user thought, not a compressed assistant summary.
|
||||
- `link`: external source URL only.
|
||||
- `context_id`: the node's primary context. Prefer setting it when the scope is explicit. Leave it null rather than guessing.
|
||||
- `context_id`: the node's primary context. Prefer setting it only when it is an obvious existing match. Leave it null rather than guessing.
|
||||
- `metadata`: use the canonical node metadata contract when metadata is needed:
|
||||
- `type`
|
||||
- `state` (`processed` or `not_processed`)
|
||||
|
||||
@@ -7,7 +7,7 @@ description: "Use for new-user setup, empty or near-empty graphs, or major reset
|
||||
|
||||
## Your Job
|
||||
|
||||
Three things: help the user understand the basic structure of the system, help them start building useful context in it, and bootstrap the context capsule when durable cross-session facts become clear.
|
||||
Three things: help the user understand the basic structure of the system, help them start building useful graph data in it, and bootstrap the context capsule when durable cross-session facts become clear.
|
||||
|
||||
Adapt to the user.
|
||||
|
||||
@@ -67,7 +67,7 @@ Explain the structure in simple terms:
|
||||
|
||||
Then say:
|
||||
|
||||
> "If you know specifically how you'd like to create your context corpus, feel free to tell me what you'd like to add and I can help you set things up. Otherwise, I can guide you through bootstrapping your context with a few suggested prompts."
|
||||
> "If you know specifically what you'd like to add, tell me and I can help you capture it. Otherwise, I can guide you through bootstrapping the graph with a few suggested prompts."
|
||||
|
||||
Also explain one practical thing early:
|
||||
|
||||
@@ -101,7 +101,7 @@ Keep it conversational. Use these buckets and adapt based on what the user gives
|
||||
Work these in naturally when they are relevant:
|
||||
|
||||
- **First node creation** — explain that a node is one concrete thing worth keeping: a project, source, person, belief, decision, or idea.
|
||||
- **Contexts** — explain that contexts are the primary folders or scopes for the graph.
|
||||
- **Contexts** — explain that contexts are optional helpers, not required setup. If the user has none, that is fine.
|
||||
- **MCP connection** — if the user mentions Claude Code or external agents, offer a quick setup path and point them to the MCP docs/skill flow rather than reciting a giant config block immediately.
|
||||
- **What to do after setup** — once the graph has a few solid nodes, the next useful move is usually one of:
|
||||
- connect related nodes with explicit edges
|
||||
@@ -113,7 +113,7 @@ Work these in naturally when they are relevant:
|
||||
Do your best to build the graph as useful context emerges.
|
||||
|
||||
- Add nodes when the user mentions concrete things worth keeping.
|
||||
- Assign a primary context when one is clear. Prefer leaving context empty over low-confidence guessing.
|
||||
- Assign a context only when it is an obvious match to one of the user's existing contexts. Prefer leaving context empty over low-confidence guessing.
|
||||
- Add edges when relationships are clear enough to explain well.
|
||||
- Explain what you're adding in plain language so the user understands the structure as it develops.
|
||||
|
||||
@@ -125,7 +125,7 @@ Before writing anything, call `readSkill('db-operations')` for full quality stan
|
||||
|
||||
- Search before creating — avoid duplicates from day one
|
||||
- Every description must be concrete: what it IS and why it matters to them, not what it "explores" or "discusses"
|
||||
- Contexts should hold the primary scope when clear, otherwise leave them empty
|
||||
- Contexts are optional and should only be used for an obvious existing match; otherwise leave them empty
|
||||
- Every edge needs an explicit explanation sentence
|
||||
|
||||
## Propose Before Writing
|
||||
|
||||
@@ -44,9 +44,9 @@ let logger = (message) => console.log(`[mcp] ${message}`);
|
||||
|
||||
const instructions = [
|
||||
'RA-H is a personal knowledge graph — local-first, vendor-neutral.',
|
||||
'Core concepts: contexts (optional soft scopes), nodes (knowledge units), and edges (connections with explanations).',
|
||||
'Always call rah_get_context first to orient yourself — it returns contexts, hub nodes, stats, and available guides.',
|
||||
'Use contexts only when they are explicit and helpful. Do not expect automatic context assignment.',
|
||||
'Core concepts: contexts (optional soft scopes, max 10), nodes (knowledge units), and edges (connections with explanations).',
|
||||
'Always call rah_get_context first to orient yourself — it returns high-level graph state, contexts, hub nodes, stats, and available guides.',
|
||||
'Use contexts only when one obvious existing context is explicit and helpful. If unsure or if none exist, leave context empty. Do not expect automatic context assignment.',
|
||||
'Search before creating: use rah_search_nodes to check if content already exists.',
|
||||
'Every edge needs an explanation: why does this connection exist?',
|
||||
'All data stays local on this device; nothing leaves 127.0.0.1.',
|
||||
@@ -321,7 +321,7 @@ mcpServer.registerTool(
|
||||
'rah_add_node',
|
||||
{
|
||||
title: 'Add RA-H node',
|
||||
description: 'Create a new node in the local RA-H knowledge base. Set context explicitly when clear and useful; otherwise leave it empty.',
|
||||
description: 'Create a new node in the local RA-H knowledge base. Set context only when one obvious existing context clearly fits; otherwise leave it empty.',
|
||||
inputSchema: addNodeInputSchema,
|
||||
outputSchema: addNodeOutputSchema
|
||||
},
|
||||
|
||||
@@ -11,9 +11,9 @@ const packageJson = require('../../package.json');
|
||||
|
||||
const instructions = [
|
||||
'RA-H is a personal knowledge graph — local-first, vendor-neutral.',
|
||||
'Core concepts: contexts (optional soft scopes), nodes (knowledge units), and edges (connections with explanations).',
|
||||
'Always call rah_get_context first to orient yourself — it returns contexts, hub nodes, stats, and available guides.',
|
||||
'Use contexts only when they are explicit and helpful. Do not expect automatic context assignment.',
|
||||
'Core concepts: contexts (optional soft scopes, max 10), nodes (knowledge units), and edges (connections with explanations).',
|
||||
'Always call rah_get_context first to orient yourself — it returns high-level graph state, contexts, hub nodes, stats, and available guides.',
|
||||
'Use contexts only when one obvious existing context is explicit and helpful. If unsure or if none exist, leave context empty. Do not expect automatic context assignment.',
|
||||
'Search before creating: use rah_search_nodes to check if content already exists.',
|
||||
'Every edge needs an explanation: why does this connection exist?',
|
||||
'All data stays local on this device; nothing leaves 127.0.0.1.',
|
||||
@@ -295,7 +295,7 @@ server.registerTool(
|
||||
'rah_add_node',
|
||||
{
|
||||
title: 'Add RA-H node',
|
||||
description: 'Create a new node in the local RA-H knowledge base. Set context explicitly when clear and useful; otherwise leave it empty.',
|
||||
description: 'Create a new node in the local RA-H knowledge base. Set context only when one obvious existing context clearly fits; otherwise leave it empty.',
|
||||
inputSchema: addNodeInputSchema,
|
||||
outputSchema: addNodeOutputSchema
|
||||
},
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function ContextsPane({
|
||||
{loading ? (
|
||||
<div style={emptyStateStyle}>Loading contexts...</div>
|
||||
) : contexts.length === 0 ? (
|
||||
<div style={emptyStateStyle}>No contexts yet.</div>
|
||||
<div style={emptyStateStyle}>No contexts yet. That is optional.</div>
|
||||
) : (
|
||||
contexts.map((context) => (
|
||||
<button
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const RAH_EASY_SYSTEM_PROMPT = `You are the user's agent for building, organizing, and improving an external corpus of context.
|
||||
export const RAH_EASY_SYSTEM_PROMPT = `You are the user's agent for building, organizing, and improving an external knowledge graph.
|
||||
|
||||
Mission:
|
||||
1. Resolve the user's request quickly and accurately using the tools provided.
|
||||
@@ -21,7 +21,7 @@ Tool strategy:
|
||||
- webSearch only when the knowledge base lacks the answer.
|
||||
|
||||
Contexts:
|
||||
- Contexts are optional. Only set one when it is explicit and useful.
|
||||
- Contexts are optional. Only set one when one obvious existing context is explicit and useful.
|
||||
- Do not expect automatic context assignment.
|
||||
- Improve organization through title, description, source, metadata, and edges instead of dimensions.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const RAH_MAIN_SYSTEM_PROMPT = `You are the user's agent for building, organizing, and improving an external corpus of context.
|
||||
export const RAH_MAIN_SYSTEM_PROMPT = `You are the user's agent for building, organizing, and improving an external knowledge graph.
|
||||
|
||||
Core responsibilities:
|
||||
- Keep the conversation tightly focused on the user's goal.
|
||||
@@ -26,7 +26,7 @@ Tool strategy:
|
||||
|
||||
Context handling:
|
||||
- Contexts are optional soft organization, not a required taxonomy.
|
||||
- Only set a context when it is explicit and genuinely helpful.
|
||||
- Only set a context when one obvious existing context is explicit and genuinely helpful.
|
||||
- Never rely on inferred dimensions or automatic context assignment.
|
||||
- Node quality should come from strong title, description, source, metadata, and edges.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Calibration
|
||||
description: "Run periodic check-ins to recalibrate goals, projects, preferences, and graph structure."
|
||||
when_to_use: "User asks for a check-in, reset, review, or strategic recalibration."
|
||||
when_not_to_use: "Single isolated question with no strategic update needed."
|
||||
success_criteria: "Graph reflects current reality: updated contexts, changed priorities, and explicit deltas."
|
||||
success_criteria: "Graph reflects current reality: updated nodes, changed priorities, and explicit deltas. Contexts are secondary when clearly useful."
|
||||
---
|
||||
|
||||
# Calibration
|
||||
@@ -14,7 +14,7 @@ Re-anchor the graph to the user's current state.
|
||||
|
||||
## Check-in Sequence
|
||||
|
||||
1. Review major contexts, anchor nodes, and active project nodes.
|
||||
1. Review the strongest active nodes first and use contexts only as a secondary check when they already exist and are clearly relevant.
|
||||
2. Ask what changed: goals, priorities, constraints, beliefs, preferences.
|
||||
3. Identify stale nodes and missing nodes.
|
||||
4. Propose precise updates: update existing vs create new.
|
||||
|
||||
@@ -10,7 +10,7 @@ description: "Use for graph read, write, connect, classify, or traverse operatio
|
||||
1. Search before create to avoid duplicates.
|
||||
2. Every create/update must include a natural description that clearly says what the thing is, why it matters here, and its current workflow status.
|
||||
3. Use event dates when known (when it happened, not when saved).
|
||||
4. Apply context only when it is explicit and useful. One node gets at most one primary context, and leaving context blank is valid.
|
||||
4. Apply context only when it is an obvious match to one of the user's existing contexts and genuinely useful. One node gets at most one primary context, and leaving context blank is valid.
|
||||
5. Create edges when relationships are meaningful; edge explanations should read as a sentence.
|
||||
6. For user-authored ideas, notes, or dictated thoughts, preserve the user's wording in `source` as fully as possible with only minimal cleanup.
|
||||
|
||||
@@ -21,7 +21,7 @@ description: "Use for graph read, write, connect, classify, or traverse operatio
|
||||
- `source`: full verbatim or canonical content of the node (transcript, article text, book passage, user's thoughts). This is what gets chunked and embedded for semantic search.
|
||||
- For idea capture from chat, the `source` should usually be the raw user thought, not a compressed assistant summary.
|
||||
- `link`: external source URL only.
|
||||
- `context_id`: the node's primary context. Prefer setting it when the scope is explicit. Leave it null rather than guessing.
|
||||
- `context_id`: the node's primary context. Prefer setting it only when it is an obvious existing match. Leave it null rather than guessing.
|
||||
- `metadata`: use the canonical node metadata contract when metadata is needed:
|
||||
- `type`
|
||||
- `state` (`processed` or `not_processed`)
|
||||
|
||||
@@ -7,7 +7,7 @@ description: "Use for new-user setup, empty or near-empty graphs, or major reset
|
||||
|
||||
## Your Job
|
||||
|
||||
Three things: help the user understand the basic structure of the system, help them start building useful context in it, and bootstrap the context capsule when durable cross-session facts become clear.
|
||||
Three things: help the user understand the basic structure of the system, help them start building useful graph data in it, and bootstrap the context capsule when durable cross-session facts become clear.
|
||||
|
||||
Adapt to the user.
|
||||
|
||||
@@ -67,7 +67,7 @@ Explain the structure in simple terms:
|
||||
|
||||
Then say:
|
||||
|
||||
> "If you know specifically how you'd like to create your context corpus, feel free to tell me what you'd like to add and I can help you set things up. Otherwise, I can guide you through bootstrapping your context with a few suggested prompts."
|
||||
> "If you know specifically what you'd like to add, tell me and I can help you capture it. Otherwise, I can guide you through bootstrapping the graph with a few suggested prompts."
|
||||
|
||||
Also explain one practical thing early:
|
||||
|
||||
@@ -101,7 +101,7 @@ Keep it conversational. Use these buckets and adapt based on what the user gives
|
||||
Work these in naturally when they are relevant:
|
||||
|
||||
- **First node creation** — explain that a node is one concrete thing worth keeping: a project, source, person, belief, decision, or idea.
|
||||
- **Contexts** — explain that contexts are the primary folders or scopes for the graph.
|
||||
- **Contexts** — explain that contexts are optional helpers, not required setup. If the user has none, that is fine.
|
||||
- **MCP connection** — if the user mentions Claude Code or external agents, offer a quick setup path and point them to the MCP docs/skill flow rather than reciting a giant config block immediately.
|
||||
- **What to do after setup** — once the graph has a few solid nodes, the next useful move is usually one of:
|
||||
- connect related nodes with explicit edges
|
||||
@@ -113,7 +113,7 @@ Work these in naturally when they are relevant:
|
||||
Do your best to build the graph as useful context emerges.
|
||||
|
||||
- Add nodes when the user mentions concrete things worth keeping.
|
||||
- Assign a primary context when one is clear. Prefer leaving context empty over low-confidence guessing.
|
||||
- Assign a context only when it is an obvious match to one of the user's existing contexts. Prefer leaving context empty over low-confidence guessing.
|
||||
- Add edges when relationships are clear enough to explain well.
|
||||
- Explain what you're adding in plain language so the user understands the structure as it develops.
|
||||
|
||||
@@ -125,7 +125,7 @@ Before writing anything, call `readSkill('db-operations')` for full quality stan
|
||||
|
||||
- Search before creating — avoid duplicates from day one
|
||||
- Every description must be concrete: what it IS and why it matters to them, not what it "explores" or "discusses"
|
||||
- Contexts should hold the primary scope when clear, otherwise leave them empty
|
||||
- Contexts are optional and should only be used for an obvious existing match; otherwise leave them empty
|
||||
- Every edge needs an explicit explanation sentence
|
||||
|
||||
## Propose Before Writing
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Context, ContextSummary, Node } from '@/types/database';
|
||||
import { nodeService } from './nodes';
|
||||
|
||||
type ContextRow = Context;
|
||||
export const MAX_CONTEXTS_PER_ACCOUNT = 10;
|
||||
|
||||
function normalizeContextName(name: string): string {
|
||||
return name.trim().replace(/\s+/g, ' ');
|
||||
@@ -107,6 +108,15 @@ export class ContextService {
|
||||
throw new Error(`Context "${name}" already exists.`);
|
||||
}
|
||||
|
||||
const contextCountRow = sqlite.query<{ count: number }>(`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM contexts
|
||||
`).rows[0];
|
||||
const existingCount = Number(contextCountRow?.count ?? 0);
|
||||
if (existingCount >= MAX_CONTEXTS_PER_ACCOUNT) {
|
||||
throw new Error(`Context limit reached. Maximum ${MAX_CONTEXTS_PER_ACCOUNT} contexts are allowed per account.`);
|
||||
}
|
||||
|
||||
const result = sqlite.prepare(`
|
||||
INSERT INTO contexts (name, description, icon, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('@/services/database/sqlite-client', () => ({
|
||||
getSQLiteClient: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/services/database/nodes', () => ({
|
||||
nodeService: {
|
||||
getNodes: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { getSQLiteClient } from '@/services/database/sqlite-client';
|
||||
import { ContextService, MAX_CONTEXTS_PER_ACCOUNT } from '@/services/database/contextService';
|
||||
|
||||
type QueryResult = { rows: Array<Record<string, unknown>> };
|
||||
|
||||
describe('ContextService', () => {
|
||||
const query = vi.fn<(...args: unknown[]) => QueryResult>();
|
||||
const run = vi.fn();
|
||||
const prepare = vi.fn(() => ({ run }));
|
||||
const sqlite = { query, prepare };
|
||||
|
||||
beforeEach(() => {
|
||||
query.mockReset();
|
||||
prepare.mockClear();
|
||||
run.mockReset();
|
||||
vi.mocked(getSQLiteClient).mockReturnValue(sqlite as never);
|
||||
});
|
||||
|
||||
it('rejects creating a context once the account reaches the hard cap', async () => {
|
||||
query.mockImplementation((sql: unknown) => {
|
||||
const text = String(sql);
|
||||
if (text.includes('WHERE LOWER(TRIM(name))')) {
|
||||
return { rows: [] };
|
||||
}
|
||||
if (text.includes('SELECT COUNT(*) AS count')) {
|
||||
return { rows: [{ count: MAX_CONTEXTS_PER_ACCOUNT }] };
|
||||
}
|
||||
return { rows: [] };
|
||||
});
|
||||
|
||||
const service = new ContextService();
|
||||
|
||||
await expect(
|
||||
service.createContext({
|
||||
name: 'Health',
|
||||
description: 'Health-related items.',
|
||||
})
|
||||
).rejects.toThrow(`Maximum ${MAX_CONTEXTS_PER_ACCOUNT} contexts are allowed per account.`);
|
||||
|
||||
expect(prepare).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a context when the account is below the hard cap', async () => {
|
||||
run.mockReturnValue({ lastInsertRowid: 11 });
|
||||
query.mockImplementation((sql: unknown) => {
|
||||
const text = String(sql);
|
||||
if (text.includes('WHERE LOWER(TRIM(name))')) {
|
||||
return { rows: [] };
|
||||
}
|
||||
if (text.includes('SELECT COUNT(*) AS count')) {
|
||||
return { rows: [{ count: MAX_CONTEXTS_PER_ACCOUNT - 1 }] };
|
||||
}
|
||||
if (text.includes('WHERE c.id = ?')) {
|
||||
return {
|
||||
rows: [{
|
||||
id: 11,
|
||||
name: 'Health',
|
||||
description: 'Health-related items.',
|
||||
icon: null,
|
||||
count: 0,
|
||||
}],
|
||||
};
|
||||
}
|
||||
return { rows: [] };
|
||||
});
|
||||
|
||||
const service = new ContextService();
|
||||
const created = await service.createContext({
|
||||
name: 'Health',
|
||||
description: 'Health-related items.',
|
||||
});
|
||||
|
||||
expect(prepare).toHaveBeenCalledOnce();
|
||||
expect(created).toMatchObject({
|
||||
id: 11,
|
||||
name: 'Health',
|
||||
description: 'Health-related items.',
|
||||
icon: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user