@obsidicore/cascade-engine 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/cascade/checkpoints.d.ts +55 -0
- package/dist/cascade/checkpoints.js +123 -0
- package/dist/cascade/checkpoints.js.map +1 -0
- package/dist/cascade/engine.d.ts +72 -0
- package/dist/cascade/engine.js +170 -0
- package/dist/cascade/engine.js.map +1 -0
- package/dist/cascade/gates.d.ts +46 -0
- package/dist/cascade/gates.js +199 -0
- package/dist/cascade/gates.js.map +1 -0
- package/dist/cascade/research.d.ts +50 -0
- package/dist/cascade/research.js +127 -0
- package/dist/cascade/research.js.map +1 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +165 -0
- package/dist/cli.js.map +1 -0
- package/dist/control/kalman.d.ts +53 -0
- package/dist/control/kalman.js +83 -0
- package/dist/control/kalman.js.map +1 -0
- package/dist/control/pid.d.ts +57 -0
- package/dist/control/pid.js +95 -0
- package/dist/control/pid.js.map +1 -0
- package/dist/control/stability.d.ts +42 -0
- package/dist/control/stability.js +117 -0
- package/dist/control/stability.js.map +1 -0
- package/dist/db/index.d.ts +26 -0
- package/dist/db/index.js +116 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.sql +282 -0
- package/dist/graph/amem.d.ts +80 -0
- package/dist/graph/amem.js +190 -0
- package/dist/graph/amem.js.map +1 -0
- package/dist/graph/entities.d.ts +66 -0
- package/dist/graph/entities.js +187 -0
- package/dist/graph/entities.js.map +1 -0
- package/dist/graph/queries.d.ts +48 -0
- package/dist/graph/queries.js +176 -0
- package/dist/graph/queries.js.map +1 -0
- package/dist/hitl/dashboard.d.ts +51 -0
- package/dist/hitl/dashboard.js +135 -0
- package/dist/hitl/dashboard.js.map +1 -0
- package/dist/hitl/interventions.d.ts +36 -0
- package/dist/hitl/interventions.js +150 -0
- package/dist/hitl/interventions.js.map +1 -0
- package/dist/hitl/steering.d.ts +37 -0
- package/dist/hitl/steering.js +118 -0
- package/dist/hitl/steering.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +701 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/consolidation.d.ts +51 -0
- package/dist/memory/consolidation.js +122 -0
- package/dist/memory/consolidation.js.map +1 -0
- package/dist/memory/ncd.d.ts +40 -0
- package/dist/memory/ncd.js +90 -0
- package/dist/memory/ncd.js.map +1 -0
- package/dist/memory/sm2.d.ts +44 -0
- package/dist/memory/sm2.js +119 -0
- package/dist/memory/sm2.js.map +1 -0
- package/dist/memory/tiers.d.ts +49 -0
- package/dist/memory/tiers.js +145 -0
- package/dist/memory/tiers.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +6 -0
- package/dist/server.js.map +1 -0
- package/dist/trust/ingestion.d.ts +38 -0
- package/dist/trust/ingestion.js +147 -0
- package/dist/trust/ingestion.js.map +1 -0
- package/dist/trust/patterns.d.ts +26 -0
- package/dist/trust/patterns.js +78 -0
- package/dist/trust/patterns.js.map +1 -0
- package/dist/trust/scoring.d.ts +39 -0
- package/dist/trust/scoring.js +206 -0
- package/dist/trust/scoring.js.map +1 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Research Cascade MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Stdio-based MCP server providing 12+ tools for progressive research
|
|
6
|
+
* with knowledge graph, trust scoring, and self-regulation.
|
|
7
|
+
*
|
|
8
|
+
* NEVER console.log() — corrupts stdio JSON-RPC. Use console.error() only.
|
|
9
|
+
*/
|
|
10
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { getDb, closeDb, generateId, contentHash } from './db/index.js';
|
|
14
|
+
import { ingestFinding } from './trust/ingestion.js';
|
|
15
|
+
import { consolidateRound } from './memory/consolidation.js';
|
|
16
|
+
import { checkInterventions } from './hitl/interventions.js';
|
|
17
|
+
import { renderDashboard, buildDashboardData } from './hitl/dashboard.js';
|
|
18
|
+
import { createNote, linkNotes, extractNotesFromFinding, updateMaturity, getNoteStats, searchNotes } from './graph/amem.js';
|
|
19
|
+
const server = new McpServer({
|
|
20
|
+
name: 'cascade-engine',
|
|
21
|
+
version: '0.1.0',
|
|
22
|
+
});
|
|
23
|
+
// ============================================================
|
|
24
|
+
// TOOL 1: store_plan — Save immutable research plan
|
|
25
|
+
// ============================================================
|
|
26
|
+
server.tool('store_plan', 'Save an immutable research plan for a cascade. Locks questions and criteria at round start to prevent HARKing.', {
|
|
27
|
+
cascade_id: z.string().describe('Cascade ID to attach the plan to'),
|
|
28
|
+
plan: z.object({
|
|
29
|
+
questions: z.array(z.string()).describe('Research questions to investigate'),
|
|
30
|
+
success_criteria: z.array(z.string()).describe('How we know when we have a good answer'),
|
|
31
|
+
scope_boundaries: z.array(z.string()).optional().describe('What is explicitly out of scope'),
|
|
32
|
+
max_rounds: z.number().optional().default(5),
|
|
33
|
+
token_budget: z.number().optional().default(500000),
|
|
34
|
+
}).describe('The research plan to lock in'),
|
|
35
|
+
}, async ({ cascade_id, plan }) => {
|
|
36
|
+
const db = getDb();
|
|
37
|
+
// Check if cascade exists
|
|
38
|
+
const existing = db.prepare('SELECT id, plan_json FROM cascades WHERE id = ?').get(cascade_id);
|
|
39
|
+
if (existing?.plan_json) {
|
|
40
|
+
return { content: [{ type: 'text', text: `Error: Plan already locked for cascade ${cascade_id}. Plans are immutable to prevent HARKing.` }] };
|
|
41
|
+
}
|
|
42
|
+
const planJson = JSON.stringify(plan);
|
|
43
|
+
if (existing) {
|
|
44
|
+
db.prepare('UPDATE cascades SET plan_json = ?, max_rounds = ?, token_budget = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
45
|
+
.run(planJson, plan.max_rounds, plan.token_budget, cascade_id);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
db.prepare('INSERT INTO cascades (id, question, plan_json, max_rounds, token_budget) VALUES (?, ?, ?, ?, ?)')
|
|
49
|
+
.run(cascade_id, plan.questions[0] || 'Unnamed cascade', planJson, plan.max_rounds, plan.token_budget);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: 'text', text: `Plan locked for cascade ${cascade_id}. ${plan.questions.length} questions, max ${plan.max_rounds} rounds, ${plan.token_budget} token budget.` }],
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
// ============================================================
|
|
56
|
+
// TOOL 2: store_finding — Ingest finding with trust scoring
|
|
57
|
+
// ============================================================
|
|
58
|
+
server.tool('store_finding', 'Store a research finding. Generates content-addressable ID. Trust scoring applied automatically.', {
|
|
59
|
+
cascade_id: z.string(),
|
|
60
|
+
thread_id: z.string().optional(),
|
|
61
|
+
claim: z.string().describe('The factual claim or finding'),
|
|
62
|
+
evidence: z.string().optional().describe('Supporting evidence or context'),
|
|
63
|
+
source_url: z.string().optional(),
|
|
64
|
+
source_type: z.enum(['primary', 'secondary', 'tertiary']).optional(),
|
|
65
|
+
confidence: z.number().min(0).max(1).optional().default(0.5),
|
|
66
|
+
cascade_round: z.number(),
|
|
67
|
+
}, async ({ cascade_id, thread_id, claim, evidence, source_url, source_type, confidence, cascade_round }) => {
|
|
68
|
+
// Route through full trust scoring pipeline
|
|
69
|
+
const result = ingestFinding(cascade_id, claim, evidence, source_url, source_type, confidence, cascade_round, thread_id);
|
|
70
|
+
const statusIcon = result.action === 'admitted' ? 'ADMITTED' :
|
|
71
|
+
result.action === 'quarantined' ? 'QUARANTINED' : 'REJECTED';
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: 'text', text: `Finding ${result.findingId}: ${statusIcon} (trust: ${result.trustScore.toFixed(3)}, confidence: ${confidence})\nSignals: source=${result.signals.sourceReputation.toFixed(2)} corroboration=${result.signals.crossCorroboration.toFixed(2)} instruction=${result.signals.instructionScore.toFixed(2)} grade=${result.signals.gradeAssessment.toFixed(2)}\nReason: ${result.reason}` }],
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
// ============================================================
|
|
77
|
+
// TOOL 3: get_findings — Query findings (FTS + filters)
|
|
78
|
+
// ============================================================
|
|
79
|
+
server.tool('get_findings', 'Query stored findings using full-text search and/or filters.', {
|
|
80
|
+
cascade_id: z.string().optional(),
|
|
81
|
+
query: z.string().optional().describe('FTS search query'),
|
|
82
|
+
min_confidence: z.number().min(0).max(1).optional(),
|
|
83
|
+
include_quarantined: z.boolean().optional().default(false),
|
|
84
|
+
round: z.number().optional(),
|
|
85
|
+
limit: z.number().optional().default(20),
|
|
86
|
+
}, async ({ cascade_id, query, min_confidence, include_quarantined, round, limit }) => {
|
|
87
|
+
const db = getDb();
|
|
88
|
+
const params = [];
|
|
89
|
+
let sql;
|
|
90
|
+
if (query) {
|
|
91
|
+
sql = `SELECT f.id, f.claim, f.evidence, f.source_url, f.confidence, f.trust_composite,
|
|
92
|
+
f.grade_level, f.quarantined, f.cascade_round, f.created_at
|
|
93
|
+
FROM findings f
|
|
94
|
+
JOIN findings_fts fts ON f.rowid = fts.rowid
|
|
95
|
+
WHERE findings_fts MATCH ?`;
|
|
96
|
+
params.push(query);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
sql = `SELECT id, claim, evidence, source_url, confidence, trust_composite,
|
|
100
|
+
grade_level, quarantined, cascade_round, created_at
|
|
101
|
+
FROM findings WHERE 1=1`;
|
|
102
|
+
}
|
|
103
|
+
if (cascade_id) {
|
|
104
|
+
sql += ' AND cascade_id = ?';
|
|
105
|
+
params.push(cascade_id);
|
|
106
|
+
}
|
|
107
|
+
if (min_confidence !== undefined) {
|
|
108
|
+
sql += ' AND confidence >= ?';
|
|
109
|
+
params.push(min_confidence);
|
|
110
|
+
}
|
|
111
|
+
if (!include_quarantined) {
|
|
112
|
+
sql += ' AND quarantined = 0';
|
|
113
|
+
}
|
|
114
|
+
if (round !== undefined) {
|
|
115
|
+
sql += ' AND cascade_round = ?';
|
|
116
|
+
params.push(round);
|
|
117
|
+
}
|
|
118
|
+
sql += ' ORDER BY confidence DESC LIMIT ?';
|
|
119
|
+
params.push(limit);
|
|
120
|
+
const findings = db.prepare(sql).all(...params);
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: 'text', text: JSON.stringify(findings, null, 2) }],
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
// ============================================================
|
|
126
|
+
// TOOL 4: add_entity — Add KG entity
|
|
127
|
+
// ============================================================
|
|
128
|
+
server.tool('add_entity', 'Add an entity to the knowledge graph. Upserts on (name, entity_type).', {
|
|
129
|
+
name: z.string(),
|
|
130
|
+
entity_type: z.string().describe('e.g., concept, person, tool, technique, paper'),
|
|
131
|
+
properties: z.record(z.any()).optional().default({}),
|
|
132
|
+
tier: z.enum(['peripheral', 'working', 'core']).optional().default('working'),
|
|
133
|
+
importance: z.number().min(0).max(1).optional().default(0.5),
|
|
134
|
+
}, async ({ name, entity_type, properties, tier, importance }) => {
|
|
135
|
+
const db = getDb();
|
|
136
|
+
const propsJson = JSON.stringify(properties);
|
|
137
|
+
const result = db.prepare(`INSERT INTO kg_entities (name, entity_type, properties, tier, importance)
|
|
138
|
+
VALUES (?, ?, ?, ?, ?)
|
|
139
|
+
ON CONFLICT(name, entity_type) DO UPDATE SET
|
|
140
|
+
properties = json_patch(properties, ?),
|
|
141
|
+
importance = MAX(importance, ?),
|
|
142
|
+
last_accessed = datetime('now')`)
|
|
143
|
+
.run(name, entity_type, propsJson, tier, importance, propsJson, importance);
|
|
144
|
+
const entity = db.prepare('SELECT id FROM kg_entities WHERE name = ? AND entity_type = ?')
|
|
145
|
+
.get(name, entity_type);
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: 'text', text: `Entity ${entity.id}: "${name}" (${entity_type}, tier: ${tier})` }],
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
// ============================================================
|
|
151
|
+
// TOOL 5: add_link — Add KG edge
|
|
152
|
+
// ============================================================
|
|
153
|
+
server.tool('add_link', 'Add a directional link between two knowledge graph entities.', {
|
|
154
|
+
source_name: z.string(),
|
|
155
|
+
source_type: z.string(),
|
|
156
|
+
target_name: z.string(),
|
|
157
|
+
target_type: z.string(),
|
|
158
|
+
relation_type: z.string().describe('e.g., relates_to, causes, contradicts, supports, uses, part_of'),
|
|
159
|
+
weight: z.number().min(0).max(1).optional().default(1.0),
|
|
160
|
+
properties: z.record(z.any()).optional().default({}),
|
|
161
|
+
}, async ({ source_name, source_type, target_name, target_type, relation_type, weight, properties }) => {
|
|
162
|
+
const db = getDb();
|
|
163
|
+
const source = db.prepare('SELECT id FROM kg_entities WHERE name = ? AND entity_type = ?')
|
|
164
|
+
.get(source_name, source_type);
|
|
165
|
+
const target = db.prepare('SELECT id FROM kg_entities WHERE name = ? AND entity_type = ?')
|
|
166
|
+
.get(target_name, target_type);
|
|
167
|
+
if (!source)
|
|
168
|
+
return { content: [{ type: 'text', text: `Error: Source entity "${source_name}" (${source_type}) not found. Add it first.` }] };
|
|
169
|
+
if (!target)
|
|
170
|
+
return { content: [{ type: 'text', text: `Error: Target entity "${target_name}" (${target_type}) not found. Add it first.` }] };
|
|
171
|
+
db.prepare(`INSERT INTO kg_edges (source_id, target_id, relation_type, weight, properties)
|
|
172
|
+
VALUES (?, ?, ?, ?, ?)
|
|
173
|
+
ON CONFLICT(source_id, target_id, relation_type) DO UPDATE SET
|
|
174
|
+
weight = MAX(weight, ?),
|
|
175
|
+
activation_count = activation_count + 1,
|
|
176
|
+
last_activated = datetime('now')`)
|
|
177
|
+
.run(source.id, target.id, relation_type, weight, JSON.stringify(properties), weight);
|
|
178
|
+
return {
|
|
179
|
+
content: [{ type: 'text', text: `Link: "${source_name}" -[${relation_type}]-> "${target_name}" (weight: ${weight})` }],
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
// ============================================================
|
|
183
|
+
// TOOL 6: query_graph — Recursive CTE traversal (≤3 hops)
|
|
184
|
+
// ============================================================
|
|
185
|
+
server.tool('query_graph', 'Traverse the knowledge graph from a starting entity. Uses recursive CTE, bounded to 3 hops max. Follows edges in both directions by default.', {
|
|
186
|
+
start_name: z.string(),
|
|
187
|
+
start_type: z.string(),
|
|
188
|
+
max_hops: z.number().min(1).max(3).optional().default(2),
|
|
189
|
+
direction: z.enum(['outgoing', 'incoming', 'both']).optional().default('both').describe('Edge direction to follow'),
|
|
190
|
+
relation_filter: z.string().optional().describe('Filter edges by relation type'),
|
|
191
|
+
min_weight: z.number().min(0).max(1).optional().default(0.0),
|
|
192
|
+
}, async ({ start_name, start_type, max_hops, direction, relation_filter, min_weight }) => {
|
|
193
|
+
const db = getDb();
|
|
194
|
+
const start = db.prepare('SELECT id FROM kg_entities WHERE name = ? AND entity_type = ?')
|
|
195
|
+
.get(start_name, start_type);
|
|
196
|
+
if (!start)
|
|
197
|
+
return { content: [{ type: 'text', text: `Entity "${start_name}" (${start_type}) not found.` }] };
|
|
198
|
+
let relationClause = '';
|
|
199
|
+
if (relation_filter) {
|
|
200
|
+
relationClause = `AND e.relation_type = '${relation_filter.replace(/'/g, "''")}'`;
|
|
201
|
+
}
|
|
202
|
+
// Build directional join clauses
|
|
203
|
+
let edgeJoins;
|
|
204
|
+
if (direction === 'outgoing') {
|
|
205
|
+
edgeJoins = `
|
|
206
|
+
SELECT e.target_id, t.depth + 1,
|
|
207
|
+
t.path || ' -[' || e.relation_type || ']-> ' || tgt.name,
|
|
208
|
+
t.visited || ',' || CAST(e.target_id AS TEXT)
|
|
209
|
+
FROM traverse t
|
|
210
|
+
JOIN kg_edges e ON e.source_id = t.entity_id
|
|
211
|
+
JOIN kg_entities tgt ON tgt.id = e.target_id
|
|
212
|
+
WHERE t.depth < ${max_hops} AND e.weight >= ${min_weight}
|
|
213
|
+
AND t.visited NOT LIKE '%,' || CAST(e.target_id AS TEXT) || ',%'
|
|
214
|
+
${relationClause}`;
|
|
215
|
+
}
|
|
216
|
+
else if (direction === 'incoming') {
|
|
217
|
+
edgeJoins = `
|
|
218
|
+
SELECT e.source_id, t.depth + 1,
|
|
219
|
+
t.path || ' <-[' || e.relation_type || ']- ' || src.name,
|
|
220
|
+
t.visited || ',' || CAST(e.source_id AS TEXT)
|
|
221
|
+
FROM traverse t
|
|
222
|
+
JOIN kg_edges e ON e.target_id = t.entity_id
|
|
223
|
+
JOIN kg_entities src ON src.id = e.source_id
|
|
224
|
+
WHERE t.depth < ${max_hops} AND e.weight >= ${min_weight}
|
|
225
|
+
AND t.visited NOT LIKE '%,' || CAST(e.source_id AS TEXT) || ',%'
|
|
226
|
+
${relationClause}`;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// both directions
|
|
230
|
+
edgeJoins = `
|
|
231
|
+
SELECT e.target_id, t.depth + 1,
|
|
232
|
+
t.path || ' -[' || e.relation_type || ']-> ' || tgt.name,
|
|
233
|
+
t.visited || ',' || CAST(e.target_id AS TEXT)
|
|
234
|
+
FROM traverse t
|
|
235
|
+
JOIN kg_edges e ON e.source_id = t.entity_id
|
|
236
|
+
JOIN kg_entities tgt ON tgt.id = e.target_id
|
|
237
|
+
WHERE t.depth < ${max_hops} AND e.weight >= ${min_weight}
|
|
238
|
+
AND t.visited NOT LIKE '%,' || CAST(e.target_id AS TEXT) || ',%'
|
|
239
|
+
${relationClause}
|
|
240
|
+
UNION ALL
|
|
241
|
+
SELECT e.source_id, t.depth + 1,
|
|
242
|
+
t.path || ' <-[' || e.relation_type || ']- ' || src.name,
|
|
243
|
+
t.visited || ',' || CAST(e.source_id AS TEXT)
|
|
244
|
+
FROM traverse t
|
|
245
|
+
JOIN kg_edges e ON e.target_id = t.entity_id
|
|
246
|
+
JOIN kg_entities src ON src.id = e.source_id
|
|
247
|
+
WHERE t.depth < ${max_hops} AND e.weight >= ${min_weight}
|
|
248
|
+
AND t.visited NOT LIKE '%,' || CAST(e.source_id AS TEXT) || ',%'
|
|
249
|
+
${relationClause}`;
|
|
250
|
+
}
|
|
251
|
+
const sql = `
|
|
252
|
+
WITH RECURSIVE traverse(entity_id, depth, path, visited) AS (
|
|
253
|
+
SELECT ${start.id}, 0, '${start_name.replace(/'/g, "''")}', ',${start.id},'
|
|
254
|
+
UNION ALL
|
|
255
|
+
${edgeJoins}
|
|
256
|
+
)
|
|
257
|
+
SELECT DISTINCT
|
|
258
|
+
ent.id, ent.name, ent.entity_type, ent.tier,
|
|
259
|
+
ent.community_id, ent.betweenness, ent.importance,
|
|
260
|
+
t.depth, t.path
|
|
261
|
+
FROM traverse t
|
|
262
|
+
JOIN kg_entities ent ON ent.id = t.entity_id
|
|
263
|
+
WHERE t.depth > 0
|
|
264
|
+
ORDER BY t.depth, ent.importance DESC`;
|
|
265
|
+
const results = db.prepare(sql).all();
|
|
266
|
+
// Update activation counts for traversed edges
|
|
267
|
+
db.prepare(`UPDATE kg_edges SET activation_count = activation_count + 1, last_activated = datetime('now')
|
|
268
|
+
WHERE source_id = ? OR target_id = ?`).run(start.id, start.id);
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
// ============================================================
|
|
274
|
+
// TOOL 7: store_hypothesis — Add/update hypothesis
|
|
275
|
+
// ============================================================
|
|
276
|
+
server.tool('store_hypothesis', 'Store or update a research hypothesis in the cascade.', {
|
|
277
|
+
cascade_id: z.string(),
|
|
278
|
+
statement: z.string(),
|
|
279
|
+
parent_id: z.string().optional(),
|
|
280
|
+
affinity: z.number().min(0).max(1).optional().default(0.5),
|
|
281
|
+
status: z.enum(['proposed', 'testing', 'supported', 'refuted', 'uncertain', 'archived']).optional().default('proposed'),
|
|
282
|
+
supporting_ids: z.array(z.string()).optional().default([]),
|
|
283
|
+
contradicting_ids: z.array(z.string()).optional().default([]),
|
|
284
|
+
}, async ({ cascade_id, statement, parent_id, affinity, status, supporting_ids, contradicting_ids }) => {
|
|
285
|
+
const db = getDb();
|
|
286
|
+
const id = contentHash(statement);
|
|
287
|
+
const existing = db.prepare('SELECT id FROM hypotheses WHERE id = ?').get(id);
|
|
288
|
+
if (existing) {
|
|
289
|
+
// Merge new IDs into existing arrays (flat, no nesting)
|
|
290
|
+
const current = db.prepare('SELECT supporting, contradicting FROM hypotheses WHERE id = ?').get(id);
|
|
291
|
+
const existingSupporting = JSON.parse(current.supporting || '[]');
|
|
292
|
+
const existingContradicting = JSON.parse(current.contradicting || '[]');
|
|
293
|
+
const mergedSupporting = [...new Set([...existingSupporting, ...supporting_ids])];
|
|
294
|
+
const mergedContradicting = [...new Set([...existingContradicting, ...contradicting_ids])];
|
|
295
|
+
db.prepare(`UPDATE hypotheses SET
|
|
296
|
+
affinity = ?, status = ?,
|
|
297
|
+
supporting = ?,
|
|
298
|
+
contradicting = ?,
|
|
299
|
+
updated_at = datetime('now')
|
|
300
|
+
WHERE id = ?`)
|
|
301
|
+
.run(affinity, status, JSON.stringify(mergedSupporting), JSON.stringify(mergedContradicting), id);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const generation = parent_id
|
|
305
|
+
? (db.prepare('SELECT generation FROM hypotheses WHERE id = ?').get(parent_id)?.generation ?? 0) + 1
|
|
306
|
+
: 0;
|
|
307
|
+
db.prepare(`INSERT INTO hypotheses (id, cascade_id, statement, parent_id, affinity, generation, status, supporting, contradicting)
|
|
308
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
309
|
+
.run(id, cascade_id, statement, parent_id, affinity, generation, status, JSON.stringify(supporting_ids), JSON.stringify(contradicting_ids));
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
content: [{ type: 'text', text: `Hypothesis ${id}: "${statement.slice(0, 80)}..." (affinity: ${affinity}, status: ${status})` }],
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
// ============================================================
|
|
316
|
+
// TOOL 8: get_hypotheses — Query hypothesis population
|
|
317
|
+
// ============================================================
|
|
318
|
+
server.tool('get_hypotheses', 'Query hypotheses for a cascade, optionally filtered by status.', {
|
|
319
|
+
cascade_id: z.string(),
|
|
320
|
+
status: z.enum(['proposed', 'testing', 'supported', 'refuted', 'uncertain', 'archived']).optional(),
|
|
321
|
+
min_affinity: z.number().min(0).max(1).optional(),
|
|
322
|
+
}, async ({ cascade_id, status, min_affinity }) => {
|
|
323
|
+
const db = getDb();
|
|
324
|
+
let sql = 'SELECT * FROM hypotheses WHERE cascade_id = ?';
|
|
325
|
+
const params = [cascade_id];
|
|
326
|
+
if (status) {
|
|
327
|
+
sql += ' AND status = ?';
|
|
328
|
+
params.push(status);
|
|
329
|
+
}
|
|
330
|
+
if (min_affinity !== undefined) {
|
|
331
|
+
sql += ' AND affinity >= ?';
|
|
332
|
+
params.push(min_affinity);
|
|
333
|
+
}
|
|
334
|
+
sql += ' ORDER BY affinity DESC';
|
|
335
|
+
const results = db.prepare(sql).all(...params);
|
|
336
|
+
return {
|
|
337
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
// ============================================================
|
|
341
|
+
// TOOL 9: cascade_init — Initialize a new research cascade
|
|
342
|
+
// ============================================================
|
|
343
|
+
server.tool('cascade_init', 'Initialize a new research cascade with a question. Returns cascade ID.', {
|
|
344
|
+
question: z.string().describe('The research question to investigate'),
|
|
345
|
+
max_rounds: z.number().optional().default(5),
|
|
346
|
+
token_budget: z.number().optional().default(500000),
|
|
347
|
+
}, async ({ question, max_rounds, token_budget }) => {
|
|
348
|
+
const db = getDb();
|
|
349
|
+
const id = generateId();
|
|
350
|
+
db.prepare(`INSERT INTO cascades (id, question, status, max_rounds, token_budget)
|
|
351
|
+
VALUES (?, ?, 'planning', ?, ?)`)
|
|
352
|
+
.run(id, question, max_rounds, token_budget);
|
|
353
|
+
return {
|
|
354
|
+
content: [{ type: 'text', text: `Cascade initialized: ${id}\nQuestion: ${question}\nMax rounds: ${max_rounds}\nToken budget: ${token_budget}\n\nNext: store_plan to lock research criteria, then update_status to begin.` }],
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
// ============================================================
|
|
358
|
+
// TOOL 10: get_status — Cascade state + metrics
|
|
359
|
+
// ============================================================
|
|
360
|
+
server.tool('get_status', 'Get current cascade status including progress, findings count, hypothesis count, and PID state.', {
|
|
361
|
+
cascade_id: z.string().optional().describe('Specific cascade ID, or omit for all active cascades'),
|
|
362
|
+
}, async ({ cascade_id }) => {
|
|
363
|
+
const db = getDb();
|
|
364
|
+
if (cascade_id) {
|
|
365
|
+
const cascade = db.prepare('SELECT * FROM cascades WHERE id = ?').get(cascade_id);
|
|
366
|
+
if (!cascade)
|
|
367
|
+
return { content: [{ type: 'text', text: `Cascade ${cascade_id} not found.` }] };
|
|
368
|
+
const findingsCount = db.prepare('SELECT COUNT(*) as n FROM findings WHERE cascade_id = ?').get(cascade_id).n;
|
|
369
|
+
const quarantinedCount = db.prepare('SELECT COUNT(*) as n FROM findings WHERE cascade_id = ? AND quarantined = 1').get(cascade_id).n;
|
|
370
|
+
const hypothesesCount = db.prepare('SELECT COUNT(*) as n FROM hypotheses WHERE cascade_id = ?').get(cascade_id).n;
|
|
371
|
+
const threadsCount = db.prepare('SELECT COUNT(*) as n FROM threads WHERE cascade_id = ?').get(cascade_id).n;
|
|
372
|
+
const entityCount = db.prepare('SELECT COUNT(*) as n FROM kg_entities').get().n;
|
|
373
|
+
const edgeCount = db.prepare('SELECT COUNT(*) as n FROM kg_edges').get().n;
|
|
374
|
+
// Auto-apply pending steer events
|
|
375
|
+
const pendingSteers = db.prepare('SELECT * FROM steer_events WHERE cascade_id = ? AND applied = 0 ORDER BY created_at').all(cascade_id);
|
|
376
|
+
const appliedSteers = [];
|
|
377
|
+
for (const steer of pendingSteers) {
|
|
378
|
+
db.prepare('UPDATE steer_events SET applied = 1 WHERE id = ?').run(steer.id);
|
|
379
|
+
appliedSteers.push(`[${steer.event_type}] ${steer.instruction}`);
|
|
380
|
+
}
|
|
381
|
+
// --- Phase 7 activation: check interventions ---
|
|
382
|
+
const interventions = checkInterventions(cascade_id);
|
|
383
|
+
const blockingInterventions = interventions.filter(i => i.level === 'blocking');
|
|
384
|
+
const advisoryInterventions = interventions.filter(i => i.level === 'advisory');
|
|
385
|
+
// Note stats
|
|
386
|
+
const noteCount = db.prepare('SELECT COUNT(*) as n FROM atomic_notes WHERE cascade_id = ?').get(cascade_id)?.n || 0;
|
|
387
|
+
const status = {
|
|
388
|
+
...cascade,
|
|
389
|
+
pid_state: cascade.pid_state_json ? JSON.parse(cascade.pid_state_json) : null,
|
|
390
|
+
plan: cascade.plan_json ? JSON.parse(cascade.plan_json) : null,
|
|
391
|
+
counts: { findings: findingsCount, quarantined: quarantinedCount, hypotheses: hypothesesCount, threads: threadsCount, entities: entityCount, edges: edgeCount, notes: noteCount },
|
|
392
|
+
applied_steers: appliedSteers.length > 0 ? appliedSteers : undefined,
|
|
393
|
+
interventions: blockingInterventions.length > 0
|
|
394
|
+
? { blocking: blockingInterventions.map(i => i.description), advisory: advisoryInterventions.map(i => i.description) }
|
|
395
|
+
: advisoryInterventions.length > 0
|
|
396
|
+
? { advisory: advisoryInterventions.map(i => i.description) }
|
|
397
|
+
: undefined,
|
|
398
|
+
exploration_budget: Math.max(0, 1 - cascade.current_round / cascade.max_rounds),
|
|
399
|
+
};
|
|
400
|
+
return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] };
|
|
401
|
+
}
|
|
402
|
+
// All active cascades
|
|
403
|
+
const cascades = db.prepare("SELECT id, question, status, current_round, max_rounds, created_at FROM cascades WHERE status NOT IN ('complete') ORDER BY updated_at DESC").all();
|
|
404
|
+
return { content: [{ type: 'text', text: JSON.stringify(cascades, null, 2) }] };
|
|
405
|
+
});
|
|
406
|
+
// ============================================================
|
|
407
|
+
// TOOL 11: update_status — Advance phase/round
|
|
408
|
+
// ============================================================
|
|
409
|
+
server.tool('update_status', 'Update cascade status and/or advance to next round.', {
|
|
410
|
+
cascade_id: z.string(),
|
|
411
|
+
status: z.enum(['planning', 'investigating', 'validating', 'synthesizing', 'complete', 'stalled']).optional(),
|
|
412
|
+
advance_round: z.boolean().optional().default(false).describe('Increment current_round by 1'),
|
|
413
|
+
pid_state: z.object({
|
|
414
|
+
error: z.number(),
|
|
415
|
+
integral: z.number(),
|
|
416
|
+
derivative: z.number(),
|
|
417
|
+
output: z.number(),
|
|
418
|
+
kp: z.number().optional(),
|
|
419
|
+
ki: z.number().optional(),
|
|
420
|
+
kd: z.number().optional(),
|
|
421
|
+
}).optional(),
|
|
422
|
+
tokens_used: z.number().optional().describe('Add to running token count'),
|
|
423
|
+
}, async ({ cascade_id, status, advance_round, pid_state, tokens_used }) => {
|
|
424
|
+
const db = getDb();
|
|
425
|
+
const cascade = db.prepare('SELECT * FROM cascades WHERE id = ?').get(cascade_id);
|
|
426
|
+
if (!cascade)
|
|
427
|
+
return { content: [{ type: 'text', text: `Cascade ${cascade_id} not found.` }] };
|
|
428
|
+
const updates = ["updated_at = datetime('now')"];
|
|
429
|
+
const params = [];
|
|
430
|
+
if (status) {
|
|
431
|
+
updates.push('status = ?');
|
|
432
|
+
params.push(status);
|
|
433
|
+
}
|
|
434
|
+
if (advance_round) {
|
|
435
|
+
const newRound = cascade.current_round + 1;
|
|
436
|
+
updates.push('current_round = ?');
|
|
437
|
+
params.push(newRound);
|
|
438
|
+
updates.push('exploration_budget = ?');
|
|
439
|
+
params.push(Math.max(0, 1 - newRound / cascade.max_rounds));
|
|
440
|
+
}
|
|
441
|
+
if (pid_state) {
|
|
442
|
+
updates.push('pid_state_json = ?');
|
|
443
|
+
params.push(JSON.stringify(pid_state));
|
|
444
|
+
}
|
|
445
|
+
if (tokens_used) {
|
|
446
|
+
updates.push('tokens_used = tokens_used + ?');
|
|
447
|
+
params.push(tokens_used);
|
|
448
|
+
}
|
|
449
|
+
params.push(cascade_id);
|
|
450
|
+
db.prepare(`UPDATE cascades SET ${updates.join(', ')} WHERE id = ?`).run(...params);
|
|
451
|
+
// --- Phase 6 activation: run consolidation when advancing rounds ---
|
|
452
|
+
let consolidationReport;
|
|
453
|
+
if (advance_round) {
|
|
454
|
+
try {
|
|
455
|
+
const result = consolidateRound(cascade_id, cascade.current_round);
|
|
456
|
+
// Extract notes from this round's findings
|
|
457
|
+
const roundFindings = db.prepare('SELECT id FROM findings WHERE cascade_id = ? AND cascade_round = ? AND quarantined = 0')
|
|
458
|
+
.all(cascade_id, cascade.current_round);
|
|
459
|
+
let notesCreated = 0;
|
|
460
|
+
for (const f of roundFindings) {
|
|
461
|
+
const noteIds = extractNotesFromFinding(f.id, cascade_id, cascade.current_round);
|
|
462
|
+
notesCreated += noteIds.length;
|
|
463
|
+
}
|
|
464
|
+
// Update note maturity
|
|
465
|
+
const maturityResult = updateMaturity();
|
|
466
|
+
consolidationReport = `Consolidation: dedup ${result.deduped.removed} removed, ${result.tierChanges.promoted} promoted, ${result.tierChanges.demoted} demoted, ${result.pruned.archived} pruned, ${result.scheduled} scheduled for SM-2. ${notesCreated} notes created, ${maturityResult.promoted} matured. (${result.durationMs}ms)`;
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
consolidationReport = `Consolidation error: ${err.message}`;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const updated = db.prepare('SELECT id, status, current_round, max_rounds, tokens_used, token_budget FROM cascades WHERE id = ?').get(cascade_id);
|
|
473
|
+
const response = { ...updated };
|
|
474
|
+
if (consolidationReport)
|
|
475
|
+
response.consolidation = consolidationReport;
|
|
476
|
+
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
|
477
|
+
});
|
|
478
|
+
// ============================================================
|
|
479
|
+
// TOOL 12: get_metrics — Information-theoretic dashboard
|
|
480
|
+
// ============================================================
|
|
481
|
+
server.tool('get_metrics', 'Get cascade quality metrics: coverage, depth, confidence distribution, source diversity.', {
|
|
482
|
+
cascade_id: z.string(),
|
|
483
|
+
}, async ({ cascade_id }) => {
|
|
484
|
+
const db = getDb();
|
|
485
|
+
const cascade = db.prepare('SELECT * FROM cascades WHERE id = ?').get(cascade_id);
|
|
486
|
+
if (!cascade)
|
|
487
|
+
return { content: [{ type: 'text', text: `Cascade ${cascade_id} not found.` }] };
|
|
488
|
+
// Aggregate metrics
|
|
489
|
+
const findings = db.prepare('SELECT confidence, trust_composite, source_url, grade_level, cascade_round FROM findings WHERE cascade_id = ? AND quarantined = 0').all(cascade_id);
|
|
490
|
+
const avgConfidence = findings.length ? findings.reduce((s, f) => s + f.confidence, 0) / findings.length : 0;
|
|
491
|
+
const avgTrust = findings.length ? findings.reduce((s, f) => s + (f.trust_composite || 0.5), 0) / findings.length : 0;
|
|
492
|
+
// Source diversity
|
|
493
|
+
const domains = new Set(findings.map((f) => {
|
|
494
|
+
try {
|
|
495
|
+
return new URL(f.source_url || '').hostname;
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
return 'unknown';
|
|
499
|
+
}
|
|
500
|
+
}));
|
|
501
|
+
// Confidence distribution
|
|
502
|
+
const confBuckets = { high: 0, medium: 0, low: 0 };
|
|
503
|
+
for (const f of findings) {
|
|
504
|
+
if (f.confidence >= 0.7)
|
|
505
|
+
confBuckets.high++;
|
|
506
|
+
else if (f.confidence >= 0.4)
|
|
507
|
+
confBuckets.medium++;
|
|
508
|
+
else
|
|
509
|
+
confBuckets.low++;
|
|
510
|
+
}
|
|
511
|
+
// Grade distribution
|
|
512
|
+
const gradeDist = {};
|
|
513
|
+
for (const f of findings) {
|
|
514
|
+
const g = f.grade_level || 'ungraded';
|
|
515
|
+
gradeDist[g] = (gradeDist[g] || 0) + 1;
|
|
516
|
+
}
|
|
517
|
+
// Hypotheses status
|
|
518
|
+
const hypStats = db.prepare(`SELECT status, COUNT(*) as n FROM hypotheses WHERE cascade_id = ? GROUP BY status`).all(cascade_id);
|
|
519
|
+
// Graph stats
|
|
520
|
+
const entityCount = db.prepare('SELECT COUNT(*) as n FROM kg_entities').get().n;
|
|
521
|
+
const edgeCount = db.prepare('SELECT COUNT(*) as n FROM kg_edges').get().n;
|
|
522
|
+
const avgDegree = entityCount > 0 ? (2 * edgeCount) / entityCount : 0;
|
|
523
|
+
// Recent metrics from metrics table
|
|
524
|
+
const recentMetrics = db.prepare(`SELECT metric_name, metric_value, recorded_at FROM metrics
|
|
525
|
+
WHERE cascade_id = ? ORDER BY recorded_at DESC LIMIT 20`).all(cascade_id);
|
|
526
|
+
// Note stats
|
|
527
|
+
const noteStats = getNoteStats();
|
|
528
|
+
// Consolidation history
|
|
529
|
+
const lastConsolidation = db.prepare(`SELECT * FROM consolidation_log
|
|
530
|
+
WHERE cascade_id = ? ORDER BY created_at DESC LIMIT 1`)
|
|
531
|
+
.get(cascade_id);
|
|
532
|
+
// SM-2 review stats
|
|
533
|
+
const sm2Due = db.prepare("SELECT COUNT(*) as n FROM sm2_schedule WHERE next_review <= datetime('now')").get().n;
|
|
534
|
+
const dashboard = {
|
|
535
|
+
cascade: { id: cascade_id, status: cascade.status, round: cascade.current_round, maxRounds: cascade.max_rounds },
|
|
536
|
+
quality: {
|
|
537
|
+
totalFindings: findings.length,
|
|
538
|
+
avgConfidence: Math.round(avgConfidence * 1000) / 1000,
|
|
539
|
+
avgTrust: Math.round(avgTrust * 1000) / 1000,
|
|
540
|
+
confidenceDistribution: confBuckets,
|
|
541
|
+
gradeDistribution: gradeDist,
|
|
542
|
+
sourceDiversity: domains.size,
|
|
543
|
+
},
|
|
544
|
+
hypotheses: hypStats,
|
|
545
|
+
graph: { entities: entityCount, edges: edgeCount, avgDegree: Math.round(avgDegree * 100) / 100 },
|
|
546
|
+
notes: noteStats,
|
|
547
|
+
memory: {
|
|
548
|
+
lastConsolidation: lastConsolidation ? {
|
|
549
|
+
promoted: lastConsolidation.items_promoted,
|
|
550
|
+
demoted: lastConsolidation.items_demoted,
|
|
551
|
+
pruned: lastConsolidation.items_pruned,
|
|
552
|
+
durationMs: lastConsolidation.duration_ms,
|
|
553
|
+
} : null,
|
|
554
|
+
sm2ItemsDue: sm2Due,
|
|
555
|
+
},
|
|
556
|
+
tokens: { used: cascade.tokens_used, budget: cascade.token_budget, remaining: cascade.token_budget - cascade.tokens_used },
|
|
557
|
+
explorationBudget: Math.max(0, 1 - cascade.current_round / cascade.max_rounds),
|
|
558
|
+
recentMetrics,
|
|
559
|
+
};
|
|
560
|
+
// --- Phase 7 activation: render ANSI dashboard to stderr ---
|
|
561
|
+
const dashData = buildDashboardData(cascade_id);
|
|
562
|
+
if (dashData) {
|
|
563
|
+
console.error(renderDashboard(dashData));
|
|
564
|
+
}
|
|
565
|
+
return { content: [{ type: 'text', text: JSON.stringify(dashboard, null, 2) }] };
|
|
566
|
+
});
|
|
567
|
+
// ============================================================
|
|
568
|
+
// TOOL 13: store_checkpoint — Step-level checkpointing
|
|
569
|
+
// ============================================================
|
|
570
|
+
server.tool('store_checkpoint', 'Save a checkpoint for crash recovery. Each step is checkpointed independently.', {
|
|
571
|
+
task_id: z.string(),
|
|
572
|
+
round_index: z.number(),
|
|
573
|
+
step_index: z.number(),
|
|
574
|
+
step_name: z.string(),
|
|
575
|
+
status: z.enum(['pending', 'running', 'done', 'failed', 'skipped']),
|
|
576
|
+
state_snapshot: z.string().optional().describe('JSON state to restore from'),
|
|
577
|
+
error_message: z.string().optional(),
|
|
578
|
+
}, async ({ task_id, round_index, step_index, step_name, status, state_snapshot, error_message }) => {
|
|
579
|
+
const db = getDb();
|
|
580
|
+
const idempotencyKey = `${task_id}:${round_index}:${step_index}:${step_name}`;
|
|
581
|
+
db.prepare(`INSERT INTO cascade_checkpoints (task_id, round_index, step_index, step_name, status, state_snapshot, idempotency_key, error_message)
|
|
582
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
583
|
+
ON CONFLICT(task_id, round_index, step_index) DO UPDATE SET
|
|
584
|
+
status = excluded.status,
|
|
585
|
+
state_snapshot = COALESCE(excluded.state_snapshot, state_snapshot),
|
|
586
|
+
error_message = excluded.error_message,
|
|
587
|
+
completed_at = CASE WHEN excluded.status IN ('done','failed','skipped') THEN datetime('now') ELSE NULL END`)
|
|
588
|
+
.run(task_id, round_index, step_index, step_name, status, state_snapshot, idempotencyKey, error_message);
|
|
589
|
+
return {
|
|
590
|
+
content: [{ type: 'text', text: `Checkpoint: ${task_id} R${round_index}S${step_index} "${step_name}" → ${status}` }],
|
|
591
|
+
};
|
|
592
|
+
});
|
|
593
|
+
// ============================================================
|
|
594
|
+
// TOOL 14: steer — Submit human steering event
|
|
595
|
+
// ============================================================
|
|
596
|
+
server.tool('steer', 'Submit a steering event to redirect an active cascade.', {
|
|
597
|
+
cascade_id: z.string(),
|
|
598
|
+
event_type: z.enum(['redirect', 'narrow', 'broaden', 'add_question', 'drop_hypothesis', 'approve', 'reject']),
|
|
599
|
+
instruction: z.string(),
|
|
600
|
+
target_id: z.string().optional().describe('ID of hypothesis or finding to target'),
|
|
601
|
+
}, async ({ cascade_id, event_type, instruction, target_id }) => {
|
|
602
|
+
const db = getDb();
|
|
603
|
+
db.prepare('INSERT INTO steer_events (cascade_id, event_type, instruction, target_id) VALUES (?, ?, ?, ?)')
|
|
604
|
+
.run(cascade_id, event_type, instruction, target_id);
|
|
605
|
+
return {
|
|
606
|
+
content: [{ type: 'text', text: `Steer event queued: ${event_type} — "${instruction}". Will be applied on next cascade iteration.` }],
|
|
607
|
+
};
|
|
608
|
+
});
|
|
609
|
+
// ============================================================
|
|
610
|
+
// TOOL 15: record_metric — Store a metric value
|
|
611
|
+
// ============================================================
|
|
612
|
+
server.tool('record_metric', 'Record a metric value for tracking cascade health over time.', {
|
|
613
|
+
cascade_id: z.string(),
|
|
614
|
+
round_index: z.number().optional(),
|
|
615
|
+
metric_name: z.string().describe('e.g., entropy, coverage, confidence_avg, pid_error, ncd_dedup_ratio'),
|
|
616
|
+
metric_value: z.number(),
|
|
617
|
+
}, async ({ cascade_id, round_index, metric_name, metric_value }) => {
|
|
618
|
+
const db = getDb();
|
|
619
|
+
db.prepare('INSERT INTO metrics (cascade_id, round_index, metric_name, metric_value) VALUES (?, ?, ?, ?)')
|
|
620
|
+
.run(cascade_id, round_index, metric_name, metric_value);
|
|
621
|
+
return {
|
|
622
|
+
content: [{ type: 'text', text: `Metric recorded: ${metric_name} = ${metric_value}` }],
|
|
623
|
+
};
|
|
624
|
+
});
|
|
625
|
+
// ============================================================
|
|
626
|
+
// TOOL 16: create_note — A-MEM atomic note creation
|
|
627
|
+
// ============================================================
|
|
628
|
+
server.tool('create_note', 'Create an atomic Zettelkasten note from an insight. Auto-links to related notes by keyword overlap. Content-addressable (idempotent).', {
|
|
629
|
+
content: z.string().describe('The atomic insight or observation'),
|
|
630
|
+
note_type: z.enum(['insight', 'connection', 'question', 'contradiction', 'synthesis']).optional().default('insight'),
|
|
631
|
+
keywords: z.array(z.string()).optional().default([]),
|
|
632
|
+
source_finding_id: z.string().optional(),
|
|
633
|
+
cascade_id: z.string().optional(),
|
|
634
|
+
cascade_round: z.number().optional(),
|
|
635
|
+
}, async ({ content, note_type, keywords, source_finding_id, cascade_id, cascade_round }) => {
|
|
636
|
+
const noteId = createNote(content, note_type, keywords, source_finding_id, undefined, cascade_id, cascade_round);
|
|
637
|
+
// Auto-link to related notes
|
|
638
|
+
if (cascade_id) {
|
|
639
|
+
const db = getDb();
|
|
640
|
+
const existing = db.prepare(`SELECT id, content FROM atomic_notes WHERE id != ? AND cascade_id = ? LIMIT 50`)
|
|
641
|
+
.all(noteId, cascade_id);
|
|
642
|
+
let linksCreated = 0;
|
|
643
|
+
const contentWords = new Set(content.toLowerCase().split(/\s+/).filter(w => w.length > 5));
|
|
644
|
+
for (const other of existing) {
|
|
645
|
+
const otherWords = new Set(other.content.toLowerCase().split(/\s+/).filter((w) => w.length > 5));
|
|
646
|
+
let overlap = 0;
|
|
647
|
+
for (const w of contentWords) {
|
|
648
|
+
if (otherWords.has(w))
|
|
649
|
+
overlap++;
|
|
650
|
+
}
|
|
651
|
+
if (overlap >= 2) {
|
|
652
|
+
linkNotes(noteId, other.id, 'relates_to', Math.min(1.0, overlap * 0.2));
|
|
653
|
+
linksCreated++;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
content: [{ type: 'text', text: `Note ${noteId}: "${content.slice(0, 60)}..." (${note_type}, ${linksCreated} auto-links)` }],
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
content: [{ type: 'text', text: `Note ${noteId}: "${content.slice(0, 60)}..." (${note_type})` }],
|
|
662
|
+
};
|
|
663
|
+
});
|
|
664
|
+
// ============================================================
|
|
665
|
+
// TOOL 17: search_notes — Search Zettelkasten notes
|
|
666
|
+
// ============================================================
|
|
667
|
+
server.tool('search_notes', 'Search atomic notes by keyword. Returns notes sorted by access frequency.', {
|
|
668
|
+
keyword: z.string(),
|
|
669
|
+
limit: z.number().optional().default(10),
|
|
670
|
+
}, async ({ keyword, limit }) => {
|
|
671
|
+
const notes = searchNotes(keyword, limit);
|
|
672
|
+
return { content: [{ type: 'text', text: JSON.stringify(notes, null, 2) }] };
|
|
673
|
+
});
|
|
674
|
+
// ============================================================
|
|
675
|
+
// Server startup
|
|
676
|
+
// ============================================================
|
|
677
|
+
export async function startServer() {
|
|
678
|
+
// Initialize DB on startup to catch schema errors early
|
|
679
|
+
try {
|
|
680
|
+
getDb();
|
|
681
|
+
console.error('[cascade-engine] Database initialized');
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
console.error('[cascade-engine] DB initialization failed:', err);
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
const transport = new StdioServerTransport();
|
|
688
|
+
await server.connect(transport);
|
|
689
|
+
console.error('[cascade-engine] MCP server running on stdio');
|
|
690
|
+
}
|
|
691
|
+
// Direct execution (node dist/index.js)
|
|
692
|
+
const isDirectRun = process.argv[1]?.endsWith('index.js');
|
|
693
|
+
if (isDirectRun) {
|
|
694
|
+
startServer().catch((err) => {
|
|
695
|
+
console.error('[cascade-engine] Fatal:', err);
|
|
696
|
+
closeDb();
|
|
697
|
+
process.exit(1);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
export default startServer;
|
|
701
|
+
//# sourceMappingURL=index.js.map
|