@aigne/doc-smith 0.9.10 → 0.9.11-beta

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 (308) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +189 -219
  3. package/README.zh.md +270 -0
  4. package/agents/bash-executor/index.mjs +347 -0
  5. package/agents/clear/ai/intent.md +142 -0
  6. package/agents/clear/choose-contents.mjs +13 -65
  7. package/agents/clear/clear-auth-tokens.mjs +17 -21
  8. package/agents/clear/clear-deployment-config.mjs +33 -24
  9. package/agents/clear/index.yaml +1 -9
  10. package/agents/content-checker/ai/intent.md +209 -0
  11. package/agents/content-checker/clean-invalid-docs.mjs +254 -0
  12. package/agents/content-checker/index.mjs +191 -0
  13. package/agents/content-checker/validate-content.mjs +983 -0
  14. package/agents/generate-images/generate-image.yaml +75 -0
  15. package/agents/generate-images/generate-summary.mjs +213 -0
  16. package/agents/generate-images/index.yaml +39 -0
  17. package/agents/generate-images/prepare-generation.mjs +286 -0
  18. package/agents/generate-images/prepare-image-generation.mjs +130 -0
  19. package/{prompts/detail/diagram/generate-image-system.md → agents/generate-images/prompts/system.md} +22 -56
  20. package/agents/generate-images/prompts/user.md +85 -0
  21. package/agents/generate-images/save-image-result.mjs +247 -0
  22. package/agents/generate-images/scan-image-slots.mjs +247 -0
  23. package/agents/localize/index.yaml +19 -42
  24. package/{prompts/translate → agents/localize/prompts}/translate-document.md +0 -139
  25. package/agents/localize/translate-documents/generate-summary.mjs +163 -0
  26. package/agents/localize/translate-documents/load-glossary.mjs +52 -0
  27. package/agents/localize/translate-documents/prepare-translation.mjs +249 -0
  28. package/agents/localize/translate-documents/save-translation.mjs +171 -0
  29. package/agents/localize/translate-documents/translate-document-to-language.mjs +209 -0
  30. package/agents/localize/translate-documents/translate-document.yaml +23 -0
  31. package/agents/localize/translate-documents/translate-to-languages.yaml +10 -0
  32. package/agents/localize/translate-images/check-image-translation.mjs +225 -0
  33. package/agents/localize/translate-images/detect-text/detect-and-update-shared.mjs +148 -0
  34. package/agents/localize/translate-images/detect-text/detect-image-text.yaml +44 -0
  35. package/agents/localize/translate-images/detect-text/detect-images-text.yaml +21 -0
  36. package/agents/localize/translate-images/detect-text/prompts/detect-image-text-system.md +43 -0
  37. package/agents/localize/translate-images/detect-text/prompts/detect-image-text-user.md +14 -0
  38. package/agents/localize/translate-images/detect-text/save-text-detection.mjs +105 -0
  39. package/agents/localize/translate-images/prepare-image-input.mjs +124 -0
  40. package/agents/localize/translate-images/save-image-translation.mjs +172 -0
  41. package/agents/localize/translate-images/scan-doc-images.mjs +165 -0
  42. package/agents/localize/translate-images/translate-doc-images.yaml +24 -0
  43. package/agents/localize/{translate-diagram.yaml → translate-images/translate-image.yaml} +25 -14
  44. package/agents/publish/ai/intent.md +182 -0
  45. package/agents/publish/check.mjs +107 -0
  46. package/agents/publish/index.yaml +9 -14
  47. package/agents/publish/publish-docs.mjs +81 -61
  48. package/agents/publish/translate-meta.mjs +79 -58
  49. package/agents/save-document/index.mjs +260 -0
  50. package/agents/structure-checker/index.mjs +307 -0
  51. package/agents/structure-checker/validate-structure.mjs +477 -0
  52. package/agents/update-image/analyze-feedback.yaml +37 -0
  53. package/agents/update-image/index.yaml +78 -0
  54. package/agents/update-image/load-existing-image.mjs +211 -0
  55. package/agents/update-image/prompts/analyze-feedback-system.md +43 -0
  56. package/agents/update-image/prompts/analyze-feedback-user.md +15 -0
  57. package/aigne.yaml +26 -139
  58. package/package.json +16 -48
  59. package/scripts/README.md +90 -0
  60. package/scripts/install.sh +86 -0
  61. package/scripts/uninstall.sh +52 -0
  62. package/skills/doc-smith/SKILL.md +285 -0
  63. package/skills/doc-smith/ai/intent/sources-improve.md +290 -0
  64. package/skills/doc-smith/references/changeset-guide.md +171 -0
  65. package/skills/doc-smith/references/document-content-guide.md +214 -0
  66. package/skills/doc-smith/references/document-structure-schema.md +138 -0
  67. package/skills/doc-smith/references/patch-guide.md +96 -0
  68. package/skills/doc-smith/references/structure-confirmation-guide.md +133 -0
  69. package/skills/doc-smith/references/structure-planning-guide.md +149 -0
  70. package/skills/doc-smith/references/update-workflow.md +108 -0
  71. package/skills/doc-smith/references/user-intent-guide.md +175 -0
  72. package/skills/doc-smith/references/workspace-initialization.md +376 -0
  73. package/skills/doc-smith-docs-detail/SKILL.md +356 -0
  74. package/skills/doc-smith-docs-detail/ai/intent.md +271 -0
  75. package/skills-entry/doc-smith/ai/intent.md +260 -0
  76. package/skills-entry/doc-smith/index.mjs +66 -0
  77. package/skills-entry/doc-smith/prompt.md +57 -0
  78. package/skills-entry/doc-smith/utils.mjs +27 -0
  79. package/skills-entry/doc-smith-docs-detail/batch.yaml +56 -0
  80. package/skills-entry/doc-smith-docs-detail/index.mjs +95 -0
  81. package/skills-entry/doc-smith-docs-detail/prompt.md +64 -0
  82. package/utils/afs-factory.mjs +183 -0
  83. package/utils/agent-constants.mjs +97 -0
  84. package/utils/{auth-utils.mjs → auth.mjs} +6 -9
  85. package/{agents/utils/update-branding.mjs → utils/branding.mjs} +3 -4
  86. package/utils/config.mjs +261 -0
  87. package/utils/constants.mjs +32 -0
  88. package/utils/deploy.mjs +3 -3
  89. package/utils/docs-converter.mjs +454 -0
  90. package/utils/docs.mjs +212 -0
  91. package/utils/document-paths.mjs +172 -0
  92. package/utils/files.mjs +74 -0
  93. package/utils/git.mjs +65 -0
  94. package/utils/{blocklet.mjs → http.mjs} +18 -0
  95. package/utils/image-slots.mjs +57 -0
  96. package/utils/image-utils.mjs +114 -0
  97. package/utils/project.mjs +95 -0
  98. package/utils/sources-path-resolver.mjs +76 -0
  99. package/utils/{upload-files.mjs → upload.mjs} +3 -3
  100. package/utils/workspace.mjs +371 -0
  101. package/agents/chat/chat-system.md +0 -38
  102. package/agents/chat/index.mjs +0 -59
  103. package/agents/chat/skills/generate-document.yaml +0 -15
  104. package/agents/chat/skills/list-documents.mjs +0 -15
  105. package/agents/chat/skills/update-document.yaml +0 -24
  106. package/agents/clear/clear-document-config.mjs +0 -36
  107. package/agents/clear/clear-document-structure.mjs +0 -102
  108. package/agents/clear/clear-generated-docs.mjs +0 -142
  109. package/agents/clear/clear-media-description.mjs +0 -129
  110. package/agents/create/aggregate-document-structure.mjs +0 -21
  111. package/agents/create/analyze-diagram-type-llm.yaml +0 -159
  112. package/agents/create/analyze-diagram-type.mjs +0 -455
  113. package/agents/create/check-document-structure.yaml +0 -30
  114. package/agents/create/check-need-generate-structure.mjs +0 -138
  115. package/agents/create/document-structure-tools/add-document.mjs +0 -85
  116. package/agents/create/document-structure-tools/delete-document.mjs +0 -116
  117. package/agents/create/document-structure-tools/move-document.mjs +0 -109
  118. package/agents/create/document-structure-tools/update-document.mjs +0 -84
  119. package/agents/create/generate-diagram-image.yaml +0 -91
  120. package/agents/create/generate-structure.yaml +0 -106
  121. package/agents/create/index.yaml +0 -45
  122. package/agents/create/refine-document-structure.yaml +0 -12
  123. package/agents/create/replace-d2-with-image.mjs +0 -610
  124. package/agents/create/update-document-structure.yaml +0 -54
  125. package/agents/create/user-add-document/add-documents-to-structure.mjs +0 -90
  126. package/agents/create/user-add-document/find-documents-to-add-links.yaml +0 -47
  127. package/agents/create/user-add-document/index.yaml +0 -46
  128. package/agents/create/user-add-document/prepare-documents-to-translate.mjs +0 -22
  129. package/agents/create/user-add-document/print-add-document-summary.mjs +0 -63
  130. package/agents/create/user-add-document/review-documents-with-new-links.mjs +0 -110
  131. package/agents/create/user-remove-document/find-documents-with-invalid-links.mjs +0 -78
  132. package/agents/create/user-remove-document/index.yaml +0 -40
  133. package/agents/create/user-remove-document/prepare-documents-to-translate.mjs +0 -22
  134. package/agents/create/user-remove-document/print-remove-document-summary.mjs +0 -53
  135. package/agents/create/user-remove-document/remove-documents-from-structure.mjs +0 -99
  136. package/agents/create/user-remove-document/review-documents-with-invalid-links.mjs +0 -115
  137. package/agents/create/user-review-document-structure.mjs +0 -139
  138. package/agents/create/utils/init-current-content.mjs +0 -34
  139. package/agents/create/utils/merge-document-structures.mjs +0 -36
  140. package/agents/evaluate/code-snippet.mjs +0 -97
  141. package/agents/evaluate/document-structure.yaml +0 -67
  142. package/agents/evaluate/document.yaml +0 -82
  143. package/agents/evaluate/generate-report.mjs +0 -85
  144. package/agents/evaluate/index.yaml +0 -46
  145. package/agents/history/index.yaml +0 -6
  146. package/agents/history/view.mjs +0 -78
  147. package/agents/init/check.mjs +0 -16
  148. package/agents/init/index.mjs +0 -643
  149. package/agents/init/validate.mjs +0 -16
  150. package/agents/localize/choose-language.mjs +0 -107
  151. package/agents/localize/record-translation-history.mjs +0 -23
  152. package/agents/localize/save-doc-translation-or-skip.mjs +0 -18
  153. package/agents/localize/set-review-content.mjs +0 -58
  154. package/agents/localize/translate-document-wrapper.mjs +0 -34
  155. package/agents/localize/translate-document.yaml +0 -24
  156. package/agents/localize/translate-multilingual.yaml +0 -57
  157. package/agents/localize/translate-or-skip-diagram.mjs +0 -52
  158. package/agents/media/batch-generate-media-description.yaml +0 -46
  159. package/agents/media/generate-media-description.yaml +0 -50
  160. package/agents/media/load-media-description.mjs +0 -454
  161. package/agents/prefs/index.mjs +0 -203
  162. package/agents/schema/document-structure-item.yaml +0 -26
  163. package/agents/schema/document-structure-refine-item.yaml +0 -23
  164. package/agents/schema/document-structure.yaml +0 -29
  165. package/agents/update/batch-generate-document.yaml +0 -27
  166. package/agents/update/batch-update-document.yaml +0 -7
  167. package/agents/update/check-diagram-flag.mjs +0 -116
  168. package/agents/update/check-document.mjs +0 -162
  169. package/agents/update/check-generate-diagram.mjs +0 -106
  170. package/agents/update/check-update-is-single.mjs +0 -53
  171. package/agents/update/document-tools/update-document-content.mjs +0 -303
  172. package/agents/update/generate-diagram.yaml +0 -80
  173. package/agents/update/generate-document.yaml +0 -70
  174. package/agents/update/handle-document-update.yaml +0 -103
  175. package/agents/update/index.yaml +0 -69
  176. package/agents/update/pre-check-generate-diagram.yaml +0 -44
  177. package/agents/update/save-and-translate-document.mjs +0 -80
  178. package/agents/update/update-document-detail.yaml +0 -71
  179. package/agents/update/update-single/update-single-document-detail.mjs +0 -322
  180. package/agents/update/update-single-document.yaml +0 -7
  181. package/agents/update/user-review-document.mjs +0 -272
  182. package/agents/utils/action-success.mjs +0 -16
  183. package/agents/utils/analyze-document-feedback-intent.yaml +0 -32
  184. package/agents/utils/analyze-feedback-intent.mjs +0 -253
  185. package/agents/utils/analyze-structure-feedback-intent.yaml +0 -29
  186. package/agents/utils/check-detail-result.mjs +0 -51
  187. package/agents/utils/check-feedback-refiner.mjs +0 -81
  188. package/agents/utils/choose-docs.mjs +0 -251
  189. package/agents/utils/document-icon-generate.yaml +0 -52
  190. package/agents/utils/document-title-streamline.yaml +0 -48
  191. package/agents/utils/ensure-document-icons.mjs +0 -129
  192. package/agents/utils/exit.mjs +0 -6
  193. package/agents/utils/feedback-refiner.yaml +0 -50
  194. package/agents/utils/find-item-by-path.mjs +0 -114
  195. package/agents/utils/find-user-preferences-by-path.mjs +0 -37
  196. package/agents/utils/format-document-structure.mjs +0 -35
  197. package/agents/utils/generate-document-or-skip.mjs +0 -41
  198. package/agents/utils/handle-diagram-operations.mjs +0 -263
  199. package/agents/utils/load-all-document-content.mjs +0 -30
  200. package/agents/utils/load-document-all-content.mjs +0 -96
  201. package/agents/utils/load-sources.mjs +0 -405
  202. package/agents/utils/map-reasoning-effort-level.mjs +0 -15
  203. package/agents/utils/post-generate.mjs +0 -133
  204. package/agents/utils/read-current-document-content.mjs +0 -46
  205. package/agents/utils/save-doc-translation.mjs +0 -30
  206. package/agents/utils/save-doc.mjs +0 -54
  207. package/agents/utils/save-output.mjs +0 -26
  208. package/agents/utils/save-sidebar.mjs +0 -38
  209. package/agents/utils/skip-if-content-exists.mjs +0 -27
  210. package/agents/utils/streamline-document-titles-if-needed.mjs +0 -88
  211. package/agents/utils/transform-detail-data-sources.mjs +0 -45
  212. package/assets/report-template/report.html +0 -198
  213. package/docs-mcp/analyze-content-relevance.yaml +0 -50
  214. package/docs-mcp/analyze-docs-relevance.yaml +0 -59
  215. package/docs-mcp/docs-search.yaml +0 -42
  216. package/docs-mcp/get-docs-detail.mjs +0 -41
  217. package/docs-mcp/get-docs-structure.mjs +0 -16
  218. package/docs-mcp/read-doc-content.mjs +0 -119
  219. package/prompts/common/document/content-rules-core.md +0 -20
  220. package/prompts/common/document/markdown-syntax-rules.md +0 -65
  221. package/prompts/common/document/media-file-list-usage-rules.md +0 -18
  222. package/prompts/common/document/openapi-usage-rules.md +0 -189
  223. package/prompts/common/document/role-and-personality.md +0 -16
  224. package/prompts/common/document/user-preferences.md +0 -9
  225. package/prompts/common/document-structure/conflict-resolution-guidance.md +0 -16
  226. package/prompts/common/document-structure/document-icon-generate.md +0 -116
  227. package/prompts/common/document-structure/document-structure-rules.md +0 -43
  228. package/prompts/common/document-structure/document-title-streamline.md +0 -86
  229. package/prompts/common/document-structure/glossary.md +0 -7
  230. package/prompts/common/document-structure/intj-traits.md +0 -5
  231. package/prompts/common/document-structure/openapi-usage-rules.md +0 -28
  232. package/prompts/common/document-structure/output-constraints.md +0 -18
  233. package/prompts/common/document-structure/user-locale-rules.md +0 -10
  234. package/prompts/common/document-structure/user-preferences.md +0 -9
  235. package/prompts/detail/custom/admonition-usage-rules.md +0 -94
  236. package/prompts/detail/custom/code-block-usage-rules.md +0 -163
  237. package/prompts/detail/custom/custom-components/x-card-usage-rules.md +0 -63
  238. package/prompts/detail/custom/custom-components/x-cards-usage-rules.md +0 -83
  239. package/prompts/detail/custom/custom-components/x-field-desc-usage-rules.md +0 -120
  240. package/prompts/detail/custom/custom-components/x-field-group-usage-rules.md +0 -80
  241. package/prompts/detail/custom/custom-components/x-field-usage-rules.md +0 -189
  242. package/prompts/detail/custom/custom-components-usage-rules.md +0 -18
  243. package/prompts/detail/diagram/generate-image-user.md +0 -81
  244. package/prompts/detail/diagram/guide.md +0 -29
  245. package/prompts/detail/diagram/official-examples.md +0 -712
  246. package/prompts/detail/diagram/pre-check.md +0 -23
  247. package/prompts/detail/diagram/role-and-personality.md +0 -2
  248. package/prompts/detail/diagram/rules.md +0 -46
  249. package/prompts/detail/diagram/system-prompt.md +0 -1139
  250. package/prompts/detail/diagram/user-prompt.md +0 -43
  251. package/prompts/detail/generate/detail-example.md +0 -457
  252. package/prompts/detail/generate/document-rules.md +0 -45
  253. package/prompts/detail/generate/system-prompt.md +0 -61
  254. package/prompts/detail/generate/user-prompt.md +0 -99
  255. package/prompts/detail/jsx/rules.md +0 -6
  256. package/prompts/detail/update/system-prompt.md +0 -121
  257. package/prompts/detail/update/user-prompt.md +0 -41
  258. package/prompts/evaluate/document-structure.md +0 -93
  259. package/prompts/evaluate/document.md +0 -149
  260. package/prompts/media/media-description/system-prompt.md +0 -43
  261. package/prompts/media/media-description/user-prompt.md +0 -17
  262. package/prompts/structure/check-document-structure.md +0 -93
  263. package/prompts/structure/document-rules.md +0 -21
  264. package/prompts/structure/find-documents-to-add-links.md +0 -52
  265. package/prompts/structure/generate/system-prompt.md +0 -13
  266. package/prompts/structure/generate/user-prompt.md +0 -137
  267. package/prompts/structure/review/structure-review-system.md +0 -81
  268. package/prompts/structure/structure-example.md +0 -89
  269. package/prompts/structure/structure-getting-started.md +0 -10
  270. package/prompts/structure/update/system-prompt.md +0 -93
  271. package/prompts/structure/update/user-prompt.md +0 -43
  272. package/prompts/translate/admonition.md +0 -20
  273. package/prompts/translate/code-block.md +0 -33
  274. package/prompts/utils/analyze-document-feedback-intent.md +0 -54
  275. package/prompts/utils/analyze-structure-feedback-intent.md +0 -43
  276. package/prompts/utils/feedback-refiner.md +0 -105
  277. package/types/document-schema.mjs +0 -55
  278. package/types/document-structure-schema.mjs +0 -261
  279. package/utils/check-document-has-diagram.mjs +0 -95
  280. package/utils/conflict-detector.mjs +0 -149
  281. package/utils/constants/index.mjs +0 -620
  282. package/utils/constants/linter.mjs +0 -102
  283. package/utils/d2-utils.mjs +0 -205
  284. package/utils/debug.mjs +0 -3
  285. package/utils/delete-diagram-images.mjs +0 -99
  286. package/utils/diagram-version-utils.mjs +0 -14
  287. package/utils/docs-finder-utils.mjs +0 -548
  288. package/utils/evaluate/report-utils.mjs +0 -132
  289. package/utils/extract-api.mjs +0 -32
  290. package/utils/file-utils.mjs +0 -960
  291. package/utils/history-utils.mjs +0 -203
  292. package/utils/icon-map.mjs +0 -26
  293. package/utils/image-compress.mjs +0 -154
  294. package/utils/kroki-utils.mjs +0 -173
  295. package/utils/linter/index.mjs +0 -50
  296. package/utils/load-config.mjs +0 -78
  297. package/utils/markdown/index.mjs +0 -26
  298. package/utils/markdown-checker.mjs +0 -694
  299. package/utils/mermaid-validator.mjs +0 -140
  300. package/utils/mermaid-worker-pool.mjs +0 -250
  301. package/utils/mermaid-worker.mjs +0 -233
  302. package/utils/openapi/index.mjs +0 -28
  303. package/utils/preferences-utils.mjs +0 -175
  304. package/utils/request.mjs +0 -10
  305. package/utils/sync-diagram-to-translations.mjs +0 -272
  306. package/utils/translate-diagram-images.mjs +0 -807
  307. package/utils/utils.mjs +0 -1354
  308. /package/{prompts/translate → agents/localize/prompts}/glossary.md +0 -0
