@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
|
@@ -0,0 +1,162 @@
|
|
|
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.generateDependencyGraph = generateDependencyGraph;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const list_modules_1 = require("./list-modules");
|
|
9
|
+
async function generateDependencyGraph(input) {
|
|
10
|
+
try {
|
|
11
|
+
const projectPath = node_path_1.default.resolve(input.project_path);
|
|
12
|
+
// Scan all modules
|
|
13
|
+
const listResult = await (0, list_modules_1.listModules)({
|
|
14
|
+
path: projectPath,
|
|
15
|
+
extensions: input.extensions,
|
|
16
|
+
});
|
|
17
|
+
const modules = listResult.modules ?? [];
|
|
18
|
+
if (modules.length === 0) {
|
|
19
|
+
return {
|
|
20
|
+
nodes: [],
|
|
21
|
+
edges: [],
|
|
22
|
+
shared_tables: {},
|
|
23
|
+
shared_endpoints: {},
|
|
24
|
+
most_connected: [],
|
|
25
|
+
summary: "No source files found in the project.",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Build id from relative path (normalised for use as node id)
|
|
29
|
+
const relPath = (filePath) => node_path_1.default.relative(projectPath, filePath).replace(/\\/g, "/");
|
|
30
|
+
// Build nodes map
|
|
31
|
+
const nodeMap = new Map();
|
|
32
|
+
for (const mod of modules) {
|
|
33
|
+
const id = relPath(mod.path);
|
|
34
|
+
nodeMap.set(id, {
|
|
35
|
+
id,
|
|
36
|
+
label: node_path_1.default.basename(mod.path),
|
|
37
|
+
language: mod.language,
|
|
38
|
+
classes: mod.classes.length,
|
|
39
|
+
functions: mod.functions.length,
|
|
40
|
+
db_tables: mod.db_tables,
|
|
41
|
+
endpoints: mod.endpoints,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Build import edges: for each module's dependency list, look for matches in node IDs
|
|
45
|
+
const edges = [];
|
|
46
|
+
const edgeSet = new Set(); // avoid duplicates
|
|
47
|
+
for (const mod of modules) {
|
|
48
|
+
const fromId = relPath(mod.path);
|
|
49
|
+
for (const dep of mod.dependencies) {
|
|
50
|
+
// Try to resolve dep to a known node (relative path match or basename match)
|
|
51
|
+
for (const [nodeId] of nodeMap) {
|
|
52
|
+
// Match if the dependency string is contained in the node path
|
|
53
|
+
// (e.g., dep = "./user-service" matches "src/user-service.ts")
|
|
54
|
+
const depNorm = dep.replace(/^[./]+/, "").replace(/\\/g, "/").toLowerCase();
|
|
55
|
+
const nodeNorm = nodeId.replace(/\\/g, "/").toLowerCase();
|
|
56
|
+
if (depNorm.length > 2 &&
|
|
57
|
+
(nodeNorm.includes(depNorm) || nodeNorm.replace(/\.\w+$/, "").endsWith(depNorm))) {
|
|
58
|
+
const key = `${fromId}->${nodeId}`;
|
|
59
|
+
if (fromId !== nodeId && !edgeSet.has(key)) {
|
|
60
|
+
edgeSet.add(key);
|
|
61
|
+
edges.push({ from: fromId, to: nodeId, type: "imports" });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Shared DB tables
|
|
68
|
+
const tableToModules = new Map();
|
|
69
|
+
for (const mod of modules) {
|
|
70
|
+
for (const table of mod.db_tables) {
|
|
71
|
+
const key = table.toLowerCase();
|
|
72
|
+
if (!tableToModules.has(key))
|
|
73
|
+
tableToModules.set(key, []);
|
|
74
|
+
tableToModules.get(key).push(relPath(mod.path));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const shared_tables = {};
|
|
78
|
+
for (const [table, mods] of tableToModules) {
|
|
79
|
+
if (mods.length > 1) {
|
|
80
|
+
shared_tables[table] = mods;
|
|
81
|
+
// Add shared_table edges
|
|
82
|
+
for (let i = 0; i < mods.length; i++) {
|
|
83
|
+
for (let j = i + 1; j < mods.length; j++) {
|
|
84
|
+
const key = `${mods[i]}<>${mods[j]}:${table}`;
|
|
85
|
+
if (!edgeSet.has(key)) {
|
|
86
|
+
edgeSet.add(key);
|
|
87
|
+
edges.push({ from: mods[i], to: mods[j], type: "shared_table" });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Shared endpoints
|
|
94
|
+
const endpointToModules = new Map();
|
|
95
|
+
for (const mod of modules) {
|
|
96
|
+
for (const ep of mod.endpoints) {
|
|
97
|
+
if (!endpointToModules.has(ep))
|
|
98
|
+
endpointToModules.set(ep, []);
|
|
99
|
+
endpointToModules.get(ep).push(relPath(mod.path));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const shared_endpoints = {};
|
|
103
|
+
for (const [ep, mods] of endpointToModules) {
|
|
104
|
+
if (mods.length > 1)
|
|
105
|
+
shared_endpoints[ep] = mods;
|
|
106
|
+
}
|
|
107
|
+
// Most connected nodes (by total edge count)
|
|
108
|
+
const connectionCount = new Map();
|
|
109
|
+
for (const edge of edges) {
|
|
110
|
+
connectionCount.set(edge.from, (connectionCount.get(edge.from) ?? 0) + 1);
|
|
111
|
+
connectionCount.set(edge.to, (connectionCount.get(edge.to) ?? 0) + 1);
|
|
112
|
+
}
|
|
113
|
+
const most_connected = Array.from(connectionCount.entries())
|
|
114
|
+
.map(([id, count]) => ({
|
|
115
|
+
id,
|
|
116
|
+
label: nodeMap.get(id)?.label ?? id,
|
|
117
|
+
connection_count: count,
|
|
118
|
+
}))
|
|
119
|
+
.sort((a, b) => b.connection_count - a.connection_count)
|
|
120
|
+
.slice(0, 10);
|
|
121
|
+
// Apply focus filter: if focus is set, only include nodes/edges reachable from it
|
|
122
|
+
let nodes = Array.from(nodeMap.values());
|
|
123
|
+
let filteredEdges = edges;
|
|
124
|
+
if (input.focus) {
|
|
125
|
+
const focusNorm = input.focus.toLowerCase();
|
|
126
|
+
const focusId = nodes.find((n) => n.id.toLowerCase().includes(focusNorm))?.id;
|
|
127
|
+
if (focusId) {
|
|
128
|
+
const reachable = new Set([focusId]);
|
|
129
|
+
// BFS: include direct neighbours
|
|
130
|
+
for (const e of edges) {
|
|
131
|
+
if (e.from === focusId)
|
|
132
|
+
reachable.add(e.to);
|
|
133
|
+
if (e.to === focusId)
|
|
134
|
+
reachable.add(e.from);
|
|
135
|
+
}
|
|
136
|
+
nodes = nodes.filter((n) => reachable.has(n.id));
|
|
137
|
+
filteredEdges = edges.filter((e) => reachable.has(e.from) && reachable.has(e.to));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const sharedTableCount = Object.keys(shared_tables).length;
|
|
141
|
+
const summary = [
|
|
142
|
+
`${nodes.length} modules, ${filteredEdges.length} dependencies`,
|
|
143
|
+
sharedTableCount > 0 ? `${sharedTableCount} shared DB table(s)` : null,
|
|
144
|
+
most_connected.length > 0
|
|
145
|
+
? `Most connected: ${most_connected[0].label} (${most_connected[0].connection_count} links)`
|
|
146
|
+
: null,
|
|
147
|
+
]
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.join(" · ");
|
|
150
|
+
return {
|
|
151
|
+
nodes,
|
|
152
|
+
edges: filteredEdges,
|
|
153
|
+
shared_tables,
|
|
154
|
+
shared_endpoints,
|
|
155
|
+
most_connected,
|
|
156
|
+
summary,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
return { error: e?.message ?? String(e) };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
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.ingestSource = ingestSource;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const filesystem_1 = require("../filesystem");
|
|
9
|
+
/**
|
|
10
|
+
* Find the first paragraph in the source that mentions the given name.
|
|
11
|
+
* Returns cleaned text (stripped of markdown syntax), up to 400 chars.
|
|
12
|
+
*/
|
|
13
|
+
function findContextParagraph(content, name) {
|
|
14
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15
|
+
const re = new RegExp(`\\b${escaped}\\b`, "i");
|
|
16
|
+
const paragraphs = content.split(/\n{2,}/);
|
|
17
|
+
for (const para of paragraphs) {
|
|
18
|
+
if (!re.test(para))
|
|
19
|
+
continue;
|
|
20
|
+
const clean = para
|
|
21
|
+
.replace(/^#+\s*/gm, "")
|
|
22
|
+
.replace(/\*\*?([^*]+)\*\*?/g, "$1")
|
|
23
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
24
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
25
|
+
.trim();
|
|
26
|
+
if (clean.length > 20)
|
|
27
|
+
return clean.substring(0, 400);
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Simple markdown-based entity extraction.
|
|
33
|
+
* Looks for patterns like:
|
|
34
|
+
* - **Entity:** or ### Entity Headers
|
|
35
|
+
* - Lists of concepts
|
|
36
|
+
* - Tool/component mentions
|
|
37
|
+
*/
|
|
38
|
+
function extractFromMarkdown(content) {
|
|
39
|
+
const lines = content.split("\n");
|
|
40
|
+
const entities = [];
|
|
41
|
+
const concepts = new Set();
|
|
42
|
+
const relationships = [];
|
|
43
|
+
// Extract first 500 chars as summary (non-empty)
|
|
44
|
+
const summary = content
|
|
45
|
+
.split("\n")
|
|
46
|
+
.filter((l) => l.trim().length > 0 && !l.startsWith("#") && !l.startsWith("```") && !l.startsWith("["))
|
|
47
|
+
.slice(0, 3)
|
|
48
|
+
.join(" ")
|
|
49
|
+
.substring(0, 500);
|
|
50
|
+
// Find headers (entities/concepts)
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
// ### Header → entity/concept
|
|
53
|
+
if (line.match(/^###\s+/)) {
|
|
54
|
+
const header = line.replace(/^###\s+/, "").trim();
|
|
55
|
+
if (header && !header.startsWith("[") && !header.startsWith("{")) {
|
|
56
|
+
entities.push({ name: header, type: "concept" });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// **bold text** → potential entity (but not arrays or JSON)
|
|
60
|
+
const boldMatches = line.matchAll(/\*\*([^*]+)\*\*/g);
|
|
61
|
+
for (const match of boldMatches) {
|
|
62
|
+
const text = match[1].trim();
|
|
63
|
+
// Skip if it looks like JSON or code
|
|
64
|
+
if (text.length > 2 &&
|
|
65
|
+
text.length < 100 &&
|
|
66
|
+
!text.includes("[") &&
|
|
67
|
+
!text.includes("{") &&
|
|
68
|
+
!text.includes('"') &&
|
|
69
|
+
!text.includes("`")) {
|
|
70
|
+
entities.push({ name: text, type: "entity" });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Find relationship patterns like "X integrates Y" or "X depends on Y"
|
|
75
|
+
const relWords = [
|
|
76
|
+
{ word: "integrates", rel: "integrates" },
|
|
77
|
+
{ word: "depends on", rel: "depends_on" },
|
|
78
|
+
{ word: "extends", rel: "extends" },
|
|
79
|
+
{ word: "uses", rel: "uses" },
|
|
80
|
+
{ word: "manages", rel: "manages" },
|
|
81
|
+
];
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
for (const { word, rel } of relWords) {
|
|
84
|
+
const regex = new RegExp(`([A-Z][A-Za-z0-9_]+)\\s+${word}\\s+([A-Z][A-Za-z0-9_]+)`, "gi");
|
|
85
|
+
const matches = line.matchAll(regex);
|
|
86
|
+
for (const match of matches) {
|
|
87
|
+
relationships.push({ from: match[1], to: match[2], relation: rel });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Collect concepts from lines containing "concept:", "pattern:", etc (skip arrays)
|
|
92
|
+
const conceptLines = lines.filter((l) => /concept|pattern|principle|approach/i.test(l) && !l.startsWith("[") && !l.startsWith("{"));
|
|
93
|
+
for (const line of conceptLines) {
|
|
94
|
+
const conceptMatch = line.match(/:\s*([^[\]{}"`.]+?)(?:\.|$)/);
|
|
95
|
+
if (conceptMatch) {
|
|
96
|
+
const concept = conceptMatch[1].trim();
|
|
97
|
+
if (concept && concept.length < 100) {
|
|
98
|
+
concepts.add(concept);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
summary,
|
|
104
|
+
entities: Array.from(new Set(entities.map((e) => JSON.stringify(e)))).map((s) => JSON.parse(s)),
|
|
105
|
+
concepts: Array.from(concepts),
|
|
106
|
+
relationships,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Convert a source document into a collection of wiki pages.
|
|
111
|
+
* For each distinct entity/concept, create a page.
|
|
112
|
+
*/
|
|
113
|
+
function generateWikiPages(sourceId, sourceTitle, extracted, sourceContent) {
|
|
114
|
+
const now = new Date().toISOString();
|
|
115
|
+
const pages = [];
|
|
116
|
+
// Create summary page (synthesis)
|
|
117
|
+
const summaryId = `source_${sourceId.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`;
|
|
118
|
+
const summaryPage = {
|
|
119
|
+
id: summaryId,
|
|
120
|
+
title: `Source: ${sourceTitle}`,
|
|
121
|
+
category: "synthesis",
|
|
122
|
+
content: `# ${sourceTitle}\n\n${extracted.summary}\n\n## Key Entities\n\n${extracted.entities.map((e) => `- **${e.name}** (${e.type})`).join("\n")}`,
|
|
123
|
+
frontmatter: {
|
|
124
|
+
created_at: now,
|
|
125
|
+
updated_at: now,
|
|
126
|
+
sources: [sourceId],
|
|
127
|
+
tags: ["source", "ingested"],
|
|
128
|
+
inbound_links: [],
|
|
129
|
+
outbound_links: extracted.entities.map((e) => `entity_${e.name.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
pages.push(summaryPage);
|
|
133
|
+
// Create entity pages
|
|
134
|
+
for (const entity of extracted.entities) {
|
|
135
|
+
const entityId = `entity_${entity.name.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`;
|
|
136
|
+
const contextPara = findContextParagraph(sourceContent, entity.name);
|
|
137
|
+
const overview = contextPara
|
|
138
|
+
? `${contextPara}\n\n*Introduced in: ${sourceTitle}*`
|
|
139
|
+
: `Introduced in: ${sourceTitle}`;
|
|
140
|
+
const entityPage = {
|
|
141
|
+
id: entityId,
|
|
142
|
+
title: entity.name,
|
|
143
|
+
category: "entity",
|
|
144
|
+
content: `# ${entity.name}\n\n**Type:** ${entity.type}\n\n## Overview\n\n${overview}`,
|
|
145
|
+
frontmatter: {
|
|
146
|
+
created_at: now,
|
|
147
|
+
updated_at: now,
|
|
148
|
+
sources: [sourceId],
|
|
149
|
+
tags: [entity.type],
|
|
150
|
+
inbound_links: [summaryId],
|
|
151
|
+
outbound_links: [],
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
pages.push(entityPage);
|
|
155
|
+
}
|
|
156
|
+
// Create concept pages
|
|
157
|
+
for (const concept of extracted.concepts) {
|
|
158
|
+
const conceptId = `concept_${concept.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`;
|
|
159
|
+
const contextPara = findContextParagraph(sourceContent, concept);
|
|
160
|
+
const definition = contextPara
|
|
161
|
+
? `${contextPara}\n\n*Introduced in: ${sourceTitle}*`
|
|
162
|
+
: `To be expanded with additional sources.\n\n*Introduced in: ${sourceTitle}*`;
|
|
163
|
+
const conceptPage = {
|
|
164
|
+
id: conceptId,
|
|
165
|
+
title: concept,
|
|
166
|
+
category: "concept",
|
|
167
|
+
content: `# ${concept}\n\n## Definition\n\n${definition}`,
|
|
168
|
+
frontmatter: {
|
|
169
|
+
created_at: now,
|
|
170
|
+
updated_at: now,
|
|
171
|
+
sources: [sourceId],
|
|
172
|
+
tags: ["concept"],
|
|
173
|
+
inbound_links: [summaryId],
|
|
174
|
+
outbound_links: [],
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
pages.push(conceptPage);
|
|
178
|
+
}
|
|
179
|
+
return pages;
|
|
180
|
+
}
|
|
181
|
+
async function ingestSource(input) {
|
|
182
|
+
try {
|
|
183
|
+
const projectPath = node_path_1.default.resolve(input.project_path);
|
|
184
|
+
const docuDir = node_path_1.default.join(projectPath, ".docuflow");
|
|
185
|
+
const sourcesDir = node_path_1.default.join(docuDir, "sources");
|
|
186
|
+
const wikiDir = node_path_1.default.join(docuDir, "wiki");
|
|
187
|
+
// Read source file
|
|
188
|
+
const sourceFile = node_path_1.default.join(sourcesDir, input.source_filename);
|
|
189
|
+
const fileRead = await (0, filesystem_1.safeReadFile)(sourceFile);
|
|
190
|
+
if (fileRead.error) {
|
|
191
|
+
return {
|
|
192
|
+
source_id: input.source_filename,
|
|
193
|
+
summary: `Error reading source: ${fileRead.error}`,
|
|
194
|
+
pages_created: [],
|
|
195
|
+
pages_updated: [],
|
|
196
|
+
entities_discovered: [],
|
|
197
|
+
contradictions: [],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const sourceContent = fileRead.content ?? "";
|
|
201
|
+
const sourceTitle = input.source_filename.replace(".md", "");
|
|
202
|
+
// Extract information
|
|
203
|
+
const extracted = extractFromMarkdown(sourceContent);
|
|
204
|
+
// Generate wiki pages
|
|
205
|
+
const wikiPages = generateWikiPages(sourceTitle, sourceTitle, extracted, sourceContent);
|
|
206
|
+
// Write all pages
|
|
207
|
+
const pagesCreated = [];
|
|
208
|
+
for (const page of wikiPages) {
|
|
209
|
+
// Determine category subdirectory - use correct plural forms
|
|
210
|
+
const pluralMap = {
|
|
211
|
+
entity: "entities",
|
|
212
|
+
concept: "concepts",
|
|
213
|
+
timeline: "timelines",
|
|
214
|
+
synthesis: "syntheses",
|
|
215
|
+
};
|
|
216
|
+
const categoryDir = node_path_1.default.join(wikiDir, pluralMap[page.category] || page.category + "s");
|
|
217
|
+
await (0, filesystem_1.ensureDir)(categoryDir);
|
|
218
|
+
// Create page file with frontmatter
|
|
219
|
+
const frontmatterYaml = `---
|
|
220
|
+
created_at: ${page.frontmatter.created_at}
|
|
221
|
+
updated_at: ${page.frontmatter.updated_at}
|
|
222
|
+
sources: ${JSON.stringify(page.frontmatter.sources)}
|
|
223
|
+
tags: ${JSON.stringify(page.frontmatter.tags)}
|
|
224
|
+
inbound_links: ${JSON.stringify(page.frontmatter.inbound_links)}
|
|
225
|
+
outbound_links: ${JSON.stringify(page.frontmatter.outbound_links)}
|
|
226
|
+
---
|
|
227
|
+
`;
|
|
228
|
+
const pageContent = frontmatterYaml + "\n" + page.content;
|
|
229
|
+
const pageFile = node_path_1.default.join(categoryDir, `${page.id}.md`);
|
|
230
|
+
await (0, filesystem_1.writeFileAtomic)(pageFile, pageContent);
|
|
231
|
+
pagesCreated.push(page.id);
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
source_id: sourceTitle,
|
|
235
|
+
summary: extracted.summary,
|
|
236
|
+
pages_created: pagesCreated,
|
|
237
|
+
pages_updated: [],
|
|
238
|
+
entities_discovered: extracted.entities.map((e) => e.name),
|
|
239
|
+
contradictions: [],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
return {
|
|
244
|
+
source_id: input.source_filename,
|
|
245
|
+
summary: `Ingest failed: ${e?.message ?? String(e)}`,
|
|
246
|
+
pages_created: [],
|
|
247
|
+
pages_updated: [],
|
|
248
|
+
entities_discovered: [],
|
|
249
|
+
contradictions: [],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|