@aigne/doc-smith 0.4.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/agents/batch-translate.yaml +3 -0
  3. package/agents/check-detail-result.mjs +2 -1
  4. package/agents/check-detail.mjs +1 -0
  5. package/agents/check-feedback-refiner.mjs +79 -0
  6. package/agents/check-structure-plan.mjs +16 -0
  7. package/agents/detail-generator-and-translate.yaml +3 -0
  8. package/agents/detail-regenerator.yaml +3 -0
  9. package/agents/docs-generator.yaml +3 -0
  10. package/agents/feedback-refiner.yaml +48 -0
  11. package/agents/find-items-by-paths.mjs +5 -1
  12. package/agents/find-user-preferences-by-path.mjs +37 -0
  13. package/agents/input-generator.mjs +8 -9
  14. package/agents/load-sources.mjs +63 -9
  15. package/agents/manage-prefs.mjs +203 -0
  16. package/agents/publish-docs.mjs +3 -1
  17. package/agents/retranslate.yaml +3 -0
  18. package/agents/structure-planning.yaml +3 -0
  19. package/aigne.yaml +4 -0
  20. package/package.json +10 -9
  21. package/prompts/content-detail-generator.md +13 -1
  22. package/prompts/document/detail-generator.md +1 -0
  23. package/prompts/feedback-refiner.md +84 -0
  24. package/prompts/structure-planning.md +8 -0
  25. package/prompts/translator.md +8 -0
  26. package/tests/{test-all-validation-cases.mjs → all-validation-cases.test.mjs} +60 -137
  27. package/tests/check-detail-result.test.mjs +90 -77
  28. package/tests/load-sources.test.mjs +103 -291
  29. package/tests/preferences-utils.test.mjs +369 -0
  30. package/tests/{test-save-docs.mjs → save-docs.test.mjs} +29 -47
  31. package/tests/save-value-to-config.test.mjs +165 -288
  32. package/utils/auth-utils.mjs +1 -1
  33. package/utils/constants.mjs +22 -10
  34. package/utils/markdown-checker.mjs +89 -9
  35. package/utils/preferences-utils.mjs +175 -0
  36. package/utils/utils.mjs +3 -3
