@doquflow/server 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extractor.js +46 -1
- package/dist/index.js +228 -0
- package/dist/tools/answer-synthesis.js +189 -0
- package/dist/tools/generate-dependency-graph.js +162 -0
- package/dist/tools/get-schema-guidance.js +213 -0
- package/dist/tools/ingest-source.js +252 -0
- package/dist/tools/lint-wiki.js +342 -0
- package/dist/tools/list-wiki.js +129 -0
- package/dist/tools/preview-generation.js +228 -0
- package/dist/tools/query-wiki.js +67 -0
- package/dist/tools/read-specs.js +5 -1
- package/dist/tools/save-answer-as-page.js +102 -0
- package/dist/tools/update-index.js +157 -0
- package/dist/tools/wiki-search.js +157 -0
- package/package.json +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
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.wikiSearch = wikiSearch;
|
|
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
|
+
* Simple BM25-inspired scoring with term frequency and document frequency.
|
|
12
|
+
* Weights entity pages more heavily than concepts, titles match higher.
|
|
13
|
+
*/
|
|
14
|
+
function scoreMatch(query, content, title, category, totalPages) {
|
|
15
|
+
const queryTerms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 2);
|
|
16
|
+
let score = 0;
|
|
17
|
+
const matched_terms = new Set();
|
|
18
|
+
for (const term of queryTerms) {
|
|
19
|
+
// Title match (highest weight)
|
|
20
|
+
if (title.toLowerCase().includes(term)) {
|
|
21
|
+
score += 50;
|
|
22
|
+
matched_terms.add(term);
|
|
23
|
+
}
|
|
24
|
+
// Content match with term frequency (TF)
|
|
25
|
+
const contentLower = content.toLowerCase();
|
|
26
|
+
const termRegex = new RegExp(`\\b${term}\\b`, "gi");
|
|
27
|
+
const matches = contentLower.match(termRegex);
|
|
28
|
+
if (matches) {
|
|
29
|
+
const tf = matches.length;
|
|
30
|
+
// Inverse document frequency approximation
|
|
31
|
+
const idf = Math.log(totalPages / Math.max(1, matches.length));
|
|
32
|
+
score += tf * idf * 2;
|
|
33
|
+
matched_terms.add(term);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Category boost: entities score higher than concepts
|
|
37
|
+
if (category === "entity") {
|
|
38
|
+
score *= 1.3;
|
|
39
|
+
}
|
|
40
|
+
else if (category === "synthesis") {
|
|
41
|
+
score *= 1.1;
|
|
42
|
+
}
|
|
43
|
+
return { score, matched_terms: Array.from(matched_terms) };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Extract a preview snippet from content around matched terms
|
|
47
|
+
*/
|
|
48
|
+
function extractPreview(content, matchedTerms, maxLength = 150) {
|
|
49
|
+
if (!matchedTerms.length) {
|
|
50
|
+
// Return first non-empty paragraph
|
|
51
|
+
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#") && !l.startsWith("---"));
|
|
52
|
+
return lines.slice(0, 2).join(" ").substring(0, maxLength);
|
|
53
|
+
}
|
|
54
|
+
// Find first occurrence of any matched term
|
|
55
|
+
const contentLower = content.toLowerCase();
|
|
56
|
+
const term = matchedTerms[0];
|
|
57
|
+
const index = contentLower.indexOf(term);
|
|
58
|
+
if (index === -1) {
|
|
59
|
+
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
60
|
+
return lines.slice(0, 2).join(" ").substring(0, maxLength);
|
|
61
|
+
}
|
|
62
|
+
const start = Math.max(0, index - 60);
|
|
63
|
+
const end = Math.min(content.length, index + 150);
|
|
64
|
+
let preview = content.substring(start, end);
|
|
65
|
+
// Trim to word boundary
|
|
66
|
+
const lastSpace = preview.lastIndexOf(" ");
|
|
67
|
+
if (lastSpace > 0) {
|
|
68
|
+
preview = preview.substring(0, lastSpace) + "...";
|
|
69
|
+
}
|
|
70
|
+
return preview;
|
|
71
|
+
}
|
|
72
|
+
async function wikiSearch(input) {
|
|
73
|
+
try {
|
|
74
|
+
const projectPath = node_path_1.default.resolve(input.project_path);
|
|
75
|
+
const docuDir = node_path_1.default.join(projectPath, ".docuflow");
|
|
76
|
+
const wikiDir = node_path_1.default.join(docuDir, "wiki");
|
|
77
|
+
const limit = input.limit ?? 10;
|
|
78
|
+
const allResults = [];
|
|
79
|
+
// Build list of categories to search
|
|
80
|
+
let categoriesToScan = ["entities", "concepts", "timelines", "syntheses"];
|
|
81
|
+
if (input.category) {
|
|
82
|
+
categoriesToScan = [`${input.category}s`];
|
|
83
|
+
}
|
|
84
|
+
// First pass: count total pages (for IDF calculation)
|
|
85
|
+
let totalPages = 0;
|
|
86
|
+
for (const categoryDir of categoriesToScan) {
|
|
87
|
+
const fullCategoryPath = node_path_1.default.join(wikiDir, categoryDir);
|
|
88
|
+
try {
|
|
89
|
+
const files = await promises_1.default.readdir(fullCategoryPath);
|
|
90
|
+
totalPages += files.filter((f) => f.endsWith(".md")).length;
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
// Category may not exist
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const PLURAL_TO_SINGULAR = {
|
|
97
|
+
entities: "entity",
|
|
98
|
+
concepts: "concept",
|
|
99
|
+
timelines: "timeline",
|
|
100
|
+
syntheses: "synthesis",
|
|
101
|
+
};
|
|
102
|
+
// Second pass: search all pages
|
|
103
|
+
for (const categoryDir of categoriesToScan) {
|
|
104
|
+
const fullCategoryPath = node_path_1.default.join(wikiDir, categoryDir);
|
|
105
|
+
const category = PLURAL_TO_SINGULAR[categoryDir] ?? categoryDir.replace(/s$/, "");
|
|
106
|
+
try {
|
|
107
|
+
const files = await promises_1.default.readdir(fullCategoryPath);
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
if (!file.endsWith(".md"))
|
|
110
|
+
continue;
|
|
111
|
+
const filePath = node_path_1.default.join(fullCategoryPath, file);
|
|
112
|
+
const read = await (0, filesystem_1.safeReadFile)(filePath);
|
|
113
|
+
if (read.error || read.binary || !read.content)
|
|
114
|
+
continue;
|
|
115
|
+
// Extract title from content (first H1)
|
|
116
|
+
const titleMatch = read.content.match(/^#\s+(.+?)$/m);
|
|
117
|
+
const title = titleMatch ? titleMatch[1].trim() : file.replace(".md", "");
|
|
118
|
+
const pageId = file.replace(".md", "");
|
|
119
|
+
// Score the match
|
|
120
|
+
const { score, matched_terms } = scoreMatch(input.query, read.content, title, category, totalPages);
|
|
121
|
+
// Only include if there's at least one match
|
|
122
|
+
if (score > 0 && matched_terms.length > 0) {
|
|
123
|
+
const preview = extractPreview(read.content, matched_terms);
|
|
124
|
+
allResults.push({
|
|
125
|
+
page_id: pageId,
|
|
126
|
+
title,
|
|
127
|
+
category,
|
|
128
|
+
path: node_path_1.default.relative(docuDir, filePath),
|
|
129
|
+
relevance_score: Math.round(score * 10) / 10, // Round to 1 decimal
|
|
130
|
+
preview,
|
|
131
|
+
matched_terms,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
// Category directory may not exist
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Sort by relevance score (descending) and return top N
|
|
141
|
+
allResults.sort((a, b) => b.relevance_score - a.relevance_score);
|
|
142
|
+
const results = allResults.slice(0, limit);
|
|
143
|
+
return {
|
|
144
|
+
query: input.query,
|
|
145
|
+
results,
|
|
146
|
+
total_results: allResults.length,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
return {
|
|
151
|
+
query: input.query,
|
|
152
|
+
results: [],
|
|
153
|
+
total_results: 0,
|
|
154
|
+
error: e?.message ?? String(e),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|