@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,157 +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.updateIndex = updateIndex;
|
|
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 a markdown file.
|
|
12
|
-
* Expects YAML format between --- markers.
|
|
13
|
-
*/
|
|
14
|
-
function parseFrontmatter(content) {
|
|
15
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
16
|
-
if (!match)
|
|
17
|
-
return {};
|
|
18
|
-
const yaml = match[1];
|
|
19
|
-
const result = {};
|
|
20
|
-
for (const line of yaml.split("\n")) {
|
|
21
|
-
if (!line.trim())
|
|
22
|
-
continue;
|
|
23
|
-
const [key, ...valueParts] = line.split(":");
|
|
24
|
-
const value = valueParts.join(":").trim();
|
|
25
|
-
try {
|
|
26
|
-
// Try to parse as JSON (arrays, objects)
|
|
27
|
-
if (value.startsWith("[") || value.startsWith("{")) {
|
|
28
|
-
result[key.trim()] = JSON.parse(value);
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
result[key.trim()] = value;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
result[key.trim()] = value;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return result;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Extract title from markdown content.
|
|
42
|
-
* Looks for first H1 header.
|
|
43
|
-
*/
|
|
44
|
-
function extractTitle(content) {
|
|
45
|
-
const match = content.match(/^#\s+(.+?)$/m);
|
|
46
|
-
return match ? match[1].trim() : "Untitled";
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Scan all wiki pages and regenerate index.md
|
|
50
|
-
*/
|
|
51
|
-
async function updateIndex(input) {
|
|
52
|
-
try {
|
|
53
|
-
const projectPath = node_path_1.default.resolve(input.project_path);
|
|
54
|
-
const docuDir = node_path_1.default.join(projectPath, ".docuflow");
|
|
55
|
-
const wikiDir = node_path_1.default.join(docuDir, "wiki");
|
|
56
|
-
const indexFile = node_path_1.default.join(docuDir, "index.md");
|
|
57
|
-
const logFile = node_path_1.default.join(docuDir, "log.md");
|
|
58
|
-
// Scan all wiki pages
|
|
59
|
-
const entries = [];
|
|
60
|
-
const categories = ["entities", "concepts", "timelines", "syntheses"];
|
|
61
|
-
const PLURAL_TO_SINGULAR = {
|
|
62
|
-
entities: "entity",
|
|
63
|
-
concepts: "concept",
|
|
64
|
-
timelines: "timeline",
|
|
65
|
-
syntheses: "synthesis",
|
|
66
|
-
};
|
|
67
|
-
for (const category of categories) {
|
|
68
|
-
const categoryDir = node_path_1.default.join(wikiDir, category);
|
|
69
|
-
try {
|
|
70
|
-
const files = await promises_1.default.readdir(categoryDir);
|
|
71
|
-
for (const file of files) {
|
|
72
|
-
if (!file.endsWith(".md"))
|
|
73
|
-
continue;
|
|
74
|
-
const filePath = node_path_1.default.join(categoryDir, file);
|
|
75
|
-
const read = await (0, filesystem_1.safeReadFile)(filePath);
|
|
76
|
-
if (read.error || read.binary || !read.content)
|
|
77
|
-
continue;
|
|
78
|
-
const fm = parseFrontmatter(read.content);
|
|
79
|
-
const title = extractTitle(read.content);
|
|
80
|
-
const pageId = file.replace(".md", "");
|
|
81
|
-
entries.push({
|
|
82
|
-
id: pageId,
|
|
83
|
-
title,
|
|
84
|
-
category: PLURAL_TO_SINGULAR[category] ?? category.replace(/s$/, ""),
|
|
85
|
-
path: node_path_1.default.relative(docuDir, filePath),
|
|
86
|
-
created_at: fm.created_at ?? new Date().toISOString(),
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch (e) {
|
|
91
|
-
// Category dir may not exist yet
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// Generate index.md content
|
|
95
|
-
const now = new Date().toISOString();
|
|
96
|
-
let indexContent = `# Wiki Index
|
|
97
|
-
|
|
98
|
-
Generated: ${now}
|
|
99
|
-
|
|
100
|
-
## Overview
|
|
101
|
-
|
|
102
|
-
Total pages: ${entries.length}
|
|
103
|
-
|
|
104
|
-
## By Category
|
|
105
|
-
|
|
106
|
-
`;
|
|
107
|
-
// Group by category
|
|
108
|
-
const byCategory = {};
|
|
109
|
-
for (const entry of entries) {
|
|
110
|
-
if (!byCategory[entry.category])
|
|
111
|
-
byCategory[entry.category] = [];
|
|
112
|
-
byCategory[entry.category].push(entry);
|
|
113
|
-
}
|
|
114
|
-
// Add each category section
|
|
115
|
-
for (const category of ["entity", "concept", "timeline", "synthesis"]) {
|
|
116
|
-
const categoryEntries = byCategory[category] || [];
|
|
117
|
-
if (categoryEntries.length === 0)
|
|
118
|
-
continue;
|
|
119
|
-
indexContent += `### ${category.charAt(0).toUpperCase() + category.slice(1)} Pages (${categoryEntries.length})\n\n`;
|
|
120
|
-
for (const entry of categoryEntries.sort((a, b) => a.title.localeCompare(b.title))) {
|
|
121
|
-
indexContent += `- [\`${entry.id}\`](./${entry.path}) — **${entry.title}**\n`;
|
|
122
|
-
}
|
|
123
|
-
indexContent += "\n";
|
|
124
|
-
}
|
|
125
|
-
// Write index.md
|
|
126
|
-
await (0, filesystem_1.ensureDir)(docuDir);
|
|
127
|
-
await (0, filesystem_1.writeFileAtomic)(indexFile, indexContent);
|
|
128
|
-
// Append to log.md
|
|
129
|
-
let logUpdated = false;
|
|
130
|
-
try {
|
|
131
|
-
const logRead = await (0, filesystem_1.safeReadFile)(logFile);
|
|
132
|
-
let logContent = logRead.content ?? "# Operation Log\n\n";
|
|
133
|
-
// Add entry
|
|
134
|
-
const timestamp = now.split("T")[0]; // YYYY-MM-DD
|
|
135
|
-
const logEntry = `## [${timestamp}] index-update | ${entries.length} pages indexed\n\n`;
|
|
136
|
-
logContent += logEntry;
|
|
137
|
-
await (0, filesystem_1.writeFileAtomic)(logFile, logContent);
|
|
138
|
-
logUpdated = true;
|
|
139
|
-
}
|
|
140
|
-
catch (e) {
|
|
141
|
-
// Log may not exist, that's ok
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
entries_indexed: entries.length,
|
|
145
|
-
index_file: indexFile,
|
|
146
|
-
log_appended: logUpdated,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
catch (e) {
|
|
150
|
-
return {
|
|
151
|
-
entries_indexed: 0,
|
|
152
|
-
index_file: "",
|
|
153
|
-
log_appended: false,
|
|
154
|
-
error: e?.message ?? String(e),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
}
|
|
@@ -1,157 +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.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
|
-
}
|
package/dist/tools/write-spec.js
DELETED
|
@@ -1,55 +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.writeSpec = writeSpec;
|
|
7
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
-
const filesystem_1 = require("../filesystem");
|
|
9
|
-
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
10
|
-
// Per-project write lock: maps projectPath → promise chain so that concurrent
|
|
11
|
-
// calls to writeSpec on the same project are always serialised. This prevents
|
|
12
|
-
// the read-modify-write on index.json from racing when the agent issues several
|
|
13
|
-
// write_spec calls in parallel (e.g. writing one spec per module at once).
|
|
14
|
-
const indexLocks = new Map();
|
|
15
|
-
function withLock(key, fn) {
|
|
16
|
-
const prev = indexLocks.get(key) ?? Promise.resolve();
|
|
17
|
-
const next = prev.then(fn).catch(() => { });
|
|
18
|
-
indexLocks.set(key, next);
|
|
19
|
-
return next;
|
|
20
|
-
}
|
|
21
|
-
async function writeSpec(input) {
|
|
22
|
-
const projectPath = node_path_1.default.resolve(input.project_path);
|
|
23
|
-
const docuDir = node_path_1.default.join(projectPath, ".docuflow");
|
|
24
|
-
const specsDir = node_path_1.default.join(docuDir, "specs");
|
|
25
|
-
await (0, filesystem_1.ensureDir)(specsDir);
|
|
26
|
-
const cleanName = input.filename.replace(/\.md$/i, "");
|
|
27
|
-
const targetFile = node_path_1.default.join(specsDir, `${cleanName}.md`);
|
|
28
|
-
// Write the markdown file immediately — each spec file is independent so
|
|
29
|
-
// parallel writes to different filenames are safe without locking.
|
|
30
|
-
const bytes = await (0, filesystem_1.writeFileAtomic)(targetFile, input.content);
|
|
31
|
-
// Serialise index updates per project to avoid read-modify-write races.
|
|
32
|
-
const indexPath = node_path_1.default.join(docuDir, "index.json");
|
|
33
|
-
let indexUpdated = false;
|
|
34
|
-
let writeError;
|
|
35
|
-
await withLock(projectPath, async () => {
|
|
36
|
-
try {
|
|
37
|
-
const existing = (await (0, filesystem_1.readJsonIfExists)(indexPath)) ?? { specs: [] };
|
|
38
|
-
const now = new Date().toISOString();
|
|
39
|
-
const idx = existing.specs.findIndex((s) => s.filename === `${cleanName}.md`);
|
|
40
|
-
if (idx >= 0)
|
|
41
|
-
existing.specs[idx].written_at = now;
|
|
42
|
-
else
|
|
43
|
-
existing.specs.push({ filename: `${cleanName}.md`, written_at: now });
|
|
44
|
-
await promises_1.default.writeFile(indexPath, JSON.stringify(existing, null, 2), "utf8");
|
|
45
|
-
indexUpdated = true;
|
|
46
|
-
}
|
|
47
|
-
catch (e) {
|
|
48
|
-
writeError = e?.message ?? String(e);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
if (writeError) {
|
|
52
|
-
return { written_to: targetFile, bytes_written: bytes, index_updated: false, error: writeError };
|
|
53
|
-
}
|
|
54
|
-
return { written_to: targetFile, bytes_written: bytes, index_updated: indexUpdated };
|
|
55
|
-
}
|
package/dist/types.js
DELETED