@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,342 @@
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(pageMap) {
78
+ const issues = [];
79
+ for (const [pageId, filePath] of pageMap) {
80
+ if (!fs.existsSync(filePath))
81
+ continue;
82
+ const content = fs.readFileSync(filePath, "utf-8");
83
+ const { metadata } = parsePageMetadata(content);
84
+ if (metadata && metadata.inbound_links.length === 0) {
85
+ issues.push({
86
+ type: "orphan",
87
+ page_id: pageId,
88
+ page_title: extractPageTitle(content),
89
+ severity: "medium",
90
+ detail: `Page has no inbound links from other wiki pages`,
91
+ suggestion: `Consider linking this page from related entity or concept pages, or remove if not needed.`,
92
+ });
93
+ }
94
+ }
95
+ return issues;
96
+ }
97
+ function findStalePages(pageMap) {
98
+ const issues = [];
99
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
100
+ for (const [pageId, filePath] of pageMap) {
101
+ if (!fs.existsSync(filePath))
102
+ continue;
103
+ const content = fs.readFileSync(filePath, "utf-8");
104
+ const { metadata } = parsePageMetadata(content);
105
+ if (metadata) {
106
+ const updatedAt = new Date(metadata.updated_at);
107
+ if (updatedAt < thirtyDaysAgo) {
108
+ issues.push({
109
+ type: "stale",
110
+ page_id: pageId,
111
+ page_title: extractPageTitle(content),
112
+ severity: "low",
113
+ detail: `Page last updated ${Math.floor((Date.now() - updatedAt.getTime()) / (24 * 60 * 60 * 1000))} days ago`,
114
+ suggestion: `Consider reviewing and updating if new information is available.`,
115
+ });
116
+ }
117
+ }
118
+ }
119
+ return issues;
120
+ }
121
+ function findMissingReferences(pageMap) {
122
+ const issues = [];
123
+ for (const [pageId, filePath] of pageMap) {
124
+ if (!fs.existsSync(filePath))
125
+ continue;
126
+ const content = fs.readFileSync(filePath, "utf-8");
127
+ const { body } = parsePageMetadata(content);
128
+ const links = extractLinks(body);
129
+ for (const link of links) {
130
+ if (!pageMap.has(link)) {
131
+ issues.push({
132
+ type: "missing_ref",
133
+ page_id: pageId,
134
+ page_title: extractPageTitle(content),
135
+ severity: "high",
136
+ detail: `References non-existent page: ${link}`,
137
+ suggestion: `Check if ${link} should be created or if the reference should be updated.`,
138
+ });
139
+ }
140
+ }
141
+ }
142
+ return issues;
143
+ }
144
+ function findMetadataGaps(pageMap) {
145
+ const issues = [];
146
+ for (const [pageId, filePath] of pageMap) {
147
+ if (!fs.existsSync(filePath))
148
+ continue;
149
+ const content = fs.readFileSync(filePath, "utf-8");
150
+ const { metadata } = parsePageMetadata(content);
151
+ if (!metadata) {
152
+ issues.push({
153
+ type: "metadata_gap",
154
+ page_id: pageId,
155
+ page_title: extractPageTitle(content),
156
+ severity: "medium",
157
+ detail: `Page missing YAML frontmatter with metadata`,
158
+ suggestion: `Add frontmatter with created_at, updated_at, sources, tags, and links.`,
159
+ });
160
+ }
161
+ else {
162
+ if (!metadata.created_at || !metadata.updated_at) {
163
+ issues.push({
164
+ type: "metadata_gap",
165
+ page_id: pageId,
166
+ page_title: extractPageTitle(content),
167
+ severity: "low",
168
+ detail: `Page missing timestamp metadata (created_at or updated_at)`,
169
+ suggestion: `Ensure all pages have creation and update timestamps in frontmatter.`,
170
+ });
171
+ }
172
+ if (metadata.sources.length === 0) {
173
+ issues.push({
174
+ type: "metadata_gap",
175
+ page_id: pageId,
176
+ page_title: extractPageTitle(content),
177
+ severity: "low",
178
+ detail: `Page has no source references`,
179
+ suggestion: `Add source document references to improve traceability.`,
180
+ });
181
+ }
182
+ }
183
+ }
184
+ return issues;
185
+ }
186
+ function findContradictions(wikiPath) {
187
+ const issues = [];
188
+ const pageContents = new Map();
189
+ // Load all pages
190
+ for (const categoryDir of fs.readdirSync(wikiPath)) {
191
+ const categoryPath = path.join(wikiPath, categoryDir);
192
+ if (!fs.statSync(categoryPath).isDirectory())
193
+ continue;
194
+ for (const file of fs.readdirSync(categoryPath)) {
195
+ if (!file.endsWith(".md"))
196
+ continue;
197
+ const pageId = file.replace(".md", "");
198
+ const content = fs.readFileSync(path.join(categoryPath, file), "utf-8");
199
+ const title = extractPageTitle(content);
200
+ pageContents.set(pageId, { title, content });
201
+ }
202
+ }
203
+ // Look for contradiction patterns
204
+ const contradictionPatterns = [
205
+ { pattern: /should be|must be/i, opposite: /should not be|must not be/i },
206
+ { pattern: /recommended/i, opposite: /not recommended/i },
207
+ { pattern: /required/i, opposite: /optional/i },
208
+ ];
209
+ const contentArray = Array.from(pageContents.values()).map((p) => p.content);
210
+ for (let i = 0; i < contentArray.length; i++) {
211
+ for (let j = i + 1; j < contentArray.length; j++) {
212
+ for (const { pattern, opposite } of contradictionPatterns) {
213
+ if (pattern.test(contentArray[i]) && opposite.test(contentArray[j])) {
214
+ const key1 = Array.from(pageContents.entries()).find(([, v]) => v.content === contentArray[i])?.[0];
215
+ const key2 = Array.from(pageContents.entries()).find(([, v]) => v.content === contentArray[j])?.[0];
216
+ if (key1 && key2) {
217
+ issues.push({
218
+ type: "contradiction",
219
+ page_id: key1,
220
+ page_title: pageContents.get(key1)?.title || key1,
221
+ severity: "high",
222
+ detail: `Potential contradiction with ${key2}`,
223
+ suggestion: `Review both pages and resolve conflicting statements.`,
224
+ });
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ return issues;
231
+ }
232
+ function extractPageTitle(content) {
233
+ const match = content.match(/^#+\s+(.+)$/m);
234
+ return match ? match[1].trim() : "Untitled";
235
+ }
236
+ function calculateHealthScore(result) {
237
+ const { issues_found, total_pages } = result;
238
+ if (total_pages === 0)
239
+ return 100;
240
+ const issueWeights = {
241
+ high: 10,
242
+ medium: 5,
243
+ low: 2,
244
+ };
245
+ let penalty = 0;
246
+ for (const issue of issues_found) {
247
+ penalty += issueWeights[issue.severity];
248
+ }
249
+ const maxPenalty = total_pages * 10;
250
+ const score = Math.max(0, 100 - (penalty / maxPenalty) * 100);
251
+ return Math.round(score);
252
+ }
253
+ function generateRecommendations(result) {
254
+ const recommendations = [];
255
+ if (result.metrics.orphan_pages > 0) {
256
+ recommendations.push(`${result.metrics.orphan_pages} orphan pages found. Consider linking them or removing if outdated.`);
257
+ }
258
+ if (result.metrics.missing_refs > 0) {
259
+ recommendations.push(`${result.metrics.missing_refs} broken references found. Update or create missing pages.`);
260
+ }
261
+ if (result.metrics.stale_pages > 0) {
262
+ recommendations.push(`${result.metrics.stale_pages} pages not updated in 30+ days. Review and refresh content.`);
263
+ }
264
+ if (result.metrics.metadata_gaps > 0) {
265
+ recommendations.push(`${result.metrics.metadata_gaps} pages have metadata gaps. Add source references and timestamps.`);
266
+ }
267
+ if (result.metrics.contradictions > 0) {
268
+ recommendations.push(`${result.metrics.contradictions} potential contradictions found. Review and resolve.`);
269
+ }
270
+ if (result.health_score >= 90) {
271
+ recommendations.push("✓ Wiki is in excellent health! Continue maintaining current standards.");
272
+ }
273
+ else if (result.health_score >= 70) {
274
+ recommendations.push("Wiki health is good. Address high-severity issues to improve further.");
275
+ }
276
+ else if (result.health_score < 50) {
277
+ recommendations.push("⚠ Wiki needs maintenance. Prioritize fixing high-severity issues.");
278
+ }
279
+ return recommendations;
280
+ }
281
+ async function lintWiki(params) {
282
+ const { project_path, check_type = "all" } = params;
283
+ const wikiPath = path.join(project_path, ".docuflow", "wiki");
284
+ if (!fs.existsSync(wikiPath)) {
285
+ throw new Error(`Wiki not found at ${wikiPath}`);
286
+ }
287
+ // Collect all page IDs mapped to their full file paths
288
+ const pageMap = new Map();
289
+ for (const categoryDir of fs.readdirSync(wikiPath)) {
290
+ const categoryPath = path.join(wikiPath, categoryDir);
291
+ if (!fs.statSync(categoryPath).isDirectory())
292
+ continue;
293
+ for (const file of fs.readdirSync(categoryPath)) {
294
+ if (file.endsWith(".md")) {
295
+ const pageId = file.replace(".md", "");
296
+ pageMap.set(pageId, path.join(categoryPath, file));
297
+ }
298
+ }
299
+ }
300
+ // Run checks
301
+ let issues = [];
302
+ if (check_type === "all" || check_type === "orphans") {
303
+ issues.push(...findOrphanPages(pageMap));
304
+ }
305
+ if (check_type === "all" || check_type === "contradictions") {
306
+ issues.push(...findContradictions(wikiPath));
307
+ }
308
+ if (check_type === "all" || check_type === "stale") {
309
+ issues.push(...findStalePages(pageMap));
310
+ }
311
+ if (check_type === "all" || check_type === "metadata") {
312
+ issues.push(...findMissingReferences(pageMap));
313
+ issues.push(...findMetadataGaps(pageMap));
314
+ }
315
+ // Calculate metrics
316
+ const metrics = {
317
+ orphan_pages: issues.filter((i) => i.type === "orphan").length,
318
+ contradictions: issues.filter((i) => i.type === "contradiction").length,
319
+ stale_pages: issues.filter((i) => i.type === "stale").length,
320
+ missing_refs: issues.filter((i) => i.type === "missing_ref").length,
321
+ metadata_gaps: issues.filter((i) => i.type === "metadata_gap").length,
322
+ };
323
+ // Build result
324
+ const result = {
325
+ total_pages: pageMap.size,
326
+ issues_found: issues,
327
+ metrics,
328
+ recommendations: [],
329
+ health_score: 0,
330
+ };
331
+ // Calculate health score and recommendations
332
+ result.health_score = calculateHealthScore(result);
333
+ result.recommendations = generateRecommendations(result);
334
+ // Append to log.md
335
+ const logPath = path.join(project_path, ".docuflow", "log.md");
336
+ if (fs.existsSync(logPath)) {
337
+ const timestamp = new Date().toISOString();
338
+ 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`;
339
+ fs.appendFileSync(logPath, logEntry);
340
+ }
341
+ return result;
342
+ }
@@ -0,0 +1,129 @@
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
+ const PLURAL_TO_SINGULAR = {
11
+ entities: "entity",
12
+ concepts: "concept",
13
+ timelines: "timeline",
14
+ syntheses: "synthesis",
15
+ };
16
+ const SINGULAR_TO_PLURAL = {
17
+ entity: "entities",
18
+ concept: "concepts",
19
+ timeline: "timelines",
20
+ synthesis: "syntheses",
21
+ };
22
+ const STALE_DAYS = 30;
23
+ /**
24
+ * Parse frontmatter from markdown
25
+ */
26
+ function parseFrontmatter(content) {
27
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
28
+ if (!match)
29
+ return {};
30
+ const yaml = match[1];
31
+ const result = {};
32
+ for (const line of yaml.split("\n")) {
33
+ if (!line.trim())
34
+ continue;
35
+ const [key, ...valueParts] = line.split(":");
36
+ const value = valueParts.join(":").trim();
37
+ try {
38
+ if (value.startsWith("[") || value.startsWith("{")) {
39
+ result[key.trim()] = JSON.parse(value);
40
+ }
41
+ else {
42
+ result[key.trim()] = value;
43
+ }
44
+ }
45
+ catch {
46
+ result[key.trim()] = value;
47
+ }
48
+ }
49
+ return result;
50
+ }
51
+ /**
52
+ * Extract title from markdown
53
+ */
54
+ function extractTitle(content) {
55
+ const match = content.match(/^#\s+(.+?)$/m);
56
+ return match ? match[1].trim() : "Untitled";
57
+ }
58
+ async function listWiki(input) {
59
+ try {
60
+ const projectPath = node_path_1.default.resolve(input.project_path);
61
+ const docuDir = node_path_1.default.join(projectPath, ".docuflow");
62
+ const wikiDir = node_path_1.default.join(docuDir, "wiki");
63
+ const pages = [];
64
+ const categories = {};
65
+ const now = Date.now();
66
+ // Build list of categories to scan — always use correct plural directory names
67
+ let categoriesToScan = ["entities", "concepts", "timelines", "syntheses"];
68
+ if (input.category) {
69
+ const pluralDir = SINGULAR_TO_PLURAL[input.category] ?? `${input.category}s`;
70
+ categoriesToScan = [pluralDir];
71
+ }
72
+ // Scan each category
73
+ for (const categoryDir of categoriesToScan) {
74
+ const fullCategoryPath = node_path_1.default.join(wikiDir, categoryDir);
75
+ const category = PLURAL_TO_SINGULAR[categoryDir] ?? categoryDir;
76
+ try {
77
+ const files = await promises_1.default.readdir(fullCategoryPath);
78
+ let categoryCount = 0;
79
+ for (const file of files) {
80
+ if (!file.endsWith(".md"))
81
+ continue;
82
+ const filePath = node_path_1.default.join(fullCategoryPath, file);
83
+ const read = await (0, filesystem_1.safeReadFile)(filePath);
84
+ if (read.error || read.binary || !read.content)
85
+ continue;
86
+ const fm = parseFrontmatter(read.content);
87
+ const title = extractTitle(read.content);
88
+ const pageId = file.replace(".md", "");
89
+ const updatedAt = fm.updated_at ?? new Date().toISOString();
90
+ const updatedMs = new Date(updatedAt).getTime();
91
+ const stale = !isNaN(updatedMs) && (now - updatedMs) > STALE_DAYS * 86_400_000;
92
+ pages.push({
93
+ id: pageId,
94
+ title,
95
+ category,
96
+ path: node_path_1.default.relative(docuDir, filePath),
97
+ created_at: fm.created_at ?? new Date().toISOString(),
98
+ updated_at: updatedAt,
99
+ sources: fm.sources ?? [],
100
+ tags: fm.tags ?? [],
101
+ stale,
102
+ });
103
+ categoryCount++;
104
+ }
105
+ if (categoryCount > 0) {
106
+ categories[category] = categoryCount;
107
+ }
108
+ }
109
+ catch (e) {
110
+ // Category directory may not exist yet
111
+ }
112
+ }
113
+ return {
114
+ total_pages: pages.length,
115
+ stale_pages: pages.filter((p) => p.stale).length,
116
+ pages: pages.sort((a, b) => a.title.localeCompare(b.title)),
117
+ categories,
118
+ };
119
+ }
120
+ catch (e) {
121
+ return {
122
+ total_pages: 0,
123
+ stale_pages: 0,
124
+ pages: [],
125
+ categories: {},
126
+ error: e?.message ?? String(e),
127
+ };
128
+ }
129
+ }