@aigne/doc-smith 0.4.5 → 0.6.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/CHANGELOG.md +26 -0
- package/agents/batch-translate.yaml +3 -0
- package/agents/check-detail-result.mjs +2 -1
- package/agents/check-detail.mjs +1 -0
- package/agents/check-feedback-refiner.mjs +79 -0
- package/agents/check-structure-plan.mjs +16 -0
- package/agents/detail-generator-and-translate.yaml +3 -0
- package/agents/detail-regenerator.yaml +3 -0
- package/agents/docs-generator.yaml +3 -0
- package/agents/feedback-refiner.yaml +48 -0
- package/agents/find-items-by-paths.mjs +5 -1
- package/agents/find-user-preferences-by-path.mjs +37 -0
- package/agents/input-generator.mjs +8 -9
- package/agents/load-sources.mjs +63 -9
- package/agents/manage-prefs.mjs +203 -0
- package/agents/publish-docs.mjs +3 -1
- package/agents/retranslate.yaml +3 -0
- package/agents/structure-planning.yaml +3 -0
- package/aigne.yaml +4 -0
- package/package.json +10 -9
- package/prompts/content-detail-generator.md +13 -1
- package/prompts/document/detail-generator.md +1 -0
- package/prompts/feedback-refiner.md +84 -0
- package/prompts/structure-planning.md +8 -0
- package/prompts/translator.md +8 -0
- package/tests/{test-all-validation-cases.mjs → all-validation-cases.test.mjs} +60 -137
- package/tests/check-detail-result.test.mjs +90 -77
- package/tests/load-sources.test.mjs +103 -291
- package/tests/preferences-utils.test.mjs +369 -0
- package/tests/{test-save-docs.mjs → save-docs.test.mjs} +29 -47
- package/tests/save-value-to-config.test.mjs +165 -288
- package/utils/auth-utils.mjs +1 -1
- package/utils/constants.mjs +22 -10
- package/utils/markdown-checker.mjs +89 -9
- package/utils/preferences-utils.mjs +175 -0
- package/utils/utils.mjs +3 -3
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import remarkGfm from "remark-gfm";
|
|
2
4
|
import remarkLint from "remark-lint";
|
|
3
5
|
import remarkParse from "remark-parse";
|
|
@@ -159,6 +161,72 @@ function checkCodeBlockIndentation(codeBlockContent, codeBlockIndent, source, er
|
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Check for local images and verify their existence
|
|
166
|
+
* @param {string} markdown - The markdown content
|
|
167
|
+
* @param {string} source - Source description for error reporting
|
|
168
|
+
* @param {Array} errorMessages - Array to push error messages to
|
|
169
|
+
* @param {string} [markdownFilePath] - Path to the markdown file for resolving relative paths
|
|
170
|
+
* @param {string} [baseDir] - Base directory for resolving relative paths (alternative to markdownFilePath)
|
|
171
|
+
*/
|
|
172
|
+
function checkLocalImages(markdown, source, errorMessages, markdownFilePath, baseDir) {
|
|
173
|
+
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
174
|
+
let match;
|
|
175
|
+
|
|
176
|
+
while ((match = imageRegex.exec(markdown)) !== null) {
|
|
177
|
+
const imagePath = match[2].trim();
|
|
178
|
+
const altText = match[1];
|
|
179
|
+
|
|
180
|
+
// Skip external URLs (http/https)
|
|
181
|
+
if (/^https?:\/\//.test(imagePath)) continue;
|
|
182
|
+
|
|
183
|
+
// Skip data URLs
|
|
184
|
+
if (/^data:/.test(imagePath)) continue;
|
|
185
|
+
|
|
186
|
+
// Check if it's a local path
|
|
187
|
+
if (!imagePath.startsWith("/") && !imagePath.includes("://")) {
|
|
188
|
+
// It's a relative local path, check if file exists
|
|
189
|
+
try {
|
|
190
|
+
let resolvedPath;
|
|
191
|
+
if (markdownFilePath) {
|
|
192
|
+
// Resolve relative to the markdown file's directory
|
|
193
|
+
const markdownDir = path.dirname(markdownFilePath);
|
|
194
|
+
resolvedPath = path.resolve(markdownDir, imagePath);
|
|
195
|
+
} else if (baseDir) {
|
|
196
|
+
// Resolve relative to the provided base directory
|
|
197
|
+
resolvedPath = path.resolve(baseDir, imagePath);
|
|
198
|
+
} else {
|
|
199
|
+
// Fallback to current working directory
|
|
200
|
+
resolvedPath = path.resolve(imagePath);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
204
|
+
errorMessages.push(
|
|
205
|
+
`Found invalid local image in ${source}:  - only valid media resources can be used`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
errorMessages.push(
|
|
210
|
+
`Found invalid local image in ${source}:  - only valid media resources can be used`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
} else if (imagePath.startsWith("/")) {
|
|
214
|
+
// Absolute local path
|
|
215
|
+
try {
|
|
216
|
+
if (!fs.existsSync(imagePath)) {
|
|
217
|
+
errorMessages.push(
|
|
218
|
+
`Found invalid local image in ${source}:  - only valid media resources can be used`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
errorMessages.push(
|
|
223
|
+
`Found invalid local image in ${source}:  - only valid media resources can be used`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
162
230
|
/**
|
|
163
231
|
* Check content structure and formatting issues
|
|
164
232
|
* @param {string} markdown - The markdown content
|
|
@@ -224,7 +292,7 @@ function checkContentStructure(markdown, source, errorMessages) {
|
|
|
224
292
|
}
|
|
225
293
|
|
|
226
294
|
// Check if content ends with proper punctuation (indicating completeness)
|
|
227
|
-
const validEndingPunctuation = [".", "。", ")", "|"];
|
|
295
|
+
const validEndingPunctuation = [".", "。", ")", "|", "*"];
|
|
228
296
|
const trimmedText = markdown.trim();
|
|
229
297
|
const hasValidEnding = validEndingPunctuation.some((punct) => trimmedText.endsWith(punct));
|
|
230
298
|
|
|
@@ -241,6 +309,8 @@ function checkContentStructure(markdown, source, errorMessages) {
|
|
|
241
309
|
* @param {string} [source] - Source description for error reporting (e.g., "result")
|
|
242
310
|
* @param {Object} [options] - Additional options for validation
|
|
243
311
|
* @param {Array} [options.allowedLinks] - Set of allowed links for link validation
|
|
312
|
+
* @param {string} [options.filePath] - Path to the markdown file for resolving relative image paths
|
|
313
|
+
* @param {string} [options.baseDir] - Base directory for resolving relative image paths (alternative to filePath)
|
|
244
314
|
* @returns {Promise<Array<string>>} - Array of error messages in check-detail-result format
|
|
245
315
|
*/
|
|
246
316
|
export async function checkMarkdown(markdown, source = "content", options = {}) {
|
|
@@ -248,8 +318,8 @@ export async function checkMarkdown(markdown, source = "content", options = {})
|
|
|
248
318
|
const errorMessages = [];
|
|
249
319
|
|
|
250
320
|
try {
|
|
251
|
-
// Extract allowed links from options
|
|
252
|
-
const { allowedLinks } = options;
|
|
321
|
+
// Extract allowed links, file path, and base directory from options
|
|
322
|
+
const { allowedLinks, filePath, baseDir } = options;
|
|
253
323
|
|
|
254
324
|
// Create unified processor with markdown parsing and linting
|
|
255
325
|
// Use individual rules instead of presets to have better control
|
|
@@ -292,7 +362,10 @@ export async function checkMarkdown(markdown, source = "content", options = {})
|
|
|
292
362
|
checkDeadLinks(markdown, source, allowedLinks, errorMessages);
|
|
293
363
|
}
|
|
294
364
|
|
|
295
|
-
// 2. Check
|
|
365
|
+
// 2. Check local images existence
|
|
366
|
+
checkLocalImages(markdown, source, errorMessages, filePath, baseDir);
|
|
367
|
+
|
|
368
|
+
// 3. Check content structure and formatting issues
|
|
296
369
|
checkContentStructure(markdown, source, errorMessages);
|
|
297
370
|
|
|
298
371
|
// Check mermaid code blocks and other custom validations
|
|
@@ -319,30 +392,35 @@ export async function checkMarkdown(markdown, source = "content", options = {})
|
|
|
319
392
|
// Check for backticks in node labels
|
|
320
393
|
const nodeLabelRegex = /[A-Za-z0-9_]+\["([^"]*`[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*`[^}]*)"}/g;
|
|
321
394
|
let match;
|
|
322
|
-
|
|
395
|
+
match = nodeLabelRegex.exec(mermaidContent);
|
|
396
|
+
while (match !== null) {
|
|
323
397
|
const label = match[1] || match[2];
|
|
324
398
|
errorMessages.push(
|
|
325
399
|
`Found backticks in Mermaid node label in ${source} at line ${line}: "${label}" - backticks in node labels cause rendering issues in Mermaid diagrams`,
|
|
326
400
|
);
|
|
401
|
+
match = nodeLabelRegex.exec(mermaidContent);
|
|
327
402
|
}
|
|
328
403
|
|
|
329
404
|
// Check for numbered list format in edge descriptions
|
|
330
405
|
const edgeDescriptionRegex = /--\s*"([^"]*)"\s*-->/g;
|
|
331
406
|
let edgeMatch;
|
|
332
|
-
|
|
407
|
+
edgeMatch = edgeDescriptionRegex.exec(mermaidContent);
|
|
408
|
+
while (edgeMatch !== null) {
|
|
333
409
|
const description = edgeMatch[1];
|
|
334
410
|
if (/^\d+\.\s/.test(description)) {
|
|
335
411
|
errorMessages.push(
|
|
336
412
|
`Unsupported markdown: list - Found numbered list format in Mermaid edge description in ${source} at line ${line}: "${description}" - numbered lists in edge descriptions are not supported`,
|
|
337
413
|
);
|
|
338
414
|
}
|
|
415
|
+
edgeMatch = edgeDescriptionRegex.exec(mermaidContent);
|
|
339
416
|
}
|
|
340
417
|
|
|
341
418
|
// Check for numbered list format in node labels (for both [] and {} syntax)
|
|
342
419
|
const nodeLabelWithNumberRegex =
|
|
343
420
|
/[A-Za-z0-9_]+\["([^"]*\d+\.\s[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*\d+\.\s[^}]*)"}/g;
|
|
344
421
|
let numberMatch;
|
|
345
|
-
|
|
422
|
+
numberMatch = nodeLabelWithNumberRegex.exec(mermaidContent);
|
|
423
|
+
while (numberMatch !== null) {
|
|
346
424
|
const label = numberMatch[1] || numberMatch[2];
|
|
347
425
|
// Check if the label contains numbered list format
|
|
348
426
|
if (/\d+\.\s/.test(label)) {
|
|
@@ -350,12 +428,14 @@ export async function checkMarkdown(markdown, source = "content", options = {})
|
|
|
350
428
|
`Unsupported markdown: list - Found numbered list format in Mermaid node label in ${source} at line ${line}: "${label}" - numbered lists in node labels cause rendering issues`,
|
|
351
429
|
);
|
|
352
430
|
}
|
|
431
|
+
numberMatch = nodeLabelWithNumberRegex.exec(mermaidContent);
|
|
353
432
|
}
|
|
354
433
|
|
|
355
434
|
// Check for special characters in node labels that should be quoted
|
|
356
435
|
const nodeWithSpecialCharsRegex = /([A-Za-z0-9_]+)\[([^\]]*[(){}:;,\-\s.][^\]]*)\]/g;
|
|
357
436
|
let specialCharMatch;
|
|
358
|
-
|
|
437
|
+
specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent);
|
|
438
|
+
while (specialCharMatch !== null) {
|
|
359
439
|
const nodeId = specialCharMatch[1];
|
|
360
440
|
const label = specialCharMatch[2];
|
|
361
441
|
|
|
@@ -373,6 +453,7 @@ export async function checkMarkdown(markdown, source = "content", options = {})
|
|
|
373
453
|
);
|
|
374
454
|
}
|
|
375
455
|
}
|
|
456
|
+
specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent);
|
|
376
457
|
}
|
|
377
458
|
}
|
|
378
459
|
});
|
|
@@ -431,7 +512,6 @@ export async function checkMarkdown(markdown, source = "content", options = {})
|
|
|
431
512
|
// Format messages in check-detail-result style
|
|
432
513
|
file.messages.forEach((message) => {
|
|
433
514
|
const line = message.line || "unknown";
|
|
434
|
-
const _column = message.column || "unknown";
|
|
435
515
|
const reason = message.reason || "Unknown markdown issue";
|
|
436
516
|
const ruleId = message.ruleId || message.source || "markdown";
|
|
437
517
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { parse, stringify } from "yaml";
|
|
5
|
+
|
|
6
|
+
const PREFERENCES_DIR = ".aigne/doc-smith";
|
|
7
|
+
const PREFERENCES_FILE = "preferences.yml";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate a random preference ID
|
|
11
|
+
* @returns {string} Random ID with pref_ prefix
|
|
12
|
+
*/
|
|
13
|
+
function generatePreferenceId() {
|
|
14
|
+
return `pref_${randomBytes(8).toString("hex")}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the full path to the preferences file
|
|
19
|
+
* @returns {string} Full path to preferences.yml
|
|
20
|
+
*/
|
|
21
|
+
function getPreferencesFilePath() {
|
|
22
|
+
return join(process.cwd(), PREFERENCES_DIR, PREFERENCES_FILE);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ensure the preferences directory exists
|
|
27
|
+
*/
|
|
28
|
+
function ensurePreferencesDir() {
|
|
29
|
+
const preferencesDir = join(process.cwd(), PREFERENCES_DIR);
|
|
30
|
+
if (!existsSync(preferencesDir)) {
|
|
31
|
+
mkdirSync(preferencesDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read existing preferences from file
|
|
37
|
+
* @returns {Object} Preferences object with rules array
|
|
38
|
+
*/
|
|
39
|
+
export function readPreferences() {
|
|
40
|
+
const filePath = getPreferencesFilePath();
|
|
41
|
+
|
|
42
|
+
if (!existsSync(filePath)) {
|
|
43
|
+
return { rules: [] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const content = readFileSync(filePath, "utf8");
|
|
48
|
+
const preferences = parse(content);
|
|
49
|
+
return preferences || { rules: [] };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.warn(`Warning: Failed to read preferences file at ${filePath}: ${error.message}`);
|
|
52
|
+
return { rules: [] };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Write preferences to file
|
|
58
|
+
* @param {Object} preferences - Preferences object to save
|
|
59
|
+
*/
|
|
60
|
+
export function writePreferences(preferences) {
|
|
61
|
+
ensurePreferencesDir();
|
|
62
|
+
const filePath = getPreferencesFilePath();
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const yamlContent = stringify(preferences, {
|
|
66
|
+
indent: 2,
|
|
67
|
+
lineWidth: 120,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
writeFileSync(filePath, yamlContent, "utf8");
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Error(`Failed to write preferences file: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Add a new preference rule
|
|
78
|
+
* @param {Object} ruleData - Rule data from feedbackRefiner
|
|
79
|
+
* @param {string} ruleData.rule - The rule text
|
|
80
|
+
* @param {string} ruleData.scope - Rule scope (global, structure, document, translation)
|
|
81
|
+
* @param {boolean} ruleData.limitToInputPaths - Whether to limit to input paths
|
|
82
|
+
* @param {string} feedback - Original user feedback
|
|
83
|
+
* @param {string[]} [paths] - Optional paths to save with the rule
|
|
84
|
+
* @returns {Object} The created preference rule
|
|
85
|
+
*/
|
|
86
|
+
export function addPreferenceRule(ruleData, feedback, paths = []) {
|
|
87
|
+
const preferences = readPreferences();
|
|
88
|
+
|
|
89
|
+
const newRule = {
|
|
90
|
+
id: generatePreferenceId(),
|
|
91
|
+
active: true,
|
|
92
|
+
scope: ruleData.scope,
|
|
93
|
+
rule: ruleData.rule,
|
|
94
|
+
feedback: feedback,
|
|
95
|
+
createdAt: new Date().toISOString(),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Add paths if limitToInputPaths is true and paths are provided
|
|
99
|
+
if (ruleData.limitToInputPaths && paths && paths.length > 0) {
|
|
100
|
+
newRule.paths = paths;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Add the new rule to the beginning of the array (newest first)
|
|
104
|
+
preferences.rules.unshift(newRule);
|
|
105
|
+
|
|
106
|
+
writePreferences(preferences);
|
|
107
|
+
|
|
108
|
+
return newRule;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get all active preference rules for a specific scope
|
|
113
|
+
* @param {string} scope - The scope to filter by (global, structure, document, translation)
|
|
114
|
+
* @param {string[]} [currentPaths] - Current paths to match against rules with path restrictions
|
|
115
|
+
* @returns {Object[]} Array of matching active rules
|
|
116
|
+
*/
|
|
117
|
+
export function getActiveRulesForScope(scope, currentPaths = []) {
|
|
118
|
+
const preferences = readPreferences();
|
|
119
|
+
|
|
120
|
+
return preferences.rules.filter((rule) => {
|
|
121
|
+
// Must be active and match scope
|
|
122
|
+
if (!rule.active || rule.scope !== scope) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// If rule has path restrictions, check if any current path matches
|
|
127
|
+
if (rule.paths && rule.paths.length > 0) {
|
|
128
|
+
if (currentPaths.length === 0) {
|
|
129
|
+
return false; // Rule has path restrictions but no current paths provided
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check if any current path matches any rule path pattern
|
|
133
|
+
return currentPaths.some((currentPath) => rule.paths.includes(currentPath));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return true; // No path restrictions, include the rule
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Deactivate a preference rule by ID
|
|
142
|
+
* @param {string} ruleId - The ID of the rule to deactivate
|
|
143
|
+
* @returns {boolean} True if rule was found and deactivated
|
|
144
|
+
*/
|
|
145
|
+
export function deactivateRule(ruleId) {
|
|
146
|
+
const preferences = readPreferences();
|
|
147
|
+
const rule = preferences.rules.find((r) => r.id === ruleId);
|
|
148
|
+
|
|
149
|
+
if (rule) {
|
|
150
|
+
rule.active = false;
|
|
151
|
+
writePreferences(preferences);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Remove a preference rule by ID
|
|
160
|
+
* @param {string} ruleId - The ID of the rule to remove
|
|
161
|
+
* @returns {boolean} True if rule was found and removed
|
|
162
|
+
*/
|
|
163
|
+
export function removeRule(ruleId) {
|
|
164
|
+
const preferences = readPreferences();
|
|
165
|
+
const initialLength = preferences.rules.length;
|
|
166
|
+
|
|
167
|
+
preferences.rules = preferences.rules.filter((r) => r.id !== ruleId);
|
|
168
|
+
|
|
169
|
+
if (preferences.rules.length < initialLength) {
|
|
170
|
+
writePreferences(preferences);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return false;
|
|
175
|
+
}
|
package/utils/utils.mjs
CHANGED
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
DEFAULT_INCLUDE_PATTERNS,
|
|
9
9
|
DOCUMENT_STYLES,
|
|
10
10
|
DOCUMENTATION_DEPTH,
|
|
11
|
-
SUPPORTED_FILE_EXTENSIONS,
|
|
12
11
|
READER_KNOWLEDGE_LEVELS,
|
|
12
|
+
SUPPORTED_FILE_EXTENSIONS,
|
|
13
13
|
SUPPORTED_LANGUAGES,
|
|
14
14
|
TARGET_AUDIENCES,
|
|
15
15
|
} from "./constants.mjs";
|
|
@@ -149,8 +149,8 @@ export function getCurrentGitHead() {
|
|
|
149
149
|
* @param {string} gitHead - The current git HEAD commit hash
|
|
150
150
|
*/
|
|
151
151
|
export async function saveGitHeadToConfig(gitHead) {
|
|
152
|
-
if (!gitHead) {
|
|
153
|
-
return; // Skip if no git HEAD available
|
|
152
|
+
if (!gitHead || process.env.NODE_ENV === 'test' || process.env.BUN_TEST) {
|
|
153
|
+
return; // Skip if no git HEAD available or in test environment
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
try {
|