@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 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
+ }