@@ -0,0 +1,203 @@
1
+ import { readPreferences, removeRule, writePreferences } from "../utils/preferences-utils.mjs";
2
+
3
+ /**
4
+ * List all user preferences with formatted display
5
+ * @returns {Object} Result with formatted message
6
+ */
7
+ function listPreferences() {
8
+ const preferences = readPreferences();
9
+
10
+ if (preferences.rules.length === 0) {
11
+ return { message: "No preferences found." };
12
+ }
13
+
14
+ let message = "# User Preferences\n\n";
15
+
16
+ // Add format explanation
17
+ message += "**Format explanation:**\n";
18
+ message += "- 🟢 = Active preference, ⚪ = Inactive preference\n";
19
+ message += "- [scope] = Preference scope (global, structure, document, translation)\n";
20
+ message += "- ID = Unique preference identifier\n";
21
+ message += "- Paths = Specific file paths (if applicable)\n\n";
22
+
23
+ preferences.rules.forEach((rule) => {
24
+ const status = rule.active ? "🟢" : "⚪";
25
+ const pathsInfo = rule.paths ? ` | Paths: ${rule.paths.join(", ")}` : "";
26
+
27
+ // First line: status, scope, ID and paths info
28
+ message += `${status} [${rule.scope}] ${rule.id}${pathsInfo}\n`;
29
+
30
+ // Second line: rule content (truncated if too long)
31
+ const maxRuleLength = 120;
32
+ const ruleText =
33
+ rule.rule.length > maxRuleLength ? `${rule.rule.substring(0, maxRuleLength)}...` : rule.rule;
34
+ message += ` ${ruleText}\n `;
35
+
36
+ // Add blank line after each record
37
+ message += `\n`;
38
+ });
39
+
40
+ return { message };
41
+ }
42
+
43
+ /**
44
+ * Remove preferences by IDs or interactive selection
45
+ * @param {string[]} id - Array of preference IDs to remove
46
+ * @param {Object} options - Options with prompts interface
47
+ * @returns {Object} Result with success message
48
+ */
49
+ async function removePreferences(id, options) {
50
+ const preferences = readPreferences();
51
+ let targetIds = id;
52
+
53
+ if (!targetIds || targetIds.length === 0) {
54
+ // Interactive selection
55
+ if (preferences.rules.length === 0) {
56
+ return { message: "No preferences found to remove." };
57
+ }
58
+
59
+ const choices = preferences.rules.map((rule) => ({
60
+ name: `${rule.active ? "🟢" : "⚪"} [${rule.scope}] ${rule.rule.substring(0, 60)}${rule.rule.length > 60 ? "..." : ""}`,
61
+ value: rule.id,
62
+ description: `ID: ${rule.id}`,
63
+ }));
64
+
65
+ targetIds = await options.prompts.checkbox({
66
+ message: "Select preferences to remove:",
67
+ choices,
68
+ validate: (answer) => {
69
+ if (answer.length === 0) {
70
+ return "Please select at least one preference to remove";
71
+ }
72
+ return true;
73
+ },
74
+ });
75
+
76
+ if (!targetIds || targetIds.length === 0) {
77
+ return { message: "No preferences selected for removal." };
78
+ }
79
+ }
80
+
81
+ // Process the target IDs
82
+ const results = [];
83
+ for (const ruleId of targetIds) {
84
+ const success = removeRule(ruleId);
85
+ results.push({ id: ruleId, success });
86
+ }
87
+
88
+ const successCount = results.filter((r) => r.success).length;
89
+ const failedCount = targetIds.length - successCount;
90
+ const message =
91
+ failedCount > 0
92
+ ? `Successfully removed ${successCount} preferences, ${failedCount} failed.`
93
+ : `Successfully removed ${successCount} preferences.`;
94
+
95
+ return { message };
96
+ }
97
+
98
+ /**
99
+ * Toggle preferences active status by IDs or interactive selection
100
+ * @param {string[]} id - Array of preference IDs to toggle
101
+ * @param {Object} options - Options with prompts interface
102
+ * @returns {Object} Result with success message
103
+ */
104
+ async function togglePreferences(id, options) {
105
+ const preferences = readPreferences();
106
+ let targetIds = id;
107
+
108
+ if (!targetIds || targetIds.length === 0) {
109
+ // Interactive selection
110
+ if (preferences.rules.length === 0) {
111
+ return { message: "No preferences found to toggle." };
112
+ }
113
+
114
+ const choices = preferences.rules.map((rule) => ({
115
+ name: `${rule.active ? "🟢" : "⚪"} [${rule.scope}] ${rule.rule.substring(0, 60)}${rule.rule.length > 60 ? "..." : ""}`,
116
+ value: rule.id,
117
+ description: `ID: ${rule.id}`,
118
+ }));
119
+
120
+ targetIds = await options.prompts.checkbox({
121
+ message: "Select preferences to toggle active status:",
122
+ choices,
123
+ validate: (answer) => {
124
+ if (answer.length === 0) {
125
+ return "Please select at least one preference to toggle";
126
+ }
127
+ return true;
128
+ },
129
+ });
130
+
131
+ if (!targetIds || targetIds.length === 0) {
132
+ return { message: "No preferences selected for toggling." };
133
+ }
134
+ }
135
+
136
+ // Process the target IDs
137
+ const results = [];
138
+ const prefs = readPreferences();
139
+
140
+ for (const ruleId of targetIds) {
141
+ const rule = prefs.rules.find((r) => r.id === ruleId);
142
+ if (rule) {
143
+ rule.active = !rule.active;
144
+ results.push({ id: ruleId, success: true, newStatus: rule.active });
145
+ } else {
146
+ results.push({ id: ruleId, success: false, error: "Rule not found" });
147
+ }
148
+ }
149
+
150
+ writePreferences(prefs);
151
+
152
+ const successCount = results.filter((r) => r.success).length;
153
+ const failedCount = targetIds.length - successCount;
154
+ const message =
155
+ failedCount > 0
156
+ ? `Successfully toggled ${successCount} preferences, ${failedCount} failed.`
157
+ : `Successfully toggled ${successCount} preferences.`;
158
+
159
+ return { message };
160
+ }
161
+
162
+ export default async function prefs({ list, remove, toggle, id }, options) {
163
+ if (list) {
164
+ return listPreferences();
165
+ }
166
+
167
+ if (remove) {
168
+ return await removePreferences(id, options);
169
+ }
170
+
171
+ if (toggle) {
172
+ return await togglePreferences(id, options);
173
+ }
174
+
175
+ return { message: "Please specify an action: --list, --remove, or --toggle." };
176
+ }
177
+
178
+ prefs.input_schema = {
179
+ type: "object",
180
+ properties: {
181
+ list: {
182
+ type: "boolean",
183
+ description: "List all preferences",
184
+ },
185
+ remove: {
186
+ type: "boolean",
187
+ description: "Remove preferences",
188
+ },
189
+ toggle: {
190
+ type: "boolean",
191
+ description: "Toggle preferences active status",
192
+ },
193
+ id: {
194
+ type: "array",
195
+ items: {
196
+ type: "string",
197
+ },
198
+ description: "Preference IDs to manage",
199
+ },
200
+ },
201
+ };
202
+
203
+ prefs.description = "Manage user preferences learned from feedback";
@@ -3,7 +3,7 @@ import { publishDocs as publishDocsFn } from "@aigne/publish-docs";
3
3
  import chalk from "chalk";
