@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.
Files changed (42) hide show
  1. package/.github/workflows/ci.yml +46 -0
  2. package/.github/workflows/reviewer.yml +2 -1
  3. package/CHANGELOG.md +17 -0
  4. package/agents/chat.yaml +30 -0
  5. package/agents/check-detail-result.mjs +2 -1
  6. package/agents/check-detail.mjs +1 -0
  7. package/agents/check-structure-plan.mjs +1 -1
  8. package/agents/docs-fs.yaml +25 -0
  9. package/agents/exit.mjs +6 -0
  10. package/agents/feedback-refiner.yaml +5 -1
  11. package/agents/find-items-by-paths.mjs +10 -4
  12. package/agents/fs.mjs +60 -0
  13. package/agents/input-generator.mjs +159 -90
  14. package/agents/load-config.mjs +0 -5
  15. package/agents/load-sources.mjs +119 -12
  16. package/agents/publish-docs.mjs +28 -11
  17. package/agents/retranslate.yaml +1 -1
  18. package/agents/team-publish-docs.yaml +2 -2
  19. package/aigne.yaml +1 -0
  20. package/package.json +13 -10
  21. package/prompts/content-detail-generator.md +12 -4
  22. package/prompts/document/custom-components.md +80 -0
  23. package/prompts/document/d2-chart/diy-examples.md +44 -0
  24. package/prompts/document/d2-chart/official-examples.md +708 -0
  25. package/prompts/document/d2-chart/rules.md +48 -0
  26. package/prompts/document/detail-generator.md +13 -15
  27. package/prompts/document/structure-planning.md +1 -3
  28. package/prompts/feedback-refiner.md +81 -60
  29. package/prompts/structure-planning.md +20 -3
  30. package/tests/check-detail-result.test.mjs +50 -2
  31. package/tests/conflict-resolution.test.mjs +237 -0
  32. package/tests/input-generator.test.mjs +940 -0
  33. package/tests/load-sources.test.mjs +627 -3
  34. package/tests/preferences-utils.test.mjs +94 -0
  35. package/tests/save-value-to-config.test.mjs +182 -5
  36. package/tests/utils.test.mjs +49 -0
  37. package/utils/auth-utils.mjs +1 -1
  38. package/utils/conflict-detector.mjs +72 -1
  39. package/utils/constants.mjs +139 -126
  40. package/utils/kroki-utils.mjs +162 -0
  41. package/utils/markdown-checker.mjs +175 -67
  42. 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 ((match = linkRegex.exec(markdown)) !== null) {
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}: ![${altText}](${imagePath}) - only valid media resources can be used`,
214
+ );
215
+ }
216
+ } catch {
217
+ errorMessages.push(
218
+ `Found invalid local image in ${source}: ![${altText}](${imagePath}) - 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}: ![${altText}](${imagePath}) - only valid media resources can be used`,
227
+ );
228
+ }
229
+ } catch {
230
+ errorMessages.push(
231
+ `Found invalid local image in ${source}: ![${altText}](${imagePath}) - 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 content structure and formatting issues
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 && node.lang.toLowerCase() === "mermaid") {
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
- // Check for backticks in node labels
320
- const nodeLabelRegex = /[A-Za-z0-9_]+\["([^"]*`[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*`[^}]*)"}/g;
321
- let match;
322
- while ((match = nodeLabelRegex.exec(mermaidContent)) !== null) {
323
- const label = match[1] || match[2];
324
- errorMessages.push(
325
- `Found backticks in Mermaid node label in ${source} at line ${line}: "${label}" - backticks in node labels cause rendering issues in Mermaid diagrams`,
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
- // Check for numbered list format in edge descriptions
330
- const edgeDescriptionRegex = /--\s*"([^"]*)"\s*-->/g;
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
- // Check for numbered list format in node labels (for both [] and {} syntax)
342
- const nodeLabelWithNumberRegex =
343
- /[A-Za-z0-9_]+\["([^"]*\d+\.\s[^"]*)"\]|[A-Za-z0-9_]+{"([^}]*\d+\.\s[^}]*)"}/g;
344
- let numberMatch;
345
- while ((numberMatch = nodeLabelWithNumberRegex.exec(mermaidContent)) !== null) {
346
- const label = numberMatch[1] || numberMatch[2];
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
- `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`,
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
- // Check for special characters in node labels that should be quoted
356
- const nodeWithSpecialCharsRegex = /([A-Za-z0-9_]+)\[([^\]]*[(){}:;,\-\s.][^\]]*)\]/g;
357
- let specialCharMatch;
358
- while ((specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent)) !== null) {
359
- const nodeId = specialCharMatch[1];
360
- const label = specialCharMatch[2];
361
-
362
- // Check if label contains special characters but is not quoted
363
- if (!/^".*"$/.test(label)) {
364
- // List of characters that typically need quoting
365
- const specialChars = ["(", ")", "{", "}", ":", ";", ",", "-", "."];
366
- const foundSpecialChars = specialChars.filter((char) => label.includes(char));
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
- if (foundSpecialChars.length > 0) {
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 unquoted special characters in Mermaid node label in ${source} at line ${line}: "${label}" contains ${foundSpecialChars.join(
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 ![text](link)
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
- const newLastGitHeadLine = `lastGitHead: ${gitHead}`;
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
- // Convert glob pattern to regex for matching
291
- const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
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
- // Convert glob pattern to regex for matching
303
- const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
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
- // Format array value
355
- const formattedValue =
356
- value.length === 0 ? `${key}: []` : `${key}:\n${value.map((item) => ` - ${item}`).join("\n")}`;
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
- const formattedValue = `${key}: "${value}"`;
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
- purposeContents = config.documentPurpose
874
- .map((key) => DOCUMENT_STYLES[key]?.content)
875
- .filter(Boolean)
876
- .join("\n\n");
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 (purposeContents) {
879
- allRulesContent.push(purposeContents);
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
- audienceContents = config.targetAudienceTypes
889
- .map((key) => TARGET_AUDIENCES[key]?.content)
890
- .filter(Boolean)
891
- .join("\n\n");
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
+ }