@doquflow/server 1.6.0 → 2.0.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/README.md +24 -72
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -328
- package/package.json +5 -7
- package/dist/category-dir.js +0 -12
- package/dist/extractor-rules.js +0 -166
- package/dist/extractor-stoplist.js +0 -89
- package/dist/extractor.js +0 -171
- package/dist/filesystem.js +0 -132
- package/dist/language-map.js +0 -58
- package/dist/tools/answer-synthesis.js +0 -189
- package/dist/tools/build-graph.js +0 -111
- package/dist/tools/generate-dependency-graph.js +0 -162
- package/dist/tools/get-schema-guidance.js +0 -213
- package/dist/tools/ingest-source.js +0 -272
- package/dist/tools/lint-wiki.js +0 -342
- package/dist/tools/list-modules.js +0 -50
- package/dist/tools/list-wiki.js +0 -198
- package/dist/tools/preview-generation.js +0 -228
- package/dist/tools/query-wiki.js +0 -67
- package/dist/tools/read-module.js +0 -53
- package/dist/tools/read-specs.js +0 -39
- package/dist/tools/save-answer-as-page.js +0 -91
- package/dist/tools/update-index.js +0 -157
- package/dist/tools/wiki-search.js +0 -157
- package/dist/tools/write-spec.js +0 -55
- package/dist/types.js +0 -2
|
@@ -1,272 +0,0 @@
|
|
|
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
|
-
const category_dir_1 = require("../category-dir");
|
|
10
|
-
const extractor_rules_1 = require("../extractor-rules");
|
|
11
|
-
/**
|
|
12
|
-
* Find the first paragraph in the source that mentions the given name.
|
|
13
|
-
* Returns cleaned text (stripped of markdown syntax), up to 400 chars.
|
|
14
|
-
*/
|
|
15
|
-
function findContextParagraph(content, name) {
|
|
16
|
-
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17
|
-
const re = new RegExp(`\\b${escaped}\\b`, "i");
|
|
18
|
-
const paragraphs = content.split(/\n{2,}/);
|
|
19
|
-
for (const para of paragraphs) {
|
|
20
|
-
if (!re.test(para))
|
|
21
|
-
continue;
|
|
22
|
-
const clean = para
|
|
23
|
-
.replace(/^#+\s*/gm, "")
|
|
24
|
-
.replace(/\*\*?([^*]+)\*\*?/g, "$1")
|
|
25
|
-
.replace(/`([^`]+)`/g, "$1")
|
|
26
|
-
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
27
|
-
.trim();
|
|
28
|
-
if (clean.length > 20)
|
|
29
|
-
return clean.substring(0, 400);
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Simple markdown-based entity extraction.
|
|
35
|
-
* Looks for patterns like:
|
|
36
|
-
* - **Entity:** or ### Entity Headers
|
|
37
|
-
* - Lists of concepts
|
|
38
|
-
* - Tool/component mentions
|
|
39
|
-
*/
|
|
40
|
-
function extractFromMarkdown(content) {
|
|
41
|
-
const lines = content.split("\n");
|
|
42
|
-
const entities = [];
|
|
43
|
-
const concepts = new Set();
|
|
44
|
-
const relationships = [];
|
|
45
|
-
// Extract first 500 chars as summary (non-empty)
|
|
46
|
-
const summary = content
|
|
47
|
-
.split("\n")
|
|
48
|
-
.filter((l) => l.trim().length > 0 && !l.startsWith("#") && !l.startsWith("```") && !l.startsWith("["))
|
|
49
|
-
.slice(0, 3)
|
|
50
|
-
.join(" ")
|
|
51
|
-
.substring(0, 500);
|
|
52
|
-
// Find headers (entities/concepts)
|
|
53
|
-
for (let i = 0; i < lines.length; i++) {
|
|
54
|
-
const line = lines[i];
|
|
55
|
-
// ## / ### / #### Header → entity/concept candidate
|
|
56
|
-
const headingMatch = line.match(/^#{2,4}\s+(.+)/);
|
|
57
|
-
if (headingMatch) {
|
|
58
|
-
const header = headingMatch[1].trim();
|
|
59
|
-
if (header && !header.startsWith("[") && !header.startsWith("{")) {
|
|
60
|
-
// Gather surrounding context: lines after this heading until next heading or blank+blank
|
|
61
|
-
const contextLines = [];
|
|
62
|
-
for (let j = i + 1; j < Math.min(i + 8, lines.length); j++) {
|
|
63
|
-
const l = lines[j];
|
|
64
|
-
if (/^#{1,4}\s/.test(l))
|
|
65
|
-
break;
|
|
66
|
-
contextLines.push(l);
|
|
67
|
-
}
|
|
68
|
-
const context = contextLines.join(" ").trim();
|
|
69
|
-
const candidate = { name: header, type: "concept", source: "heading", context };
|
|
70
|
-
if ((0, extractor_rules_1.passesEntityRules)(candidate).ok) {
|
|
71
|
-
entities.push({ name: header, type: "concept" });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
// **bold text** → potential entity candidate (but not arrays or JSON)
|
|
76
|
-
const boldMatches = line.matchAll(/\*\*([^*]+)\*\*/g);
|
|
77
|
-
for (const match of boldMatches) {
|
|
78
|
-
const text = match[1].trim();
|
|
79
|
-
// Skip if it looks like JSON or code
|
|
80
|
-
if (text.length > 2 &&
|
|
81
|
-
text.length < 100 &&
|
|
82
|
-
!text.includes("[") &&
|
|
83
|
-
!text.includes("{") &&
|
|
84
|
-
!text.includes('"') &&
|
|
85
|
-
!text.includes("`")) {
|
|
86
|
-
// Gather surrounding context: the current paragraph
|
|
87
|
-
const paraLines = [line];
|
|
88
|
-
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
89
|
-
if (lines[j].trim() === "")
|
|
90
|
-
break;
|
|
91
|
-
paraLines.push(lines[j]);
|
|
92
|
-
}
|
|
93
|
-
const context = paraLines.join(" ").trim();
|
|
94
|
-
const candidate = { name: text, type: "entity", source: "bold", context };
|
|
95
|
-
if ((0, extractor_rules_1.passesEntityRules)(candidate).ok) {
|
|
96
|
-
entities.push({ name: text, type: "entity" });
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// Find relationship patterns like "X integrates Y" or "X depends on Y"
|
|
102
|
-
const relWords = [
|
|
103
|
-
{ word: "integrates", rel: "integrates" },
|
|
104
|
-
{ word: "depends on", rel: "depends_on" },
|
|
105
|
-
{ word: "extends", rel: "extends" },
|
|
106
|
-
{ word: "uses", rel: "uses" },
|
|
107
|
-
{ word: "manages", rel: "manages" },
|
|
108
|
-
];
|
|
109
|
-
for (const line of lines) {
|
|
110
|
-
for (const { word, rel } of relWords) {
|
|
111
|
-
const regex = new RegExp(`([A-Z][A-Za-z0-9_]+)\\s+${word}\\s+([A-Z][A-Za-z0-9_]+)`, "gi");
|
|
112
|
-
const matches = line.matchAll(regex);
|
|
113
|
-
for (const match of matches) {
|
|
114
|
-
relationships.push({ from: match[1], to: match[2], relation: rel });
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
// Collect concepts from lines containing "concept:", "pattern:", etc (skip arrays)
|
|
119
|
-
const conceptLines = lines.filter((l) => /concept|pattern|principle|approach/i.test(l) && !l.startsWith("[") && !l.startsWith("{"));
|
|
120
|
-
for (const line of conceptLines) {
|
|
121
|
-
const conceptMatch = line.match(/:\s*([^[\]{}"`.]+?)(?:\.|$)/);
|
|
122
|
-
if (conceptMatch) {
|
|
123
|
-
const concept = conceptMatch[1].trim();
|
|
124
|
-
if (concept && concept.length < 100) {
|
|
125
|
-
concepts.add(concept);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return {
|
|
130
|
-
summary,
|
|
131
|
-
entities: Array.from(new Set(entities.map((e) => JSON.stringify(e)))).map((s) => JSON.parse(s)),
|
|
132
|
-
concepts: Array.from(concepts),
|
|
133
|
-
relationships,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Convert a source document into a collection of wiki pages.
|
|
138
|
-
* For each distinct entity/concept, create a page.
|
|
139
|
-
*/
|
|
140
|
-
function generateWikiPages(sourceId, sourceTitle, extracted, sourceContent) {
|
|
141
|
-
const now = new Date().toISOString();
|
|
142
|
-
const pages = [];
|
|
143
|
-
// Create summary page (synthesis)
|
|
144
|
-
const summaryId = `source_${sourceId.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`;
|
|
145
|
-
const summaryPage = {
|
|
146
|
-
id: summaryId,
|
|
147
|
-
title: `Source: ${sourceTitle}`,
|
|
148
|
-
category: "synthesis",
|
|
149
|
-
content: `# ${sourceTitle}\n\n${extracted.summary}\n\n## Key Entities\n\n${extracted.entities.map((e) => `- **${e.name}** (${e.type})`).join("\n")}`,
|
|
150
|
-
frontmatter: {
|
|
151
|
-
created_at: now,
|
|
152
|
-
updated_at: now,
|
|
153
|
-
sources: [sourceId],
|
|
154
|
-
tags: ["source", "ingested"],
|
|
155
|
-
inbound_links: [],
|
|
156
|
-
outbound_links: extracted.entities.map((e) => `entity_${e.name.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`),
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
pages.push(summaryPage);
|
|
160
|
-
// Create entity pages
|
|
161
|
-
for (const entity of extracted.entities) {
|
|
162
|
-
const entityId = `entity_${entity.name.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`;
|
|
163
|
-
const contextPara = findContextParagraph(sourceContent, entity.name);
|
|
164
|
-
const overview = contextPara
|
|
165
|
-
? `${contextPara}\n\n*Introduced in: ${sourceTitle}*`
|
|
166
|
-
: `Introduced in: ${sourceTitle}`;
|
|
167
|
-
const entityPage = {
|
|
168
|
-
id: entityId,
|
|
169
|
-
title: entity.name,
|
|
170
|
-
category: "entity",
|
|
171
|
-
content: `# ${entity.name}\n\n**Type:** ${entity.type}\n\n## Overview\n\n${overview}`,
|
|
172
|
-
frontmatter: {
|
|
173
|
-
created_at: now,
|
|
174
|
-
updated_at: now,
|
|
175
|
-
sources: [sourceId],
|
|
176
|
-
tags: [entity.type],
|
|
177
|
-
inbound_links: [summaryId],
|
|
178
|
-
outbound_links: [],
|
|
179
|
-
},
|
|
180
|
-
};
|
|
181
|
-
pages.push(entityPage);
|
|
182
|
-
}
|
|
183
|
-
// Create concept pages
|
|
184
|
-
for (const concept of extracted.concepts) {
|
|
185
|
-
const conceptId = `concept_${concept.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`;
|
|
186
|
-
const contextPara = findContextParagraph(sourceContent, concept);
|
|
187
|
-
const definition = contextPara
|
|
188
|
-
? `${contextPara}\n\n*Introduced in: ${sourceTitle}*`
|
|
189
|
-
: `To be expanded with additional sources.\n\n*Introduced in: ${sourceTitle}*`;
|
|
190
|
-
const conceptPage = {
|
|
191
|
-
id: conceptId,
|
|
192
|
-
title: concept,
|
|
193
|
-
category: "concept",
|
|
194
|
-
content: `# ${concept}\n\n## Definition\n\n${definition}`,
|
|
195
|
-
frontmatter: {
|
|
196
|
-
created_at: now,
|
|
197
|
-
updated_at: now,
|
|
198
|
-
sources: [sourceId],
|
|
199
|
-
tags: ["concept"],
|
|
200
|
-
inbound_links: [summaryId],
|
|
201
|
-
outbound_links: [],
|
|
202
|
-
},
|
|
203
|
-
};
|
|
204
|
-
pages.push(conceptPage);
|
|
205
|
-
}
|
|
206
|
-
return pages;
|
|
207
|
-
}
|
|
208
|
-
async function ingestSource(input) {
|
|
209
|
-
try {
|
|
210
|
-
const projectPath = node_path_1.default.resolve(input.project_path);
|
|
211
|
-
const docuDir = node_path_1.default.join(projectPath, ".docuflow");
|
|
212
|
-
const sourcesDir = node_path_1.default.join(docuDir, "sources");
|
|
213
|
-
const wikiDir = node_path_1.default.join(docuDir, "wiki");
|
|
214
|
-
// Read source file
|
|
215
|
-
const sourceFile = node_path_1.default.join(sourcesDir, input.source_filename);
|
|
216
|
-
const fileRead = await (0, filesystem_1.safeReadFile)(sourceFile);
|
|
217
|
-
if (fileRead.error) {
|
|
218
|
-
return {
|
|
219
|
-
source_id: input.source_filename,
|
|
220
|
-
summary: `Error reading source: ${fileRead.error}`,
|
|
221
|
-
pages_created: [],
|
|
222
|
-
pages_updated: [],
|
|
223
|
-
entities_discovered: [],
|
|
224
|
-
contradictions: [],
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
const sourceContent = fileRead.content ?? "";
|
|
228
|
-
const sourceTitle = input.source_filename.replace(".md", "");
|
|
229
|
-
// Extract information
|
|
230
|
-
const extracted = extractFromMarkdown(sourceContent);
|
|
231
|
-
// Generate wiki pages
|
|
232
|
-
const wikiPages = generateWikiPages(sourceTitle, sourceTitle, extracted, sourceContent);
|
|
233
|
-
// Write all pages
|
|
234
|
-
const pagesCreated = [];
|
|
235
|
-
for (const page of wikiPages) {
|
|
236
|
-
const catDirPath = node_path_1.default.join(wikiDir, (0, category_dir_1.categoryDir)(page.category));
|
|
237
|
-
await (0, filesystem_1.ensureDir)(catDirPath);
|
|
238
|
-
// Create page file with frontmatter
|
|
239
|
-
const frontmatterYaml = `---
|
|
240
|
-
created_at: ${page.frontmatter.created_at}
|
|
241
|
-
updated_at: ${page.frontmatter.updated_at}
|
|
242
|
-
sources: ${JSON.stringify(page.frontmatter.sources)}
|
|
243
|
-
tags: ${JSON.stringify(page.frontmatter.tags)}
|
|
244
|
-
inbound_links: ${JSON.stringify(page.frontmatter.inbound_links)}
|
|
245
|
-
outbound_links: ${JSON.stringify(page.frontmatter.outbound_links)}
|
|
246
|
-
---
|
|
247
|
-
`;
|
|
248
|
-
const pageContent = frontmatterYaml + "\n" + page.content;
|
|
249
|
-
const pageFile = node_path_1.default.join(catDirPath, `${page.id}.md`);
|
|
250
|
-
await (0, filesystem_1.writeFileAtomic)(pageFile, pageContent);
|
|
251
|
-
pagesCreated.push(page.id);
|
|
252
|
-
}
|
|
253
|
-
return {
|
|
254
|
-
source_id: sourceTitle,
|
|
255
|
-
summary: extracted.summary,
|
|
256
|
-
pages_created: pagesCreated,
|
|
257
|
-
pages_updated: [],
|
|
258
|
-
entities_discovered: extracted.entities.map((e) => e.name),
|
|
259
|
-
contradictions: [],
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
catch (e) {
|
|
263
|
-
return {
|
|
264
|
-
source_id: input.source_filename,
|
|
265
|
-
summary: `Ingest failed: ${e?.message ?? String(e)}`,
|
|
266
|
-
pages_created: [],
|
|
267
|
-
pages_updated: [],
|
|
268
|
-
entities_discovered: [],
|
|
269
|
-
contradictions: [],
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
}
|
package/dist/tools/lint-wiki.js
DELETED
|
@@ -1,342 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
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.listModules = listModules;
|
|
7
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
-
const filesystem_1 = require("../filesystem");
|
|
9
|
-
const language_map_1 = require("../language-map");
|
|
10
|
-
const extractor_1 = require("../extractor");
|
|
11
|
-
async function listModules(input) {
|
|
12
|
-
const projectPath = node_path_1.default.resolve(input.path);
|
|
13
|
-
const exts = input.extensions?.map((e) => (e.startsWith(".") ? e.toLowerCase() : "." + e.toLowerCase()));
|
|
14
|
-
const { files, skipped } = await (0, filesystem_1.walk)(projectPath);
|
|
15
|
-
const allSkipped = [...skipped];
|
|
16
|
-
const modules = [];
|
|
17
|
-
const langs = new Set();
|
|
18
|
-
for (const f of files) {
|
|
19
|
-
if (exts && !exts.includes(node_path_1.default.extname(f.path).toLowerCase())) {
|
|
20
|
-
allSkipped.push({ path: f.path, reason: "extension filter" });
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
const read = await (0, filesystem_1.safeReadFile)(f.path);
|
|
24
|
-
if (read.error) {
|
|
25
|
-
allSkipped.push({ path: f.path, reason: `read failed: ${read.error}` });
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
if (read.binary) {
|
|
29
|
-
allSkipped.push({ path: f.path, reason: "binary" });
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
const language = (0, language_map_1.extensionToLanguage)(f.path);
|
|
33
|
-
langs.add(language);
|
|
34
|
-
const facts = (0, extractor_1.extract)(read.content ?? "");
|
|
35
|
-
modules.push({
|
|
36
|
-
path: node_path_1.default.relative(projectPath, f.path) || f.path,
|
|
37
|
-
language,
|
|
38
|
-
size_bytes: read.size,
|
|
39
|
-
...facts,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
scanned_at: new Date().toISOString(),
|
|
44
|
-
project_path: projectPath,
|
|
45
|
-
total_files: modules.length,
|
|
46
|
-
skipped_files: allSkipped,
|
|
47
|
-
languages_found: Array.from(langs).sort(),
|
|
48
|
-
modules,
|
|
49
|
-
};
|
|
50
|
-
}
|