4
4
  import { getAccessToken } from "../utils/auth-utils.mjs";
5
5
  import { DISCUSS_KIT_STORE_URL } from "../utils/constants.mjs";
6
- import { loadConfigFromFile, saveValueToConfig, getGithubRepoUrl } from "../utils/utils.mjs";
6
+ import { getGithubRepoUrl, loadConfigFromFile, saveValueToConfig } from "../utils/utils.mjs";
7
7
 
8
8
  const DEFAULT_APP_URL = "https://docsmith.aigne.io";
9
9
 
@@ -98,6 +98,8 @@ export default async function publishDocs(
98
98
  boardName: projectInfo.name,
99
99
  boardDesc: projectInfo.description,
100
100
  boardCover: projectInfo.icon,
101
+ mediaFolder: docsDir,
102
+ cacheFilePath: join(".aigne", "doc-smith", "upload-cache.yaml"),
101
103
  boardMeta,
102
104
  });
103
105
 
@@ -37,6 +37,9 @@ skills:
37
37
  isTranslate: true
38
38
  iterate_on: selectedDocs
39
39
  concurrency: 3
40
+ - url: ./check-feedback-refiner.mjs
41
+ default_input:
42
+ stage: translation_refine
40
43
  - url: ./action-success.mjs
41
44
  default_input:
42
45
  action: "Document translated"
@@ -26,6 +26,9 @@ input_schema:
26
26
  feedback:
27
27
  type: string
28
28
  description: 结构规划的反馈
29
+ userPreferences:
30
+ type: string
31
+ description: 用户偏好规则,YAML格式的结构规划和全局范围偏好
29
32
  docsType:
30
33
  type: string
31
34
  description: 文档类型,支持:general、getting-started、reference、faq
package/aigne.yaml CHANGED
@@ -37,6 +37,9 @@ agents:
37
37
  - ./docs-mcp/analyze-docs-relevance.yaml
38
38
  - ./docs-mcp/read-doc-content.mjs
39
39
  - ./docs-mcp/analyze-content-relevance.yaml
40
+ - ./agents/check-feedback-refiner.mjs
41
+ - ./agents/feedback-refiner.yaml
42
+ - ./agents/manage-prefs.mjs
40
43
  cli:
41
44
  agents:
42
45
  - ./agents/input-generator.mjs
@@ -44,6 +47,7 @@ cli:
44
47
  - ./agents/detail-regenerator.yaml
