pdd-skills 3.0.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 (261) hide show
  1. package/README.md +1478 -0
  2. package/bin/pdd.js +354 -0
  3. package/config/bpmn-rules.yaml +166 -0
  4. package/config/checkstyle.xml +105 -0
  5. package/config/eslint.config.js +48 -0
  6. package/config/pmd.xml +91 -0
  7. package/config/prd-rules.yaml +113 -0
  8. package/config/ruff.toml +45 -0
  9. package/config/sqlfluff.cfg +82 -0
  10. package/hooks/hook-executor.js +332 -0
  11. package/index.js +43 -0
  12. package/lib/api-routes.js +750 -0
  13. package/lib/api-server.js +408 -0
  14. package/lib/cache/cache-config.js +209 -0
  15. package/lib/cache/system-cache.js +852 -0
  16. package/lib/config-manager.js +373 -0
  17. package/lib/generate.js +528 -0
  18. package/lib/grpc/grpc-routes.js +1134 -0
  19. package/lib/grpc/grpc-server.js +912 -0
  20. package/lib/grpc/proto-definitions.js +1033 -0
  21. package/lib/init.js +172 -0
  22. package/lib/iteration/auto-fixer.js +1025 -0
  23. package/lib/iteration/auto-reviewer.js +923 -0
  24. package/lib/iteration/controller.js +577 -0
  25. package/lib/list.js +130 -0
  26. package/lib/mcp-server.js +548 -0
  27. package/lib/openclaw/api-integration.js +535 -0
  28. package/lib/openclaw/cli-integration.js +567 -0
  29. package/lib/openclaw/data-sync.js +845 -0
  30. package/lib/openclaw/openclaw-adapter.js +783 -0
  31. package/lib/plugin/example-plugins/code-stats/index.js +332 -0
  32. package/lib/plugin/example-plugins/code-stats/plugin.json +1 -0
  33. package/lib/plugin/example-plugins/custom-linter/index.js +472 -0
  34. package/lib/plugin/example-plugins/custom-linter/plugin.json +1 -0
  35. package/lib/plugin/example-plugins/hello-world/index.js +86 -0
  36. package/lib/plugin/example-plugins/hello-world/plugin.json +1 -0
  37. package/lib/plugin/plugin-manager.js +655 -0
  38. package/lib/plugin/plugin-sdk.js +565 -0
  39. package/lib/plugin/sandbox.js +627 -0
  40. package/lib/quality/rules/maintainability.js +418 -0
  41. package/lib/quality/rules/performance.js +498 -0
  42. package/lib/quality/rules/readability.js +441 -0
  43. package/lib/quality/rules/robustness.js +504 -0
  44. package/lib/quality/rules/security.js +444 -0
  45. package/lib/quality/scorer.js +576 -0
  46. package/lib/report.js +669 -0
  47. package/lib/sdk-base.js +301 -0
  48. package/lib/sdk-js.js +446 -0
  49. package/lib/sdk-python/README.md +546 -0
  50. package/lib/sdk-python/examples/basic_usage.py +450 -0
  51. package/lib/sdk-python/pdd_sdk/__init__.py +180 -0
  52. package/lib/sdk-python/pdd_sdk/client.py +1170 -0
  53. package/lib/sdk-python/pdd_sdk/events.py +423 -0
  54. package/lib/sdk-python/pdd_sdk/exceptions.py +158 -0
  55. package/lib/sdk-python/pdd_sdk/models.py +518 -0
  56. package/lib/sdk-python/pdd_sdk/utils.py +759 -0
  57. package/lib/token/budget-alert.js +367 -0
  58. package/lib/token/budget-manager.js +485 -0
  59. package/lib/update.js +54 -0
  60. package/lib/utils/logger.js +88 -0
  61. package/lib/verify.js +741 -0
  62. package/lib/version.js +52 -0
  63. package/lib/vm/README.md +102 -0
  64. package/lib/vm/dashboard/api-routes.js +669 -0
  65. package/lib/vm/dashboard/server.js +391 -0
  66. package/lib/vm/dashboard/sse.js +358 -0
  67. package/lib/vm/dashboard/static/css/dashboard.css +1378 -0
  68. package/lib/vm/dashboard/static/index.html +118 -0
  69. package/lib/vm/dashboard/static/js/app.js +949 -0
  70. package/lib/vm/dashboard/static/js/charts.js +913 -0
  71. package/lib/vm/dashboard/static/js/kanban-view.js +1053 -0
  72. package/lib/vm/dashboard/static/js/pipeline-view.js +463 -0
  73. package/lib/vm/dashboard/static/js/quality-view.js +598 -0
  74. package/lib/vm/dashboard/static/js/system-view.js +1021 -0
  75. package/lib/vm/data-provider.js +1191 -0
  76. package/lib/vm/event-bus.js +402 -0
  77. package/lib/vm/hooks/extract-hook.js +307 -0
  78. package/lib/vm/hooks/generate-hook.js +374 -0
  79. package/lib/vm/hooks/hook-interface.js +458 -0
  80. package/lib/vm/hooks/report-hook.js +331 -0
  81. package/lib/vm/hooks/verify-hook.js +454 -0
  82. package/lib/vm/models.js +1003 -0
  83. package/lib/vm/reconciler.js +855 -0
  84. package/lib/vm/scanner.js +988 -0
  85. package/lib/vm/state-schema.js +955 -0
  86. package/lib/vm/state-store.js +733 -0
  87. package/lib/vm/tui/components/card.js +339 -0
  88. package/lib/vm/tui/components/progress-bar.js +368 -0
  89. package/lib/vm/tui/components/sparkline.js +327 -0
  90. package/lib/vm/tui/components/status-light.js +294 -0
  91. package/lib/vm/tui/components/table.js +370 -0
  92. package/lib/vm/tui/input.js +335 -0
  93. package/lib/vm/tui/renderer.js +548 -0
  94. package/lib/vm/tui/screens/kanban-screen.js +397 -0
  95. package/lib/vm/tui/screens/overview-screen.js +357 -0
  96. package/lib/vm/tui/screens/quality-screen.js +336 -0
  97. package/lib/vm/tui/screens/system-screen.js +379 -0
  98. package/lib/vm/tui/tui.js +805 -0
  99. package/package.json +1 -0
  100. package/scripts/cso-analyzer.js +198 -0
  101. package/scripts/eval-runner.js +359 -0
  102. package/scripts/i18n-checker.js +109 -0
  103. package/scripts/linter/activiti-linter.js +272 -0
  104. package/scripts/linter/prd-linter.js +162 -0
  105. package/scripts/linter/report-generator.js +207 -0
  106. package/scripts/linter/run-linters.js +285 -0
  107. package/scripts/linter/sql-linter.js +166 -0
  108. package/scripts/token-analyzer.js +162 -0
  109. package/scripts/vm-test.js +180 -0
  110. package/skills/core/official-doc-writer/LICENSE +21 -0
  111. package/skills/core/official-doc-writer/README.md +232 -0
  112. package/skills/core/official-doc-writer/SKILL.md +475 -0
  113. package/skills/core/official-doc-writer/_meta.json +1 -0
  114. package/skills/core/official-doc-writer/document_generator.py +580 -0
  115. package/skills/core/official-doc-writer/evals/default-evals.json +1 -0
  116. package/skills/core/official-doc-writer/examples.md +150 -0
  117. package/skills/core/official-doc-writer/fonts/FONTS_LIST.md +45 -0
  118. package/skills/core/official-doc-writer/fonts/README.md +141 -0
  119. package/skills/core/official-doc-writer/fonts/SIMFANG.TTF +0 -0
  120. package/skills/core/official-doc-writer/fonts/SIMHEI.TTF +0 -0
  121. package/skills/core/official-doc-writer/fonts/SIMKAI.TTF +0 -0
  122. package/skills/core/official-doc-writer/fonts/SIMSUN.TTC +0 -0
  123. package/skills/core/official-doc-writer/fonts//346/226/271/346/255/243/345/260/217/346/240/207/345/256/213GBK.TTF +0 -0
  124. package/skills/core/official-doc-writer/references/GBT_9704-2012_/345/205/232/346/224/277/346/234/272/345/205/263/345/205/254/346/226/207/346/240/274/345/274/217.md +422 -0
  125. package/skills/core/official-doc-writer/scripts/__pycache__/generate_official_doc.cpython-313.pyc +0 -0
  126. package/skills/core/official-doc-writer/scripts/dialog_manager.py +564 -0
  127. package/skills/core/official-doc-writer/scripts/generate_official_doc.py +252 -0
  128. package/skills/core/official-doc-writer/scripts/install_fonts.py +390 -0
  129. package/skills/core/official-doc-writer/scripts/smart_prompts.py +363 -0
  130. package/skills/core/pdd-ba/SKILL.md +305 -0
  131. package/skills/core/pdd-ba/_meta.json +1 -0
  132. package/skills/core/pdd-ba/evals/default-evals.json +1 -0
  133. package/skills/core/pdd-code-reviewer/SKILL.md +378 -0
  134. package/skills/core/pdd-code-reviewer/_meta.json +1 -0
  135. package/skills/core/pdd-code-reviewer/evals/default-evals.json +1 -0
  136. package/skills/core/pdd-doc-change/SKILL.md +350 -0
  137. package/skills/core/pdd-doc-change/_meta.json +1 -0
  138. package/skills/core/pdd-doc-change/evals/default-evals.json +1 -0
  139. package/skills/core/pdd-doc-gardener/SKILL.md +248 -0
  140. package/skills/core/pdd-doc-gardener/_meta.json +1 -0
  141. package/skills/core/pdd-doc-gardener/evals/default-evals.json +1 -0
  142. package/skills/core/pdd-entropy-reduction/SKILL.md +360 -0
  143. package/skills/core/pdd-entropy-reduction/_meta.json +1 -0
  144. package/skills/core/pdd-entropy-reduction/evals/default-evals.json +1 -0
  145. package/skills/core/pdd-entropy-reduction/references/entropy-report-template.md +287 -0
  146. package/skills/core/pdd-entropy-reduction/references/golden-principles.md +573 -0
  147. package/skills/core/pdd-entropy-reduction/scripts/entropy_scan.py +712 -0
  148. package/skills/core/pdd-extract-features/SKILL.md +320 -0
  149. package/skills/core/pdd-extract-features/_meta.json +1 -0
  150. package/skills/core/pdd-extract-features/evals/default-evals.json +1 -0
  151. package/skills/core/pdd-generate-spec/SKILL.md +418 -0
  152. package/skills/core/pdd-generate-spec/_meta.json +1 -0
  153. package/skills/core/pdd-generate-spec/evals/default-evals.json +1 -0
  154. package/skills/core/pdd-implement-feature/SKILL.md +332 -0
  155. package/skills/core/pdd-implement-feature/_meta.json +1 -0
  156. package/skills/core/pdd-implement-feature/evals/default-evals.json +1 -0
  157. package/skills/core/pdd-main/SKILL.md +540 -0
  158. package/skills/core/pdd-main/_meta.json +1 -0
  159. package/skills/core/pdd-main/evals/default-evals.json +1 -0
  160. package/skills/core/pdd-main/evals/evals.json +215 -0
  161. package/skills/core/pdd-verify-feature/SKILL.md +474 -0
  162. package/skills/core/pdd-verify-feature/_meta.json +1 -0
  163. package/skills/core/pdd-verify-feature/evals/default-evals.json +1 -0
  164. package/skills/core/pdd-vm/evals/default-evals.json +1 -0
  165. package/skills/core/traffic-accident-assessor/LICENSE +29 -0
  166. package/skills/core/traffic-accident-assessor/SKILL.md +439 -0
  167. package/skills/core/traffic-accident-assessor/evals/evals.json +1 -0
  168. package/skills/core/traffic-accident-assessor/references/accident-types.md +369 -0
  169. package/skills/core/traffic-accident-assessor/references/liability-rules.md +287 -0
  170. package/skills/core/traffic-accident-assessor/references/traffic-laws.md +226 -0
  171. package/skills/core/traffic-accident-assessor/references//351/253/230/345/260/224/345/244/253/350/257/264/346/230/216/344/271/246.pdf +32576 -106
  172. package/skills/core/traffic-accident-assessor/scripts/generate_official_statement.py +588 -0
  173. package/skills/core/traffic-accident-assessor/scripts/generate_report.py +495 -0
  174. package/skills/core/traffic-accident-assessor/scripts/generate_statement.py +528 -0
  175. package/skills/core/traffic-accident-assessor.zip +0 -0
  176. package/skills/entropy/expert-arch-enforcer/SKILL.md +292 -0
  177. package/skills/entropy/expert-arch-enforcer/_meta.json +1 -0
  178. package/skills/entropy/expert-arch-enforcer/evals/default-evals.json +1 -0
  179. package/skills/entropy/expert-auto-refactor/SKILL.md +327 -0
  180. package/skills/entropy/expert-auto-refactor/_meta.json +1 -0
  181. package/skills/entropy/expert-auto-refactor/evals/default-evals.json +1 -0
  182. package/skills/entropy/expert-code-quality/SKILL.md +468 -0
  183. package/skills/entropy/expert-code-quality/_meta.json +1 -0
  184. package/skills/entropy/expert-code-quality/evals/default-evals.json +1 -0
  185. package/skills/entropy/expert-code-quality/evals/evals.json +109 -0
  186. package/skills/entropy/expert-code-quality/references/code-smells.md +605 -0
  187. package/skills/entropy/expert-code-quality/references/design-patterns.md +1111 -0
  188. package/skills/entropy/expert-code-quality/references/refactoring-catalog.md +1281 -0
  189. package/skills/entropy/expert-code-quality/references/solid-principles.md +524 -0
  190. package/skills/entropy/expert-entropy-auditor/SKILL.md +276 -0
  191. package/skills/entropy/expert-entropy-auditor/_meta.json +1 -0
  192. package/skills/entropy/expert-entropy-auditor/evals/default-evals.json +1 -0
  193. package/skills/expert/expert-activiti/SKILL.md +497 -0
  194. package/skills/expert/expert-activiti/_meta.json +1 -0
  195. package/skills/expert/expert-mysql/SKILL.md +832 -0
  196. package/skills/expert/expert-mysql/_meta.json +1 -0
  197. package/skills/expert/expert-performance/SKILL.md +379 -0
  198. package/skills/expert/expert-performance/_meta.json +1 -0
  199. package/skills/expert/expert-performance/evals/default-evals.json +1 -0
  200. package/skills/expert/expert-ruoyi/SKILL.md +472 -0
  201. package/skills/expert/expert-ruoyi/_meta.json +1 -0
  202. package/skills/expert/expert-security/SKILL.md +1341 -0
  203. package/skills/expert/expert-security/_meta.json +1 -0
  204. package/skills/expert/expert-security/evals/default-evals.json +1 -0
  205. package/skills/expert/software-architect/SKILL.md +350 -0
  206. package/skills/expert/software-architect/_meta.json +1 -0
  207. package/skills/expert/software-engineer/SKILL.md +437 -0
  208. package/skills/expert/software-engineer/_meta.json +1 -0
  209. package/skills/expert/software-engineer/architecture.md +130 -0
  210. package/skills/expert/software-engineer/patterns.md +151 -0
  211. package/skills/expert/software-engineer/testing.md +135 -0
  212. package/skills/expert/system-architect/SKILL.md +628 -0
  213. package/skills/expert/system-architect/_meta.json +1 -0
  214. package/skills/expert/system-architect/assets/templates/ARCHITECTURE.md +25 -0
  215. package/skills/expert/system-architect/assets/templates/README.md +44 -0
  216. package/skills/expert/system-architect/references/js-ts-standards.md +18 -0
  217. package/skills/expert/system-architect/references/python-standards.md +19 -0
  218. package/skills/expert/system-architect/references/scaffolding.md +61 -0
  219. package/skills/expert/system-architect/references/security-checklist.md +21 -0
  220. package/skills/openspec/openspec-apply-change/SKILL.md +156 -0
  221. package/skills/openspec/openspec-apply-change/_meta.json +1 -0
  222. package/skills/openspec/openspec-archive-change/SKILL.md +114 -0
  223. package/skills/openspec/openspec-archive-change/_meta.json +1 -0
  224. package/skills/openspec/openspec-bulk-archive-change/SKILL.md +246 -0
  225. package/skills/openspec/openspec-bulk-archive-change/_meta.json +1 -0
  226. package/skills/openspec/openspec-continue-change/SKILL.md +118 -0
  227. package/skills/openspec/openspec-continue-change/_meta.json +1 -0
  228. package/skills/openspec/openspec-explore/SKILL.md +288 -0
  229. package/skills/openspec/openspec-explore/_meta.json +1 -0
  230. package/skills/openspec/openspec-ff-change/SKILL.md +101 -0
  231. package/skills/openspec/openspec-ff-change/_meta.json +1 -0
  232. package/skills/openspec/openspec-new-change/SKILL.md +74 -0
  233. package/skills/openspec/openspec-new-change/_meta.json +1 -0
  234. package/skills/openspec/openspec-onboard/SKILL.md +554 -0
  235. package/skills/openspec/openspec-onboard/_meta.json +1 -0
  236. package/skills/openspec/openspec-sync-specs/SKILL.md +138 -0
  237. package/skills/openspec/openspec-sync-specs/_meta.json +1 -0
  238. package/skills/openspec/openspec-verify-change/SKILL.md +168 -0
  239. package/skills/openspec/openspec-verify-change/_meta.json +1 -0
  240. package/skills/pr/pdd-multi-review/SKILL.md +534 -0
  241. package/skills/pr/pdd-multi-review/_meta.json +1 -0
  242. package/skills/pr/pdd-pr-batch/SKILL.md +303 -0
  243. package/skills/pr/pdd-pr-batch/_meta.json +1 -0
  244. package/skills/pr/pdd-pr-create/SKILL.md +344 -0
  245. package/skills/pr/pdd-pr-create/_meta.json +1 -0
  246. package/skills/pr/pdd-pr-merge/SKILL.md +286 -0
  247. package/skills/pr/pdd-pr-merge/_meta.json +1 -0
  248. package/skills/pr/pdd-pr-review/SKILL.md +217 -0
  249. package/skills/pr/pdd-pr-review/_meta.json +1 -0
  250. package/skills/pr/pdd-task-manager/SKILL.md +636 -0
  251. package/skills/pr/pdd-task-manager/_meta.json +1 -0
  252. package/skills/pr/pdd-template-engine/SKILL.md +306 -0
  253. package/skills/pr/pdd-template-engine/_meta.json +1 -0
  254. package/templates/behavior-shaping/iron-law-template.md +87 -0
  255. package/templates/behavior-shaping/rationalization-template.md +62 -0
  256. package/templates/behavior-shaping/red-flags-template.md +70 -0
  257. package/templates/bilingual-template.md +139 -0
  258. package/templates/config/default.yaml +47 -0
  259. package/templates/project/default/README.md +31 -0
  260. package/templates/project/frontend/README.md +46 -0
  261. package/templates/project/java/README.md +48 -0