@@ -0,0 +1,983 @@
1
+ import { readFile, access, readdir, stat } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { parse as yamlParse } from "yaml";
4
+ import path from "node:path";
5
+ import { collectDocumentPaths } from "../../utils/document-paths.mjs";
6
+ import { PATHS, ERROR_CODES } from "../../utils/agent-constants.mjs";
7
+ import { isSourcesAbsolutePath, resolveSourcesPath } from "../../utils/sources-path-resolver.mjs";
8
+ import { loadConfigFromFile } from "../../utils/config.mjs";
9
+
10
+ /**
11
+ * Document Content Validator Class
12
+ */
13
+ class DocumentContentValidator {
14
+ constructor(yamlPath = PATHS.DOCUMENT_STRUCTURE, docsDir = PATHS.DOCS_DIR, docs = undefined) {
15
+ this.yamlPath = yamlPath;
16
+ this.docsDir = docsDir;
17
+ this.docsFilter = docs ? new Set(docs) : null;
18
+ this.errors = {
19
+ fatal: [],
20
+ fixable: [],
21
+ warnings: [],
22
+ };
23
+ this.stats = {
24
+ totalDocs: 0,
25
+ checkedDocs: 0,
26
+ totalLinks: 0,
27
+ totalImages: 0,
28
+ localImages: 0,
29
+ remoteImages: 0,
30
+ brokenLinks: 0,
31
+ missingImages: 0,
32
+ inaccessibleRemoteImages: 0,
33
+ };
34
+ this.documents = [];
35
+ this.documentPaths = new Set();
36
+ this.remoteImageCache = new Map();
37
+ this.workspaceConfig = null; // Cache workspace configuration
38
+ }
39
+
40
+ /**
41
+ * Load workspace configuration (lazy loading)
42
+ */
43
+ async loadWorkspaceConfig() {
44
+ if (this.workspaceConfig === null) {
45
+ this.workspaceConfig = (await loadConfigFromFile()) || {};
46
+ }
47
+ return this.workspaceConfig;
48
+ }
49
+
50
+ /**
51
+ * Load sources configuration (lazy loading)
52
+ */
53
+ async loadSourcesConfig() {
54
+ const config = await this.loadWorkspaceConfig();
55
+ return config.sources || [];
56
+ }
57
+
58
+ /**
59
+ * Load translateLanguages configuration (lazy loading)
60
+ */
61
+ async loadTranslateLanguages() {
62
+ const config = await this.loadWorkspaceConfig();
63
+ return config.translateLanguages || [];
64
+ }
65
+
66
+ /**
67
+ * Execute complete validation
68
+ */
69
+ async validate(checkRemoteImages = true) {
70
+ try {
71
+ // Layer 1: Load document structure and validate file existence
72
+ await this.loadDocumentStructure();
73
+ await this.validateDocumentFiles();
74
+
75
+ // Layer 2-4: Check document content one by one
76
+ for (const doc of this.documents) {
77
+ await this.validateDocument(doc, checkRemoteImages);
78
+ }
79
+
80
+ return this.getResult();
81
+ } catch (error) {
82
+ this.errors.fatal.push({
83
+ type: "VALIDATION_ERROR",
84
+ message: `Validation error: ${error.message}`,
85
+ });
86
+ return this.getResult();
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Layer 1: Load document structure
92
+ */
93
+ async loadDocumentStructure() {
94
+ try {
95
+ const content = await readFile(this.yamlPath, "utf8");
96
+ const data = yamlParse(content);
97
+
98
+ if (!data.documents || !Array.isArray(data.documents)) {
99
+ throw new Error(`${this.yamlPath} missing documents field or format error`);
100
+ }
101
+
102
+ // Use shared tool to collect document paths and metadata
103
+ const docsWithMeta = collectDocumentPaths(data.documents, { collectMetadata: true });
104
+
105
+ // Convert to internal format
106
+ for (const doc of docsWithMeta) {
107
+ // If docs filter is specified, only add matching documents
108
+ if (this.docsFilter && !this.docsFilter.has(doc.displayPath)) {
109
+ // Still need to add to documentPaths for link validation
110
+ this.documentPaths.add(doc.displayPath);
111
+ continue;
112
+ }
113
+
114
+ this.documents.push({
115
+ path: doc.displayPath,
116
+ filePath: doc.path,
117
+ title: doc.title || "Unknown document",
118
+ });
119
+ this.documentPaths.add(doc.displayPath);
120
+ }
121
+
122
+ this.stats.totalDocs = this.documents.length;
123
+ } catch (error) {
124
+ if (error.code === "ENOENT") {
125
+ throw new Error(`File not found: ${this.yamlPath}`);
126
+ }
127
+ throw error;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Layer 1: Validate document file existence
133
+ */
134
+ async validateDocumentFiles() {
135
+ for (const doc of this.documents) {
136
+ const docFolder = path.join(this.docsDir, doc.filePath);
137
+
138
+ // Check 1: Folder exists and is a directory
139
+ let folderExists = false;
140
+ try {
141
+ const stats = await stat(docFolder);
142
+ if (!stats.isDirectory()) {
143
+ this.errors.fatal.push({
144
+ type: "INVALID_DOCUMENT_FOLDER",
145
+ path: doc.path,
146
+ filePath: docFolder,
147
+ message: `Path is not a folder: ${doc.path}`,
148
+ suggestion: "Please ensure path points to a folder",
149
+ });
150
+ continue;
151
+ }
152
+ folderExists = true;
153
+ } catch (_error) {
154
+ this.errors.fatal.push({
155
+ type: "MISSING_DOCUMENT_FOLDER",
156
+ path: doc.path,
157
+ filePath: docFolder,
158
+ message: `Document folder missing: ${doc.path}`,
159
+ suggestion: `Please generate this document folder in the specified format`,
160
+ });
161
+ continue;
162
+ }
163
+
164
+ // Check 2: .meta.yaml exists and has correct format
165
+ if (folderExists) {
166
+ await this.validateMetaFile(docFolder, doc);
167
+
168
+ // Check 3: At least one language file exists
169
+ await this.validateLanguageFiles(docFolder, doc);
170
+ }
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Validate .meta.yaml
176
+ */
177
+ async validateMetaFile(docFolder, doc) {
178
+ const metaPath = path.join(docFolder, ".meta.yaml");
179
+
180
+ try {
181
+ await access(metaPath, constants.F_OK | constants.R_OK);
182
+ } catch (_error) {
183
+ this.errors.fatal.push({
184
+ type: "MISSING_META_FILE",
185
+ path: doc.path,
186
+ filePath: metaPath,
187
+ message: `.meta.yaml missing: ${doc.path}`,
188
+ suggestion: "Please create .meta.yaml in the document folder",
189
+ });
190
+ return;
191
+ }
192
+
193
+ // Read and validate content
194
+ try {
195
+ const content = await readFile(metaPath, "utf8");
196
+ const meta = yamlParse(content);
197
+
198
+ // Required field validation
199
+ const requiredFields = ["kind", "source", "default"];
200
+ for (const field of requiredFields) {
201
+ if (!meta[field]) {
202
+ this.errors.fatal.push({
203
+ type: "INVALID_META",
204
+ path: doc.path,
205
+ field,
206
+ message: `.meta.yaml missing required field "${field}": ${doc.path}`,
207
+ suggestion: `Add ${field} field to .meta.yaml`,
208
+ });
209
+ }
210
+ }
211
+
212
+ // kind value validation
213
+ if (meta.kind && meta.kind !== "doc") {
214
+ this.errors.fatal.push({
215
+ type: "INVALID_META",
216
+ path: doc.path,
217
+ field: "kind",
218
+ message: `.meta.yaml kind should be "doc", currently "${meta.kind}"`,
219
+ suggestion: "Change to kind: doc",
220
+ });
221
+ }
222
+
223
+ // source and project locale consistency validation
224
+ if (meta.source) {
225
+ const config = await this.loadWorkspaceConfig();
226
+ const projectLocale = config?.locale;
227
+ if (projectLocale && meta.source !== projectLocale) {
228
+ this.errors.fatal.push({
229
+ type: ERROR_CODES.SOURCE_LOCALE_MISMATCH,
230
+ path: doc.path,
231
+ source: meta.source,
232
+ locale: projectLocale,
233
+ message: `Document source (${meta.source}) does not match project locale (${projectLocale}): ${doc.path}`,
234
+ suggestion: `Change document source to "${projectLocale}", or regenerate the main language version of this document`,
235
+ });
236
+ }
237
+ }
238
+ } catch (error) {
239
+ this.errors.fatal.push({
240
+ type: "INVALID_META",
241
+ path: doc.path,
242
+ message: `.meta.yaml format error: ${error.message}`,
243
+ suggestion: "Check if YAML syntax is correct",
244
+ });
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Validate language files
250
+ */
251
+ async validateLanguageFiles(docFolder, doc) {
252
+ try {
253
+ const files = await readdir(docFolder);
254
+ const langFiles = files.filter(
255
+ (f) => f.endsWith(".md") && !f.startsWith(".") && /^[a-z]{2}(-[A-Z]{2})?\.md$/.test(f),
256
+ );
257
+
258
+ if (langFiles.length === 0) {
259
+ this.errors.fatal.push({
260
+ type: "MISSING_LANGUAGE_FILE",
261
+ path: doc.path,
262
+ message: `No language version files: ${doc.path}`,
263
+ suggestion: "Please generate at least one language version file (e.g., zh.md, en.md)",
264
+ });
265
+ return;
266
+ }
267
+
268
+ // Check if default and source language files exist
269
+ const metaPath = path.join(docFolder, ".meta.yaml");
270
+ try {
271
+ const metaContent = await readFile(metaPath, "utf8");
272
+ const meta = yamlParse(metaContent);
273
+
274
+ if (meta.default) {
275
+ const defaultFile = `${meta.default}.md`;
276
+ if (!langFiles.includes(defaultFile)) {
277
+ this.errors.fatal.push({
278
+ type: "MISSING_DEFAULT_LANGUAGE",
279
+ path: doc.path,
280
+ defaultLang: meta.default,
281
+ message: `Default language file missing: ${defaultFile}`,
282
+ suggestion: `Generate ${defaultFile} or modify the default field in .meta.yaml`,
283
+ });
284
+ }
285
+ }
286
+
287
+ if (meta.source) {
288
+ const sourceFile = `${meta.source}.md`;
289
+ if (!langFiles.includes(sourceFile)) {
290
+ this.errors.fatal.push({
291
+ type: "MISSING_SOURCE_LANGUAGE",
292
+ path: doc.path,
293
+ sourceLang: meta.source,
294
+ message: `Source language file missing: ${sourceFile}`,
295
+ suggestion: `Generate ${sourceFile} or modify the source field in .meta.yaml`,
296
+ });
297
+ }
298
+ }
299
+
300
+ // Check if target language files configured in translateLanguages exist
301
+ const translateLanguages = await this.loadTranslateLanguages();
302
+ if (translateLanguages.length > 0) {
303
+ for (const lang of translateLanguages) {
304
+ // Skip source language (source language doesn't need to be a translation target)
305
+ if (lang === meta.source) continue;
306
+
307
+ const langFile = `${lang}.md`;
308
+ if (!langFiles.includes(langFile)) {
309
+ this.errors.fatal.push({
310
+ type: ERROR_CODES.MISSING_TRANSLATE_LANGUAGE,
311
+ path: doc.path,
312
+ lang,
313
+ message: `Translation language file missing: ${langFile}`,
314
+ suggestion: `Please translate document to ${lang} language, or remove ${lang} from translateLanguages in config.yaml`,
315
+ });
316
+ }
317
+ }
318
+ }
319
+ } catch (_error) {
320
+ // .meta.yaml errors already reported in validateMetaFile
321
+ }
322
+ } catch (error) {
323
+ this.errors.fatal.push({
324
+ type: "READ_FOLDER_ERROR",
325
+ path: doc.path,
326
+ message: `Cannot read document folder: ${error.message}`,
327
+ });
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Layer 2-4: Validate single document content
333
+ */
334
+ async validateDocument(doc, checkRemoteImages) {
335
+ const docFolder = path.join(this.docsDir, doc.filePath);
336
+
337
+ try {
338
+ // Read .meta.yaml to get language list
339
+ const metaPath = path.join(docFolder, ".meta.yaml");
340
+ const metaContent = await readFile(metaPath, "utf8");
341
+ const _meta = yamlParse(metaContent);
342
+
343
+ // Get all language files
344
+ const files = await readdir(docFolder);
345
+ const langFiles = files.filter((f) => f.endsWith(".md") && !f.startsWith("."));
346
+
347
+ // Check each language version
348
+ for (const langFile of langFiles) {
349
+ const fullPath = path.join(docFolder, langFile);
350
+ const content = await readFile(fullPath, "utf8");
351
+
352
+ this.stats.checkedDocs++;
353
+
354
+ // Layer 2: Content parsing and checking
355
+ this.checkEmptyDocument(content, doc, langFile);
356
+ this.checkHeadingHierarchy(content, doc, langFile);
357
+
358
+ // Layer 3: Link and image validation
359
+ await this.validateLinks(content, doc, langFile);
360
+ await this.validateImages(content, doc, langFile, checkRemoteImages);
361
+ }
362
+ } catch (_error) {
363
+ // Errors already reported in Layer 1
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Layer 4: Empty document detection
369
+ */
370
+ checkEmptyDocument(content, doc, langFile) {
371
+ // Remove all headings
372
+ let cleaned = content.replace(/^#{1,6}\s+.+$/gm, "");
373
+ // Remove whitespace
374
+ cleaned = cleaned.replace(/\s+/g, "");
375
+
376
+ if (cleaned.length < 50) {
377
+ this.errors.fatal.push({
378
+ type: "EMPTY_DOCUMENT",
379
+ path: doc.path,
380
+ langFile,
381
+ message: `Empty document: ${doc.path} (${langFile})`,
382
+ suggestion: `Document content is insufficient (less than 50 characters), please add substantial content or remove from structure`,
383
+ });
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Layer 4: Heading hierarchy check
389
+ */
390
+ checkHeadingHierarchy(content, doc, langFile) {
391
+ // First remove content in code blocks to avoid false positives
392
+ const contentWithoutCodeBlocks = this.removeCodeBlocks(content);
393
+
394
+ const headingRegex = /^(#{1,6})\s+(.+)$/gm;
395
+ const headings = [];
396
+
397
+ for (const match of contentWithoutCodeBlocks.matchAll(headingRegex)) {
398
+ headings.push({
399
+ level: match[1].length,
400
+ text: match[2],
401
+ line: contentWithoutCodeBlocks.substring(0, match.index).split("\n").length,
402
+ });
403
+ }
404
+
405
+ for (let i = 1; i < headings.length; i++) {
406
+ const prev = headings[i - 1];
407
+ const curr = headings[i];
408
+
409
+ // Check if heading level was skipped
410
+ if (curr.level > prev.level + 1) {
411
+ this.errors.fatal.push({
412
+ type: "HEADING_SKIP",
413
+ path: doc.path,
414
+ langFile,
415
+ line: curr.line,
416
+ message: `Heading skipped from H${prev.level} to H${curr.level}`,
417
+ suggestion: `Consider changing "${"#".repeat(curr.level)} ${curr.text}" to "${"#".repeat(prev.level + 1)} ${curr.text}"`,
418
+ });
419
+ }
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Remove content in Markdown code blocks
425
+ */
426
+ removeCodeBlocks(content) {
427
+ // Remove fenced code blocks (```...```)
428
+ let result = content.replace(/^```[\s\S]*?^```$/gm, "");
429
+
430
+ // Remove indented code blocks (lines starting with 4 spaces or 1 tab)
431
+ result = result.replace(/^( {4}|\t).+$/gm, "");
432
+
433
+ return result;
434
+ }
435
+
436
+ /**
437
+ * Get position ranges of code blocks
438
+ * @returns {Array<{start: number, end: number}>} Array of code block start/end positions
439
+ */
440
+ getCodeBlockRanges(content) {
441
+ const ranges = [];
442
+
443
+ // Match fenced code blocks (```...```)
444
+ const fencedCodeRegex = /^```[\s\S]*?^```$/gm;
445
+
446
+ for (const match of content.matchAll(fencedCodeRegex)) {
447
+ ranges.push({
448
+ start: match.index,
449
+ end: match.index + match[0].length,
450
+ });
451
+ }
452
+
453
+ // Match inline code blocks (`...`)
454
+ const inlineCodeRegex = /`[^`\n]+`/g;
455
+ for (const match of content.matchAll(inlineCodeRegex)) {
456
+ ranges.push({
457
+ start: match.index,
458
+ end: match.index + match[0].length,
459
+ });
460
+ }
461
+
462
+ // Match indented code blocks (lines starting with 4 spaces or 1 tab)
463
+ const indentedCodeRegex = /^( {4}|\t).+$/gm;
464
+ for (const match of content.matchAll(indentedCodeRegex)) {
465
+ ranges.push({
466
+ start: match.index,
467
+ end: match.index + match[0].length,
468
+ });
469
+ }
470
+
471
+ return ranges;
472
+ }
473
+
474
+ /**
475
+ * Check if position is inside a code block
476
+ * @param {number} position - Position to check
477
+ * @param {Array<{start: number, end: number}>} ranges - Array of code block ranges
478
+ * @returns {boolean} Whether position is in a code block
479
+ */
480
+ isInCodeBlock(position, ranges) {
481
+ return ranges.some((range) => position >= range.start && position < range.end);
482
+ }
483
+
484
+ /**
485
+ * Layer 3: Validate internal links
486
+ */
487
+ async validateLinks(content, doc, langFile) {
488
+ // Get code block position ranges
489
+ const codeBlockRanges = this.getCodeBlockRanges(content);
490
+
491
+ const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
492
+
493
+ for (const match of content.matchAll(linkRegex)) {
494
+ // Check if link is in code block, skip if so
495
+ if (this.isInCodeBlock(match.index, codeBlockRanges)) {
496
+ continue;
497
+ }
498
+
499
+ const linkText = match[1];
500
+ const linkUrl = match[2];
501
+
502
+ this.stats.totalLinks++;
503
+
504
+ // Ignore external links and anchor links
505
+ if (
506
+ linkUrl.startsWith("http://") ||
507
+ linkUrl.startsWith("https://") ||
508
+ linkUrl.startsWith("#")
509
+ ) {
510
+ continue;
511
+ }
512
+
513
+ // Ignore resource file links
514
+ if (this.isResourceFile(linkUrl)) {
515
+ continue;
516
+ }
517
+
518
+ // All other links are treated as internal document links
519
+ await this.validateInternalLink(linkUrl, doc, linkText, langFile);
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Check if link points to resource file (not document)
525
+ */
526
+ isResourceFile(url) {
527
+ // Remove query parameters and anchors
528
+ const cleanUrl = url.split("?")[0].split("#")[0].toLowerCase();
529
+ // Resource file extensions
530
+ const resourceExtensions = [
531
+ ".png",
532
+ ".jpg",
533
+ ".jpeg",
534
+ ".gif",
535
+ ".svg",
536
+ ".webp",
537
+ ".ico",
538
+ ".bmp",
539
+ ".pdf",
540
+ ".doc",
541
+ ".docx",
542
+ ".xls",
543
+ ".xlsx",
544
+ ".ppt",
545
+ ".pptx",
546
+ ".zip",
547
+ ".tar",
548
+ ".gz",
549
+ ".rar",
550
+ ".7z",
551
+ ".mp3",
552
+ ".mp4",
553
+ ".wav",
554
+ ".avi",
555
+ ".mov",
556
+ ".webm",
557
+ ".json",
558
+ ".xml",
559
+ ".csv",
560
+ ".txt",
561
+ ".js",
562
+ ".ts",
563
+ ".css",
564
+ ".scss",
565
+ ".less",
566
+ ".py",
567
+ ".rb",
568
+ ".go",
569
+ ".rs",
570
+ ".java",
571
+ ".c",
572
+ ".cpp",
573
+ ".h",
574
+ ];
575
+ return resourceExtensions.some((ext) => cleanUrl.endsWith(ext));
576
+ }
577
+
578
+ /**
579
+ * Validate internal link
580
+ */
581
+ async validateInternalLink(linkUrl, doc, linkText, langFile) {
582
+ let targetPath;
583
+
584
+ // Remove anchor part for format checking
585
+ const urlWithoutAnchor = linkUrl.split("#")[0];
586
+
587
+ // Check if link format is correct: internal links should not contain .md suffix
588
+ const langSuffixPattern = /\/[a-z]{2}(-[A-Z]{2})?\.md$/; // Match /en.md, /zh.md, /en-US.md
589
+ const mdSuffixPattern = /\.md$/;
590
+
591
+ if (mdSuffixPattern.test(urlWithoutAnchor)) {
592
+ // Link contains .md suffix, this is a format error
593
+ // If it's a language suffix pattern, remove the entire /xx.md part; otherwise only remove .md
594
+ const isLangSuffix = langSuffixPattern.test(urlWithoutAnchor);
595
+ const suggestedLink = isLangSuffix
596
+ ? urlWithoutAnchor.replace(langSuffixPattern, "")
597
+ : urlWithoutAnchor.replace(mdSuffixPattern, "");
598
+
599
+ this.stats.brokenLinks++;
600
+ this.errors.fatal.push({
601
+ type: ERROR_CODES.INVALID_LINK_FORMAT,
602
+ path: doc.path,
603
+ langFile,
604
+ link: linkUrl,
605
+ linkText,
606
+ message: `Internal link format error: [${linkText}](${linkUrl})`,
607
+ suggestion: `Link should not contain .md suffix, suggest changing to: ${suggestedLink}`,
608
+ });
609
+ return;
610
+ }
611
+
612
+ // Link format is correct, continue to validate target existence
613
+ const cleanLinkUrl = urlWithoutAnchor;
614
+
615
+ // If link is just an anchor (like #section), cleanLinkUrl will be empty string, skip check
616
+ if (!cleanLinkUrl) {
617
+ return;
618
+ }
619
+
620
+ if (cleanLinkUrl.startsWith("/")) {
621
+ // Absolute path
622
+ targetPath = cleanLinkUrl;
623
+ } else {
624
+ // Relative path: based on document's "containing directory"
625
+ // Document /getting-started/claude-code's containing directory is /getting-started
626
+ // Example: document /getting-started/claude-code, link ../getting-started -> /getting-started
627
+ // Example: document /getting-started, link ./claude-code -> /getting-started/claude-code
628
+ const docDir = path.dirname(doc.path); // /getting-started/claude-code -> /getting-started
629
+ const upLevels = (cleanLinkUrl.match(/\.\.\//g) || []).length;
630
+ const currentDepth = docDir === "/" ? 0 : docDir.split("/").filter((p) => p).length;
631
+
632
+ if (upLevels > currentDepth) {
633
+ this.stats.brokenLinks++;
634
+ this.errors.fatal.push({
635
+ type: "BROKEN_LINK",
636
+ path: doc.path,
637
+ langFile,
638
+ link: linkUrl,
639
+ linkText,
640
+ message: `Internal link path exceeds root directory: [${linkText}](${linkUrl})`,
641
+ suggestion: `Link goes up ${upLevels} levels, but current document's directory is only at level ${currentDepth}`,
642
+ });
643
+ return;
644
+ }
645
+
646
+ // Merge document's containing directory and relative link
647
+ targetPath = path.posix.normalize(path.posix.join(docDir, cleanLinkUrl));
648
+ if (!targetPath.startsWith("/")) {
649
+ targetPath = `/${targetPath}`;
650
+ }
651
+ }
652
+
653
+ if (!this.documentPaths.has(targetPath)) {
654
+ this.stats.brokenLinks++;
655
+ this.errors.fatal.push({
656
+ type: "BROKEN_LINK",
657
+ path: doc.path,
658
+ langFile,
659
+ link: linkUrl,
660
+ linkText,
661
+ targetPath,
662
+ message: `Internal broken link: [${linkText}](${linkUrl})`,
663
+ suggestion: `Target document ${targetPath} does not exist`,
664
+ });
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Layer 3: Validate images
670
+ */
671
+ async validateImages(content, doc, langFile, checkRemoteImages) {
672
+ // Get code block position ranges
673
+ const codeBlockRanges = this.getCodeBlockRanges(content);
674
+
675
+ const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
676
+
677
+ for (const match of content.matchAll(imageRegex)) {
678
+ // Check if image is in code block, skip if so
679
+ if (this.isInCodeBlock(match.index, codeBlockRanges)) {
680
+ continue;
681
+ }
682
+
683
+ const altText = match[1];
684
+ const imageUrl = match[2];
685
+
686
+ this.stats.totalImages++;
687
+
688
+ // Categorize: local vs remote
689
+ if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) {
690
+ // Remote image
691
+ this.stats.remoteImages++;
692
+ if (checkRemoteImages) {
693
+ await this.validateRemoteImage(imageUrl, doc, altText, langFile);
694
+ }
695
+ } else {
696
+ // Local image
697
+ this.stats.localImages++;
698
+ await this.validateLocalImage(imageUrl, doc, altText, langFile);
699
+ }
700
+ }
701
+ }
702
+
703
+ /**
704
+ * Validate local image
705
+ */
706
+ async validateLocalImage(imageUrl, doc, altText, langFile) {
707
+ let imagePath;
708
+
709
+ // Check if it's a /sources/... absolute path
710
+ if (isSourcesAbsolutePath(imageUrl)) {
711
+ const sourcesConfig = await this.loadSourcesConfig();
712
+ const resolved = await resolveSourcesPath(imageUrl, sourcesConfig, PATHS.WORKSPACE_BASE);
713
+
714
+ if (!resolved) {
715
+ this.stats.missingImages++;
716
+ this.errors.fatal.push({
717
+ type: "INVALID_SOURCES_PATH",
718
+ path: doc.path,
719
+ langFile,
720
+ imageUrl,
721
+ altText,
722
+ message: `Cannot find image in any source: ${imageUrl}`,
723
+ suggestion: `Check if the image file exists in the sources directory`,
724
+ });
725
+ return;
726
+ }
727
+ imagePath = resolved.physicalPath;
728
+ } else {
729
+ // Original relative path handling logic
730
+ const fullDocPath = path.join(doc.filePath, langFile);
731
+ const docDir = path.dirname(path.join(this.docsDir, fullDocPath));
732
+ imagePath = path.resolve(docDir, imageUrl);
733
+ }
734
+
735
+ // Check if file exists
736
+ try {
737
+ await access(imagePath, constants.F_OK);
738
+
739
+ // Only validate path levels for relative paths (absolute paths don't need this)
740
+ if (!isSourcesAbsolutePath(imageUrl)) {
741
+ const fullDocPath = path.join(doc.filePath, langFile);
742
+ const expectedRelativePath = this.calculateExpectedRelativePath(fullDocPath, imagePath);
743
+ if (expectedRelativePath && imageUrl !== expectedRelativePath) {
744
+ this.errors.warnings.push({
745
+ type: "IMAGE_PATH_LEVEL",
746
+ path: doc.path,
747
+ langFile,
748
+ imageUrl,
749
+ expectedPath: expectedRelativePath,
750
+ message: `Image path level may be incorrect: ${imageUrl}`,
751
+ suggestion: `Suggest using: ${expectedRelativePath}`,
752
+ });
753
+ }
754
+ }
755
+ } catch (_error) {
756
+ this.stats.missingImages++;
757
+ this.errors.fatal.push({
758
+ type: "MISSING_IMAGE",
759
+ path: doc.path,
760
+ langFile,
761
+ imageUrl,
762
+ altText,
763
+ message: `Local image does not exist: ${imageUrl}`,
764
+ suggestion: `Check the image path or remove the image reference`,
765
+ });
766
+ }
767
+ }
768
+
769
+ /**
770
+ * Calculate expected relative path
771
+ */
772
+ calculateExpectedRelativePath(docFilePath, absoluteImagePath) {
773
+ // Calculate document level (levels under docs/ directory, including language file)
774
+ // Example: overview/zh.md → ['overview', 'zh.md'] → 2 levels → ../../
775
+ // api/auth/zh.md → ['api', 'auth', 'zh.md'] → 3 levels → ../../../
776
+ const pathParts = docFilePath.split("/").filter((p) => p);
777
+ const depth = pathParts.length;
778
+
779
+ // Generate back path
780
+ const backPath = "../".repeat(depth);
781
+
782
+ // Get workspace root directory
783
+ const workspaceRoot = process.cwd();
784
+
785
+ // Calculate image path relative to workspace
786
+ const relativeToWorkspace = path.relative(workspaceRoot, absoluteImagePath);
787
+
788
+ // Combine complete relative path
789
+ return backPath + relativeToWorkspace.replace(/\\/g, "/");
790
+ }
791
+
792
+ /**
793
+ * Validate remote image
794
+ */
795
+ async validateRemoteImage(imageUrl, doc, altText, langFile) {
796
+ // Check cache
797
+ if (this.remoteImageCache.has(imageUrl)) {
798
+ const cached = this.remoteImageCache.get(imageUrl);
799
+ if (!cached.accessible) {
800
+ this.stats.inaccessibleRemoteImages++;
801
+ this.errors.warnings.push({
802
+ type: "REMOTE_IMAGE_INACCESSIBLE",
803
+ path: doc.path,
804
+ langFile,
805
+ imageUrl,
806
+ altText,
807
+ statusCode: cached.statusCode,
808
+ error: cached.error,
809
+ message: `Remote image inaccessible: ${imageUrl}`,
810
+ suggestion: `Check if URL is correct, or replace with an accessible image`,
811
+ });
812
+ }
813
+ return;
814
+ }
815
+
816
+ // Check remote image accessibility
817
+ const result = await this.checkRemoteImage(imageUrl);
818
+ this.remoteImageCache.set(imageUrl, result);
819
+
820
+ if (!result.accessible) {
821
+ this.stats.inaccessibleRemoteImages++;
822
+ this.errors.warnings.push({
823
+ type: "REMOTE_IMAGE_INACCESSIBLE",
824
+ path: doc.path,
825
+ langFile,
826
+ imageUrl,
827
+ altText,
828
+ statusCode: result.statusCode,
829
+ error: result.error,
830
+ message: `Remote image inaccessible: ${imageUrl}`,
831
+ suggestion: `Check if URL is correct, or replace with an accessible image`,
832
+ });
833
+ }
834
+ }
835
+
836
+ /**
837
+ * Check remote image (HTTP HEAD request)
838
+ */
839
+ async checkRemoteImage(url, timeout = 3000) {
840
+ const controller = new AbortController();
841
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
842
+
843
+ try {
844
+ const response = await fetch(url, {
845
+ method: "HEAD",
846
+ signal: controller.signal,
847
+ headers: {
848
+ "User-Agent": "DocSmith-Content-Checker/1.0",
849
+ },
850
+ });
851
+
852
+ clearTimeout(timeoutId);
853
+
854
+ return {
855
+ accessible: response.ok,
856
+ statusCode: response.status,
857
+ statusText: response.statusText,
858
+ };
859
+ } catch (error) {
860
+ clearTimeout(timeoutId);
861
+
862
+ return {
863
+ accessible: false,
864
+ error: error.message,
865
+ isTimeout: error.name === "AbortError",
866
+ };
867
+ }
868
+ }
869
+
870
+ /**
871
+ * Get validation result
872
+ */
873
+ getResult() {
874
+ const hasErrors = this.errors.fatal.length > 0 || this.errors.fixable.length > 0;
875
+
876
+ return {
877
+ valid: !hasErrors,
878
+ errors: this.errors,
879
+ stats: this.stats,
880
+ };
881
+ }
882
+ }
883
+
884
+ /**
885
+ * Format output
886
+ */
887
+ function formatOutput(result) {
888
+ let output = "";
889
+
890
+ if (result.valid) {
891
+ output += "✅ PASS: Document content check passed\n\n";
892
+ output += "Statistics:\n";
893
+ output += ` Total documents: ${result.stats.totalDocs}\n`;
894
+ output += ` Checked: ${result.stats.checkedDocs}\n`;
895
+ output += ` Internal links: ${result.stats.totalLinks}\n`;
896
+ output += ` Local images: ${result.stats.localImages}\n`;
897
+ output += ` Remote images: ${result.stats.remoteImages}\n`;
898
+
899
+ if (result.errors.warnings.length > 0) {
900
+ output += `\nWarnings: ${result.errors.warnings.length}\n`;
901
+ }
902
+
903
+ return output;
904
+ }
905
+
906
+ output += "❌ FAIL: Document content has errors\n\n";
907
+ output += "Statistics:\n";
908
+ output += ` Total documents: ${result.stats.totalDocs}\n`;
909
+ output += ` Checked: ${result.stats.checkedDocs}\n`;
910
+ output += ` Fatal errors: ${result.errors.fatal.length}\n`;
911
+ output += ` Fixable errors: ${result.errors.fixable.length}\n`;
912
+ output += ` Warnings: ${result.errors.warnings.length}\n\n`;
913
+
914
+ // FATAL errors
915
+ if (result.errors.fatal.length > 0) {
916
+ output += "Fatal errors (must fix):\n\n";
917
+ result.errors.fatal.forEach((err, idx) => {
918
+ output += `${idx + 1}. ${err.message}\n`;
919
+ if (err.path) output += ` Document: ${err.path}\n`;
920
+ if (err.link) output += ` Link: ${err.link}\n`;
921
+ if (err.imageUrl) output += ` Image: ${err.imageUrl}\n`;
922
+ if (err.suggestion) output += ` Action: ${err.suggestion}\n`;
923
+ output += "\n";
924
+ });
925
+ }
926
+
927
+ // FIXABLE errors
928
+ if (result.errors.fixable.length > 0) {
929
+ output += "Fixable errors (auto-fixed):\n";
930
+ output += "(Fixes applied, files updated)\n\n";
931
+ }
932
+
933
+ // WARNING
934
+ if (result.errors.warnings.length > 0) {
935
+ output += "Warnings (non-blocking):\n\n";
936
+ result.errors.warnings.forEach((warn, idx) => {
937
+ output += `${idx + 1}. ${warn.message}\n`;
938
+ if (warn.path) output += ` Document: ${warn.path}\n`;
939
+ if (warn.suggestion) output += ` Suggestion: ${warn.suggestion}\n`;
940
+ output += "\n";
941
+ });
942
+ }
943
+
944
+ return output;
945
+ }
946
+
947
+ /**
948
+ * Main function - Function Agent
949
+ * @param {Object} params
950
+ * @param {string} params.yamlPath - Document structure YAML file path
951
+ * @param {string} params.docsDir - Document directory path
952
+ * @param {string[]} params.docs - Array of document paths to check, e.g., ['/overview', '/api/introduction'], checks all documents if not provided
953
+ * @param {boolean} params.checkRemoteImages - Whether to check remote images
954
+ * @returns {Promise<Object>} - Validation result
955
+ */
956
+ export default async function validateDocumentContent({
957
+ yamlPath = PATHS.DOCUMENT_STRUCTURE,
958
+ docsDir = PATHS.DOCS_DIR,
959
+ docs = undefined,
960
+ checkRemoteImages = true,
961
+ } = {}) {
962
+ try {
963
+ const validator = new DocumentContentValidator(yamlPath, docsDir, docs);
964
+ const result = await validator.validate(checkRemoteImages);
965
+
966
+ const formattedOutput = formatOutput(result);
967
+
968
+ return {
969
+ valid: result.valid,
970
+ errors: result.errors,
971
+ stats: result.stats,
972
+ message: formattedOutput,
973
+ };
974
+ } catch (error) {
975
+ return {
976
+ valid: false,
977
+ message: `❌ FAIL: ${error.message}`,
978
+ };
979
+ }
980
+ }
981
+
982
+ // Note: This function is for internal use only, not directly exposed as a skill
983
+ // External calls are made through the checkContent function in content-checker.mjs