@doquflow/server 0.1.1 → 0.3.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/dist/extractor.js +9 -0
- package/dist/index.js +204 -0
- package/dist/tools/answer-synthesis.js +189 -0
- package/dist/tools/get-schema-guidance.js +213 -0
- package/dist/tools/ingest-source.js +222 -0
- package/dist/tools/lint-wiki.js +346 -0
- package/dist/tools/list-wiki.js +108 -0
- package/dist/tools/preview-generation.js +212 -0
- package/dist/tools/query-wiki.js +67 -0
- package/dist/tools/save-answer-as-page.js +96 -0
- package/dist/tools/update-index.js +151 -0
- package/dist/tools/wiki-search.js +151 -0
- package/package.json +33 -9
package/dist/extractor.js
CHANGED
|
@@ -37,6 +37,12 @@ const RE_MIN_API = /\bapp\.Map(?:Get|Post|Put|Delete|Patch)\s*\(\s*["']([^"']+)[
|
|
|
37
37
|
const RE_EXPRESS = /\b(?:router|app)\.(?:get|post|put|delete|patch|use)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
38
38
|
const RE_NEST = /@(?:Get|Post|Put|Delete|Patch)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
39
39
|
const RE_NG_PATH = /\bpath\s*:\s*['"]([^'"]+)['"]/g;
|
|
40
|
+
// Flask: @app.route('/path') @bp.route('/path')
|
|
41
|
+
const RE_FLASK = /@\w+\.route\s*\(\s*['"]([^'"]+)['"]/g;
|
|
42
|
+
// FastAPI / Flask-style: @app.get('/x') @router.post('/x')
|
|
43
|
+
const RE_FASTAPI = /@(?:app|router|bp)\s*\.\s*(?:get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
44
|
+
// Django: path('url/', view) url('pattern/', view)
|
|
45
|
+
const RE_DJANGO = /\bpath\s*\(\s*['"]([^'"]+)['"]/g;
|
|
40
46
|
const RE_CFG_CONNSTR = /\bConnectionStrings:([\w.]+)/g;
|
|
41
47
|
const RE_PROCESS_ENV = /\bprocess\.env\.([A-Z_][A-Z0-9_]*)/g;
|
|
42
48
|
const RE_DOTNET_ENV = /\bEnvironment\.GetEnvironmentVariable\(\s*["']([^"']+)["']/g;
|
|
@@ -97,6 +103,9 @@ function extract(content) {
|
|
|
97
103
|
...collect(new RegExp(RE_EXPRESS.source, "g"), content),
|
|
98
104
|
...collect(new RegExp(RE_NEST.source, "g"), content),
|
|
99
105
|
...collect(new RegExp(RE_NG_PATH.source, "g"), content),
|
|
106
|
+
...collect(new RegExp(RE_FLASK.source, "g"), content),
|
|
107
|
+
...collect(new RegExp(RE_FASTAPI.source, "g"), content),
|
|
108
|
+
...collect(new RegExp(RE_DJANGO.source, "g"), content),
|
|
100
109
|
]);
|
|
101
110
|
const config_refs = [];
|
|
102
111
|
config_refs.push(...collect(new RegExp(RE_CFG_CONNSTR.source, "g"), content).map((s) => `ConnectionStrings:${s}`));
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,16 @@ const read_module_1 = require("./tools/read-module");
|
|
|
8
8
|
const list_modules_1 = require("./tools/list-modules");
|
|
9
9
|
const write_spec_1 = require("./tools/write-spec");
|
|
10
10
|
const read_specs_1 = require("./tools/read-specs");
|
|
11
|
+
const ingest_source_1 = require("./tools/ingest-source");
|
|
12
|
+
const update_index_1 = require("./tools/update-index");
|
|
13
|
+
const list_wiki_1 = require("./tools/list-wiki");
|
|
14
|
+
const wiki_search_1 = require("./tools/wiki-search");
|
|
15
|
+
const answer_synthesis_1 = require("./tools/answer-synthesis");
|
|
16
|
+
const query_wiki_1 = require("./tools/query-wiki");
|
|
17
|
+
const save_answer_as_page_1 = require("./tools/save-answer-as-page");
|
|
18
|
+
const lint_wiki_1 = require("./tools/lint-wiki");
|
|
19
|
+
const get_schema_guidance_1 = require("./tools/get-schema-guidance");
|
|
20
|
+
const preview_generation_1 = require("./tools/preview-generation");
|
|
11
21
|
const server = new index_js_1.Server({ name: "docuflow", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
12
22
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
13
23
|
tools: [
|
|
@@ -66,6 +76,170 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
|
66
76
|
required: ["project_path"],
|
|
67
77
|
},
|
|
68
78
|
},
|
|
79
|
+
{
|
|
80
|
+
name: "ingest_source",
|
|
81
|
+
description: "Ingest a markdown source document from .docuflow/sources/ and generate wiki pages (entities, concepts) with cross-references. Returns pages created and entities discovered.",
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
86
|
+
source_filename: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: "Filename in .docuflow/sources/ to ingest (e.g., 'overview.md').",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
required: ["project_path", "source_filename"],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "update_index",
|
|
96
|
+
description: "Scan all wiki pages in .docuflow/wiki/ and regenerate .docuflow/index.md organized by category. Appends operation to log.md.",
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: "object",
|
|
99
|
+
properties: {
|
|
100
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
101
|
+
},
|
|
102
|
+
required: ["project_path"],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "list_wiki",
|
|
107
|
+
description: "List all wiki pages in .docuflow/wiki/, optionally filtered by category. Returns metadata (title, created_at, sources, tags) and page counts by category.",
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
112
|
+
category: {
|
|
113
|
+
type: "string",
|
|
114
|
+
enum: ["entity", "concept", "timeline", "synthesis"],
|
|
115
|
+
description: "Optional: filter to a specific category.",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
required: ["project_path"],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "wiki_search",
|
|
123
|
+
description: "Search the wiki for pages matching a query using relevance scoring. Returns ranked results with preview snippets and matched terms. BM25-inspired ranking weights entity pages higher.",
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: {
|
|
127
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
128
|
+
query: { type: "string", description: "Search query (e.g., 'MCP protocol design')." },
|
|
129
|
+
limit: {
|
|
130
|
+
type: "number",
|
|
131
|
+
description: "Optional: max results to return (default: 10).",
|
|
132
|
+
},
|
|
133
|
+
category: {
|
|
134
|
+
type: "string",
|
|
135
|
+
enum: ["entity", "concept", "timeline", "synthesis"],
|
|
136
|
+
description: "Optional: filter to a specific category.",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
required: ["project_path", "query"],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "synthesize_answer",
|
|
144
|
+
description: "Generate a synthesis answer from multiple wiki pages. Extracts relevant sentences, key concepts, and builds a markdown answer with citations.",
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
149
|
+
query: { type: "string", description: "The question being answered." },
|
|
150
|
+
source_page_ids: {
|
|
151
|
+
type: "array",
|
|
152
|
+
items: { type: "string" },
|
|
153
|
+
description: "List of wiki page IDs to synthesize from.",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
required: ["project_path", "query", "source_page_ids"],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "query_wiki",
|
|
161
|
+
description: "Ask a question against the wiki. Automatically searches for relevant pages, synthesizes an answer, and returns source pages with confidence score. One-stop tool for querying accumulated knowledge.",
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
166
|
+
question: { type: "string", description: "The question to ask (e.g., 'How does the MCP protocol work?')." },
|
|
167
|
+
max_sources: {
|
|
168
|
+
type: "number",
|
|
169
|
+
description: "Optional: max source pages to use in synthesis (default: 5).",
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
required: ["project_path", "question"],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: "save_answer_as_page",
|
|
177
|
+
description: "Save a generated answer as a new wiki page. Allows query results to compound back into the knowledge base, growing the wiki with new synthesis pages. Automatically adds frontmatter and updates log.md.",
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: "object",
|
|
180
|
+
properties: {
|
|
181
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
182
|
+
question: { type: "string", description: "The original question that was answered." },
|
|
183
|
+
answer: { type: "string", description: "The markdown answer text to save." },
|
|
184
|
+
page_title: { type: "string", description: "Title for the new page (e.g., 'How MCP Protocol Works')." },
|
|
185
|
+
category: {
|
|
186
|
+
type: "string",
|
|
187
|
+
enum: ["synthesis", "entity", "concept", "timeline"],
|
|
188
|
+
description: "Optional: wiki category for the page (default: synthesis).",
|
|
189
|
+
},
|
|
190
|
+
source_page_ids: {
|
|
191
|
+
type: "array",
|
|
192
|
+
items: { type: "string" },
|
|
193
|
+
description: "Optional: list of source wiki page IDs used to generate this answer.",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
required: ["project_path", "question", "answer", "page_title"],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "lint_wiki",
|
|
201
|
+
description: "Health check wiki for quality issues: orphan pages, broken references, stale content, metadata gaps, and contradictions. Returns issues found, metrics, health score, and recommendations.",
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: "object",
|
|
204
|
+
properties: {
|
|
205
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
206
|
+
check_type: {
|
|
207
|
+
type: "string",
|
|
208
|
+
enum: ["all", "orphans", "contradictions", "stale", "metadata"],
|
|
209
|
+
description: "Optional: type of check to run (default: all).",
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
required: ["project_path"],
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "get_schema_guidance",
|
|
217
|
+
description: "Analyze what documents should exist based on project schema and current wiki. Removes decision fatigue by suggesting what to create next.",
|
|
218
|
+
inputSchema: {
|
|
219
|
+
type: "object",
|
|
220
|
+
properties: {
|
|
221
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
222
|
+
domain: {
|
|
223
|
+
type: "string",
|
|
224
|
+
description: "Optional: domain hint (Code/Architecture, Research, Business, Personal). Auto-detected if not provided.",
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
required: ["project_path"],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "preview_generation",
|
|
232
|
+
description: "Preview what a tool will generate before running it. Removes black-box feeling by showing predicted actions, outputs, and impact.",
|
|
233
|
+
inputSchema: {
|
|
234
|
+
type: "object",
|
|
235
|
+
properties: {
|
|
236
|
+
tool_name: { type: "string", description: "Name of the tool to preview (e.g., ingest_source, query_wiki)." },
|
|
237
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
238
|
+
params: { type: "object", description: "Parameters you would pass to that tool." },
|
|
239
|
+
},
|
|
240
|
+
required: ["tool_name", "project_path", "params"],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
69
243
|
],
|
|
70
244
|
}));
|
|
71
245
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
@@ -84,6 +258,36 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
84
258
|
else if (name === "read_specs") {
|
|
85
259
|
result = await (0, read_specs_1.readSpecs)(args);
|
|
86
260
|
}
|
|
261
|
+
else if (name === "ingest_source") {
|
|
262
|
+
result = await (0, ingest_source_1.ingestSource)(args);
|
|
263
|
+
}
|
|
264
|
+
else if (name === "update_index") {
|
|
265
|
+
result = await (0, update_index_1.updateIndex)(args);
|
|
266
|
+
}
|
|
267
|
+
else if (name === "list_wiki") {
|
|
268
|
+
result = await (0, list_wiki_1.listWiki)(args);
|
|
269
|
+
}
|
|
270
|
+
else if (name === "wiki_search") {
|
|
271
|
+
result = await (0, wiki_search_1.wikiSearch)(args);
|
|
272
|
+
}
|
|
273
|
+
else if (name === "synthesize_answer") {
|
|
274
|
+
result = await (0, answer_synthesis_1.synthesizeAnswer)(args);
|
|
275
|
+
}
|
|
276
|
+
else if (name === "query_wiki") {
|
|
277
|
+
result = await (0, query_wiki_1.queryWiki)(args);
|
|
278
|
+
}
|
|
279
|
+
else if (name === "save_answer_as_page") {
|
|
280
|
+
result = await (0, save_answer_as_page_1.saveAnswerAsPage)(args);
|
|
281
|
+
}
|
|
282
|
+
else if (name === "lint_wiki") {
|
|
283
|
+
result = await (0, lint_wiki_1.lintWiki)(args);
|
|
284
|
+
}
|
|
285
|
+
else if (name === "get_schema_guidance") {
|
|
286
|
+
result = await (0, get_schema_guidance_1.getSchemataGuidance)(args);
|
|
287
|
+
}
|
|
288
|
+
else if (name === "preview_generation") {
|
|
289
|
+
result = await (0, preview_generation_1.previewGeneration)(args);
|
|
290
|
+
}
|
|
87
291
|
else {
|
|
88
292
|
result = { error: `Unknown tool: ${name}` };
|
|
89
293
|
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.synthesizeAnswer = synthesizeAnswer;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const filesystem_1 = require("../filesystem");
|
|
9
|
+
/**
|
|
10
|
+
* Extract key sentences from content that relate to the query
|
|
11
|
+
*/
|
|
12
|
+
function extractRelevantSentences(content, queryTerms, maxSentences = 3) {
|
|
13
|
+
const sentences = content
|
|
14
|
+
.split(/[.!?]+/)
|
|
15
|
+
.map((s) => s.trim())
|
|
16
|
+
.filter((s) => s.length > 10 && !s.startsWith("#") && !s.startsWith("---"));
|
|
17
|
+
const scoredSentences = [];
|
|
18
|
+
for (const sentence of sentences) {
|
|
19
|
+
let score = 0;
|
|
20
|
+
const sentenceLower = sentence.toLowerCase();
|
|
21
|
+
for (const term of queryTerms) {
|
|
22
|
+
if (sentenceLower.includes(term)) {
|
|
23
|
+
// Count occurrences
|
|
24
|
+
const matches = sentenceLower.split(term).length - 1;
|
|
25
|
+
score += matches * 2;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (score > 0) {
|
|
29
|
+
scoredSentences.push({ text: sentence, score });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Sort by score and take top N
|
|
33
|
+
return scoredSentences
|
|
34
|
+
.sort((a, b) => b.score - a.score)
|
|
35
|
+
.slice(0, maxSentences)
|
|
36
|
+
.map((s) => s.text);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Extract key concepts from page content (from H3 headers, bold text)
|
|
40
|
+
*/
|
|
41
|
+
function extractKeyConcepts(content) {
|
|
42
|
+
const concepts = new Set();
|
|
43
|
+
// Extract from H3 headers
|
|
44
|
+
const h3Matches = content.matchAll(/^###\s+(.+?)$/gm);
|
|
45
|
+
for (const match of h3Matches) {
|
|
46
|
+
const concept = match[1].trim();
|
|
47
|
+
if (concept && concept.length < 80) {
|
|
48
|
+
concepts.add(concept);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Extract from bold text (but not arrays/JSON)
|
|
52
|
+
const boldMatches = content.matchAll(/\*\*([^*]+)\*\*/g);
|
|
53
|
+
for (const match of boldMatches) {
|
|
54
|
+
const text = match[1].trim();
|
|
55
|
+
if (text.length > 3 &&
|
|
56
|
+
text.length < 60 &&
|
|
57
|
+
!text.includes("[") &&
|
|
58
|
+
!text.includes("{") &&
|
|
59
|
+
!text.includes('"')) {
|
|
60
|
+
concepts.add(text);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return Array.from(concepts).slice(0, 5);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build a synthesis answer from multiple source pages
|
|
67
|
+
*/
|
|
68
|
+
function buildSynthesis(query, sourcePages, queryTerms) {
|
|
69
|
+
if (!sourcePages.length) {
|
|
70
|
+
return {
|
|
71
|
+
answer: `No information found related to: ${query}`,
|
|
72
|
+
concepts: [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const allConcepts = new Set();
|
|
76
|
+
const sections = [];
|
|
77
|
+
// Add introduction
|
|
78
|
+
sections.push(`## Synthesis: ${query}\n`);
|
|
79
|
+
sections.push(`Based on ${sourcePages.length} source page(s):\n`);
|
|
80
|
+
// Add content from each source
|
|
81
|
+
for (const page of sourcePages) {
|
|
82
|
+
sections.push(`### ${page.title}`);
|
|
83
|
+
sections.push(`*Category: ${page.category} | Relevance: ${Math.round(page.relevance * 100)}%*\n`);
|
|
84
|
+
// Extract relevant sentences
|
|
85
|
+
const relevantSentences = extractRelevantSentences(page.content, queryTerms, 2);
|
|
86
|
+
if (relevantSentences.length > 0) {
|
|
87
|
+
sections.push(relevantSentences.map((s) => `- ${s.trim()}`).join("\n"));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Fallback: use first paragraph
|
|
91
|
+
const firstPara = page.content
|
|
92
|
+
.split("\n")
|
|
93
|
+
.filter((l) => l.trim() && !l.startsWith("#") && !l.startsWith("---"))
|
|
94
|
+
.slice(0, 1);
|
|
95
|
+
sections.push(firstPara.join("\n"));
|
|
96
|
+
}
|
|
97
|
+
// Collect concepts
|
|
98
|
+
const pageConcepts = extractKeyConcepts(page.content);
|
|
99
|
+
pageConcepts.forEach((c) => allConcepts.add(c));
|
|
100
|
+
sections.push("");
|
|
101
|
+
}
|
|
102
|
+
// Add summary
|
|
103
|
+
sections.push("\n## Key Concepts Found");
|
|
104
|
+
Array.from(allConcepts).forEach((c) => {
|
|
105
|
+
sections.push(`- ${c}`);
|
|
106
|
+
});
|
|
107
|
+
sections.push("\n## How to Extend This\n");
|
|
108
|
+
sections.push(`This synthesis was generated from ${sourcePages.length} page(s) containing the key terms: `);
|
|
109
|
+
sections.push(queryTerms.join(", "));
|
|
110
|
+
sections.push("\n\nAdd more source pages to deepen this synthesis.");
|
|
111
|
+
return {
|
|
112
|
+
answer: sections.join("\n"),
|
|
113
|
+
concepts: Array.from(allConcepts),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function synthesizeAnswer(input) {
|
|
117
|
+
try {
|
|
118
|
+
const projectPath = node_path_1.default.resolve(input.project_path);
|
|
119
|
+
const docuDir = node_path_1.default.join(projectPath, ".docuflow");
|
|
120
|
+
const wikiDir = node_path_1.default.join(docuDir, "wiki");
|
|
121
|
+
// Load each source page
|
|
122
|
+
const sourcePages = [];
|
|
123
|
+
for (const pageId of input.source_page_ids) {
|
|
124
|
+
// Try to find the page (scan all category directories)
|
|
125
|
+
let found = false;
|
|
126
|
+
for (const categoryDir of ["entities", "concepts", "timelines", "syntheses"]) {
|
|
127
|
+
const filePath = node_path_1.default.join(wikiDir, categoryDir, `${pageId}.md`);
|
|
128
|
+
try {
|
|
129
|
+
const read = await (0, filesystem_1.safeReadFile)(filePath);
|
|
130
|
+
if (!read.error && !read.binary && read.content) {
|
|
131
|
+
const titleMatch = read.content.match(/^#\s+(.+?)$/m);
|
|
132
|
+
const title = titleMatch ? titleMatch[1].trim() : pageId;
|
|
133
|
+
const category = categoryDir.replace("s", "");
|
|
134
|
+
sourcePages.push({
|
|
135
|
+
page_id: pageId,
|
|
136
|
+
title,
|
|
137
|
+
category,
|
|
138
|
+
content: read.content,
|
|
139
|
+
relevance: 1.0 - (sourcePages.length * 0.1), // Slight decay for order
|
|
140
|
+
});
|
|
141
|
+
found = true;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
// Try next category
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!found && sourcePages.length === 0) {
|
|
150
|
+
// At least one page should be found
|
|
151
|
+
return {
|
|
152
|
+
query: input.query,
|
|
153
|
+
answer: `Could not find source page: ${pageId}`,
|
|
154
|
+
source_pages: [],
|
|
155
|
+
confidence: 0,
|
|
156
|
+
key_concepts: [],
|
|
157
|
+
error: `Page not found: ${pageId}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Build query terms
|
|
162
|
+
const queryTerms = input.query.toLowerCase().split(/\s+/).filter((t) => t.length > 2);
|
|
163
|
+
// Generate synthesis
|
|
164
|
+
const { answer, concepts } = buildSynthesis(input.query, sourcePages, queryTerms);
|
|
165
|
+
// Calculate confidence based on number and relevance of sources
|
|
166
|
+
const confidence = Math.min(1.0, Math.max(0.3, sourcePages.length * 0.25));
|
|
167
|
+
return {
|
|
168
|
+
query: input.query,
|
|
169
|
+
answer,
|
|
170
|
+
source_pages: sourcePages.map((p) => ({
|
|
171
|
+
page_id: p.page_id,
|
|
172
|
+
title: p.title,
|
|
173
|
+
category: p.category,
|
|
174
|
+
})),
|
|
175
|
+
confidence,
|
|
176
|
+
key_concepts: concepts,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
return {
|
|
181
|
+
query: input.query,
|
|
182
|
+
answer: `Error synthesizing answer: ${e?.message ?? String(e)}`,
|
|
183
|
+
source_pages: [],
|
|
184
|
+
confidence: 0,
|
|
185
|
+
key_concepts: [],
|
|
186
|
+
error: e?.message ?? String(e),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getSchemataGuidance = getSchemataGuidance;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
|
+
/**
|
|
10
|
+
* get_schema_guidance
|
|
11
|
+
*
|
|
12
|
+
* Analyzes what documents should exist based on the project schema and current wiki state.
|
|
13
|
+
* Helps users understand what to create next.
|
|
14
|
+
*
|
|
15
|
+
* Input:
|
|
16
|
+
* - project_path: string
|
|
17
|
+
* - domain?: string (optional; auto-detected from schema if not provided)
|
|
18
|
+
*
|
|
19
|
+
* Output:
|
|
20
|
+
* - domain: detected domain
|
|
21
|
+
* - recommended_pages: list of pages that should exist with reasons
|
|
22
|
+
* - existing_pages: what's already in the wiki
|
|
23
|
+
* - missing_pages: high-priority missing documents
|
|
24
|
+
* - recommendations: actionable next steps
|
|
25
|
+
*/
|
|
26
|
+
async function getSchemataGuidance(input) {
|
|
27
|
+
const projectPath = node_path_1.default.resolve(input.project_path);
|
|
28
|
+
const docuDir = node_path_1.default.join(projectPath, ".docuflow");
|
|
29
|
+
const wikiDir = node_path_1.default.join(docuDir, "wiki");
|
|
30
|
+
const schemaPath = node_path_1.default.join(docuDir, "schema.md");
|
|
31
|
+
// Read schema to understand domain
|
|
32
|
+
let domain = input.domain || "General";
|
|
33
|
+
try {
|
|
34
|
+
const schemaContent = await promises_1.default.readFile(schemaPath, "utf-8");
|
|
35
|
+
if (schemaContent.includes("Research"))
|
|
36
|
+
domain = "Research";
|
|
37
|
+
else if (schemaContent.includes("Business"))
|
|
38
|
+
domain = "Business";
|
|
39
|
+
else if (schemaContent.includes("Architecture"))
|
|
40
|
+
domain = "Code/Architecture";
|
|
41
|
+
else if (schemaContent.includes("Personal"))
|
|
42
|
+
domain = "Personal";
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Use default if schema not readable
|
|
46
|
+
}
|
|
47
|
+
// Scan wiki for existing pages
|
|
48
|
+
const indexPath = node_path_1.default.join(docuDir, "index.md");
|
|
49
|
+
const indexContent = await promises_1.default
|
|
50
|
+
.readFile(indexPath, "utf-8")
|
|
51
|
+
.catch(() => "");
|
|
52
|
+
// Count pages by category
|
|
53
|
+
const existingPages = [];
|
|
54
|
+
const categories = ["entities", "concepts", "syntheses", "timelines"];
|
|
55
|
+
for (const cat of categories) {
|
|
56
|
+
const catDir = node_path_1.default.join(wikiDir, cat);
|
|
57
|
+
try {
|
|
58
|
+
const files = await promises_1.default.readdir(catDir);
|
|
59
|
+
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
60
|
+
existingPages.push({
|
|
61
|
+
name: file.replace(".md", ""),
|
|
62
|
+
category: cat,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Directory may not exist
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Define recommended pages by domain
|
|
71
|
+
const recommendedByDomain = {
|
|
72
|
+
"Code/Architecture": [
|
|
73
|
+
{
|
|
74
|
+
name: "architecture_overview",
|
|
75
|
+
category: "syntheses",
|
|
76
|
+
suggested_title: "System Architecture Overview",
|
|
77
|
+
reason: "High-level view of how all components fit together",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "core_patterns",
|
|
81
|
+
category: "concepts",
|
|
82
|
+
suggested_title: "Core Architectural Patterns",
|
|
83
|
+
reason: "Design patterns used throughout the codebase",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "module_dependencies",
|
|
87
|
+
category: "concepts",
|
|
88
|
+
suggested_title: "Module Dependencies",
|
|
89
|
+
reason: "How modules depend on and integrate with each other",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "data_flow",
|
|
93
|
+
category: "syntheses",
|
|
94
|
+
suggested_title: "Data Flow Diagram",
|
|
95
|
+
reason: "How data moves through the system",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
Research: [
|
|
99
|
+
{
|
|
100
|
+
name: "research_overview",
|
|
101
|
+
category: "syntheses",
|
|
102
|
+
suggested_title: "Research Domain Overview",
|
|
103
|
+
reason: "Big picture of the research area",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "key_findings",
|
|
107
|
+
category: "syntheses",
|
|
108
|
+
suggested_title: "Key Findings & Synthesis",
|
|
109
|
+
reason: "Major discoveries and insights",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "contradictions",
|
|
113
|
+
category: "syntheses",
|
|
114
|
+
suggested_title: "Areas of Contradiction",
|
|
115
|
+
reason: "Where researchers disagree",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "open_questions",
|
|
119
|
+
category: "concepts",
|
|
120
|
+
suggested_title: "Open Research Questions",
|
|
121
|
+
reason: "What's still unknown in this domain",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
Business: [
|
|
125
|
+
{
|
|
126
|
+
name: "market_overview",
|
|
127
|
+
category: "syntheses",
|
|
128
|
+
suggested_title: "Market Overview",
|
|
129
|
+
reason: "Big picture of the market and competitive landscape",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "competitive_analysis",
|
|
133
|
+
category: "syntheses",
|
|
134
|
+
suggested_title: "Competitive Analysis",
|
|
135
|
+
reason: "Comparison of key competitors",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "opportunities",
|
|
139
|
+
category: "syntheses",
|
|
140
|
+
suggested_title: "Market Opportunities",
|
|
141
|
+
reason: "Gaps and growth areas",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "risks",
|
|
145
|
+
category: "concepts",
|
|
146
|
+
suggested_title: "Market Risks",
|
|
147
|
+
reason: "Threats and challenges",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
Personal: [
|
|
151
|
+
{
|
|
152
|
+
name: "learning_goals",
|
|
153
|
+
category: "concepts",
|
|
154
|
+
suggested_title: "Learning Goals",
|
|
155
|
+
reason: "What you want to learn in this domain",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "key_insights",
|
|
159
|
+
category: "syntheses",
|
|
160
|
+
suggested_title: "Key Personal Insights",
|
|
161
|
+
reason: "Major learnings and takeaways",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "action_items",
|
|
165
|
+
category: "concepts",
|
|
166
|
+
suggested_title: "Action Items & Next Steps",
|
|
167
|
+
reason: "What to do with this knowledge",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "resources",
|
|
171
|
+
category: "concepts",
|
|
172
|
+
suggested_title: "Key Resources",
|
|
173
|
+
reason: "Important links, books, people",
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
const recommended = recommendedByDomain[domain] || recommendedByDomain.General;
|
|
178
|
+
// Find missing pages
|
|
179
|
+
const missingPages = [];
|
|
180
|
+
for (const rec of recommended) {
|
|
181
|
+
const found = existingPages.find((p) => p.name === rec.name);
|
|
182
|
+
if (!found) {
|
|
183
|
+
missingPages.push(rec.suggested_title);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Generate recommendations
|
|
187
|
+
const recommendations = [];
|
|
188
|
+
if (existingPages.length === 0) {
|
|
189
|
+
recommendations.push("🌱 Start by ingesting your first source");
|
|
190
|
+
recommendations.push("📝 Create an overview/synthesis page");
|
|
191
|
+
}
|
|
192
|
+
else if (existingPages.length < 10) {
|
|
193
|
+
recommendations.push("📚 Add more sources to deepen understanding");
|
|
194
|
+
if (missingPages.length > 0) {
|
|
195
|
+
recommendations.push(`⚠️ Consider creating: ${missingPages[0]}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else if (existingPages.length > 50) {
|
|
199
|
+
recommendations.push("✅ You have a solid wiki foundation");
|
|
200
|
+
recommendations.push("🔍 Run lint_wiki to health-check for gaps");
|
|
201
|
+
recommendations.push("🔗 Verify cross-references are accurate");
|
|
202
|
+
}
|
|
203
|
+
if (missingPages.length > 2) {
|
|
204
|
+
recommendations.push(`💡 Biggest gap: ${missingPages[0]} could improve wiki significantly`);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
domain,
|
|
208
|
+
recommended_pages: recommended,
|
|
209
|
+
existing_pages: existingPages,
|
|
210
|
+
missing_pages: missingPages,
|
|
211
|
+
recommendations,
|
|
212
|
+
};
|
|
213
|
+
}
|