package/package.json ADDED
@@ -0,0 +1 @@
1
+ {"name": "pdd-skills", "version": "3.0.0", "description": "PRD Driven Development Skills - AI原生软件开发工作流", "type": "module", "main": "index.js", "bin": {"pdd": "./bin/pdd.js", "pdd-skills": "./bin/pdd.js"}, "files": ["bin/", "lib/", "skills/", "templates/", "scripts/", "config/", "hooks/", "index.js"], "scripts": {"start": "node bin/pdd.js", "list": "node bin/pdd.js list", "lint": "node bin/pdd.js linter --type code prd sql activiti", "generate": "node bin/pdd.js generate", "verify": "node bin/pdd.js verify", "report": "node bin/pdd.js report", "config": "node bin/pdd.js config --list", "api": "node bin/pdd.js api", "api:dev": "node bin/pdd.js api -p 3000 --cors", "init": "node bin/pdd.js init", "update": "node bin/pdd.js update", "cso": "node bin/pdd.js cso", "eval": "node bin/pdd.js eval", "token": "node bin/pdd.js token", "i18n": "node bin/pdd.js i18n"}, "keywords": ["pdd", "prd-driven-development", "ai", "claude", "skills", "tdd", "code-generation", "linter", "api-server", "cli"], "author": "PDD Team", "license": "MIT", "engines": {"node": ">=18.0.0"}, "dependencies": {"commander": "^12.0.0", "chalk": "^5.3.0", "fs-extra": "^11.2.0", "yaml": "^2.3.0"}}
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const SKILLS_ROOT = path.join(__dirname, '..', 'skills');
8
+
9
+ const CSO_RULES = {
10
+ description: {
11
+ minLength: 20,
12
+ maxLength: 200,
13
+ mustContain: ['trigger', 'invoke', 'call', 'when', '触发', '调用', '当用户'],
14
+ shouldAvoid: ['very', 'really', 'basically', 'stuff', 'things', 'etc']
15
+ },
16
+ triggers: {
17
+ minCount: 3,
18
+ maxCount: 8,
19
+ avoidGeneric: ['help', 'do', 'make', 'create', 'fix', 'check', 'run']
20
+ }
21
+ };
22
+
23
+ export async function analyzeSkillCSO(skillDir) {
24
+ const skillMd = path.join(skillDir, 'SKILL.md');
25
+ const metaJson = path.join(skillDir, '_meta.json');
26
+
27
+ if (!fs.existsSync(skillMd)) return { error: 'SKILL.md not found' };
28
+
29
+ const content = fs.readFileSync(skillMd, 'utf-8');
30
+ let meta = {};
31
+ if (fs.existsSync(metaJson)) {
32
+ try { meta = JSON.parse(fs.readFileSync(metaJson, 'utf-8')); } catch {}
33
+ }
34
+
35
+ const issues = [];
36
+ const scores = { description: 0, triggers: 0, overall: 0 };
37
+ const suggestions = [];
38
+
39
+ const descMatch = content.match(/^description:\s*(.+)$/m);
40
+ let description = '';
41
+ if (descMatch) {
42
+ const raw = descMatch[1].trim();
43
+ if (raw === '|' || raw === '>') {
44
+ const lines = content.split('\n');
45
+ const descLineIdx = lines.findIndex(l => l.startsWith('description:'));
46
+ if (descLineIdx >= 0) {
47
+ const mlLines = [];
48
+ for (let i = descLineIdx + 1; i < lines.length; i++) {
49
+ if (lines[i].match(/^[a-zA-Z_-]+:/) || (!lines[i].startsWith(' ') && !lines[i].startsWith('\t') && lines[i].trim() !== '')) break;
50
+ mlLines.push(lines[i].trim());
51
+ }
52
+ description = mlLines.join(' ');
53
+ }
54
+ } else {
55
+ description = raw.replace(/^["']|["']$/g, '');
56
+ }
57
+ }
58
+ if (!description) description = meta.description || '';
59
+
60
+ if (!description) {
61
+ issues.push({ level: 'error', rule: 'CSO-D001', message: '缺少description字段' });
62
+ } else {
63
+ if (description.length < CSO_RULES.description.minLength) {
64
+ issues.push({ level: 'warn', rule: 'CSO-D002', message: `description过短(${description.length}字),建议至少${CSO_RULES.description.minLength}字` });
65
+ scores.description -= 20;
66
+ } else {
67
+ scores.description += 30;
68
+ }
69
+
70
+ if (description.length > CSO_RULES.description.maxLength) {
71
+ issues.push({ level: 'warn', rule: 'CSO-D003', message: `description过长(${description.length}字),建议不超过${CSO_RULES.description.maxLength}字` });
72
+ scores.description -= 10;
73
+ }
74
+
75
+ const hasTriggerWords = CSO_RULES.description.mustContain.some(w =>
76
+ description.toLowerCase().includes(w.toLowerCase())
77
+ );
78
+ if (!hasTriggerWords) {
79
+ issues.push({ level: 'warn', rule: 'CSO-D004', message: 'description缺少触发词(when/invoke/call/trigger等)' });
80
+ scores.description -= 15;
81
+ } else {
82
+ scores.description += 20;
83
+ }
84
+
85
+ const vagueWords = CSO_RULES.description.shouldAvoid.filter(w =>
86
+ new RegExp(`\\b${w}\\b`, 'i').test(description)
87
+ );
88
+ if (vagueWords.length > 0) {
89
+ issues.push({ level: 'info', rule: 'CSO-D005', message: `检测到模糊词汇: ${vagueWords.join(', ')}` });
90
+ scores.description -= vagueWords.length * 5;
91
+ }
92
+ }
93
+
94
+ const triggers = meta.triggers || [];
95
+ const allTriggers = [...triggers];
96
+
97
+ if (allTriggers.length === 0) {
98
+ issues.push({ level: 'warn', rule: 'CSO-T001', message: '未定义触发词(triggers)' });
99
+ } else {
100
+ if (allTriggers.length < CSO_RULES.triggers.minCount) {
101
+ issues.push({ level: 'warn', rule: 'CSO-T002', message: `触发词数量不足(${allTriggers.length}),建议至少${CSO_RULES.triggers.minCount}个` });
102
+ } else {
103
+ scores.triggers += 25;
104
+ }
105
+
106
+ if (allTriggers.length > CSO_RULES.triggers.maxCount) {
107
+ issues.push({ level: 'info', rule: 'CSO-T003', message: `触发词过多(${allTriggers.length}),可能降低精确度` });
108
+ }
109
+
110
+ const genericTriggers = allTriggers.filter(t =>
111
+ CSO_RULES.triggers.avoidGeneric.includes(t.toLowerCase())
112
+ );
113
+ if (genericTriggers.length > 0) {
114
+ issues.push({ level: 'warn', rule: 'CSO-T004', message: `触发词过于通用: ${genericTriggers.join(', ')}` });
115
+ scores.triggers -= genericTriggers.length * 8;
116
+ }
117
+
118
+ const specificTriggers = allTriggers.filter(t => t.length >= 4);
119
+ if (specificTriggers.length > 0) {
120
+ scores.triggers += Math.min(specificTriggers.length * 5, 25);
121
+ }
122
+ }
123
+
124
+ scores.overall = Math.max(0, Math.min(100, (scores.description + scores.triggers) / 2));
125
+
126
+ return {
127
+ skillName: path.basename(skillDir),
128
+ description: description.substring(0, 100),
129
+ triggers: allTriggers,
130
+ scores,
131
+ score: scores.overall,
132
+ grade: scores.overall >= 80 ? 'A' : scores.overall >= 60 ? 'B' : scores.overall >= 40 ? 'C' : 'D',
133
+ issues,
134
+ suggestions
135
+ };
136
+ }
137
+
138
+ export async function analyzeAllSkills(category) {
139
+ const categories = category ? [category] : ['core', 'entropy', 'expert', 'openspec', 'pr'];
140
+ const results = [];
141
+
142
+ for (const cat of categories) {
143
+ const catPath = path.join(SKILLS_ROOT, cat);
144
+ if (!fs.existsSync(catPath)) continue;
145
+
146
+ const entries = fs.readdirSync(catPath).filter(f => {
147
+ const p = path.join(catPath, f);
148
+ return fs.statSync(p).isDirectory();
149
+ });
150
+
151
+ for (const entry of entries) {
152
+ const result = await analyzeSkillCSO(path.join(catPath, entry));
153
+ results.push(result);
154
+ }
155
+ }
156
+
157
+ return results;
158
+ }
159
+
160
+ export function printCSOReport(results) {
161
+ const avgScore = results.reduce((s, r) => s + r.score, 0) / results.length;
162
+ const gradeDist = { A: 0, B: 0, C: 0, D: 0 };
163
+ results.forEach(r => gradeDist[r.grade]++);
164
+
165
+ console.log('\n📊 CSO (Claude Search Optimization) 分析报告\n');
166
+ console.log(` 总技能数: ${results.length}`);
167
+ console.log(` 平均分: ${avgScore.toFixed(1)}/100`);
168
+ console.log(` 等级分布: A:${gradeDist.A} B:${gradeDist.B} C:${gradeDist.C} D:${gradeDist.D}\n`);
169
+
170
+ const sorted = [...results].sort((a, b) => a.score - b.score);
171
+
172
+ console.log(' ┌────────────────────────┬───────┬────────┬──────────┐');
173
+ console.log(' │ 技能名称 │ 等级 │ 分数 │ 问题数 │');
174
+ console.log(' ├────────────────────────┼───────┼────────┼──────────┤');
175
+
176
+ for (const r of sorted) {
177
+ const icon = r.grade === 'A' ? '🟢' : r.grade === 'B' ? '🟡' : r.grade === 'C' ? '🟠' : '🔴';
178
+ const name = r.skillName.padEnd(24).substring(0, 24);
179
+ console.log(` │ ${icon} ${name} │ ${r.grade} │ ${r.score.toString().padStart(5)} │ ${r.issues.length.toString().padStart(6)} │`);
180
+ }
181
+
182
+ console.log(' └────────────────────────┴───────┴────────┴──────────┘');
183
+
184
+ if (results.some(r => r.issues.length > 0)) {
185
+ console.log('\n 主要问题:');
186
+ const allIssues = results.flatMap(r => r.issues.map(i => ({ ...i, skill: r.skillName })));
187
+ const byRule = {};
188
+ for (const issue of allIssues) {
189
+ byRule[issue.rule] = (byRule[issue.rule] || 0) + 1;
190
+ }
191
+ Object.entries(byRule)
192
+ .sort((a, b) => b[1] - a[1])
193
+ .slice(0, 10)
194
+ .forEach(([rule, count]) => {
195
+ console.log(` ${rule}: ${count}次`);
196
+ });
197
+ }
198
+ }
@@ -0,0 +1,359 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const SKILLS_ROOT = path.join(__dirname, '..', 'skills');
8
+
9
+ const EVAL_RESULT = {
10
+ PASS: 'pass',
11
+ FAIL: 'fail',
12
+ SKIP: 'skip',
13
+ ERROR: 'error'
14
+ };
15
+
16
+ export async function runEvals(options = {}) {
17
+ const skillName = options.skill;
18
+ const category = options.category;
19
+ const verbose = options.verbose || false;
20
+
21
+ console.log(chalk.blue.bold('\n🧪 PDD Evals - 技能评估测试\n'));
22
+
23
+ const targets = findEvalTargets(skillName, category);
24
+
25
+ if (targets.length === 0) {
26
+ console.log(chalk.yellow(' 未找到可测试的目标'));
27
+ return;
28
+ }
29
+
30
+ const results = [];
31
+
32
+ for (const target of targets) {
33
+ const result = await runSkillEvals(target, verbose);
34
+ results.push(result);
35
+ printSkillResult(result, verbose);
36
+ }
37
+
38
+ printSummary(results);
39
+ return results;
40
+ }
41
+
42
+ function findEvalTargets(skillName, category) {
43
+ if (skillName) {
44
+ for (const cat of ['core', 'entropy', 'expert', 'openspec', 'pr']) {
45
+ const p = path.join(SKILLS_ROOT, cat, skillName);
46
+ if (fs.existsSync(p)) return [{ name: skillName, path: p }];
47
+ }
48
+ return [];
49
+ }
50
+
51
+ if (category) {
52
+ const catPath = path.join(SKILLS_ROOT, category);
53
+ if (!fs.existsSync(catPath)) return [];
54
+ return fs.readdirSync(catPath)
55
+ .filter(f => fs.statSync(path.join(catPath, f)).isDirectory())
56
+ .map(f => ({ name: f, path: path.join(catPath, f) }));
57
+ }
58
+
59
+ const all = [];
60
+ for (const cat of ['core', 'entropy', 'expert']) {
61
+ const catPath = path.join(SKILLS_ROOT, cat);
62
+ if (!fs.existsSync(catPath)) continue;
63
+ for (const entry of fs.readdirSync(catPath).filter(f =>
64
+ fs.statSync(path.join(catPath, f)).isDirectory()
65
+ )) {
66
+ all.push({ name: entry, path: path.join(catPath, entry) });
67
+ }
68
+ }
69
+ return all;
70
+ }
71
+
72
+ async function runSkillEvals(target, verbose) {
73
+ const start = Date.now();
74
+ const evalDir = path.join(target.path, 'evals');
75
+ const testCases = loadTestCases(evalDir);
76
+
77
+ const skillResult = {
78
+ skill: target.name,
79
+ totalTests: 0,
80
+ passed: 0,
81
+ failed: 0,
82
+ skipped: 0,
83
+ errors: 0,
84
+ duration: 0,
85
+ cases: []
86
+ };
87
+
88
+ if (testCases.length === 0) {
89
+ skillResult.totalTests = 1;
90
+ skillResult.skipped = 1;
91
+ skillResult.cases.push({
92
+ name: 'evals-not-found',
93
+ status: EVAL_RESULT.SKIP,
94
+ message: '该技能暂无evals定义',
95
+ duration: 0
96
+ });
97
+ skillResult.duration = Date.now() - start;
98
+ return skillResult;
99
+ }
100
+
101
+ for (const tc of testCases) {
102
+ const caseStart = Date.now();
103
+ try {
104
+ const result = await executeTestCase(tc, target.path);
105
+ const caseDuration = Date.now() - caseStart;
106
+
107
+ skillResult.totalTests++;
108
+ skillResult.cases.push({
109
+ name: tc.name,
110
+ status: result.pass ? EVAL_RESULT.PASS : EVAL_RESULT.FAIL,
111
+ message: result.message,
112
+ details: result.details,
113
+ duration: caseDuration
114
+ });
115
+
116
+ if (result.pass) skillResult.passed++;
117
+ else skillResult.failed++;
118
+
119
+ } catch (e) {
120
+ skillResult.totalTests++;
121
+ skillResult.errors++;
122
+ skillResult.cases.push({
123
+ name: tc.name,
124
+ status: EVAL_RESULT.ERROR,
125
+ message: e.message,
126
+ duration: Date.now() - caseStart
127
+ });
128
+ }
129
+ }
130
+
131
+ skillResult.duration = Date.now() - start;
132
+ return skillResult;
133
+ }
134
+
135
+ function loadTestCases(evalDir) {
136
+ if (!fs.existsSync(evalDir)) return [];
137
+
138
+ const cases = [];
139
+ const files = fs.readdirSync(evalDir).filter(f => f.endsWith('.json'));
140
+
141
+ for (const file of files) {
142
+ try {
143
+ const content = JSON.parse(fs.readFileSync(path.join(evalDir, file), 'utf-8'));
144
+ if (Array.isArray(content)) {
145
+ cases.push(...content.map(c => ({ ...c, _source: file })));
146
+ } else if (content.name && content.expect) {
147
+ cases.push({ ...content, _source: file });
148
+ }
149
+ } catch {}
150
+ }
151
+
152
+ return cases.length > 0 ? cases : getDefaultTestCases();
153
+ }
154
+
155
+ function getDefaultTestCases() {
156
+ return [
157
+ {
158
+ name: 'SKILL.md-exists',
159
+ description: '技能文件存在且格式正确',
160
+ type: 'structure',
161
+ expect: { exists: true, hasDescription: true, hasIronLaw: false },
162
+ check: (skillPath) => {
163
+ const mdPath = path.join(skillPath, 'SKILL.md');
164
+ if (!fs.existsSync(mdPath)) return { pass: false, message: 'SKILL.md不存在' };
165
+ const content = fs.readFileSync(mdPath, 'utf-8');
166
+ const hasDesc = /^description:/m.test(content);
167
+ const hasIronLaw = /##\s*Iron\s*Law/mi.test(content);
168
+ return {
169
+ pass: hasDesc,
170
+ message: hasDesc ? 'description字段存在' : '缺少description字段',
171
+ details: { hasDescription: hasDesc, hasIronLaw: hasIronLaw }
172
+ };
173
+ }
174
+ },
175
+ {
176
+ name: '_meta.json-valid',
177
+ description: '元数据文件格式正确',
178
+ type: 'structure',
179
+ expect: { exists: true, hasTriggers: true },
180
+ check: (skillPath) => {
181
+ const metaPath = path.join(skillPath, '_meta.json');
182
+ if (!fs.existsSync(metaPath)) return { pass: false, message: '_meta.json不存在' };
183
+ try {
184
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
185
+ const valid = meta.name && meta.version;
186
+ const hasTriggers = Array.isArray(meta.triggers) && meta.triggers.length > 0;
187
+ return {
188
+ pass: valid,
189
+ message: valid ? '元数据格式正确' : '元数据缺少必要字段',
190
+ details: { name: !!meta.name, version: !!meta.version, triggersCount: meta.triggers?.length || 0 }
191
+ };
192
+ } catch (e) {
193
+ return { pass: false, message: `JSON解析失败: ${e.message}` };
194
+ }
195
+ }
196
+ },
197
+ {
198
+ name: 'triggers-effective',
199
+ description: '触发词有效且不冲突',
200
+ type: 'cso',
201
+ expect: { minTriggers: 3, maxGeneric: 1 },
202
+ check: (skillPath) => {
203
+ const metaPath = path.join(skillPath, '_meta.json');
204
+ if (!fs.existsSync(metaPath)) return { pass: false, skip: true, message: '无_meta.json' };
205
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
206
+ const triggers = meta.triggers || [];
207
+ const generic = ['help', 'do', 'make', 'create'].filter(t =>
208
+ triggers.includes(t)
209
+ );
210
+ return {
211
+ pass: triggers.length >= 3 && generic.length === 0,
212
+ message: `${triggers.length}个触发词${generic.length > 0 ? `, ${generic.length}个过于通用` : ''}`,
213
+ details: { triggerCount: triggers.length, genericTriggers: generic }
214
+ };
215
+ }
216
+ },
217
+ {
218
+ name: 'behavior-shaping-complete',
219
+ description: '行为塑造章节完整(Iron Law + Rationalization + Red Flags)',
220
+ type: 'quality',
221
+ expect: { ironLaw: true, rationalization: true, redFlags: true },
222
+ check: (skillPath) => {
223
+ const mdPath = path.join(skillPath, 'SKILL.md');
224
+ if (!fs.existsSync(mdPath)) return { pass: false, message: 'SKILL.md不存在' };
225
+ const content = fs.readFileSync(mdPath, 'utf-8');
226
+ const hasIronLaw = /##\s*Iron\s*Law/mi.test(content);
227
+ const hasRationalization = /##\s*Rationalization/mi.test(content);
228
+ const hasRedFlags = /##\s*Red\s*Flags/mi.test(content);
229
+ const allPresent = hasIronLaw && hasRationalization && hasRedFlags;
230
+ return {
231
+ pass: allPresent,
232
+ message: allPresent ? '行为塑造完整' : `缺少: ${[
233
+ !hasIronLaw ? 'Iron Law' : null,
234
+ !hasRationalization ? 'Rationalization Table' : null,
235
+ !hasRedFlags ? 'Red Flags' : null
236
+ ].filter(Boolean).join(', ')}`,
237
+ details: { ironLaw: hasIronLaw, rationalization: hasRationalization, redFlags: hasRedFlags }
238
+ };
239
+ }
240
+ }
241
+ ];
242
+ }
243
+
244
+ async function executeTestCase(tc, skillPath) {
245
+ if (typeof tc.check === 'function') {
246
+ return tc.check(skillPath);
247
+ }
248
+
249
+ if (tc.type === 'structure') {
250
+ return runStructureCheck(tc, skillPath);
251
+ }
252
+
253
+ if (tc.type === 'content') {
254
+ return runContentCheck(tc, skillPath);
255
+ }
256
+
257
+ return { pass: true, message: `未实现的测试类型: ${tc.type}` };
258
+ }
259
+
260
+ function runStructureCheck(tc, skillPath) {
261
+ const checks = tc.checks || {};
262
+ const results = {};
263
+ let allPass = true;
264
+
265
+ for (const [key, expected] of Object.entries(checks)) {
266
+ switch (key) {
267
+ case 'file_exists': {
268
+ const p = path.join(skillPath, expected);
269
+ results[key] = fs.existsSync(p);
270
+ break;
271
+ }
272
+ case 'dir_exists': {
273
+ const d = path.join(skillPath, expected);
274
+ results[key] = fs.existsSync(d) && fs.statSync(d).isDirectory();
275
+ break;
276
+ }
277
+ default:
278
+ results[key] = null;
279
+ }
280
+ if (results[key] === false) allPass = false;
281
+ }
282
+
283
+ return {
284
+ pass: allPass,
285
+ message: allPass ? '结构检查通过' : Object.entries(results).filter(([, v]) => v === false).map(([k]) => `${k}失败`).join(', '),
286
+ details: results
287
+ };
288
+ }
289
+
290
+ function runContentCheck(tc, skillPath) {
291
+ const filePath = tc.file ? path.join(skillPath, tc.file) : path.join(skillPath, 'SKILL.md');
292
+
293
+ if (!fs.existsSync(filePath)) {
294
+ return { pass: false, message: `文件不存在: ${tc.file || 'SKILL.md'}` };
295
+ }
296
+
297
+ const content = fs.readFileSync(filePath, 'utf-8');
298
+ let matchCount = 0;
299
+ let totalPatterns = 0;
300
+
301
+ if (tc.contains) {
302
+ const patterns = Array.isArray(tc.contains) ? tc.contains : [tc.contains];
303
+ totalPatterns = patterns.length;
304
+ for (const pattern of patterns) {
305
+ if (new RegExp(pattern, tc.flags || 'i').test(content)) matchCount++;
306
+ }
307
+ }
308
+
309
+ if (tc.notContains) {
310
+ const npatterns = Array.isArray(tc.notContains) ? tc.notContains : [tc.notContains];
311
+ for (const pattern of npatterns) {
312
+ if (new RegExp(pattern, tc.flags || 'i').test(content)) {
313
+ return { pass: false, message: `不应包含: ${pattern}` };
314
+ }
315
+ }
316
+ }
317
+
318
+ const requiredMatches = tc.minMatches || 1;
319
+ return {
320
+ pass: matchCount >= requiredMatches,
321
+ message: matchCount >= requiredMatches
322
+ ? `内容匹配通过 (${matchCount}/${totalPatterns || '?'})`
323
+ : `匹配不足 (${matchCount}/${requiredMatches})`,
324
+ details: { matchCount, requiredMatches }
325
+ };
326
+ }
327
+
328
+ function printSkillResult(result, verbose) {
329
+ const icon = result.failed === 0 && result.errors === 0
330
+ ? chalk.green('✅')
331
+ : result.errors > 0
332
+ ? chalk.red('💥')
333
+ : chalk.yellow('⚠️');
334
+
335
+ console.log(` ${icon} ${result.skill} (${result.passed}/${result.totalTests}) [${result.duration}ms]`);
336
+
337
+ if (verbose) {
338
+ for (const c of result.cases) {
339
+ const ci = c.status === EVAL_RESULT.PASS ? chalk.green('✓')
340
+ : c.status === EVAL_RESULT.FAIL ? chalk.red('✗')
341
+ : c.status === EVAL_RESULT.ERROR ? chalk.red('💥') : chalk.gray('·');
342
+ console.log(` ${ci} ${c.name}${c.message ? ` — ${c.message}` : ''}`);
343
+ }
344
+ }
345
+ }
346
+
347
+ function printSummary(results) {
348
+ const total = results.reduce((s, r) => s + r.totalTests, 0);
349
+ const passed = results.reduce((s, r) => s + r.passed, 0);
350
+ const failed = results.reduce((s, r) => s + r.failed, 0);
351
+ const errors = results.reduce((s, r) => s + r.errors, 0);
352
+ const skipped = results.reduce((s, r) => s + r.skipped, 0);
353
+ const duration = results.reduce((s, r) => s + r.duration, 0);
354
+
355
+ console.log(chalk.bold('\n━━━ Eval Summary ━━━'));
356
+ console.log(` 总测试: ${total} | 通过: ${chalk.green(passed.toString())} | 失败: ${chalk.red(failed.toString())} | 错误: ${chalk.red(errors.toString())} | 跳过: ${skipped}`);
357
+ console.log(` 总耗时: ${duration}ms`);
358
+ console.log(` 通过率: ${total > 0 ? ((passed / total) * 100).toFixed(1) : 0}%`);
359
+ }
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const SKILLS_ROOT = path.join(__dirname, '..', 'skills');
8
+
9
+ const I18N_RULES = {
10
+ meta: {
11
+ hasZhDescription: { desc: '_meta.json description contains zh field', check: (m) => m.description && (m.description.includes('zh:') || /[\u4e00-\u9fa5]/.test(m.description)) },
12
+ hasEnDescription: { desc: '_meta.json description contains en content', check: (m) => m.description && /[a-zA-Z]{10,}/.test(m.description) },
13
+ minZhTriggers: { desc: 'At least 3 Chinese triggers', minCount: 3, pattern: /[\u4e00-\u9fa5]/ },
14
+ minEnTriggers: { desc: 'At least 3 English triggers', minCount: 3, pattern: /^[a-zA-Z\s-]{3,}$/ }
15
+ },
16
+ skillMd: {
17
+ hasCnSection: { desc: 'SKILL.md contains ## 🇨🇳 section', pattern: /##\s*🇨🇳/ },
18
+ hasEnSection: { desc: 'SKILL.md contains ## 🇺🇸 section', pattern: /##\s*🇺🇸/ },
19
+ hasBilingualIronLaw: { desc: 'Iron Law has bilingual content', pattern: /Iron Law.*铁律|铁律.*Iron Law|🇺🇸/s },
20
+ hasBilingualRedFlags: { desc: 'Red Flags have bilingual content', pattern: /Red Flags.*[红旗警告三层防御]|🇺🇸.*Red Flag|Layer \d:.*Guards/s }
21
+ }
22
+ };
23
+
24
+ export async function analyzeI18N(skillDir) {
25
+ const name = path.basename(skillDir);
26
+ const metaPath = path.join(skillDir, '_meta.json');
27
+ const skillPath = path.join(skillDir, 'SKILL.md');
28
+
29
+ const result = { name, score: 0, maxScore: 100, issues: [], checks: {} };
30
+
31
+ if (!fs.existsSync(metaPath)) { result.issues.push({ rule: 'META-001', msg: '_meta.json missing' }); return result; }
32
+ if (!fs.existsSync(skillPath)) { result.issues.push({ rule: 'SKILL-001', msg: 'SKILL.md missing' }); return result; }
33
+
34
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
35
+ const content = fs.readFileSync(skillPath, 'utf-8');
36
+
37
+ // Meta checks (50 points)
38
+ let metaScore = 0;
39
+ if (I18N_RULES.meta.hasZhDescription.check(meta)) { metaScore += 15; result.checks.zhDesc = true; } else { result.issues.push({ rule: 'META-ZH', msg: 'No Chinese in description' }); }
40
+ if (I18N_RULES.meta.hasEnDescription.check(meta)) { metaScore += 15; result.checks.enDesc = true; } else { result.issues.push({ rule: 'META-EN', msg: 'No English in description' }); }
41
+
42
+ const zhTriggers = (meta.triggers || []).filter(t => I18N_RULES.meta.minZhTriggers.pattern.test(t)).length;
43
+ const enTriggers = (meta.triggers || []).filter(t => I18N_RULES.meta.minEnTriggers.pattern.test(t)).length;
44
+ if (zhTriggers >= 3) { metaScore += 10; result.checks.zhTriggers = zhTriggers; } else { result.issues.push({ rule: 'META-TZ', msg: `Only ${zhTriggers} Chinese triggers (need >=3)` }); }
45
+ if (enTriggers >= 3) { metaScore += 10; result.checks.enTriggers = enTriggers; } else { result.issues.push({ rule: 'META-TE', msg: `Only ${enTriggers} English triggers (need >=3)` }); }
46
+ result.metaScore = metaScore;
47
+ result.score += metaScore;
48
+
49
+ // SKILL.md checks (50 points)
50
+ let mdScore = 0;
51
+ if (I18N_RULES.skillMd.hasCnSection.pattern.test(content)) { mdScore += 15; result.checks.cnSection = true; } else { result.issues.push({ rule: 'MD-CN', msg: 'Missing ## 🇨🇳 section' }); }
52
+ if (I18N_RULES.skillMd.hasEnSection.pattern.test(content)) { mdScore += 15; result.checks.enSection = true; } else { result.issues.push({ rule: 'MD-EN', msg: 'Missing ## 🇺🇸 section' }); }
53
+ if (I18N_RULES.skillMd.hasBilingualIronLaw.pattern.test(content)) { mdScore += 10; result.checks.bilingualIL = true; } else { result.issues.push({ rule: 'MD-IL', msg: 'Iron Law not bilingual' }); }
54
+ if (I18N_RULES.skillMd.hasBilingualRedFlags.pattern.test(content)) { mdScore += 10; result.checks.bilingualRF = true; } else { result.issues.push({ rule: 'MD-RF', msg: 'Red Flags not bilingual' }); }
55
+ result.mdScore = mdScore;
56
+ result.score += mdScore;
57
+
58
+ // Grade
59
+ if (result.score >= 90) result.grade = 'A';
60
+ else if (result.score >= 70) result.grade = 'B';
61
+ else if (result.score >= 50) result.grade = 'C';
62
+ else result.grade = 'D';
63
+
64
+ return result;
65
+ }
66
+
67
+ export async function analyzeAllI18N(category) {
68
+ const categories = category ? [category] : ['core', 'expert', 'entropy', 'pr', 'openspec'];
69
+ const results = [];
70
+ for (const cat of categories) {
71
+ const catDir = path.join(SKILLS_ROOT, cat);
72
+ if (!fs.existsSync(catDir)) continue;
73
+ const skills = fs.readdirSync(catDir).filter(f => fs.statSync(path.join(catDir, f)).isDirectory());
74
+ for (const skill of skills) {
75
+ const r = await analyzeI18N(path.join(catDir, skill));
76
+ r.category = cat;
77
+ results.push(r);
78
+ }
79
+ }
80
+ return results;
81
+ }
82
+
83
+ export function printI18NReport(results) {
84
+ console.log('\n━━━ i18n Bilingual Report ━━━');
85
+ console.log(` Total Skills: ${results.length}`);
86
+ const avg = results.reduce((s, r) => s + r.score, 0) / results.length;
87
+ console.log(` Average Score: ${avg.toFixed(1)}/100`);
88
+ const grades = { A: 0, B: 0, C: 0, D: 0 };
89
+ results.forEach(r => grades[r.grade]++);
90
+ console.log(` Grades: A:${grades.A} B:${grades.B} C:${grades.C} D:${grades.D}`);
91
+ console.log('\n' + '─'.repeat(65));
92
+ console.log(` ${'Skill'.padEnd(30)} ${'Score'.padStart(6)} ${'Grade'.padStart(6)} Issues`);
93
+ console.log('─'.repeat(65));
94
+ for (const r of results.sort((a, b) => b.score - a.score)) {
95
+ const issues = r.issues.length > 0 ? ` (${r.issues.length})` : '';
96
+ console.log(`${r.name.padEnd(30)} ${String(r.score).padStart(6)} ${r.grade.padStart(6)}${issues}`);
97
+ if (r.issues.length > 0) {
98
+ for (const issue of r.issues) {
99
+ console.log(` ⚠ ${issue.rule}: ${issue.msg}`);
100
+ }
101
+ }
102
+ }
103
+ if (results.some(r => r.issues.length > 0)) {
104
+ console.log('\n Main Issues:');
105
+ const issueCounts = {};
106
+ results.forEach(r => r.issues.forEach(i => { issueCounts[i.rule] = (issueCounts[i.rule] || 0) + 1; }));
107
+ Object.entries(issueCounts).sort((a, b) => b[1] - a[1]).forEach(([k, v]) => console.log(` ${k}: ${v}x`));
108
+ }
109
+ }