45
48
  - ./agents/team-publish-docs.yaml
46
49
  - ./agents/retranslate.yaml
50
+ - ./agents/manage-prefs.mjs
47
51
  mcp_server:
48
52
  agents:
49
53
  - ./docs-mcp/get-docs-structure.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/doc-smith",
3
- "version": "0.4.5",
3
+ "version": "0.6.0",
4
4
  "description": "",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -12,13 +12,13 @@
12
12
  "author": "Arcblock <blocklet@arcblock.io> https://github.com/blocklet",
13
13
  "license": "MIT",
14
14
  "dependencies": {
15
- "@aigne/aigne-hub": "^0.6.9",
16
- "@aigne/anthropic": "^0.11.9",
17
- "@aigne/cli": "^1.41.0",
18
- "@aigne/core": "^1.55.0",
19
- "@aigne/gemini": "^0.9.9",
20
- "@aigne/openai": "^0.12.3",
21
- "@aigne/publish-docs": "^0.5.7",
15
+ "@aigne/aigne-hub": "^0.6.10",
16
+ "@aigne/anthropic": "^0.11.10",
17
+ "@aigne/cli": "^1.41.1",
18
+ "@aigne/core": "^1.55.1",
19
+ "@aigne/gemini": "^0.9.10",
20
+ "@aigne/openai": "^0.12.4",
21
+ "@aigne/publish-docs": "^0.6.0",
22
22
  "chalk": "^5.5.0",
23
23
  "dompurify": "^3.2.6",
24
24
  "glob": "^11.0.3",
@@ -39,7 +39,8 @@
39
39
  "@biomejs/biome": "^2.1.4"
40
40
  },
