@aigne/doc-smith 0.5.1 → 0.7.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/.github/workflows/ci.yml +46 -0
- package/.github/workflows/reviewer.yml +2 -1
- package/CHANGELOG.md +17 -0
- package/agents/chat.yaml +30 -0
- package/agents/check-detail-result.mjs +2 -1
- package/agents/check-detail.mjs +1 -0
- package/agents/check-structure-plan.mjs +1 -1
- package/agents/docs-fs.yaml +25 -0
- package/agents/exit.mjs +6 -0
- package/agents/feedback-refiner.yaml +5 -1
- package/agents/find-items-by-paths.mjs +10 -4
- package/agents/fs.mjs +60 -0
- package/agents/input-generator.mjs +159 -90
- package/agents/load-config.mjs +0 -5
- package/agents/load-sources.mjs +119 -12
- package/agents/publish-docs.mjs +28 -11
- package/agents/retranslate.yaml +1 -1
- package/agents/team-publish-docs.yaml +2 -2
- package/aigne.yaml +1 -0
- package/package.json +13 -10
- package/prompts/content-detail-generator.md +12 -4
- package/prompts/document/custom-components.md +80 -0
- package/prompts/document/d2-chart/diy-examples.md +44 -0
- package/prompts/document/d2-chart/official-examples.md +708 -0
- package/prompts/document/d2-chart/rules.md +48 -0
- package/prompts/document/detail-generator.md +13 -15
- package/prompts/document/structure-planning.md +1 -3
- package/prompts/feedback-refiner.md +81 -60
- package/prompts/structure-planning.md +20 -3
- package/tests/check-detail-result.test.mjs +50 -2
- package/tests/conflict-resolution.test.mjs +237 -0
- package/tests/input-generator.test.mjs +940 -0
- package/tests/load-sources.test.mjs +627 -3
- package/tests/preferences-utils.test.mjs +94 -0
- package/tests/save-value-to-config.test.mjs +182 -5
- package/tests/utils.test.mjs +49 -0
- package/utils/auth-utils.mjs +1 -1
- package/utils/conflict-detector.mjs +72 -1
- package/utils/constants.mjs +139 -126
- package/utils/kroki-utils.mjs +162 -0
- package/utils/markdown-checker.mjs +175 -67
- package/utils/utils.mjs +97 -29
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pMap from "p-map";
|
|
1
4
|
import remarkGfm from "remark-gfm";
|
|
2
5
|
import remarkLint from "remark-lint";
|
|
3
6
|
import remarkParse from "remark-parse";
|
|
4
7
|
import { unified } from "unified";
|
|
5
8
|
import { visit } from "unist-util-visit";
|
|
6
9
|
import { VFile } from "vfile";
|
|
10
|
+
import { KROKI_CONCURRENCY } from "./constants.mjs";
|
|
11
|
+
import { checkD2Content } from "./kroki-utils.mjs";
|
|
7
12
|
import { validateMermaidSyntax } from "./mermaid-validator.mjs";
|
|
8
13
|
|
|
9
14
|
/**
|
|
@@ -65,7 +70,10 @@ function checkDeadLinks(markdown, source, allowedLinks, errorMessages) {
|
|
|
65
70
|
const linkRegex = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g;
|
|
66
71
|
let match;
|
|
67
72
|
|
|
68
|
-
while (
|
|
73
|
+
while (true) {
|
|
74
|
+
match = linkRegex.exec(markdown);
|
|
75
|
+
if (match === null) break;
|
|
76
|
+
|
|
69
77
|
const link = match[2];
|
|
70
78
|
const trimLink = link.trim();
|
|
71
79
|
|
|
@@ -159,6 +167,74 @@ function checkCodeBlockIndentation(codeBlockContent, codeBlockIndent, source, er
|
|
|
159
167
|
}
|
|
160
168
|
}
|
|
161
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Check for local images and verify their existence
|
|
172
|
+
* @param {string} markdown - The markdown content
|
|
173
|
+
* @param {string} source - Source description for error reporting
|
|
174
|
+
* @param {Array} errorMessages - Array to push error messages to
|
|
175
|
+
* @param {string} [markdownFilePath] - Path to the markdown file for resolving relative paths
|
|
176
|
+
* @param {string} [baseDir] - Base directory for resolving relative paths (alternative to markdownFilePath)
|
|
177
|
+
*/
|
|
178
|
+
function checkLocalImages(markdown, source, errorMessages, markdownFilePath, baseDir) {
|
|
179
|
+
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
180
|
+
let match;
|
|
181
|
+
|
|
182
|
+
while (true) {
|
|
183
|
+
match = imageRegex.exec(markdown);
|
|
184
|
+
if (match === null) break;
|
|
185
|
+
const imagePath = match[2].trim();
|
|
186
|
+
const altText = match[1];
|
|
187
|
+
|
|
188
|
+
// Skip external URLs (http/https)
|
|
189
|
+
if (/^https?:\/\//.test(imagePath)) continue;
|
|
190
|
+
|
|
191
|
+
// Skip data URLs
|
|
192
|
+
if (/^data:/.test(imagePath)) continue;
|
|
193
|
+
|
|
194
|
+
// Check if it's a local path
|
|
195
|
+
if (!imagePath.startsWith("/") && !imagePath.includes("://")) {
|
|
196
|
+
// It's a relative local path, check if file exists
|
|
197
|
+
try {
|
|
198
|
+
let resolvedPath;
|
|
199
|
+
if (markdownFilePath) {
|
|
200
|
+
// Resolve relative to the markdown file's directory
|
|
201
|
+
const markdownDir = path.dirname(markdownFilePath);
|
|
202
|
+
resolvedPath = path.resolve(markdownDir, imagePath);
|
|
203
|
+
} else if (baseDir) {
|
|
204
|
+
// Resolve relative to the provided base directory
|
|
205
|
+
resolvedPath = path.resolve(baseDir, imagePath);
|
|
206
|
+
} else {
|
|
207
|
+
// Fallback to current working directory
|
|
208
|
+
resolvedPath = path.resolve(imagePath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
212
|
+
errorMessages.push(
|
|
213
|
+
`Found invalid local image in ${source}:  - only valid media resources can be used`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
errorMessages.push(
|
|
218
|
+
`Found invalid local image in ${source}:  - only valid media resources can be used`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
} else if (imagePath.startsWith("/")) {
|
|
222
|
+
// Absolute local path
|
|
223
|
+
try {
|
|
224
|
+
if (!fs.existsSync(imagePath)) {
|
|
225
|
+
errorMessages.push(
|
|
226
|
+
`Found invalid local image in ${source}:  - only valid media resources can be used`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
errorMessages.push(
|
|
231
|
+
`Found invalid local image in ${source}:  - only valid media resources can be used`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
162
238
|
/**
|
|
163
239
|
* Check content structure and formatting issues
|
|
164
240
|
* @param {string} markdown - The markdown content
|
|
@@ -224,7 +300,7 @@ function checkContentStructure(markdown, source, errorMessages) {
|
|
|
224
300
|
}
|
|
225
301
|
|
|
226
302
|
// Check if content ends with proper punctuation (indicating completeness)
|
|
227
|
-
const validEndingPunctuation = [".", "。", ")", "|"];
|
|
303
|
+
const validEndingPunctuation = [".", "。", ")", "|", "*"];
|
|
228
304
|
const trimmedText = markdown.trim();
|
|
229
305
|
const hasValidEnding = validEndingPunctuation.some((punct) => trimmedText.endsWith(punct));
|
|
230
306
|
|
|
@@ -241,6 +317,8 @@ function checkContentStructure(markdown, source, errorMessages) {
|
|
|
241
317
|
* @param {string} [source] - Source description for error reporting (e.g., "result")
|
|
242
318
|
* @param {Object} [options] - Additional options for validation
|
|
243
319
|
* @param {Array} [options.allowedLinks] - Set of allowed links for link validation
|
|
320
|
+
* @param {string} [options.filePath] - Path to the markdown file for resolving relative image paths
|
|
321
|
+
* @param {string} [options.baseDir] - Base directory for resolving relative image paths (alternative to filePath)
|
|
244
322
|
* @returns {Promise<Array<string>>} - Array of error messages in check-detail-result format
|
|
245
323
|
*/
|
|
246
324
|
export async function checkMarkdown(markdown, source = "content", options = {}) {
|
|
@@ -248,8 +326,8 @@ export async function checkMarkdown(markdown, source = "content", options = {})
|
|
|
248
326
|
const errorMessages = [];
|
|
249
327
|
|
|
250
328
|
try {
|
|
251
|
-
// Extract allowed links from options
|
|
252
|
-
const { allowedLinks } = options;
|
|
329
|
+
// Extract allowed links, file path, and base directory from options
|
|
330
|
+
const { allowedLinks, filePath, baseDir } = options;
|
|
253
331
|
|
|
254
332
|
// Create unified processor with markdown parsing and linting
|
|
255
333
|
// Use individual rules instead of presets to have better control
|
|
@@ -292,88 +370,110 @@ export async function checkMarkdown(markdown, source = "content", options = {})
|
|
|
292
370
|
checkDeadLinks(markdown, source, allowedLinks, errorMessages);
|
|
293
371
|
}
|
|
294
372
|
|
|
295
|
-
// 2. Check
|
|
373
|
+
// 2. Check local images existence
|
|
374
|
+
checkLocalImages(markdown, source, errorMessages, filePath, baseDir);
|
|
375
|
+
|
|
376
|
+
// 3. Check content structure and formatting issues
|
|
296
377
|
checkContentStructure(markdown, source, errorMessages);
|
|
297
378
|
|
|
298
379
|
// Check mermaid code blocks and other custom validations
|
|
299
380
|
const mermaidChecks = [];
|
|
381
|
+
const d2ChecksList = [];
|
|
300
382
|
visit(ast, "code", (node) => {
|
|
301
|
-
if (node.lang
|
|
302
|
-
// Check for mermaid syntax errors
|
|
303
|
-
mermaidChecks.push(
|
|
304
|
-
validateMermaidSyntax(node.value).catch((error) => {
|
|
305
|
-
const errorMessage = error?.message || String(error) || "Unknown mermaid syntax error";
|
|
306
|
-
|
|
307
|
-
// Format mermaid error in check-detail-result style
|
|
308
|
-
const line = node.position?.start?.line || "unknown";
|
|
309
|
-
errorMessages.push(
|
|
310
|
-
`Found Mermaid syntax error in ${source} at line ${line}: ${errorMessage}`,
|
|
311
|
-
);
|
|
312
|
-
}),
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
// Check for specific mermaid rendering issues
|
|
316
|
-
const mermaidContent = node.value;
|
|
383
|
+
if (node.lang) {
|
|
317
384
|
const line = node.position?.start?.line || "unknown";
|
|
318
385
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
386
|
+
if (node.lang.toLowerCase() === "mermaid") {
|
|
387
|
+
// Check for mermaid syntax errors
|
|
388
|
+
mermaidChecks.push(
|
|
389
|
+
validateMermaidSyntax(node.value).catch((error) => {
|
|
390
|
+
const errorMessage =
|
|
391
|
+
error?.message || String(error) || "Unknown mermaid syntax error";
|
|
392
|
+
|
|
393
|
+
// Format mermaid error in check-detail-result style
|
|
394
|
+
errorMessages.push(
|
|
395
|
+
`Found Mermaid syntax error in ${source} at line ${line}: ${errorMessage}`,
|
|
396
|
+
);
|
|
397
|
+
}),
|
|
326
398
|
);
|
|
327
|
-
}
|
|
328
399
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
let edgeMatch;
|
|
332
|
-
while ((edgeMatch = edgeDescriptionRegex.exec(mermaidContent)) !== null) {
|
|
333
|
-
const description = edgeMatch[1];
|
|
334
|
-
if (/^\d+\.\s/.test(description)) {
|
|
335
|
-
errorMessages.push(
|
|
336
|
-
`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
|
-
);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
400
|
+
// Check for specific mermaid rendering issues
|
|
401
|
+
const mermaidContent = node.value;
|
|
340
402
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
// Check if the label contains numbered list format
|
|
348
|
-
if (/\d+\.\s/.test(label)) {
|
|
403
|
+
// Check for backticks in node labels
|
|
404
|
+
const nodeLabelRegex = /[A-Za-z0-9_]+\["([^"]*`[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*`[^}]*)"}/g;
|
|
405
|
+
let match;
|
|
406
|
+
match = nodeLabelRegex.exec(mermaidContent);
|
|
407
|
+
while (match !== null) {
|
|
408
|
+
const label = match[1] || match[2];
|
|
349
409
|
errorMessages.push(
|
|
350
|
-
`
|
|
410
|
+
`Found backticks in Mermaid node label in ${source} at line ${line}: "${label}" - backticks in node labels cause rendering issues in Mermaid diagrams`,
|
|
351
411
|
);
|
|
412
|
+
match = nodeLabelRegex.exec(mermaidContent);
|
|
352
413
|
}
|
|
353
|
-
}
|
|
354
414
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
415
|
+
// Check for numbered list format in edge descriptions
|
|
416
|
+
const edgeDescriptionRegex = /--\s*"([^"]*)"\s*-->/g;
|
|
417
|
+
let edgeMatch;
|
|
418
|
+
edgeMatch = edgeDescriptionRegex.exec(mermaidContent);
|
|
419
|
+
while (edgeMatch !== null) {
|
|
420
|
+
const description = edgeMatch[1];
|
|
421
|
+
if (/^\d+\.\s/.test(description)) {
|
|
422
|
+
errorMessages.push(
|
|
423
|
+
`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`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
edgeMatch = edgeDescriptionRegex.exec(mermaidContent);
|
|
427
|
+
}
|
|
367
428
|
|
|
368
|
-
|
|
429
|
+
// Check for numbered list format in node labels (for both [] and {} syntax)
|
|
430
|
+
const nodeLabelWithNumberRegex =
|
|
431
|
+
/[A-Za-z0-9_]+\["([^"]*\d+\.\s[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*\d+\.\s[^}]*)"}/g;
|
|
432
|
+
let numberMatch;
|
|
433
|
+
numberMatch = nodeLabelWithNumberRegex.exec(mermaidContent);
|
|
434
|
+
while (numberMatch !== null) {
|
|
435
|
+
const label = numberMatch[1] || numberMatch[2];
|
|
436
|
+
// Check if the label contains numbered list format
|
|
437
|
+
if (/\d+\.\s/.test(label)) {
|
|
369
438
|
errorMessages.push(
|
|
370
|
-
`Found
|
|
371
|
-
", ",
|
|
372
|
-
)} - node labels with special characters should be quoted like ${nodeId}["${label}"]`,
|
|
439
|
+
`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`,
|
|
373
440
|
);
|
|
374
441
|
}
|
|
442
|
+
numberMatch = nodeLabelWithNumberRegex.exec(mermaidContent);
|
|
375
443
|
}
|
|
444
|
+
|
|
445
|
+
// Check for special characters in node labels that should be quoted
|
|
446
|
+
const nodeWithSpecialCharsRegex = /([A-Za-z0-9_]+)\[([^\]]*[(){}:;,\-\s.][^\]]*)\]/g;
|
|
447
|
+
let specialCharMatch;
|
|
448
|
+
specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent);
|
|
449
|
+
while (specialCharMatch !== null) {
|
|
450
|
+
const nodeId = specialCharMatch[1];
|
|
451
|
+
const label = specialCharMatch[2];
|
|
452
|
+
|
|
453
|
+
// Check if label contains special characters but is not quoted
|
|
454
|
+
if (!/^".*"$/.test(label)) {
|
|
455
|
+
// List of characters that typically need quoting
|
|
456
|
+
const specialChars = ["(", ")", "{", "}", ":", ";", ",", "-", "."];
|
|
457
|
+
const foundSpecialChars = specialChars.filter((char) => label.includes(char));
|
|
458
|
+
|
|
459
|
+
if (foundSpecialChars.length > 0) {
|
|
460
|
+
errorMessages.push(
|
|
461
|
+
`Found unquoted special characters in Mermaid node label in ${source} at line ${line}: "${label}" contains ${foundSpecialChars.join(
|
|
462
|
+
", ",
|
|
463
|
+
)} - node labels with special characters should be quoted like ${nodeId}["${label}"]`,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (node.lang.toLowerCase() === "d2") {
|
|
471
|
+
d2ChecksList.push({
|
|
472
|
+
content: node.value,
|
|
473
|
+
line,
|
|
474
|
+
});
|
|
376
475
|
}
|
|
476
|
+
// TODO: @zhanghan need to check correctness of every code language
|
|
377
477
|
}
|
|
378
478
|
});
|
|
379
479
|
|
|
@@ -424,6 +524,15 @@ export async function checkMarkdown(markdown, source = "content", options = {})
|
|
|
424
524
|
|
|
425
525
|
// Wait for all mermaid checks to complete
|
|
426
526
|
await Promise.all(mermaidChecks);
|
|
527
|
+
await pMap(
|
|
528
|
+
d2ChecksList,
|
|
529
|
+
async ({ content, line }) =>
|
|
530
|
+
checkD2Content({ content }).catch((err) => {
|
|
531
|
+
const errorMessage = err?.message || String(err) || "Unknown d2 syntax error";
|
|
532
|
+
errorMessages.push(`Found D2 syntax error in ${source} at line ${line}: ${errorMessage}`);
|
|
533
|
+
}),
|
|
534
|
+
{ concurrency: KROKI_CONCURRENCY },
|
|
535
|
+
);
|
|
427
536
|
|
|
428
537
|
// Run markdown linting rules
|
|
429
538
|
await processor.run(ast, file);
|
|
@@ -431,7 +540,6 @@ export async function checkMarkdown(markdown, source = "content", options = {})
|
|
|
431
540
|
// Format messages in check-detail-result style
|
|
432
541
|
file.messages.forEach((message) => {
|
|
433
542
|
const line = message.line || "unknown";
|
|
434
|
-
const _column = message.column || "unknown";
|
|
435
543
|
const reason = message.reason || "Unknown markdown issue";
|
|
436
544
|
const ruleId = message.ruleId || message.source || "markdown";
|
|
437
545
|
|
package/utils/utils.mjs
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
2
3
|
import { accessSync, constants, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
3
4
|
import fs from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
|
-
import { parse } from "yaml";
|
|
6
|
+
import { parse, stringify as yamlStringify } from "yaml";
|
|
7
|
+
import {
|
|
8
|
+
detectResolvableConflicts,
|
|
9
|
+
generateConflictResolutionRules,
|
|
10
|
+
} from "./conflict-detector.mjs";
|
|
6
11
|
import {
|
|
7
12
|
DEFAULT_EXCLUDE_PATTERNS,
|
|
8
13
|
DEFAULT_INCLUDE_PATTERNS,
|
|
@@ -32,6 +37,16 @@ export function toRelativePath(filePath) {
|
|
|
32
37
|
return path.isAbsolute(filePath) ? path.relative(process.cwd(), filePath) : filePath;
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Check if a string looks like a glob pattern
|
|
42
|
+
* @param {string} pattern - The string to check
|
|
43
|
+
* @returns {boolean} - True if the string contains glob pattern characters
|
|
44
|
+
*/
|
|
45
|
+
export function isGlobPattern(pattern) {
|
|
46
|
+
if (pattern == null) return false;
|
|
47
|
+
return /[*?[\]]|(\*\*)/.test(pattern);
|
|
48
|
+
}
|
|
49
|
+
|
|
35
50
|
export function processContent({ content }) {
|
|
36
51
|
// Match markdown regular links [text](link), exclude images 
|
|
37
52
|
return content.replace(/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g, (match, text, link) => {
|
|
@@ -95,6 +110,7 @@ export async function saveDocWithTranslations({
|
|
|
95
110
|
|
|
96
111
|
// Add labels front matter if labels are provided
|
|
97
112
|
let finalContent = processContent({ content });
|
|
113
|
+
|
|
98
114
|
if (labels && labels.length > 0) {
|
|
99
115
|
const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
|
|
100
116
|
finalContent = frontMatter + finalContent;
|
|
@@ -113,6 +129,7 @@ export async function saveDocWithTranslations({
|
|
|
113
129
|
let finalTranslationContent = processContent({
|
|
114
130
|
content: translate.translation,
|
|
115
131
|
});
|
|
132
|
+
|
|
116
133
|
if (labels && labels.length > 0) {
|
|
117
134
|
const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
|
|
118
135
|
finalTranslationContent = frontMatter + finalTranslationContent;
|
|
@@ -149,8 +166,8 @@ export function getCurrentGitHead() {
|
|
|
149
166
|
* @param {string} gitHead - The current git HEAD commit hash
|
|
150
167
|
*/
|
|
151
168
|
export async function saveGitHeadToConfig(gitHead) {
|
|
152
|
-
if (!gitHead) {
|
|
153
|
-
return; // Skip if no git HEAD available
|
|
169
|
+
if (!gitHead || process.env.NODE_ENV === "test" || process.env.BUN_TEST) {
|
|
170
|
+
return; // Skip if no git HEAD available or in test environment
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
try {
|
|
@@ -169,7 +186,9 @@ export async function saveGitHeadToConfig(gitHead) {
|
|
|
169
186
|
|
|
170
187
|
// Check if lastGitHead already exists in the file
|
|
171
188
|
const lastGitHeadRegex = /^lastGitHead:\s*.*$/m;
|
|
172
|
-
|
|
189
|
+
// Use yaml library to safely serialize the git head value
|
|
190
|
+
const yamlContent = yamlStringify({ lastGitHead: gitHead }).trim();
|
|
191
|
+
const newLastGitHeadLine = yamlContent;
|
|
173
192
|
|
|
174
193
|
if (lastGitHeadRegex.test(fileContent)) {
|
|
175
194
|
// Replace existing lastGitHead line
|
|
@@ -287,8 +306,10 @@ export function hasFileChangesBetweenCommits(
|
|
|
287
306
|
return addedOrDeletedFiles.some((filePath) => {
|
|
288
307
|
// Check if file matches any include pattern
|
|
289
308
|
const matchesInclude = includePatterns.some((pattern) => {
|
|
290
|
-
//
|
|
291
|
-
const
|
|
309
|
+
// First escape all regex special characters except * and ?
|
|
310
|
+
const escapedPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
311
|
+
// Then convert glob wildcards to regex
|
|
312
|
+
const regexPattern = escapedPattern.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
292
313
|
const regex = new RegExp(regexPattern);
|
|
293
314
|
return regex.test(filePath);
|
|
294
315
|
});
|
|
@@ -299,8 +320,10 @@ export function hasFileChangesBetweenCommits(
|
|
|
299
320
|
|
|
300
321
|
// Check if file matches any exclude pattern
|
|
301
322
|
const matchesExclude = excludePatterns.some((pattern) => {
|
|
302
|
-
//
|
|
303
|
-
const
|
|
323
|
+
// First escape all regex special characters except * and ?
|
|
324
|
+
const escapedPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
325
|
+
// Then convert glob wildcards to regex
|
|
326
|
+
const regexPattern = escapedPattern.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
304
327
|
const regex = new RegExp(regexPattern);
|
|
305
328
|
return regex.test(filePath);
|
|
306
329
|
});
|
|
@@ -351,9 +374,10 @@ export async function loadConfigFromFile() {
|
|
|
351
374
|
* @returns {string} Updated file content
|
|
352
375
|
*/
|
|
353
376
|
function handleArrayValueUpdate(key, value, comment, fileContent) {
|
|
354
|
-
//
|
|
355
|
-
const
|
|
356
|
-
|
|
377
|
+
// Use yaml library to safely serialize the key-value pair
|
|
378
|
+
const yamlObject = { [key]: value };
|
|
379
|
+
const yamlContent = yamlStringify(yamlObject).trim();
|
|
380
|
+
const formattedValue = yamlContent;
|
|
357
381
|
|
|
358
382
|
const lines = fileContent.split("\n");
|
|
359
383
|
|
|
@@ -435,7 +459,10 @@ function handleArrayValueUpdate(key, value, comment, fileContent) {
|
|
|
435
459
|
* @returns {string} Updated file content
|
|
436
460
|
*/
|
|
437
461
|
function handleStringValueUpdate(key, value, comment, fileContent) {
|
|
438
|
-
|
|
462
|
+
// Use yaml library to safely serialize the key-value pair
|
|
463
|
+
const yamlObject = { [key]: value };
|
|
464
|
+
const yamlContent = yamlStringify(yamlObject).trim();
|
|
465
|
+
const formattedValue = yamlContent;
|
|
439
466
|
const lines = fileContent.split("\n");
|
|
440
467
|
|
|
441
468
|
// Handle string values (original logic)
|
|
@@ -849,6 +876,29 @@ export function processConfigFields(config) {
|
|
|
849
876
|
const processed = {};
|
|
850
877
|
const allRulesContent = [];
|
|
851
878
|
|
|
879
|
+
// Set default values for missing or empty fields
|
|
880
|
+
const defaults = {
|
|
881
|
+
nodeName: "Section",
|
|
882
|
+
locale: "en",
|
|
883
|
+
sourcesPath: ["./"],
|
|
884
|
+
docsDir: "./.aigne/doc-smith/docs",
|
|
885
|
+
outputDir: "./.aigne/doc-smith/output",
|
|
886
|
+
translateLanguages: [],
|
|
887
|
+
rules: "",
|
|
888
|
+
targetAudience: "",
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
// Apply defaults for missing or empty fields
|
|
892
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
893
|
+
if (
|
|
894
|
+
!config[key] ||
|
|
895
|
+
(Array.isArray(defaultValue) && (!config[key] || config[key].length === 0)) ||
|
|
896
|
+
(typeof defaultValue === "string" && (!config[key] || config[key].trim() === ""))
|
|
897
|
+
) {
|
|
898
|
+
processed[key] = defaultValue;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
852
902
|
// Check if original rules field has content
|
|
853
903
|
if (config.rules) {
|
|
854
904
|
if (typeof config.rules === "string") {
|
|
@@ -868,27 +918,35 @@ export function processConfigFields(config) {
|
|
|
868
918
|
}
|
|
869
919
|
|
|
870
920
|
// Process document purpose (array)
|
|
871
|
-
let purposeContents = "";
|
|
872
921
|
if (config.documentPurpose && Array.isArray(config.documentPurpose)) {
|
|
873
|
-
|
|
874
|
-
.map((key) =>
|
|
875
|
-
|
|
876
|
-
|
|
922
|
+
const purposeRules = config.documentPurpose
|
|
923
|
+
.map((key) => {
|
|
924
|
+
const style = DOCUMENT_STYLES[key];
|
|
925
|
+
if (!style) return null;
|
|
926
|
+
return `Document Purpose - ${style.name}:\n${style.description}\n${style.content}`;
|
|
927
|
+
})
|
|
928
|
+
.filter(Boolean);
|
|
877
929
|
|
|
878
|
-
if (
|
|
879
|
-
allRulesContent.push(
|
|
930
|
+
if (purposeRules.length > 0) {
|
|
931
|
+
allRulesContent.push(purposeRules.join("\n\n"));
|
|
880
932
|
}
|
|
881
933
|
}
|
|
882
934
|
|
|
883
935
|
// Process target audience types (array)
|
|
884
|
-
let audienceContents = "";
|
|
885
936
|
let audienceNames = "";
|
|
886
937
|
if (config.targetAudienceTypes && Array.isArray(config.targetAudienceTypes)) {
|
|
887
|
-
// Get content for rules
|
|
888
|
-
|
|
889
|
-
.map((key) =>
|
|
890
|
-
|
|
891
|
-
|
|
938
|
+
// Get structured content for rules
|
|
939
|
+
const audienceRules = config.targetAudienceTypes
|
|
940
|
+
.map((key) => {
|
|
941
|
+
const audience = TARGET_AUDIENCES[key];
|
|
942
|
+
if (!audience) return null;
|
|
943
|
+
return `Target Audience - ${audience.name}:\n${audience.description}\n${audience.content}`;
|
|
944
|
+
})
|
|
945
|
+
.filter(Boolean);
|
|
946
|
+
|
|
947
|
+
if (audienceRules.length > 0) {
|
|
948
|
+
allRulesContent.push(audienceRules.join("\n\n"));
|
|
949
|
+
}
|
|
892
950
|
|
|
893
951
|
// Get names for targetAudience field
|
|
894
952
|
audienceNames = config.targetAudienceTypes
|
|
@@ -896,10 +954,6 @@ export function processConfigFields(config) {
|
|
|
896
954
|
.filter(Boolean)
|
|
897
955
|
.join(", ");
|
|
898
956
|
|
|
899
|
-
if (audienceContents) {
|
|
900
|
-
allRulesContent.push(audienceContents);
|
|
901
|
-
}
|
|
902
|
-
|
|
903
957
|
if (audienceNames) {
|
|
904
958
|
// Check if original targetAudience field has content
|
|
905
959
|
const existingTargetAudience = config.targetAudience?.trim();
|
|
@@ -933,6 +987,16 @@ export function processConfigFields(config) {
|
|
|
933
987
|
}
|
|
934
988
|
}
|
|
935
989
|
|
|
990
|
+
// Detect and handle conflicts in user selections
|
|
991
|
+
const conflicts = detectResolvableConflicts(config);
|
|
992
|
+
if (conflicts.length > 0) {
|
|
993
|
+
const conflictResolutionRules = generateConflictResolutionRules(conflicts);
|
|
994
|
+
allRulesContent.push(conflictResolutionRules);
|
|
995
|
+
|
|
996
|
+
// Store conflict information for debugging/logging
|
|
997
|
+
processed.detectedConflicts = conflicts;
|
|
998
|
+
}
|
|
999
|
+
|
|
936
1000
|
// Combine all content into rules field
|
|
937
1001
|
if (allRulesContent.length > 0) {
|
|
938
1002
|
processed.rules = allRulesContent.join("\n\n");
|
|
@@ -1087,3 +1151,7 @@ export function detectSystemLanguage() {
|
|
|
1087
1151
|
return "en";
|
|
1088
1152
|
}
|
|
1089
1153
|
}
|
|
1154
|
+
|
|
1155
|
+
export function getContentHash(str) {
|
|
1156
|
+
return crypto.createHash("sha256").update(str).digest("hex");
|
|
1157
|
+
}
|