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