41
41
  "scripts": {
42
- "test": "echo \"Error: no test specified\" && exit 1",
42
+ "test": "bun test",
43
+ "test:watch": "bun test --watch",
43
44
  "lint": "biome check && pnpm -r run lint",
44
45
  "update:deps": "npx -y taze major -r -w -f -n '/@abtnode|@aigne|@arcblock|@blocklet|@did-connect|@did-pay|@did-space|@nft-store|@nft-studio|@ocap/' && pnpm install && pnpm run deduplicate",
45
46
  "deduplicate": "pnpm dedupe",
@@ -16,6 +16,11 @@
16
16
  {{ detailDataSources }}
17
17
 
18
18
  {{ additionalInformation }}
19
+
20
+ <media_list>
21
+ {{ assetsContent }}
22
+ </media_list>
23
+
19
24
  </datasources>
20
25
 
21
26
  <terms>
@@ -56,6 +61,14 @@ parentId: {{parentId}}
56
61
  ** 使用 {{ locale }} 语言输出内容 **
57
62
  </user_rules>
58
63
 
64
+ <user_preferences>
65
+ {{userPreferences}}
66
+
67
+ 用户偏好使用规则:
68
+ - 用户偏好来自用户之前操作中提供的反馈,生成结构规划中需要考虑用户的偏好,避免出现用户反馈的问题又重复出现
69
+ - 用户偏好的权重低于本次用户提交的反馈
70
+ </user_preferences>
71
+
59
72
  <rules>
60
73
 
61
74
  目标受众:{{targetAudience}}
@@ -85,7 +98,6 @@ parentId: {{parentId}}
85
98
  - 媒体资源以 markdown 格式提供,示例:![资源描述](https://xxxx)
86
99
  - 在生成结果中以 markdown 格式展示图片
87
100
  - 根据资源描述,在上下文相关的位置,合理的展示图片,让结果展示效果更丰富
88
- - 只使用完整的远程图片URL(如 https://example.com/image.jpg),禁止使用相对路径(如 ./images/photo.png 或 ../assets/logo.png),确保发布后图片能正常访问
89
101
 
90
102
  </media_rules>
91
103
 
@@ -27,6 +27,7 @@
27
27
  - **代码块原子性**:将每个代码块(例如 ```mermaid ... ```)视为一个**不可分割的原子单元**。必须一次性完整生成,从开始标记(```mermaid)到结束标记(```)之间的所有内容都不能省略或截断。
28
28
  - **确保 Markdown 语法**:Markdown 格式正确,特别是表格的分隔线(例如 `|---|---|---|`),需要与表格数据列数一致。
29
29
  - README 文件只做参考,你需要从代码中获取最新、最完整的信息
30
+ - 忽略详情顶部的标签信息,这是程序处理的,不需要在生成时输出
30
31
  </document_rules>
31
32
 
32
33
  <TONE_STYLE>
@@ -0,0 +1,84 @@
1
+ <role>
2
+ 你是"反馈→规则"转换器。将一次性的自然语言反馈提炼为**一条单句**、**可执行**、**可复用**的指令,
3
+ 并判断是否需要**持久化保存**,以及作用域(global/structure/document/translation)与是否仅限于"输入 paths 范围"。
4
+ </role>
5
+
6
+ <input>
7
+ - feedback: {{feedback}}
8
+ - stage: {{stage}} # 可取:structure_planning | document_refine | translation_refine
9
+ - paths: {{paths}} # 本次命令输入的路径数组(可为空)。仅用于判断是否"限定到这些路径"。不要把它们写进输出。
10
+ - existingPreferences: {{existingPreferences}} # 当前已保存的用户偏好规则
11
+ </input>
12
+
13
+ <scope_rules>
14
+ 作用域判定启发式规则:
15
+
16
+ **按 stage 分类**:
17
+ - 若 stage=structure_planning:默认 `scope="structure"`,除非反馈显然是全局写作/语气/排除类政策(则用 `global`)。
18
+ - 若 stage=document_refine:默认 `scope="document"`;若反馈是通用写作政策、排除策略且不依赖具体页面,则可提升为 `global`。
19
+ - 若 stage=translation_refine:默认 `scope="translation"`;若反馈是翻译阶段的一般政策可保持此 scope。
20
+
21
+ **路径限制判定**:
22
+ - 若用户反馈显著只影响本批 `paths` 指向的范围(例如"examples 目录中的页面精简说明"),将 `limitToInputPaths=true`;否则为 `false`。
23
+ - **永远不要**在输出中返回具体的 paths 列表。
24
+ </scope_rules>
25
+
26
+ <save_rules>
27
+ 是否保存判定规则:
28
+
29
+ **一次性操作(不保存)**:
30
+ - 只修正当下版本/错别字/个别句式/局部事实错误,且无稳定可复用价值 → `save=false`
31
+
32
+ **可复用政策(保存)**:
33
+ - 写作风格、结构约定、包含/排除项、翻译约定等可广泛适用且未来应持续执行 → `save=true`
34
+
35
+ **重复性检查(不保存)**:
36
+ - 若 `existingPreferences` 中已有**相似或覆盖**本次反馈意图的规则,则 `save=false`
37
+ - 检查逻辑:对比反馈意图、规则含义、适用范围。若新反馈已被现有规则充分覆盖,无需重复保存
38
+ - 若新反馈是对现有规则的**细化、补充或矛盾修正**,则仍可 `save=true`
39
+
40
+ **判定原则**:
41
+ - 优先避免重复保存;若难以判定是否重复,优先 `save=false`,以避免规则冗余
42
+ </save_rules>
43
+
44
+ <rule_format>
45
+ 规则写法要求:
46
+
47
+ - 面向模型的**单句**指令;允许使用"必须/不得/总是"等明确措辞。
48
+ - 不引入具体路径、不绑定具体文件名。
49
+ - 例:"写作面向初学者;术语首次出现必须给出简明解释。"
50
+ </rule_format>
51
+
52
+ <examples>
53
+ 示例1:
54
+ - 输入:stage=document_refine,paths=["overview.md"],feedback="示例页废话太多,代码要最小可运行。"
55
+ - 输出:
56
+ {"rule":"示例页面以最小可运行代码为主,移除与主题无关的说明段。","scope":"document","save":true,"limitToInputPaths":true}
57
+
58
+ 示例2:
59
+ - 输入:stage=structure_planning,paths=[],feedback="概览与教程结尾加"下一步"并给出2–3个链接。"
60
+ - 输出:
61
+ {"rule":"在概览与教程文档结尾添加"下一步"小节并提供 2–3 个本仓库内链接。","scope":"structure","save":true,"limitToInputPaths":false}
62
+
63
+ 示例3:
64
+ - 输入:stage=translation_refine,paths=[],feedback="变量名和代码不要翻译。"
65
+ - 输出:
66
+ {"rule":"翻译时保持代码与标识符原样,不得翻译。","scope":"translation","save":true,"limitToInputPaths":false}
67
+
68
+ 示例4:
69
+ - 输入:stage=document_refine,paths=["overview.md"],feedback="这段话事实有误,改成 2021 年发布。"
70
+ - 输出:
71
+ {"rule":"更正事实到正确年份。","scope":"document","save":false,"limitToInputPaths":true}
72
+
73
+ 示例5(去重案例):
74
+ - 输入:stage=document_refine,paths=[],feedback="代码示例太复杂了,简化一下。",existingPreferences="rules:\n - rule: 示例页面以最小可运行代码为主,移除与主题无关的说明段。\n scope: document\n active: true"
75
+ - 输出:
76
+ {"rule":"简化代码示例的复杂度。","scope":"document","save":false,"limitToInputPaths":false}
77
+ # 理由:现有规则已覆盖简化代码示例的意图
78
+
79
+ 示例6(非重复案例):
80
+ - 输入:stage=document_refine,paths=[],feedback="代码注释要用英文写。",existingPreferences="rules:\n - rule: 示例页面以最小可运行代码为主,移除与主题无关的说明段。\n scope: document\n active: true"
81
+ - 输出:
82
+ {"rule":"代码注释必须使用英文编写。","scope":"document","save":true,"limitToInputPaths":false}
83
+ # 理由:现有规则未涉及注释语言,属于新的规则维度
84
+ </examples>
@@ -56,6 +56,14 @@
56
56
  {{ rules }}
57
57
  </user_rules>
58
58
 
59
+ <user_preferences>
60
+ {{userPreferences}}
61
+
62
+ 用户偏好使用规则:
63
+ - 用户偏好来自用户之前操作中提供的反馈,生成结构规划中需要考虑用户的偏好,避免出现用户反馈的问题又重复出现
64
+ - 用户偏好的权重低于本次用户提交的反馈
65
+ </user_preferences>
66
+
59
67
  <rules>
60
68
  DataSources 使用规则:
61
69
  1. 结构规划时要要尽可能的把 DataSources 中的信息合理的进行规划展示,不能遗漏
@@ -69,5 +69,13 @@
69
69
  {{ detailFeedback }}
70
70
  </review_feedback>
71
71
 
72
+ <user_preferences>
73
+ {{userPreferences}}
74
+
75
+ 用户偏好使用规则:
76
+ - 用户偏好来自用户之前操作中提供的反馈,生成结构规划中需要考虑用户的偏好,避免出现用户反馈的问题又重复出现
77
+ - 用户偏好的权重低于本次用户提交的反馈
78
+ </user_preferences>
79
+
72
80
  指令:
73
81
  请将 <content> 中的内容(不包含最外层的 <content> 标签) **准确** 地翻译成 **{{ language }}**,并严格遵循翻译要求。
@@ -1,5 +1,4 @@
1
- #!/usr/bin/env node
2
-
1
+ import { afterAll, describe, expect, test } from "bun:test";
3
2
  import checkDetailResult from "../agents/check-detail-result.mjs";
4
3
  import { checkMarkdown } from "../utils/markdown-checker.mjs";
5
4
  import { shutdownValidation } from "../utils/mermaid-validator.mjs";
@@ -459,7 +458,7 @@ This content ends properly.
459
458
  category: "🧩 MERMAID VALIDATION",
460
459
  name: "Mermaid with subgraph reference issues (rendering failure)",
461
460
  expectPass: false,
462
- expectedErrors: ["subgraph reference"],
461
+ expectedErrors: ["Mermaid syntax error"],
463
462
  content: `# Test Document
464
463
 
465
464
  \`\`\`mermaid
@@ -618,146 +617,70 @@ This comprehensive document demonstrates all validation rules in their correct u
618
617
  },
619
618
  ];
620
619
 
621
- async function runValidationTests() {
622
- console.log("🧪 Comprehensive Markdown Validation Test Suite\n");
623
- console.log("=".repeat(80));
624
-
625
- let totalTests = 0;
626
- let passedTests = 0;
627
- let failedTests = 0;
628
-
629
- let currentCategory = "";
630
-
631
- for (const testCase of testCases) {
632
- // Print category header if it changed
633
- if (testCase.category !== currentCategory) {
634
- currentCategory = testCase.category;
635
- console.log(`\n${currentCategory}`);
636
- console.log("-".repeat(80));
637
- }
638
-
639
- console.log(`\n📝 Testing: ${testCase.name}`);
640
- totalTests++;
641
-
620
+ describe("Markdown Validation Test Suite", () => {
621
+ afterAll(async () => {
622
+ // Shutdown worker pool to ensure clean exit
642
623
  try {
643
- // Test with checkMarkdown directly
644
- const errors = await checkMarkdown(testCase.content, "test", {
645
- allowedLinks,
646
- });
647
-
648
- // Test with checkDetailResult wrapper
649
- const wrapperResult = await checkDetailResult({
650
- structurePlan: mockStructurePlan,
651
- reviewContent: testCase.content,
652
- });
653
-
654
- const hasErrors = errors.length > 0;
655
- const expectPass = testCase.expectPass;
656
-
657
- // Verify test expectation
658
- if (expectPass && !hasErrors) {
659
- console.log("✅ PASS - Content correctly passed validation");
660
- passedTests++;
661
- } else if (!expectPass && hasErrors) {
662
- console.log("✅ PASS - Content correctly failed validation");
663
-
664
- // Check if expected error types are present
665
- if (testCase.expectedErrors) {
666
- const foundExpectedErrors = testCase.expectedErrors.every((expectedError) =>
667
- errors.some((error) => error.toLowerCase().includes(expectedError.toLowerCase())),
668
- );
624
+ await shutdownValidation();
625
+ } catch {
626
+ // Ignore shutdown errors in tests
627
+ }
628
+ });
669
629
 
670
- if (foundExpectedErrors) {
671
- console.log("✅ Expected error types found");
630
+ // Group tests by category
631
+ const testsByCategory = testCases.reduce((acc, testCase) => {
632
+ if (!acc[testCase.category]) {
633
+ acc[testCase.category] = [];
634
+ }
635
+ acc[testCase.category].push(testCase);
636
+ return acc;
637
+ }, {});
638
+
639
+ Object.entries(testsByCategory).forEach(([category, categoryTests]) => {
640
+ describe(category, () => {
641
+ categoryTests.forEach((testCase) => {
642
+ test(testCase.name, async () => {
643
+ // Test with checkMarkdown directly
644
+ const errors = await checkMarkdown(testCase.content, "test", {
645
+ allowedLinks,
646
+ });
647
+
648
+ // Test with checkDetailResult wrapper
649
+ const wrapperResult = await checkDetailResult({
650
+ structurePlan: mockStructurePlan,
651
+ reviewContent: testCase.content,
652
+ });
653
+
654
+ const hasErrors = errors.length > 0;
655
+ const expectPass = testCase.expectPass;
656
+
657
+ // Verify test expectation
658
+ if (expectPass) {
659
+ expect(hasErrors).toBe(false);
672
660
  } else {
673
- console.log("⚠️ Expected error types not all found");
674
- console.log(` Expected: ${testCase.expectedErrors.join(", ")}`);
661
+ expect(hasErrors).toBe(true);
662
+
663
+ // Check if expected error types are present
664
+ if (testCase.expectedErrors) {
665
+ const foundExpectedErrors = testCase.expectedErrors.every((expectedError) =>
666
+ errors.some((error) => error.toLowerCase().includes(expectedError.toLowerCase())),
667
+ );
668
+ expect(foundExpectedErrors).toBe(true);
669
+ }
675
670
  }
676
- }
677
-
678
- passedTests++;
679
- } else {
680
- console.log(
681
- `❌ FAIL - Expected ${expectPass ? "PASS" : "FAIL"} but got ${
682
- hasErrors ? "FAIL" : "PASS"
683
- }`,
684
- );
685
- failedTests++;
686
- }
687
-
688
- // Show error details for failing cases
689
- if (hasErrors) {
690
- console.log(` Found ${errors.length} issue(s):`);
691
- errors.slice(0, 3).forEach((error) => {
692
- console.log(` • ${error}`);
693
- });
694
- if (errors.length > 3) {
695
- console.log(` ... and ${errors.length - 3} more issues`);
696
- }
697
- }
698
-
699
- // Verify consistency between direct call and wrapper
700
- const wrapperErrors = wrapperResult.detailFeedback
701
- ? wrapperResult.detailFeedback.split("\n").filter((line) => line.trim())
702
- : [];
703
-
704
- if (errors.length === wrapperErrors.length) {
705
- console.log("✅ Direct call and wrapper consistent");
706
- } else {
707
- console.log(
708
- `⚠️ Inconsistent results: direct=${errors.length}, wrapper=${wrapperErrors.length}`,
709
- );
710
- }
711
- } catch (error) {
712
- console.log(`❌ ERROR: ${error.message}`);
713
- failedTests++;
714
- }
715
- }
716
671
 
717
- // Final summary
718
- console.log(`\n${"=".repeat(80)}`);
719
- console.log("📊 TEST SUMMARY");
720
- console.log("=".repeat(80));
721
- console.log(`Total Tests: ${totalTests}`);
722
- console.log(`Passed: ${passedTests} ✅`);
723
- console.log(`Failed: ${failedTests} ❌`);
724
- console.log(`Success Rate: ${((passedTests / totalTests) * 100).toFixed(1)}%`);
725
-
726
- console.log("\n🔍 VALIDATION COVERAGE:");
727
- console.log("✅ Link validation (dead links, allowed links)");
728
- console.log("✅ Code block validation (indentation, completeness)");
729
- console.log("✅ Content structure (line breaks, punctuation)");
730
- console.log("✅ Table validation (column count consistency)");
731
- console.log("✅ Mermaid validation (syntax, rendering issues)");
732
- console.log("✅ Standard markdown linting (formatting rules)");
733
- console.log("✅ Complex mixed scenarios");
734
- console.log("✅ Edge cases and error conditions");
735
-
736
- if (failedTests === 0) {
737
- console.log("\n🎉 ALL TESTS PASSED! Validation system is working correctly.");
738
- } else {
739
- console.log(`\n⚠️ ${failedTests} test(s) failed. Please review the validation logic.`);
740
- }
672
+ // Verify consistency between direct call and wrapper
673
+ const wrapperErrors = wrapperResult.detailFeedback
674
+ ? wrapperResult.detailFeedback.split("\n").filter((line) => line.trim())
675
+ : [];
741
676
 
742
- // Shutdown worker pool to ensure clean exit
743
- try {
744
- await shutdownValidation();
745
- } catch (error) {
746
- console.error("Error shutting down validation:", error);
747
- }
748
- }
677
+ // Note: We don't enforce exact equality as wrapper may format differently
678
+ expect(wrapperErrors.length > 0).toBe(hasErrors);
679
+ });
680
+ });
681
+ });
682
+ });
683
+ });
749
684
 
750
685
  // Export test cases for external use
751
686
  export { testCases, mockStructurePlan, allowedLinks };
752
-
753
- // Run tests if this file is executed directly
754
- if (import.meta.url === `file://${process.argv[1]}`) {
755
- runValidationTests()
756
- .then(() => {
757
- process.exit(0);
758
- })
759
- .catch((error) => {
760
- console.error("Test suite error:", error);
761
- process.exit(1);
762
- });
763
- }