@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.
- package/CHANGELOG.md +12 -0
- package/README.md +189 -219
- package/README.zh.md +270 -0
- package/agents/bash-executor/index.mjs +347 -0
- package/agents/clear/ai/intent.md +142 -0
- package/agents/clear/choose-contents.mjs +13 -65
- package/agents/clear/clear-auth-tokens.mjs +17 -21
- package/agents/clear/clear-deployment-config.mjs +33 -24
- package/agents/clear/index.yaml +1 -9
- package/agents/content-checker/ai/intent.md +209 -0
- package/agents/content-checker/clean-invalid-docs.mjs +254 -0
- package/agents/content-checker/index.mjs +191 -0
- package/agents/content-checker/validate-content.mjs +983 -0
- package/agents/generate-images/generate-image.yaml +75 -0
- package/agents/generate-images/generate-summary.mjs +213 -0
- package/agents/generate-images/index.yaml +39 -0
- package/agents/generate-images/prepare-generation.mjs +286 -0
- package/agents/generate-images/prepare-image-generation.mjs +130 -0
- package/{prompts/detail/diagram/generate-image-system.md → agents/generate-images/prompts/system.md} +22 -56
- package/agents/generate-images/prompts/user.md +85 -0
- package/agents/generate-images/save-image-result.mjs +247 -0
- package/agents/generate-images/scan-image-slots.mjs +247 -0
- package/agents/localize/index.yaml +19 -42
- package/{prompts/translate → agents/localize/prompts}/translate-document.md +0 -139
- package/agents/localize/translate-documents/generate-summary.mjs +163 -0
- package/agents/localize/translate-documents/load-glossary.mjs +52 -0
- package/agents/localize/translate-documents/prepare-translation.mjs +249 -0
- package/agents/localize/translate-documents/save-translation.mjs +171 -0
- package/agents/localize/translate-documents/translate-document-to-language.mjs +209 -0
- package/agents/localize/translate-documents/translate-document.yaml +23 -0
- package/agents/localize/translate-documents/translate-to-languages.yaml +10 -0
- package/agents/localize/translate-images/check-image-translation.mjs +225 -0
- package/agents/localize/translate-images/detect-text/detect-and-update-shared.mjs +148 -0
- package/agents/localize/translate-images/detect-text/detect-image-text.yaml +44 -0
- package/agents/localize/translate-images/detect-text/detect-images-text.yaml +21 -0
- package/agents/localize/translate-images/detect-text/prompts/detect-image-text-system.md +43 -0
- package/agents/localize/translate-images/detect-text/prompts/detect-image-text-user.md +14 -0
- package/agents/localize/translate-images/detect-text/save-text-detection.mjs +105 -0
- package/agents/localize/translate-images/prepare-image-input.mjs +124 -0
- package/agents/localize/translate-images/save-image-translation.mjs +172 -0
- package/agents/localize/translate-images/scan-doc-images.mjs +165 -0
- package/agents/localize/translate-images/translate-doc-images.yaml +24 -0
- package/agents/localize/{translate-diagram.yaml → translate-images/translate-image.yaml} +25 -14
- package/agents/publish/ai/intent.md +182 -0
- package/agents/publish/check.mjs +107 -0
- package/agents/publish/index.yaml +9 -14
- package/agents/publish/publish-docs.mjs +81 -61
- package/agents/publish/translate-meta.mjs +79 -58
- package/agents/save-document/index.mjs +260 -0
- package/agents/structure-checker/index.mjs +307 -0
- package/agents/structure-checker/validate-structure.mjs +477 -0
- package/agents/update-image/analyze-feedback.yaml +37 -0
- package/agents/update-image/index.yaml +78 -0
- package/agents/update-image/load-existing-image.mjs +211 -0
- package/agents/update-image/prompts/analyze-feedback-system.md +43 -0
- package/agents/update-image/prompts/analyze-feedback-user.md +15 -0
- package/aigne.yaml +26 -139
- package/package.json +16 -48
- package/scripts/README.md +90 -0
- package/scripts/install.sh +86 -0
- package/scripts/uninstall.sh +52 -0
- package/skills/doc-smith/SKILL.md +285 -0
- package/skills/doc-smith/ai/intent/sources-improve.md +290 -0
- package/skills/doc-smith/references/changeset-guide.md +171 -0
- package/skills/doc-smith/references/document-content-guide.md +214 -0
- package/skills/doc-smith/references/document-structure-schema.md +138 -0
- package/skills/doc-smith/references/patch-guide.md +96 -0
- package/skills/doc-smith/references/structure-confirmation-guide.md +133 -0
- package/skills/doc-smith/references/structure-planning-guide.md +149 -0
- package/skills/doc-smith/references/update-workflow.md +108 -0
- package/skills/doc-smith/references/user-intent-guide.md +175 -0
- package/skills/doc-smith/references/workspace-initialization.md +376 -0
- package/skills/doc-smith-docs-detail/SKILL.md +356 -0
- package/skills/doc-smith-docs-detail/ai/intent.md +271 -0
- package/skills-entry/doc-smith/ai/intent.md +260 -0
- package/skills-entry/doc-smith/index.mjs +66 -0
- package/skills-entry/doc-smith/prompt.md +57 -0
- package/skills-entry/doc-smith/utils.mjs +27 -0
- package/skills-entry/doc-smith-docs-detail/batch.yaml +56 -0
- package/skills-entry/doc-smith-docs-detail/index.mjs +95 -0
- package/skills-entry/doc-smith-docs-detail/prompt.md +64 -0
- package/utils/afs-factory.mjs +183 -0
- package/utils/agent-constants.mjs +97 -0
- package/utils/{auth-utils.mjs → auth.mjs} +6 -9
- package/{agents/utils/update-branding.mjs → utils/branding.mjs} +3 -4
- package/utils/config.mjs +261 -0
- package/utils/constants.mjs +32 -0
- package/utils/deploy.mjs +3 -3
- package/utils/docs-converter.mjs +454 -0
- package/utils/docs.mjs +212 -0
- package/utils/document-paths.mjs +172 -0
- package/utils/files.mjs +74 -0
- package/utils/git.mjs +65 -0
- package/utils/{blocklet.mjs → http.mjs} +18 -0
- package/utils/image-slots.mjs +57 -0
- package/utils/image-utils.mjs +114 -0
- package/utils/project.mjs +95 -0
- package/utils/sources-path-resolver.mjs +76 -0
- package/utils/{upload-files.mjs → upload.mjs} +3 -3
- package/utils/workspace.mjs +371 -0
- package/agents/chat/chat-system.md +0 -38
- package/agents/chat/index.mjs +0 -59
- package/agents/chat/skills/generate-document.yaml +0 -15
- package/agents/chat/skills/list-documents.mjs +0 -15
- package/agents/chat/skills/update-document.yaml +0 -24
- package/agents/clear/clear-document-config.mjs +0 -36
- package/agents/clear/clear-document-structure.mjs +0 -102
- package/agents/clear/clear-generated-docs.mjs +0 -142
- package/agents/clear/clear-media-description.mjs +0 -129
- package/agents/create/aggregate-document-structure.mjs +0 -21
- package/agents/create/analyze-diagram-type-llm.yaml +0 -159
- package/agents/create/analyze-diagram-type.mjs +0 -455
- package/agents/create/check-document-structure.yaml +0 -30
- package/agents/create/check-need-generate-structure.mjs +0 -138
- package/agents/create/document-structure-tools/add-document.mjs +0 -85
- package/agents/create/document-structure-tools/delete-document.mjs +0 -116
- package/agents/create/document-structure-tools/move-document.mjs +0 -109
- package/agents/create/document-structure-tools/update-document.mjs +0 -84
- package/agents/create/generate-diagram-image.yaml +0 -91
- package/agents/create/generate-structure.yaml +0 -106
- package/agents/create/index.yaml +0 -45
- package/agents/create/refine-document-structure.yaml +0 -12
- package/agents/create/replace-d2-with-image.mjs +0 -610
- package/agents/create/update-document-structure.yaml +0 -54
- package/agents/create/user-add-document/add-documents-to-structure.mjs +0 -90
- package/agents/create/user-add-document/find-documents-to-add-links.yaml +0 -47
- package/agents/create/user-add-document/index.yaml +0 -46
- package/agents/create/user-add-document/prepare-documents-to-translate.mjs +0 -22
- package/agents/create/user-add-document/print-add-document-summary.mjs +0 -63
- package/agents/create/user-add-document/review-documents-with-new-links.mjs +0 -110
- package/agents/create/user-remove-document/find-documents-with-invalid-links.mjs +0 -78
- package/agents/create/user-remove-document/index.yaml +0 -40
- package/agents/create/user-remove-document/prepare-documents-to-translate.mjs +0 -22
- package/agents/create/user-remove-document/print-remove-document-summary.mjs +0 -53
- package/agents/create/user-remove-document/remove-documents-from-structure.mjs +0 -99
- package/agents/create/user-remove-document/review-documents-with-invalid-links.mjs +0 -115
- package/agents/create/user-review-document-structure.mjs +0 -139
- package/agents/create/utils/init-current-content.mjs +0 -34
- package/agents/create/utils/merge-document-structures.mjs +0 -36
- package/agents/evaluate/code-snippet.mjs +0 -97
- package/agents/evaluate/document-structure.yaml +0 -67
- package/agents/evaluate/document.yaml +0 -82
- package/agents/evaluate/generate-report.mjs +0 -85
- package/agents/evaluate/index.yaml +0 -46
- package/agents/history/index.yaml +0 -6
- package/agents/history/view.mjs +0 -78
- package/agents/init/check.mjs +0 -16
- package/agents/init/index.mjs +0 -643
- package/agents/init/validate.mjs +0 -16
- package/agents/localize/choose-language.mjs +0 -107
- package/agents/localize/record-translation-history.mjs +0 -23
- package/agents/localize/save-doc-translation-or-skip.mjs +0 -18
- package/agents/localize/set-review-content.mjs +0 -58
- package/agents/localize/translate-document-wrapper.mjs +0 -34
- package/agents/localize/translate-document.yaml +0 -24
- package/agents/localize/translate-multilingual.yaml +0 -57
- package/agents/localize/translate-or-skip-diagram.mjs +0 -52
- package/agents/media/batch-generate-media-description.yaml +0 -46
- package/agents/media/generate-media-description.yaml +0 -50
- package/agents/media/load-media-description.mjs +0 -454
- package/agents/prefs/index.mjs +0 -203
- package/agents/schema/document-structure-item.yaml +0 -26
- package/agents/schema/document-structure-refine-item.yaml +0 -23
- package/agents/schema/document-structure.yaml +0 -29
- package/agents/update/batch-generate-document.yaml +0 -27
- package/agents/update/batch-update-document.yaml +0 -7
- package/agents/update/check-diagram-flag.mjs +0 -116
- package/agents/update/check-document.mjs +0 -162
- package/agents/update/check-generate-diagram.mjs +0 -106
- package/agents/update/check-update-is-single.mjs +0 -53
- package/agents/update/document-tools/update-document-content.mjs +0 -303
- package/agents/update/generate-diagram.yaml +0 -80
- package/agents/update/generate-document.yaml +0 -70
- package/agents/update/handle-document-update.yaml +0 -103
- package/agents/update/index.yaml +0 -69
- package/agents/update/pre-check-generate-diagram.yaml +0 -44
- package/agents/update/save-and-translate-document.mjs +0 -80
- package/agents/update/update-document-detail.yaml +0 -71
- package/agents/update/update-single/update-single-document-detail.mjs +0 -322
- package/agents/update/update-single-document.yaml +0 -7
- package/agents/update/user-review-document.mjs +0 -272
- package/agents/utils/action-success.mjs +0 -16
- package/agents/utils/analyze-document-feedback-intent.yaml +0 -32
- package/agents/utils/analyze-feedback-intent.mjs +0 -253
- package/agents/utils/analyze-structure-feedback-intent.yaml +0 -29
- package/agents/utils/check-detail-result.mjs +0 -51
- package/agents/utils/check-feedback-refiner.mjs +0 -81
- package/agents/utils/choose-docs.mjs +0 -251
- package/agents/utils/document-icon-generate.yaml +0 -52
- package/agents/utils/document-title-streamline.yaml +0 -48
- package/agents/utils/ensure-document-icons.mjs +0 -129
- package/agents/utils/exit.mjs +0 -6
- package/agents/utils/feedback-refiner.yaml +0 -50
- package/agents/utils/find-item-by-path.mjs +0 -114
- package/agents/utils/find-user-preferences-by-path.mjs +0 -37
- package/agents/utils/format-document-structure.mjs +0 -35
- package/agents/utils/generate-document-or-skip.mjs +0 -41
- package/agents/utils/handle-diagram-operations.mjs +0 -263
- package/agents/utils/load-all-document-content.mjs +0 -30
- package/agents/utils/load-document-all-content.mjs +0 -96
- package/agents/utils/load-sources.mjs +0 -405
- package/agents/utils/map-reasoning-effort-level.mjs +0 -15
- package/agents/utils/post-generate.mjs +0 -133
- package/agents/utils/read-current-document-content.mjs +0 -46
- package/agents/utils/save-doc-translation.mjs +0 -30
- package/agents/utils/save-doc.mjs +0 -54
- package/agents/utils/save-output.mjs +0 -26
- package/agents/utils/save-sidebar.mjs +0 -38
- package/agents/utils/skip-if-content-exists.mjs +0 -27
- package/agents/utils/streamline-document-titles-if-needed.mjs +0 -88
- package/agents/utils/transform-detail-data-sources.mjs +0 -45
- package/assets/report-template/report.html +0 -198
- package/docs-mcp/analyze-content-relevance.yaml +0 -50
- package/docs-mcp/analyze-docs-relevance.yaml +0 -59
- package/docs-mcp/docs-search.yaml +0 -42
- package/docs-mcp/get-docs-detail.mjs +0 -41
- package/docs-mcp/get-docs-structure.mjs +0 -16
- package/docs-mcp/read-doc-content.mjs +0 -119
- package/prompts/common/document/content-rules-core.md +0 -20
- package/prompts/common/document/markdown-syntax-rules.md +0 -65
- package/prompts/common/document/media-file-list-usage-rules.md +0 -18
- package/prompts/common/document/openapi-usage-rules.md +0 -189
- package/prompts/common/document/role-and-personality.md +0 -16
- package/prompts/common/document/user-preferences.md +0 -9
- package/prompts/common/document-structure/conflict-resolution-guidance.md +0 -16
- package/prompts/common/document-structure/document-icon-generate.md +0 -116
- package/prompts/common/document-structure/document-structure-rules.md +0 -43
- package/prompts/common/document-structure/document-title-streamline.md +0 -86
- package/prompts/common/document-structure/glossary.md +0 -7
- package/prompts/common/document-structure/intj-traits.md +0 -5
- package/prompts/common/document-structure/openapi-usage-rules.md +0 -28
- package/prompts/common/document-structure/output-constraints.md +0 -18
- package/prompts/common/document-structure/user-locale-rules.md +0 -10
- package/prompts/common/document-structure/user-preferences.md +0 -9
- package/prompts/detail/custom/admonition-usage-rules.md +0 -94
- package/prompts/detail/custom/code-block-usage-rules.md +0 -163
- package/prompts/detail/custom/custom-components/x-card-usage-rules.md +0 -63
- package/prompts/detail/custom/custom-components/x-cards-usage-rules.md +0 -83
- package/prompts/detail/custom/custom-components/x-field-desc-usage-rules.md +0 -120
- package/prompts/detail/custom/custom-components/x-field-group-usage-rules.md +0 -80
- package/prompts/detail/custom/custom-components/x-field-usage-rules.md +0 -189
- package/prompts/detail/custom/custom-components-usage-rules.md +0 -18
- package/prompts/detail/diagram/generate-image-user.md +0 -81
- package/prompts/detail/diagram/guide.md +0 -29
- package/prompts/detail/diagram/official-examples.md +0 -712
- package/prompts/detail/diagram/pre-check.md +0 -23
- package/prompts/detail/diagram/role-and-personality.md +0 -2
- package/prompts/detail/diagram/rules.md +0 -46
- package/prompts/detail/diagram/system-prompt.md +0 -1139
- package/prompts/detail/diagram/user-prompt.md +0 -43
- package/prompts/detail/generate/detail-example.md +0 -457
- package/prompts/detail/generate/document-rules.md +0 -45
- package/prompts/detail/generate/system-prompt.md +0 -61
- package/prompts/detail/generate/user-prompt.md +0 -99
- package/prompts/detail/jsx/rules.md +0 -6
- package/prompts/detail/update/system-prompt.md +0 -121
- package/prompts/detail/update/user-prompt.md +0 -41
- package/prompts/evaluate/document-structure.md +0 -93
- package/prompts/evaluate/document.md +0 -149
- package/prompts/media/media-description/system-prompt.md +0 -43
- package/prompts/media/media-description/user-prompt.md +0 -17
- package/prompts/structure/check-document-structure.md +0 -93
- package/prompts/structure/document-rules.md +0 -21
- package/prompts/structure/find-documents-to-add-links.md +0 -52
- package/prompts/structure/generate/system-prompt.md +0 -13
- package/prompts/structure/generate/user-prompt.md +0 -137
- package/prompts/structure/review/structure-review-system.md +0 -81
- package/prompts/structure/structure-example.md +0 -89
- package/prompts/structure/structure-getting-started.md +0 -10
- package/prompts/structure/update/system-prompt.md +0 -93
- package/prompts/structure/update/user-prompt.md +0 -43
- package/prompts/translate/admonition.md +0 -20
- package/prompts/translate/code-block.md +0 -33
- package/prompts/utils/analyze-document-feedback-intent.md +0 -54
- package/prompts/utils/analyze-structure-feedback-intent.md +0 -43
- package/prompts/utils/feedback-refiner.md +0 -105
- package/types/document-schema.mjs +0 -55
- package/types/document-structure-schema.mjs +0 -261
- package/utils/check-document-has-diagram.mjs +0 -95
- package/utils/conflict-detector.mjs +0 -149
- package/utils/constants/index.mjs +0 -620
- package/utils/constants/linter.mjs +0 -102
- package/utils/d2-utils.mjs +0 -205
- package/utils/debug.mjs +0 -3
- package/utils/delete-diagram-images.mjs +0 -99
- package/utils/diagram-version-utils.mjs +0 -14
- package/utils/docs-finder-utils.mjs +0 -548
- package/utils/evaluate/report-utils.mjs +0 -132
- package/utils/extract-api.mjs +0 -32
- package/utils/file-utils.mjs +0 -960
- package/utils/history-utils.mjs +0 -203
- package/utils/icon-map.mjs +0 -26
- package/utils/image-compress.mjs +0 -154
- package/utils/kroki-utils.mjs +0 -173
- package/utils/linter/index.mjs +0 -50
- package/utils/load-config.mjs +0 -78
- package/utils/markdown/index.mjs +0 -26
- package/utils/markdown-checker.mjs +0 -694
- package/utils/mermaid-validator.mjs +0 -140
- package/utils/mermaid-worker-pool.mjs +0 -250
- package/utils/mermaid-worker.mjs +0 -233
- package/utils/openapi/index.mjs +0 -28
- package/utils/preferences-utils.mjs +0 -175
- package/utils/request.mjs +0 -10
- package/utils/sync-diagram-to-translations.mjs +0 -272
- package/utils/translate-diagram-images.mjs +0 -807
- package/utils/utils.mjs +0 -1354
- /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
|