@doquflow/server 0.2.0 → 0.4.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 +46 -1
- package/dist/index.js +228 -0
- package/dist/tools/answer-synthesis.js +189 -0
- package/dist/tools/generate-dependency-graph.js +162 -0
- package/dist/tools/get-schema-guidance.js +213 -0
- package/dist/tools/ingest-source.js +252 -0
- package/dist/tools/lint-wiki.js +342 -0
- package/dist/tools/list-wiki.js +129 -0
- package/dist/tools/preview-generation.js +228 -0
- package/dist/tools/query-wiki.js +67 -0
- package/dist/tools/read-specs.js +5 -1
- package/dist/tools/save-answer-as-page.js +102 -0
- package/dist/tools/update-index.js +157 -0
- package/dist/tools/wiki-search.js +157 -0
- package/package.json +1 -1
package/dist/extractor.js
CHANGED
|
@@ -48,6 +48,35 @@ const RE_PROCESS_ENV = /\bprocess\.env\.([A-Z_][A-Z0-9_]*)/g;
|
|
|
48
48
|
const RE_DOTNET_ENV = /\bEnvironment\.GetEnvironmentVariable\(\s*["']([^"']+)["']/g;
|
|
49
49
|
const RE_ICONFIG = /\bIConfiguration\b/g;
|
|
50
50
|
const RE_APPSETTINGS = /\bappsettings(?:\.[A-Za-z0-9]+)?\.json\b/g;
|
|
51
|
+
// ─── Go ─────────────────────────────────────────────────────────────────────
|
|
52
|
+
// type FooBar struct | type FooBar interface
|
|
53
|
+
const RE_GO_TYPE = /\btype\s+([A-Z]\w*)\s+(?:struct|interface)/g;
|
|
54
|
+
// func FuncName( or func (recv *Type) MethodName(
|
|
55
|
+
const RE_GO_FUNC = /\bfunc\s+(?:\([^)]+\)\s+)?([A-Za-z_]\w*)\s*\(/g;
|
|
56
|
+
// import "pkg" or import ( "pkg" ) — individual quoted paths inside import blocks
|
|
57
|
+
const RE_GO_IMPORT = /^\s+["']([^"'\s]+)["']\s*(?:\/\/.*)?$/gm;
|
|
58
|
+
// os.Getenv("KEY") or os.LookupEnv("KEY")
|
|
59
|
+
const RE_GO_ENV = /\bos\.(?:Getenv|LookupEnv)\s*\(\s*["']([A-Z_][A-Z0-9_]*)["']/g;
|
|
60
|
+
// gorilla/mux, gin, chi, echo, stdlib: router.GET("/path", ...)
|
|
61
|
+
const RE_GO_HTTP = /\b(?:r|router|e|mux|g|app)\s*\.\s*(?:HandleFunc|GET|POST|PUT|DELETE|PATCH|Handle|Any)\s*\(\s*["']([^"']+)["']/g;
|
|
62
|
+
// GORM explicit table: db.Table("name")
|
|
63
|
+
const RE_GO_GORM_TABLE = /\bdb\s*\.\s*Table\s*\(\s*["']([^"']+)["']/g;
|
|
64
|
+
// ─── Ruby / Rails ────────────────────────────────────────────────────────────
|
|
65
|
+
// class Foo or module Bar
|
|
66
|
+
const RE_RUBY_CLASS = /\b(?:class|module)\s+([A-Z]\w*(?:::[A-Z]\w*)*)/g;
|
|
67
|
+
// def method_name or def self.method_name
|
|
68
|
+
const RE_RUBY_DEF = /^\s*def\s+(?:self\.)?([a-z_]\w*)/gm;
|
|
69
|
+
// require 'gem' or require_relative '../path'
|
|
70
|
+
const RE_RUBY_REQUIRE = /\brequire(?:_relative)?\s+["']([^"']+)["']/g;
|
|
71
|
+
// ENV['KEY'] or ENV["KEY"]
|
|
72
|
+
const RE_RUBY_ENV = /\bENV\s*\[\s*["']([A-Z_][A-Z0-9_]*)["']\s*\]/g;
|
|
73
|
+
// Rails routes: get '/path', post '/path', resources :users, root 'home#index'
|
|
74
|
+
const RE_RAILS_ROUTE = /^\s*(?:get|post|put|delete|patch|resources|resource|root)\s+["']([^"']+)["']/gm;
|
|
75
|
+
// ActiveRecord associations: has_many :users, belongs_to :user
|
|
76
|
+
const RE_AR_HAS = /\bhas_(?:many|one)\s+:(\w+)/g;
|
|
77
|
+
const RE_AR_BELONGS = /\bbelongs_to\s+:(\w+)/g;
|
|
78
|
+
// Explicit table name: self.table_name = 'table'
|
|
79
|
+
const RE_AR_TABLE = /\bself\.table_name\s*=\s*["']([^"']+)["']/g;
|
|
51
80
|
// SQL keywords and noise that the regex may pick up as table names.
|
|
52
81
|
// Also strips single-letter hits (LINQ aliases: `from u in _db.Users` → u).
|
|
53
82
|
const SQL_KEYWORD_NOISE = new Set([
|
|
@@ -77,11 +106,17 @@ function cleanTableNames(names) {
|
|
|
77
106
|
});
|
|
78
107
|
}
|
|
79
108
|
function extract(content) {
|
|
80
|
-
const classes =
|
|
109
|
+
const classes = uniq([
|
|
110
|
+
...collect(new RegExp(RE_CLASS.source, "g"), content),
|
|
111
|
+
...collect(new RegExp(RE_GO_TYPE.source, "g"), content),
|
|
112
|
+
...collect(new RegExp(RE_RUBY_CLASS.source, "g"), content),
|
|
113
|
+
]);
|
|
81
114
|
const functions = uniq([
|
|
82
115
|
...collect(new RegExp(RE_FUNC_KW.source, "g"), content),
|
|
83
116
|
...collect(new RegExp(RE_FUNC_ARROW.source, "g"), content),
|
|
84
117
|
...collect(new RegExp(RE_METHOD_TS.source, "gm"), content),
|
|
118
|
+
...collect(new RegExp(RE_GO_FUNC.source, "g"), content),
|
|
119
|
+
...collect(new RegExp(RE_RUBY_DEF.source, "gm"), content),
|
|
85
120
|
]).filter((n) => !["if", "for", "while", "switch", "catch", "return", "function"].includes(n));
|
|
86
121
|
const dependencies = uniq([
|
|
87
122
|
...collect(new RegExp(RE_USING.source, "g"), content),
|
|
@@ -89,6 +124,8 @@ function extract(content) {
|
|
|
89
124
|
...collect(new RegExp(RE_REQUIRE.source, "g"), content),
|
|
90
125
|
...collect(new RegExp(RE_DECORATOR.source, "g"), content),
|
|
91
126
|
...collect(new RegExp(RE_NEW_CLASS.source, "g"), content),
|
|
127
|
+
...collect(new RegExp(RE_GO_IMPORT.source, "gm"), content),
|
|
128
|
+
...collect(new RegExp(RE_RUBY_REQUIRE.source, "g"), content),
|
|
92
129
|
]);
|
|
93
130
|
const db_tables = uniq(cleanTableNames([
|
|
94
131
|
...collect(new RegExp(RE_SQL_TABLE.source, "gi"), content),
|
|
@@ -96,6 +133,10 @@ function extract(content) {
|
|
|
96
133
|
...collect(new RegExp(RE_TABLE_ATTR.source, "g"), content),
|
|
97
134
|
...collect(new RegExp(RE_TABLE_FLUENT.source, "g"), content),
|
|
98
135
|
...collect(new RegExp(RE_EF_PROP.source, "g"), content),
|
|
136
|
+
...collect(new RegExp(RE_GO_GORM_TABLE.source, "g"), content),
|
|
137
|
+
...collect(new RegExp(RE_AR_HAS.source, "g"), content),
|
|
138
|
+
...collect(new RegExp(RE_AR_BELONGS.source, "g"), content),
|
|
139
|
+
...collect(new RegExp(RE_AR_TABLE.source, "g"), content),
|
|
99
140
|
]));
|
|
100
141
|
const endpoints = uniq([
|
|
101
142
|
...collect(new RegExp(RE_DOTNET_ROUTE.source, "g"), content),
|
|
@@ -106,11 +147,15 @@ function extract(content) {
|
|
|
106
147
|
...collect(new RegExp(RE_FLASK.source, "g"), content),
|
|
107
148
|
...collect(new RegExp(RE_FASTAPI.source, "g"), content),
|
|
108
149
|
...collect(new RegExp(RE_DJANGO.source, "g"), content),
|
|
150
|
+
...collect(new RegExp(RE_GO_HTTP.source, "g"), content),
|
|
151
|
+
...collect(new RegExp(RE_RAILS_ROUTE.source, "gm"), content),
|
|
109
152
|
]);
|
|
110
153
|
const config_refs = [];
|
|
111
154
|
config_refs.push(...collect(new RegExp(RE_CFG_CONNSTR.source, "g"), content).map((s) => `ConnectionStrings:${s}`));
|
|
112
155
|
config_refs.push(...collect(new RegExp(RE_PROCESS_ENV.source, "g"), content).map((s) => `process.env.${s}`));
|
|
113
156
|
config_refs.push(...collect(new RegExp(RE_DOTNET_ENV.source, "g"), content));
|
|
157
|
+
config_refs.push(...collect(new RegExp(RE_GO_ENV.source, "g"), content).map((s) => `os.Getenv(${s})`));
|
|
158
|
+
config_refs.push(...collect(new RegExp(RE_RUBY_ENV.source, "g"), content).map((s) => `ENV[${s}]`));
|
|
114
159
|
if (RE_ICONFIG.test(content))
|
|
115
160
|
config_refs.push("IConfiguration");
|
|
116
161
|
if (RE_APPSETTINGS.test(content))
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,17 @@ 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");
|
|
21
|
+
const generate_dependency_graph_1 = require("./tools/generate-dependency-graph");
|
|
11
22
|
const server = new index_js_1.Server({ name: "docuflow", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
12
23
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
13
24
|
tools: [
|
|
@@ -66,6 +77,190 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
|
66
77
|
required: ["project_path"],
|
|
67
78
|
},
|
|
68
79
|
},
|
|
80
|
+
{
|
|
81
|
+
name: "ingest_source",
|
|
82
|
+
description: "Ingest a markdown source document from .docuflow/sources/ and generate wiki pages (entities, concepts) with cross-references. Returns pages created and entities discovered.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
87
|
+
source_filename: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "Filename in .docuflow/sources/ to ingest (e.g., 'overview.md').",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
required: ["project_path", "source_filename"],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "update_index",
|
|
97
|
+
description: "Scan all wiki pages in .docuflow/wiki/ and regenerate .docuflow/index.md organized by category. Appends operation to log.md.",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
102
|
+
},
|
|
103
|
+
required: ["project_path"],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "list_wiki",
|
|
108
|
+
description: "List all wiki pages in .docuflow/wiki/, optionally filtered by category. Returns metadata (title, created_at, sources, tags, stale) and page counts by category. Pages not updated in 30+ days are flagged stale:true.",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
113
|
+
category: {
|
|
114
|
+
type: "string",
|
|
115
|
+
enum: ["entity", "concept", "timeline", "synthesis"],
|
|
116
|
+
description: "Optional: filter to a specific category.",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
required: ["project_path"],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "wiki_search",
|
|
124
|
+
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.",
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
129
|
+
query: { type: "string", description: "Search query (e.g., 'MCP protocol design')." },
|
|
130
|
+
limit: {
|
|
131
|
+
type: "number",
|
|
132
|
+
description: "Optional: max results to return (default: 10).",
|
|
133
|
+
},
|
|
134
|
+
category: {
|
|
135
|
+
type: "string",
|
|
136
|
+
enum: ["entity", "concept", "timeline", "synthesis"],
|
|
137
|
+
description: "Optional: filter to a specific category.",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
required: ["project_path", "query"],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "synthesize_answer",
|
|
145
|
+
description: "Generate a synthesis answer from multiple wiki pages. Extracts relevant sentences, key concepts, and builds a markdown answer with citations.",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: {
|
|
149
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
150
|
+
query: { type: "string", description: "The question being answered." },
|
|
151
|
+
source_page_ids: {
|
|
152
|
+
type: "array",
|
|
153
|
+
items: { type: "string" },
|
|
154
|
+
description: "List of wiki page IDs to synthesize from.",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
required: ["project_path", "query", "source_page_ids"],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "query_wiki",
|
|
162
|
+
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.",
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: "object",
|
|
165
|
+
properties: {
|
|
166
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
167
|
+
question: { type: "string", description: "The question to ask (e.g., 'How does the MCP protocol work?')." },
|
|
168
|
+
max_sources: {
|
|
169
|
+
type: "number",
|
|
170
|
+
description: "Optional: max source pages to use in synthesis (default: 5).",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
required: ["project_path", "question"],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "save_answer_as_page",
|
|
178
|
+
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.",
|
|
179
|
+
inputSchema: {
|
|
180
|
+
type: "object",
|
|
181
|
+
properties: {
|
|
182
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
183
|
+
question: { type: "string", description: "The original question that was answered." },
|
|
184
|
+
answer: { type: "string", description: "The markdown answer text to save." },
|
|
185
|
+
page_title: { type: "string", description: "Title for the new page (e.g., 'How MCP Protocol Works')." },
|
|
186
|
+
category: {
|
|
187
|
+
type: "string",
|
|
188
|
+
enum: ["synthesis", "entity", "concept", "timeline"],
|
|
189
|
+
description: "Optional: wiki category for the page (default: synthesis).",
|
|
190
|
+
},
|
|
191
|
+
source_page_ids: {
|
|
192
|
+
type: "array",
|
|
193
|
+
items: { type: "string" },
|
|
194
|
+
description: "Optional: list of source wiki page IDs used to generate this answer.",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
required: ["project_path", "question", "answer", "page_title"],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "lint_wiki",
|
|
202
|
+
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.",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
207
|
+
check_type: {
|
|
208
|
+
type: "string",
|
|
209
|
+
enum: ["all", "orphans", "contradictions", "stale", "metadata"],
|
|
210
|
+
description: "Optional: type of check to run (default: all).",
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
required: ["project_path"],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "get_schema_guidance",
|
|
218
|
+
description: "Analyze what documents should exist based on project schema and current wiki. Removes decision fatigue by suggesting what to create next.",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: {
|
|
222
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
223
|
+
domain: {
|
|
224
|
+
type: "string",
|
|
225
|
+
description: "Optional: domain hint (Code/Architecture, Research, Business, Personal). Auto-detected if not provided.",
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
required: ["project_path"],
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: "preview_generation",
|
|
233
|
+
description: "Preview what a tool will generate before running it. Removes black-box feeling by showing predicted actions, outputs, and impact.",
|
|
234
|
+
inputSchema: {
|
|
235
|
+
type: "object",
|
|
236
|
+
properties: {
|
|
237
|
+
tool_name: { type: "string", description: "Name of the tool to preview (e.g., ingest_source, query_wiki)." },
|
|
238
|
+
project_path: { type: "string", description: "Root of the project." },
|
|
239
|
+
params: { type: "object", description: "Parameters you would pass to that tool." },
|
|
240
|
+
},
|
|
241
|
+
required: ["tool_name", "project_path", "params"],
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: "generate_dependency_graph",
|
|
246
|
+
description: "Scan a project and build a dependency graph showing how modules import each other, which DB tables are shared, and which files are most connected. Returns nodes, edges, shared tables, and the top 10 most-connected modules. Use this to understand coupling and identify risky files to change.",
|
|
247
|
+
inputSchema: {
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
project_path: { type: "string", description: "Root directory of the project to analyse." },
|
|
251
|
+
extensions: {
|
|
252
|
+
type: "array",
|
|
253
|
+
items: { type: "string" },
|
|
254
|
+
description: "Optional extension filter e.g. [\".ts\",\".go\"]. If omitted, all non-binary files are scanned.",
|
|
255
|
+
},
|
|
256
|
+
focus: {
|
|
257
|
+
type: "string",
|
|
258
|
+
description: "Optional: filter graph to neighbours of a specific file or module name (partial match, e.g. 'user-service').",
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
required: ["project_path"],
|
|
262
|
+
},
|
|
263
|
+
},
|
|
69
264
|
],
|
|
70
265
|
}));
|
|
71
266
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
@@ -84,6 +279,39 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
84
279
|
else if (name === "read_specs") {
|
|
85
280
|
result = await (0, read_specs_1.readSpecs)(args);
|
|
86
281
|
}
|
|
282
|
+
else if (name === "ingest_source") {
|
|
283
|
+
result = await (0, ingest_source_1.ingestSource)(args);
|
|
284
|
+
}
|
|
285
|
+
else if (name === "update_index") {
|
|
286
|
+
result = await (0, update_index_1.updateIndex)(args);
|
|
287
|
+
}
|
|
288
|
+
else if (name === "list_wiki") {
|
|
289
|
+
result = await (0, list_wiki_1.listWiki)(args);
|
|
290
|
+
}
|
|
291
|
+
else if (name === "wiki_search") {
|
|
292
|
+
result = await (0, wiki_search_1.wikiSearch)(args);
|
|
293
|
+
}
|
|
294
|
+
else if (name === "synthesize_answer") {
|
|
295
|
+
result = await (0, answer_synthesis_1.synthesizeAnswer)(args);
|
|
296
|
+
}
|
|
297
|
+
else if (name === "query_wiki") {
|
|
298
|
+
result = await (0, query_wiki_1.queryWiki)(args);
|
|
299
|
+
}
|
|
300
|
+
else if (name === "save_answer_as_page") {
|
|
301
|
+
result = await (0, save_answer_as_page_1.saveAnswerAsPage)(args);
|
|
302
|
+
}
|
|
303
|
+
else if (name === "lint_wiki") {
|
|
304
|
+
result = await (0, lint_wiki_1.lintWiki)(args);
|
|
305
|
+
}
|
|
306
|
+
else if (name === "get_schema_guidance") {
|
|
307
|
+
result = await (0, get_schema_guidance_1.getSchemataGuidance)(args);
|
|
308
|
+
}
|
|
309
|
+
else if (name === "preview_generation") {
|
|
310
|
+
result = await (0, preview_generation_1.previewGeneration)(args);
|
|
311
|
+
}
|
|
312
|
+
else if (name === "generate_dependency_graph") {
|
|
313
|
+
result = await (0, generate_dependency_graph_1.generateDependencyGraph)(args);
|
|
314
|
+
}
|
|
87
315
|
else {
|
|
88
316
|
result = { error: `Unknown tool: ${name}` };
|
|
89
317
|
}
|
|
@@ -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
|
+
}
|