@doquflow/server 0.2.0 → 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.
@@ -0,0 +1,222 @@
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
+ * Simple markdown-based entity extraction.
11
+ * Looks for patterns like:
12
+ * - **Entity:** or ### Entity Headers
13
+ * - Lists of concepts
14
+ * - Tool/component mentions
15
+ */
16
+ function extractFromMarkdown(content) {
17
+ const lines = content.split("\n");
18
+ const entities = [];
19
+ const concepts = new Set();
20
+ const relationships = [];
21
+ // Extract first 500 chars as summary (non-empty)
22
+ const summary = content
23
+ .split("\n")
24
+ .filter((l) => l.trim().length > 0 && !l.startsWith("#") && !l.startsWith("```") && !l.startsWith("["))
25
+ .slice(0, 3)
26
+ .join(" ")
27
+ .substring(0, 500);
28
+ // Find headers (entities/concepts)
29
+ for (const line of lines) {
30
+ // ### Header → entity/concept
31
+ if (line.match(/^###\s+/)) {
32
+ const header = line.replace(/^###\s+/, "").trim();
33
+ if (header && !header.startsWith("[") && !header.startsWith("{")) {
34
+ entities.push({ name: header, type: "concept" });
35
+ }
36
+ }
37
+ // **bold text** → potential entity (but not arrays or JSON)
38
+ const boldMatches = line.matchAll(/\*\*([^*]+)\*\*/g);
39
+ for (const match of boldMatches) {
40
+ const text = match[1].trim();
41
+ // Skip if it looks like JSON or code
42
+ if (text.length > 2 &&
43
+ text.length < 100 &&
44
+ !text.includes("[") &&
45
+ !text.includes("{") &&
46
+ !text.includes('"') &&
47
+ !text.includes("`")) {
48
+ entities.push({ name: text, type: "entity" });
49
+ }
50
+ }
51
+ }
52
+ // Find relationship patterns like "X integrates Y" or "X depends on Y"
53
+ const relWords = [
54
+ { word: "integrates", rel: "integrates" },
55
+ { word: "depends on", rel: "depends_on" },
56
+ { word: "extends", rel: "extends" },
57
+ { word: "uses", rel: "uses" },
58
+ { word: "manages", rel: "manages" },
59
+ ];
60
+ for (const line of lines) {
61
+ for (const { word, rel } of relWords) {
62
+ const regex = new RegExp(`([A-Z][A-Za-z0-9_]+)\\s+${word}\\s+([A-Z][A-Za-z0-9_]+)`, "gi");
63
+ const matches = line.matchAll(regex);
64
+ for (const match of matches) {
65
+ relationships.push({ from: match[1], to: match[2], relation: rel });
66
+ }
67
+ }
68
+ }
69
+ // Collect concepts from lines containing "concept:", "pattern:", etc (skip arrays)
70
+ const conceptLines = lines.filter((l) => /concept|pattern|principle|approach/i.test(l) && !l.startsWith("[") && !l.startsWith("{"));
71
+ for (const line of conceptLines) {
72
+ const conceptMatch = line.match(/:\s*([^[\]{}"`.]+?)(?:\.|$)/);
73
+ if (conceptMatch) {
74
+ const concept = conceptMatch[1].trim();
75
+ if (concept && concept.length < 100) {
76
+ concepts.add(concept);
77
+ }
78
+ }
79
+ }
80
+ return {
81
+ summary,
82
+ entities: Array.from(new Set(entities.map((e) => JSON.stringify(e)))).map((s) => JSON.parse(s)),
83
+ concepts: Array.from(concepts),
84
+ relationships,
85
+ };
86
+ }
87
+ /**
88
+ * Convert a source document into a collection of wiki pages.
89
+ * For each distinct entity/concept, create a page.
90
+ */
91
+ function generateWikiPages(sourceId, sourceTitle, extracted) {
92
+ const now = new Date().toISOString();
93
+ const pages = [];
94
+ // Create summary page (synthesis)
95
+ const summaryId = `source_${sourceId.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`;
96
+ const summaryPage = {
97
+ id: summaryId,
98
+ title: `Source: ${sourceTitle}`,
99
+ category: "synthesis",
100
+ content: `# ${sourceTitle}\n\n${extracted.summary}\n\n## Key Entities\n\n${extracted.entities.map((e) => `- **${e.name}** (${e.type})`).join("\n")}`,
101
+ frontmatter: {
102
+ created_at: now,
103
+ updated_at: now,
104
+ sources: [sourceId],
105
+ tags: ["source", "ingested"],
106
+ inbound_links: [],
107
+ outbound_links: extracted.entities.map((e) => `entity_${e.name.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`),
108
+ },
109
+ };
110
+ pages.push(summaryPage);
111
+ // Create entity pages
112
+ for (const entity of extracted.entities) {
113
+ const entityId = `entity_${entity.name.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`;
114
+ const entityPage = {
115
+ id: entityId,
116
+ title: entity.name,
117
+ category: "entity",
118
+ content: `# ${entity.name}\n\n**Type:** ${entity.type}\n\n## Overview\n\nIntroduced in: ${sourceTitle}`,
119
+ frontmatter: {
120
+ created_at: now,
121
+ updated_at: now,
122
+ sources: [sourceId],
123
+ tags: [entity.type],
124
+ inbound_links: [summaryId],
125
+ outbound_links: [],
126
+ },
127
+ };
128
+ pages.push(entityPage);
129
+ }
130
+ // Create concept pages
131
+ for (const concept of extracted.concepts) {
132
+ const conceptId = `concept_${concept.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`;
133
+ const conceptPage = {
134
+ id: conceptId,
135
+ title: concept,
136
+ category: "concept",
137
+ content: `# ${concept}\n\n**Introduced in:** ${sourceTitle}\n\n## Definition\n\nTo be expanded with additional sources.`,
138
+ frontmatter: {
139
+ created_at: now,
140
+ updated_at: now,
141
+ sources: [sourceId],
142
+ tags: ["concept"],
143
+ inbound_links: [summaryId],
144
+ outbound_links: [],
145
+ },
146
+ };
147
+ pages.push(conceptPage);
148
+ }
149
+ return pages;
150
+ }
151
+ async function ingestSource(input) {
152
+ try {
153
+ const projectPath = node_path_1.default.resolve(input.project_path);
154
+ const docuDir = node_path_1.default.join(projectPath, ".docuflow");
155
+ const sourcesDir = node_path_1.default.join(docuDir, "sources");
156
+ const wikiDir = node_path_1.default.join(docuDir, "wiki");
157
+ // Read source file
158
+ const sourceFile = node_path_1.default.join(sourcesDir, input.source_filename);
159
+ const fileRead = await (0, filesystem_1.safeReadFile)(sourceFile);
160
+ if (fileRead.error) {
161
+ return {
162
+ source_id: input.source_filename,
163
+ summary: `Error reading source: ${fileRead.error}`,
164
+ pages_created: [],
165
+ pages_updated: [],
166
+ entities_discovered: [],
167
+ contradictions: [],
168
+ };
169
+ }
170
+ const sourceContent = fileRead.content ?? "";
171
+ const sourceTitle = input.source_filename.replace(".md", "");
172
+ // Extract information
173
+ const extracted = extractFromMarkdown(sourceContent);
174
+ // Generate wiki pages
175
+ const wikiPages = generateWikiPages(sourceTitle, sourceTitle, extracted);
176
+ // Write all pages
177
+ const pagesCreated = [];
178
+ for (const page of wikiPages) {
179
+ // Determine category subdirectory - use correct plural forms
180
+ const pluralMap = {
181
+ entity: "entities",
182
+ concept: "concepts",
183
+ timeline: "timelines",
184
+ synthesis: "syntheses",
185
+ };
186
+ const categoryDir = node_path_1.default.join(wikiDir, pluralMap[page.category] || page.category + "s");
187
+ await (0, filesystem_1.ensureDir)(categoryDir);
188
+ // Create page file with frontmatter
189
+ const frontmatterYaml = `---
190
+ created_at: ${page.frontmatter.created_at}
191
+ updated_at: ${page.frontmatter.updated_at}
192
+ sources: ${JSON.stringify(page.frontmatter.sources)}
193
+ tags: ${JSON.stringify(page.frontmatter.tags)}
194
+ inbound_links: ${JSON.stringify(page.frontmatter.inbound_links)}
195
+ outbound_links: ${JSON.stringify(page.frontmatter.outbound_links)}
196
+ ---
197
+ `;
198
+ const pageContent = frontmatterYaml + "\n" + page.content;
199
+ const pageFile = node_path_1.default.join(categoryDir, `${page.id}.md`);
200
+ await (0, filesystem_1.writeFileAtomic)(pageFile, pageContent);
201
+ pagesCreated.push(page.id);
202
+ }
203
+ return {
204
+ source_id: sourceTitle,
205
+ summary: extracted.summary,
206
+ pages_created: pagesCreated,
207
+ pages_updated: [],
208
+ entities_discovered: extracted.entities.map((e) => e.name),
209
+ contradictions: [],
210
+ };
211
+ }
212
+ catch (e) {
213
+ return {
214
+ source_id: input.source_filename,
215
+ summary: `Ingest failed: ${e?.message ?? String(e)}`,
216
+ pages_created: [],
217
+ pages_updated: [],
218
+ entities_discovered: [],
219
+ contradictions: [],
220
+ };
221
+ }
222
+ }
@@ -0,0 +1,346 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.lintWiki = lintWiki;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ function parsePageMetadata(content) {
40
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
41
+ if (!frontmatterMatch) {
42
+ return { metadata: null, body: content };
43
+ }
44
+ const frontmatterStr = frontmatterMatch[1];
45
+ const body = frontmatterMatch[2];
46
+ const metadata = {
47
+ created_at: extractYamlField(frontmatterStr, "created_at") || new Date().toISOString(),
48
+ updated_at: extractYamlField(frontmatterStr, "updated_at") || new Date().toISOString(),
49
+ sources: parseYamlArray(frontmatterStr, "sources") || [],
50
+ tags: parseYamlArray(frontmatterStr, "tags") || [],
51
+ inbound_links: parseYamlArray(frontmatterStr, "inbound_links") || [],
52
+ outbound_links: parseYamlArray(frontmatterStr, "outbound_links") || [],
53
+ };
54
+ return { metadata, body };
55
+ }
56
+ function extractYamlField(yaml, field) {
57
+ const regex = new RegExp(`^${field}:\\s*(.+)$`, "m");
58
+ const match = yaml.match(regex);
59
+ return match ? match[1].trim() : null;
60
+ }
61
+ function parseYamlArray(yaml, field) {
62
+ const regex = new RegExp(`^${field}:\\s*\\[(.+)\\]$`, "m");
63
+ const match = yaml.match(regex);
64
+ if (!match)
65
+ return [];
66
+ return match[1].split(",").map((s) => s.trim().replace(/['"]/g, ""));
67
+ }
68
+ function extractLinks(content) {
69
+ const linkRegex = /\[([^\]]+)\]\(\.\/([^)]+)\)/g;
70
+ const links = [];
71
+ let match;
72
+ while ((match = linkRegex.exec(content)) !== null) {
73
+ links.push(match[2].replace(".md", ""));
74
+ }
75
+ return links;
76
+ }
77
+ function findOrphanPages(wikiPath, allPageIds) {
78
+ const issues = [];
79
+ for (const pageId of allPageIds) {
80
+ // Pages with no inbound links are orphans
81
+ const filePath = path.join(wikiPath, `${pageId}.md`);
82
+ if (!fs.existsSync(filePath))
83
+ continue;
84
+ const content = fs.readFileSync(filePath, "utf-8");
85
+ const { metadata } = parsePageMetadata(content);
86
+ if (metadata && metadata.inbound_links.length === 0) {
87
+ issues.push({
88
+ type: "orphan",
89
+ page_id: pageId,
90
+ page_title: extractPageTitle(content),
91
+ severity: "medium",
92
+ detail: `Page has no inbound links from other wiki pages`,
93
+ suggestion: `Consider linking this page from related entity or concept pages, or remove if not needed.`,
94
+ });
95
+ }
96
+ }
97
+ return issues;
98
+ }
99
+ function findStalePages(wikiPath, allPageIds) {
100
+ const issues = [];
101
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
102
+ for (const pageId of allPageIds) {
103
+ const filePath = path.join(wikiPath, `${pageId}.md`);
104
+ if (!fs.existsSync(filePath))
105
+ continue;
106
+ const content = fs.readFileSync(filePath, "utf-8");
107
+ const { metadata } = parsePageMetadata(content);
108
+ if (metadata) {
109
+ const updatedAt = new Date(metadata.updated_at);
110
+ if (updatedAt < thirtyDaysAgo) {
111
+ issues.push({
112
+ type: "stale",
113
+ page_id: pageId,
114
+ page_title: extractPageTitle(content),
115
+ severity: "low",
116
+ detail: `Page last updated ${Math.floor((Date.now() - updatedAt.getTime()) / (24 * 60 * 60 * 1000))} days ago`,
117
+ suggestion: `Consider reviewing and updating if new information is available.`,
118
+ });
119
+ }
120
+ }
121
+ }
122
+ return issues;
123
+ }
124
+ function findMissingReferences(wikiPath, allPageIds) {
125
+ const issues = [];
126
+ for (const pageId of allPageIds) {
127
+ const filePath = path.join(wikiPath, `${pageId}.md`);
128
+ if (!fs.existsSync(filePath))
129
+ continue;
130
+ const content = fs.readFileSync(filePath, "utf-8");
131
+ const { body } = parsePageMetadata(content);
132
+ const links = extractLinks(body);
133
+ for (const link of links) {
134
+ if (!allPageIds.has(link)) {
135
+ issues.push({
136
+ type: "missing_ref",
137
+ page_id: pageId,
138
+ page_title: extractPageTitle(content),
139
+ severity: "high",
140
+ detail: `References non-existent page: ${link}`,
141
+ suggestion: `Check if ${link} should be created or if the reference should be updated.`,
142
+ });
143
+ }
144
+ }
145
+ }
146
+ return issues;
147
+ }
148
+ function findMetadataGaps(wikiPath, allPageIds) {
149
+ const issues = [];
150
+ for (const pageId of allPageIds) {
151
+ const filePath = path.join(wikiPath, `${pageId}.md`);
152
+ if (!fs.existsSync(filePath))
153
+ continue;
154
+ const content = fs.readFileSync(filePath, "utf-8");
155
+ const { metadata } = parsePageMetadata(content);
156
+ if (!metadata) {
157
+ issues.push({
158
+ type: "metadata_gap",
159
+ page_id: pageId,
160
+ page_title: extractPageTitle(content),
161
+ severity: "medium",
162
+ detail: `Page missing YAML frontmatter with metadata`,
163
+ suggestion: `Add frontmatter with created_at, updated_at, sources, tags, and links.`,
164
+ });
165
+ }
166
+ else {
167
+ if (!metadata.created_at || !metadata.updated_at) {
168
+ issues.push({
169
+ type: "metadata_gap",
170
+ page_id: pageId,
171
+ page_title: extractPageTitle(content),
172
+ severity: "low",
173
+ detail: `Page missing timestamp metadata (created_at or updated_at)`,
174
+ suggestion: `Ensure all pages have creation and update timestamps in frontmatter.`,
175
+ });
176
+ }
177
+ if (metadata.sources.length === 0) {
178
+ issues.push({
179
+ type: "metadata_gap",
180
+ page_id: pageId,
181
+ page_title: extractPageTitle(content),
182
+ severity: "low",
183
+ detail: `Page has no source references`,
184
+ suggestion: `Add source document references to improve traceability.`,
185
+ });
186
+ }
187
+ }
188
+ }
189
+ return issues;
190
+ }
191
+ function findContradictions(wikiPath) {
192
+ const issues = [];
193
+ const pageContents = new Map();
194
+ // Load all pages
195
+ for (const categoryDir of fs.readdirSync(wikiPath)) {
196
+ const categoryPath = path.join(wikiPath, categoryDir);
197
+ if (!fs.statSync(categoryPath).isDirectory())
198
+ continue;
199
+ for (const file of fs.readdirSync(categoryPath)) {
200
+ if (!file.endsWith(".md"))
201
+ continue;
202
+ const pageId = file.replace(".md", "");
203
+ const content = fs.readFileSync(path.join(categoryPath, file), "utf-8");
204
+ const title = extractPageTitle(content);
205
+ pageContents.set(pageId, { title, content });
206
+ }
207
+ }
208
+ // Look for contradiction patterns
209
+ const contradictionPatterns = [
210
+ { pattern: /should be|must be/i, opposite: /should not be|must not be/i },
211
+ { pattern: /recommended/i, opposite: /not recommended/i },
212
+ { pattern: /required/i, opposite: /optional/i },
213
+ ];
214
+ const contentArray = Array.from(pageContents.values()).map((p) => p.content);
215
+ for (let i = 0; i < contentArray.length; i++) {
216
+ for (let j = i + 1; j < contentArray.length; j++) {
217
+ for (const { pattern, opposite } of contradictionPatterns) {
218
+ if (pattern.test(contentArray[i]) && opposite.test(contentArray[j])) {
219
+ const key1 = Array.from(pageContents.entries()).find(([, v]) => v.content === contentArray[i])?.[0];
220
+ const key2 = Array.from(pageContents.entries()).find(([, v]) => v.content === contentArray[j])?.[0];
221
+ if (key1 && key2) {
222
+ issues.push({
223
+ type: "contradiction",
224
+ page_id: key1,
225
+ page_title: pageContents.get(key1)?.title || key1,
226
+ severity: "high",
227
+ detail: `Potential contradiction with ${key2}`,
228
+ suggestion: `Review both pages and resolve conflicting statements.`,
229
+ });
230
+ }
231
+ }
232
+ }
233
+ }
234
+ }
235
+ return issues;
236
+ }
237
+ function extractPageTitle(content) {
238
+ const match = content.match(/^#+\s+(.+)$/m);
239
+ return match ? match[1].trim() : "Untitled";
240
+ }
241
+ function calculateHealthScore(result) {
242
+ const { issues_found, total_pages } = result;
243
+ if (total_pages === 0)
244
+ return 100;
245
+ const issueWeights = {
246
+ high: 10,
247
+ medium: 5,
248
+ low: 2,
249
+ };
250
+ let penalty = 0;
251
+ for (const issue of issues_found) {
252
+ penalty += issueWeights[issue.severity];
253
+ }
254
+ const maxPenalty = total_pages * 10;
255
+ const score = Math.max(0, 100 - (penalty / maxPenalty) * 100);
256
+ return Math.round(score);
257
+ }
258
+ function generateRecommendations(result) {
259
+ const recommendations = [];
260
+ if (result.metrics.orphan_pages > 0) {
261
+ recommendations.push(`${result.metrics.orphan_pages} orphan pages found. Consider linking them or removing if outdated.`);
262
+ }
263
+ if (result.metrics.missing_refs > 0) {
264
+ recommendations.push(`${result.metrics.missing_refs} broken references found. Update or create missing pages.`);
265
+ }
266
+ if (result.metrics.stale_pages > 0) {
267
+ recommendations.push(`${result.metrics.stale_pages} pages not updated in 30+ days. Review and refresh content.`);
268
+ }
269
+ if (result.metrics.metadata_gaps > 0) {
270
+ recommendations.push(`${result.metrics.metadata_gaps} pages have metadata gaps. Add source references and timestamps.`);
271
+ }
272
+ if (result.metrics.contradictions > 0) {
273
+ recommendations.push(`${result.metrics.contradictions} potential contradictions found. Review and resolve.`);
274
+ }
275
+ if (result.health_score >= 90) {
276
+ recommendations.push("✓ Wiki is in excellent health! Continue maintaining current standards.");
277
+ }
278
+ else if (result.health_score >= 70) {
279
+ recommendations.push("Wiki health is good. Address high-severity issues to improve further.");
280
+ }
281
+ else if (result.health_score < 50) {
282
+ recommendations.push("⚠ Wiki needs maintenance. Prioritize fixing high-severity issues.");
283
+ }
284
+ return recommendations;
285
+ }
286
+ async function lintWiki(params) {
287
+ const { project_path, check_type = "all" } = params;
288
+ const wikiPath = path.join(project_path, ".docuflow", "wiki");
289
+ if (!fs.existsSync(wikiPath)) {
290
+ throw new Error(`Wiki not found at ${wikiPath}`);
291
+ }
292
+ // Collect all page IDs
293
+ const allPageIds = new Set();
294
+ for (const categoryDir of fs.readdirSync(wikiPath)) {
295
+ const categoryPath = path.join(wikiPath, categoryDir);
296
+ if (!fs.statSync(categoryPath).isDirectory())
297
+ continue;
298
+ for (const file of fs.readdirSync(categoryPath)) {
299
+ if (file.endsWith(".md")) {
300
+ allPageIds.add(file.replace(".md", ""));
301
+ }
302
+ }
303
+ }
304
+ // Run checks
305
+ let issues = [];
306
+ if (check_type === "all" || check_type === "orphans") {
307
+ issues.push(...findOrphanPages(wikiPath, allPageIds));
308
+ }
309
+ if (check_type === "all" || check_type === "contradictions") {
310
+ issues.push(...findContradictions(wikiPath));
311
+ }
312
+ if (check_type === "all" || check_type === "stale") {
313
+ issues.push(...findStalePages(wikiPath, allPageIds));
314
+ }
315
+ if (check_type === "all" || check_type === "metadata") {
316
+ issues.push(...findMissingReferences(wikiPath, allPageIds));
317
+ issues.push(...findMetadataGaps(wikiPath, allPageIds));
318
+ }
319
+ // Calculate metrics
320
+ const metrics = {
321
+ orphan_pages: issues.filter((i) => i.type === "orphan").length,
322
+ contradictions: issues.filter((i) => i.type === "contradiction").length,
323
+ stale_pages: issues.filter((i) => i.type === "stale").length,
324
+ missing_refs: issues.filter((i) => i.type === "missing_ref").length,
325
+ metadata_gaps: issues.filter((i) => i.type === "metadata_gap").length,
326
+ };
327
+ // Build result
328
+ const result = {
329
+ total_pages: allPageIds.size,
330
+ issues_found: issues,
331
+ metrics,
332
+ recommendations: [],
333
+ health_score: 0,
334
+ };
335
+ // Calculate health score and recommendations
336
+ result.health_score = calculateHealthScore(result);
337
+ result.recommendations = generateRecommendations(result);
338
+ // Append to log.md
339
+ const logPath = path.join(project_path, ".docuflow", "log.md");
340
+ if (fs.existsSync(logPath)) {
341
+ const timestamp = new Date().toISOString();
342
+ const logEntry = `\n## [${timestamp}] lint | Wiki lint check completed\n\n- Pages checked: ${result.total_pages}\n- Issues found: ${result.issues_found.length}\n- Health score: ${result.health_score}%\n- High severity: ${metrics.contradictions + metrics.missing_refs}\n`;
343
+ fs.appendFileSync(logPath, logEntry);
344
+ }
345
+ return result;
346
+ }
@@ -0,0 +1,108 @@
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.listWiki = listWiki;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const promises_1 = __importDefault(require("node:fs/promises"));
9
+ const filesystem_1 = require("../filesystem");
10
+ /**
11
+ * Parse frontmatter from markdown
12
+ */
13
+ function parseFrontmatter(content) {
14
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
15
+ if (!match)
16
+ return {};
17
+ const yaml = match[1];
18
+ const result = {};
19
+ for (const line of yaml.split("\n")) {
20
+ if (!line.trim())
21
+ continue;
22
+ const [key, ...valueParts] = line.split(":");
23
+ const value = valueParts.join(":").trim();
24
+ try {
25
+ if (value.startsWith("[") || value.startsWith("{")) {
26
+ result[key.trim()] = JSON.parse(value);
27
+ }
28
+ else {
29
+ result[key.trim()] = value;
30
+ }
31
+ }
32
+ catch {
33
+ result[key.trim()] = value;
34
+ }
35
+ }
36
+ return result;
37
+ }
38
+ /**
39
+ * Extract title from markdown
40
+ */
41
+ function extractTitle(content) {
42
+ const match = content.match(/^#\s+(.+?)$/m);
43
+ return match ? match[1].trim() : "Untitled";
44
+ }
45
+ async function listWiki(input) {
46
+ try {
47
+ const projectPath = node_path_1.default.resolve(input.project_path);
48
+ const docuDir = node_path_1.default.join(projectPath, ".docuflow");
49
+ const wikiDir = node_path_1.default.join(docuDir, "wiki");
50
+ const pages = [];
51
+ const categories = {};
52
+ // Build list of categories to scan
53
+ let categoriesToScan = ["entities", "concepts", "timelines", "syntheses"];
54
+ if (input.category) {
55
+ categoriesToScan = [`${input.category}s`];
56
+ }
57
+ // Scan each category
58
+ for (const categoryDir of categoriesToScan) {
59
+ const fullCategoryPath = node_path_1.default.join(wikiDir, categoryDir);
60
+ const category = categoryDir.replace("s", ""); // entities → entity
61
+ try {
62
+ const files = await promises_1.default.readdir(fullCategoryPath);
63
+ let categoryCount = 0;
64
+ for (const file of files) {
65
+ if (!file.endsWith(".md"))
66
+ continue;
67
+ const filePath = node_path_1.default.join(fullCategoryPath, file);
68
+ const read = await (0, filesystem_1.safeReadFile)(filePath);
69
+ if (read.error || read.binary || !read.content)
70
+ continue;
71
+ const fm = parseFrontmatter(read.content);
72
+ const title = extractTitle(read.content);
73
+ const pageId = file.replace(".md", "");
74
+ pages.push({
75
+ id: pageId,
76
+ title,
77
+ category,
78
+ path: node_path_1.default.relative(docuDir, filePath),
79
+ created_at: fm.created_at ?? new Date().toISOString(),
80
+ updated_at: fm.updated_at ?? new Date().toISOString(),
81
+ sources: fm.sources ?? [],
82
+ tags: fm.tags ?? [],
83
+ });
84
+ categoryCount++;
85
+ }
86
+ if (categoryCount > 0) {
87
+ categories[category] = categoryCount;
88
+ }
89
+ }
90
+ catch (e) {
91
+ // Category directory may not exist yet
92
+ }
93
+ }
94
+ return {
95
+ total_pages: pages.length,
96
+ pages: pages.sort((a, b) => a.title.localeCompare(b.title)),
97
+ categories,
98
+ };
99
+ }
100
+ catch (e) {
101
+ return {
102
+ total_pages: 0,
103
+ pages: [],
104
+ categories: {},
105
+ error: e?.message ?? String(e),
106
+ };
107
+ }
108
+ }