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
@@ -0,0 +1,988 @@
1
+ /**
2
+ * PDD Visual Manager - 项目扫描引擎
3
+ *
4
+ * 扫描项目目录结构,发现和解析各种制品文件:
5
+ * - dev-specs/ 目录:规格文件 (.md, .json)
6
+ * - src/ 或 outputDir/:源代码文件
7
+ * - reports/:验证报告
8
+ * - PRD 文件:.prdx 格式
9
+ *
10
+ * 根据扫描结果推断功能点的当前阶段
11
+ *
12
+ * @module vm/scanner
13
+ */
14
+
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import { fileURLToPath } from 'url';
18
+ import crypto from 'crypto';
19
+
20
+ import { StageEnum, STAGE_VALUES, STAGE_ORDER, Priority, ArtifactType } from './models.js';
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = path.dirname(__filename);
24
+
25
+ // 可选的 chalk 彩色输出支持
26
+ let chalk;
27
+ try {
28
+ const chalkModule = await import('chalk');
29
+ chalk = chalkModule.default;
30
+ } catch {
31
+ chalk = {
32
+ cyan: (s) => s,
33
+ green: (s) => s,
34
+ yellow: (s) => s,
35
+ red: (s) => s,
36
+ blue: (s) => s,
37
+ gray: (s) => s,
38
+ bold: (s) => s
39
+ };
40
+ }
41
+
42
+ /**
43
+ * 默认扫描配置
44
+ * @type {Object}
45
+ */
46
+ const DEFAULT_CONFIG = {
47
+ specDir: 'dev-specs',
48
+ codeDir: './src',
49
+ outputDir: './generated',
50
+ reportDir: './reports',
51
+ prdFile: null, // 自动查找
52
+ supportedSpecExtensions: ['.md', '.json', '.yaml', '.yml'],
53
+ supportedCodeExtensions: [
54
+ '.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte',
55
+ '.py', '.java', '.go', '.rs', '.c', '.cpp', '.h',
56
+ '.sql', '.html', '.css', '.scss', '.less'
57
+ ],
58
+ maxDepth: 10,
59
+ ignorePatterns: ['node_modules', '.git', '__pycache__', 'dist', 'build', '.cache']
60
+ };
61
+
62
+ /**
63
+ * 规格扫描结果
64
+ * @typedef {Object} SpecScanResult
65
+ * @property {string} dir - 扫描的目录
66
+ * @property {boolean} exists - 目录是否存在
67
+ * @property {Array<SpecFile>} files - 发现的规格文件列表
68
+ * @property {number} totalCount - 文件总数
69
+ * @property {number} totalSize - 总大小(字节)
70
+ */
71
+
72
+ /**
73
+ * 规格文件信息
74
+ * @typedef {Object} SpecFile
75
+ * @property {string} path - 文件相对路径
76
+ * @property {string} absolutePath - 绝对路径
77
+ * @property {string} extension - 文件扩展名
78
+ * @property {number} size - 文件大小(字节)
79
+ * @property {Date} lastModified - 最后修改时间
80
+ * @property {Object|null} parsed - 解析后的内容
81
+ * @property {string|null} featureId - 关联的功能点 ID
82
+ * @property {string|null} featureName - 功能点名称
83
+ * @property {string|null} priority - 优先级
84
+ * @property {string[]} acceptanceCriteria - 验收标准
85
+ */
86
+
87
+ /**
88
+ * 源代码扫描结果
89
+ * @typedef {Object} SourceCodeScanResult
90
+ * @property {string} dir - 扫描的目录
91
+ * @property {boolean} exists - 目录是否存在
92
+ * @property {number} fileCount - 文件数
93
+ * @property {number} totalLOC - 总代码行数
94
+ * @property {Object<string,number>} languageDistribution - 语言分布统计
95
+ * @property {Array<CodeFile>} files - 文件列表
96
+ * @property {Date} lastModified - 最后修改时间
97
+ */
98
+
99
+ /**
100
+ * 代码文件信息
101
+ * @typedef {Object} CodeFile
102
+ * @property {string} path - 文件相对路径
103
+ * @property {string} absolutePath - 绝对路径
104
+ * @property {string} language - 编程语言
105
+ * @property {number} lines - 代码行数
106
+ * @property {number} size - 文件大小(字节)
107
+ * @property {Date} lastModified - 最后修改时间
108
+ * @property {string|null} featureId - 可能关联的功能点 ID
109
+ */
110
+
111
+ /**
112
+ * 报告扫描结果
113
+ * @typedef {Object} ReportScanResult
114
+ * @property {string} dir - 报告目录
115
+ * @property {boolean} exists - 是否存在
116
+ * @property {Array<ReportFile>} reports - 报告文件列表
117
+ * @property {Object} summary - 汇总统计
118
+ */
119
+
120
+ /**
121
+ * PRD 扫描结果
122
+ * @typedef {Object} PRDScanResult
123
+ * @property {string|null} path - PRD 文件路径
124
+ * @property {boolean} found - 是否找到
125
+ * @property {Object|null} content - 解析内容
126
+ * @property {string} projectName - 项目名称
127
+ * @property {Array<Object>} features - 功能点摘要列表
128
+ */
129
+
130
+ /**
131
+ * 项目扫描器类
132
+ * 负责扫描项目目录,收集制品信息和元数据
133
+ */
134
+ export class ProjectScanner {
135
+ /**
136
+ * 创建项目扫描器实例
137
+ * @param {string} projectRoot - 项目根目录
138
+ * @param {Object} [config={}] - 扫描配置
139
+ */
140
+ constructor(projectRoot, config = {}) {
141
+ if (!projectRoot || typeof projectRoot !== 'string') {
142
+ throw new Error('ProjectScanner: projectRoot 必须是非空字符串');
143
+ }
144
+
145
+ /** @type {string} 项目根目录绝对路径 */
146
+ this.projectRoot = path.resolve(projectRoot);
147
+
148
+ /** @type {Object} 合并后的配置 */
149
+ this.config = { ...DEFAULT_CONFIG, ...config };
150
+
151
+ /** @type {Map<string, Object>} 缓存的扫描结果 */
152
+ this._cache = new Map();
153
+
154
+ /** @type {number} 缓存过期时间(毫秒) */
155
+ this._cacheTTL = config.cacheTTL || 30000; // 默认30秒
156
+
157
+ console.log(chalk.gray(`[Scanner] 初始化项目扫描器: ${this.projectRoot}`));
158
+ }
159
+
160
+ /**
161
+ * 检查路径是否应被忽略
162
+ * @param {string} filePath - 文件路径
163
+ * @returns {boolean} 是否忽略
164
+ * @private
165
+ */
166
+ _shouldIgnore(filePath) {
167
+ const normalized = filePath.replace(/\\/g, '/');
168
+ return this.config.ignorePatterns.some(pattern =>
169
+ normalized.includes(pattern) || normalized.endsWith(`/${pattern}`)
170
+ );
171
+ }
172
+
173
+ /**
174
+ * 根据扩展名检测编程语言
175
+ * @param {string} ext - 文件扩展名
176
+ * @returns {string} 语言名称
177
+ * @private
178
+ */
179
+ _detectLanguage(ext) {
180
+ const langMap = {
181
+ '.js': 'JavaScript', '.jsx': 'JavaScript (JSX)',
182
+ '.ts': 'TypeScript', '.tsx': 'TypeScript (TSX)',
183
+ '.vue': 'Vue', '.svelte': 'Svelte',
184
+ '.py': 'Python', '.java': 'Java',
185
+ '.go': 'Go', '.rs': 'Rust',
186
+ '.c': 'C', '.cpp': 'C++', '.h': 'C/C++ Header',
187
+ '.sql': 'SQL', '.html': 'HTML',
188
+ '.css': 'CSS', '.scss': 'SCSS', '.less': 'Less'
189
+ };
190
+ return langMap[ext.toLowerCase()] || 'Unknown';
191
+ }
192
+
193
+ /**
194
+ * 计算文件行数(非空行)
195
+ * @param {string} content - 文件内容
196
+ * @returns {{total:number, code:number, blank:number, comment:number}} 行数统计
197
+ * @private
198
+ */
199
+ _countLines(content) {
200
+ const lines = content.split('\n');
201
+ let codeLines = 0;
202
+ let blankLines = 0;
203
+ let commentLines = 0;
204
+
205
+ for (const line of lines) {
206
+ const trimmed = line.trim();
207
+ if (trimmed === '') {
208
+ blankLines++;
209
+ } else if (trimmed.startsWith('//') || trimmed.startsWith('#') ||
210
+ trimmed.startsWith('/*') || trimmed.startsWith('*') ||
211
+ trimmed.startsWith('--') || trimmed.startsWith(';')) {
212
+ commentLines++;
213
+ } else {
214
+ codeLines++;
215
+ }
216
+ }
217
+
218
+ return {
219
+ total: lines.length,
220
+ code: codeLines,
221
+ blank: blankLines,
222
+ comment: commentLines
223
+ };
224
+ }
225
+
226
+ /**
227
+ * 解析规格文件内容
228
+ * 提取功能点名称、描述、优先级、验收标准等信息
229
+ *
230
+ * @param {string} filePath - 文件路径
231
+ * @param {string} content - 文件内容
232
+ * @returns {Object} 解析结果
233
+ * @private
234
+ */
235
+ _parseSpecContent(filePath, content) {
236
+ const ext = path.extname(filePath).toLowerCase();
237
+ const result = {
238
+ featureId: null,
239
+ featureName: null,
240
+ description: '',
241
+ priority: null,
242
+ acceptanceCriteria: [],
243
+ stage: null,
244
+ rawContent: content.substring(0, 500) // 保存前500字符用于预览
245
+ };
246
+
247
+ try {
248
+ if (ext === '.json') {
249
+ // JSON 格式规格
250
+ const json = JSON.parse(content);
251
+ result.featureId = json.id || json.featureId || path.basename(filePath, ext);
252
+ result.featureName = json.name || json.title || json.featureName;
253
+ result.description = json.description || json.desc || '';
254
+ result.priority = json.priority || null;
255
+ result.acceptanceCriteria = Array.isArray(json.acceptanceCriteria)
256
+ ? json.acceptanceCriteria
257
+ : (Array.isArray(json.criteria) ? json.criteria : []);
258
+ result.stage = json.stage || null;
259
+ } else if (ext === '.md') {
260
+ // Markdown 格式规格
261
+ // 提取标题(第一个 # 开头的行)
262
+ const titleMatch = content.match(/^#\s+(.+)$/m);
263
+ if (titleMatch) {
264
+ result.featureName = titleMatch[1].trim();
265
+ }
266
+
267
+ // 提取 ID(通常在标题后或 frontmatter 中)
268
+ const idMatch = content.match(/(?:ID|Feature[- ]?ID|id)[:\s]*([a-zA-Z0-9_-]+)/i);
269
+ if (idMatch) {
270
+ result.featureId = idMatch[1].trim();
271
+ }
272
+
273
+ // 提取优先级
274
+ const priorityMatch = content.match(/(?:Priority|优先级)[:\s]*(P[0-3])/i);
275
+ if (priorityMatch) {
276
+ result.priority = priorityMatch[1];
277
+ }
278
+
279
+ // 提取验收标准(通常是 ## Acceptance Criteria 或 ## 验收标准 下的列表)
280
+ const criteriaSection = content.match(
281
+ /(?:##?\s*(?:Acceptance\s*Criteria|验收标准|AC)[\s\S]*?)(?=##|\Z)/i
282
+ );
283
+ if (criteriaSection) {
284
+ const items = criteriaSection[1].match(/^\s*[-*+]\s+.+$/gm);
285
+ if (items) {
286
+ result.acceptanceCriteria = items.map(item =>
287
+ item.replace(/^\s*[-*+]\s*/, '').trim()
288
+ );
289
+ }
290
+ }
291
+
292
+ // 提取描述(标题后的第一段)
293
+ const descMatch = content.match(/^#\s+.+\n\n([\s\S]+?)(?:\n##|\n---|\Z)/);
294
+ if (descMatch) {
295
+ result.description = descMatch[1].trim().substring(0, 1000);
296
+ }
297
+
298
+ // 如果没有提取到 ID,使用文件名
299
+ if (!result.featureId) {
300
+ result.featureId = path.basename(filePath, ext);
301
+ }
302
+ } else if (ext === '.yaml' || ext === '.yml') {
303
+ // YAML 格式(简化处理)
304
+ const idMatch = content.match(/id[:\s]+([a-zA-Z0-9_-]+)/i);
305
+ const nameMatch = content.match(/name[:\s]+["']?(.+?)["']?\s*$/m);
306
+ const priorityMatch = content.match(/priority[:\s]*(P[0-3])/i);
307
+
308
+ result.featureId = idMatch ? idMatch[1] : path.basename(filePath, ext);
309
+ result.featureName = nameMatch ? nameMatch[1].trim() : null;
310
+ result.priority = priorityMatch ? priorityMatch[1] : null;
311
+ }
312
+ } catch (error) {
313
+ console.warn(chalk.yellow(`[Scanner] 解析规格文件失败 ${filePath}: ${error.message}`));
314
+ }
315
+
316
+ return result;
317
+ }
318
+
319
+ /**
320
+ * 扫描规格文件目录
321
+ * 默认扫描 dev-specs/ 目录,解析 .md/.json/.yaml 规格文件
322
+ *
323
+ * @param {string} [dir] - 要扫描的目录(默认使用配置中的 specDir)
324
+ * @returns {Promise<SpecScanResult>} 扫描结果
325
+ */
326
+ async scanSpecs(dir) {
327
+ const specDir = dir || this.config.specDir;
328
+ const absDir = path.isAbsolute(specDir) ? specDir : path.join(this.projectRoot, specDir);
329
+
330
+ const result = {
331
+ dir: specDir,
332
+ exists: false,
333
+ files: [],
334
+ totalCount: 0,
335
+ totalSize: 0
336
+ };
337
+
338
+ try {
339
+ // 检查目录是否存在
340
+ if (!fs.existsSync(absDir)) {
341
+ console.log(chalk.yellow(`[Scanner] 规格目录不存在: ${absDir}`));
342
+ return result;
343
+ }
344
+
345
+ result.exists = true;
346
+
347
+ // 递归扫描目录
348
+ const scanDir = async (currentDir, depth = 0) => {
349
+ if (depth > this.config.maxDepth) return;
350
+
351
+ const entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
352
+
353
+ for (const entry of entries) {
354
+ const fullPath = path.join(currentDir, entry.name);
355
+ const relPath = path.relative(absDir, fullPath);
356
+
357
+ if (entry.isDirectory()) {
358
+ if (!this._shouldIgnore(relPath)) {
359
+ await scanDir(fullPath, depth + 1);
360
+ }
361
+ } else if (entry.isFile()) {
362
+ const ext = path.extname(entry.name).toLowerCase();
363
+
364
+ if (this.config.supportedSpecExtensions.includes(ext)) {
365
+ try {
366
+ const stat = await fs.promises.stat(fullPath);
367
+ const content = await fs.promises.readFile(fullPath, 'utf-8');
368
+
369
+ const fileInfo = {
370
+ path: relPath,
371
+ absolutePath: fullPath,
372
+ extension: ext,
373
+ size: stat.size,
374
+ lastModified: stat.mtime,
375
+ parsed: this._parseSpecContent(fullPath, content),
376
+ checksum: crypto.createHash('sha256').update(content).digest('hex').substring(0, 16)
377
+ };
378
+
379
+ result.files.push(fileInfo);
380
+ result.totalCount++;
381
+ result.totalSize += stat.size;
382
+ } catch (fileError) {
383
+ console.warn(chalk.yellow(`[Scanner] 读取规格文件失败: ${relPath} - ${fileError.message}`));
384
+ }
385
+ }
386
+ }
387
+ }
388
+ };
389
+
390
+ await scanDir(absDir);
391
+
392
+ console.log(chalk.green(
393
+ `[Scanner] 规格扫描完成: ${result.totalCount} 个文件, ${(result.totalSize / 1024).toFixed(1)} KB`
394
+ ));
395
+
396
+ } catch (error) {
397
+ console.error(chalk.red(`[Scanner] 规格扫描错误: ${error.message}`));
398
+ }
399
+
400
+ return result;
401
+ }
402
+
403
+ /**
404
+ * 扫描源代码目录
405
+ * 默认扫描 src/ 或 outputDir/
406
+ * 统计文件数、总 LOC、语言分布等
407
+ *
408
+ * @param {string} [dir] - 要扫描的目录
409
+ * @returns {Promise<SourceCodeScanResult>} 扫描结果
410
+ */
411
+ async scanSourceCode(dir) {
412
+ // 尝试多个可能的代码目录
413
+ const dirsToCheck = dir ? [dir] : [
414
+ this.config.codeDir,
415
+ this.config.outputDir,
416
+ './src',
417
+ './lib',
418
+ './generated'
419
+ ];
420
+
421
+ let targetDir = null;
422
+ for (const d of dirsToCheck) {
423
+ const absPath = path.isAbsolute(d) ? d : path.join(this.projectRoot, d);
424
+ if (fs.existsSync(absPath)) {
425
+ targetDir = absPath;
426
+ break;
427
+ }
428
+ }
429
+
430
+ const result = {
431
+ dir: dir || targetDir ? path.relative(this.projectRoot, targetDir) : '',
432
+ exists: !!targetDir,
433
+ fileCount: 0,
434
+ totalLOC: 0,
435
+ languageDistribution: {},
436
+ files: [],
437
+ lastModified: null
438
+ };
439
+
440
+ if (!targetDir) {
441
+ console.log(chalk.yellow('[Scanner] 未找到源代码目录'));
442
+ return result;
443
+ }
444
+
445
+ try {
446
+ let latestMtime = 0;
447
+
448
+ const scanDir = async (currentDir, depth = 0) => {
449
+ if (depth > this.config.maxDepth) return;
450
+
451
+ const entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
452
+
453
+ for (const entry of entries) {
454
+ const fullPath = path.join(currentDir, entry.name);
455
+ const relPath = path.relative(targetDir, fullPath);
456
+
457
+ if (entry.isDirectory()) {
458
+ if (!this._shouldIgnore(relPath)) {
459
+ await scanDir(fullPath, depth + 1);
460
+ }
461
+ } else if (entry.isFile()) {
462
+ const ext = path.extname(entry.name).toLowerCase();
463
+
464
+ if (this.config.supportedCodeExtensions.includes(ext)) {
465
+ try {
466
+ const stat = await fs.promises.stat(fullPath);
467
+ const content = await fs.promises.readFile(fullPath, 'utf-8');
468
+ const lineStats = this._countLines(content);
469
+ const language = this._detectLanguage(ext);
470
+
471
+ // 更新最新修改时间
472
+ if (stat.mtimeMs > latestMtime) {
473
+ latestMtime = stat.mtimeMs;
474
+ }
475
+
476
+ // 从文件路径推断可能的功能点 ID
477
+ const possibleFeatureId = this._inferFeatureIdFromPath(relPath);
478
+
479
+ const fileData = {
480
+ path: relPath,
481
+ absolutePath: fullPath,
482
+ language,
483
+ lines: lineStats.code,
484
+ totalLines: lineStats.total,
485
+ size: stat.size,
486
+ lastModified: stat.mtime,
487
+ featureId: possibleFeatureId
488
+ };
489
+
490
+ result.files.push(fileData);
491
+ result.fileCount++;
492
+ result.totalLOC += lineStats.code;
493
+
494
+ // 统计语言分布
495
+ if (!result.languageDistribution[language]) {
496
+ result.languageDistribution[language] = { count: 0, loc: 0, size: 0 };
497
+ }
498
+ result.languageDistribution[language].count++;
499
+ result.languageDistribution[language].loc += lineStats.code;
500
+ result.languageDistribution[language].size += stat.size;
501
+ } catch (fileError) {
502
+ console.warn(chalk.yellow(`[Scanner] 读取代码文件失败: ${relPath}`));
503
+ }
504
+ }
505
+ }
506
+ }
507
+ };
508
+
509
+ await scanDir(targetDir);
510
+
511
+ result.lastModified = latestMtime > 0 ? new Date(latestMtime) : null;
512
+
513
+ console.log(chalk.green(
514
+ `[Scanner] 代码扫描完成: ${result.fileCount} 个文件, ${result.totalLOC} LOC`
515
+ ));
516
+
517
+ } catch (error) {
518
+ console.error(chalk.red(`[Scanner] 代码扫描错误: ${error.message}`));
519
+ }
520
+
521
+ return result;
522
+ }
523
+
524
+ /**
525
+ * 从文件路径推断功能点 ID
526
+ * @param {string} filePath - 文件相对路径
527
+ * @returns {string|null} 推断的功能点 ID
528
+ * @private
529
+ */
530
+ _inferFeatureIdFromPath(filePath) {
531
+ // 常见的命名模式:
532
+ // - feat-xxx/file.js -> feat-xxx
533
+ // - xxx/feature-name.js -> feature-name
534
+ // - features/feat-001-xxx/index.js -> feat-001
535
+
536
+ const parts = filePath.split(/[/\\]/);
537
+ const fileName = parts[parts.length - 1];
538
+
539
+ // 检查是否匹配 feat-xxx 格式
540
+ const featMatch = parts.find(p => /^feat-[a-zA-Z0-9_-]+$/.test(p));
541
+ if (featMatch) return featMatch;
542
+
543
+ // 检查父目录名
544
+ if (parts.length > 1 && /^[a-z][a-z0-9-]*$/.test(parts[0])) {
545
+ return parts[0];
546
+ }
547
+
548
+ return null;
549
+ }
550
+
551
+ /**
552
+ * 扫描报告目录
553
+ * 默认扫描 reports/ 目录
554
+ * 解析验证报告 JSON、覆盖率报告等
555
+ *
556
+ * @param {string} [dir] - 报告目录
557
+ * @returns {Promise<ReportScanResult>} 扫描结果
558
+ */
559
+ async scanReports(dir) {
560
+ const reportDir = dir || this.config.reportDir;
561
+ const absDir = path.isAbsolute(reportDir) ? reportDir : path.join(this.projectRoot, reportDir);
562
+
563
+ const result = {
564
+ dir: reportDir,
565
+ exists: false,
566
+ reports: [],
567
+ summary: {
568
+ totalReports: 0,
569
+ verificationReports: [],
570
+ coverageReports: [],
571
+ otherReports: []
572
+ }
573
+ };
574
+
575
+ try {
576
+ if (!fs.existsSync(absDir)) {
577
+ console.log(chalk.yellow(`[Scanner] 报告目录不存在: ${absDir}`));
578
+ return result;
579
+ }
580
+
581
+ result.exists = true;
582
+ const entries = await fs.promises.readdir(absDir, { withFileTypes: true });
583
+
584
+ for (const entry of entries) {
585
+ if (!entry.isFile()) continue;
586
+
587
+ const filePath = path.join(absDir, entry.name);
588
+ const ext = path.extname(entry.name).toLowerCase();
589
+ const stat = await fs.promises.stat(filePath);
590
+
591
+ const reportInfo = {
592
+ name: entry.name,
593
+ path: path.relative(this.projectRoot, filePath),
594
+ absolutePath: filePath,
595
+ type: this._classifyReport(entry.name),
596
+ size: stat.size,
597
+ lastModified: stat.mtime,
598
+ parsed: null
599
+ };
600
+
601
+ // 尝试解析 JSON 报告
602
+ if (ext === '.json') {
603
+ try {
604
+ const content = await fs.promises.readFile(filePath, 'utf-8');
605
+ reportInfo.parsed = JSON.parse(content);
606
+ } catch (e) {
607
+ // 忽略解析错误
608
+ }
609
+ }
610
+
611
+ result.reports.push(reportInfo);
612
+ result.summary.totalReports++;
613
+
614
+ // 分类统计
615
+ switch (reportInfo.type) {
616
+ case 'verification':
617
+ result.summary.verificationReports.push(reportInfo);
618
+ break;
619
+ case 'coverage':
620
+ result.summary.coverageReports.push(reportInfo);
621
+ break;
622
+ default:
623
+ result.summary.otherReports.push(reportInfo);
624
+ }
625
+ }
626
+
627
+ console.log(chalk.green(
628
+ `[Scanner] 报告扫描完成: ${result.summary.totalReports} 个报告`
629
+ ));
630
+
631
+ } catch (error) {
632
+ console.error(chalk.red(`[Scanner] 报告扫描错误: ${error.message}`));
633
+ }
634
+
635
+ return result;
636
+ }
637
+
638
+ /**
639
+ * 分类报告类型
640
+ * @param {string} fileName - 文件名
641
+ * @returns {string} 报告类型
642
+ * @private
643
+ */
644
+ _classifyReport(fileName) {
645
+ const lower = fileName.toLowerCase();
646
+
647
+ if (lower.includes('verify') || lower.includes('validation') || lower.includes('check')) {
648
+ return 'verification';
649
+ }
650
+ if (lower.includes('coverage') || lower.includes('cov') || lower.includes('test-result')) {
651
+ return 'coverage';
652
+ }
653
+ if (lower.includes('quality') || lower.includes('lint') || lower.includes('audit')) {
654
+ return 'quality';
655
+ }
656
+
657
+ return 'other';
658
+ }
659
+
660
+ /**
661
+ * 扫描 PRD 文件
662
+ * 支持 .prdx 和 .prd 格式
663
+ * 提取项目名、功能点列表摘要
664
+ *
665
+ * @param {string} [prdPath] - PRD 文件路径
666
+ * @returns {Promise<PRDScanResult>} 扫描结果
667
+ */
668
+ async scanPRD(prdPath) {
669
+ // 可能的 PRD 文件位置
670
+ const candidates = prdPath ? [prdPath] : [
671
+ 'PRD.prdx',
672
+ 'prd.prdx',
673
+ 'docs/PRD.prdx',
674
+ 'docs/prd.prdx',
675
+ '.pdd-prd.json',
676
+ 'config/prd.json'
677
+ ];
678
+
679
+ let foundPath = null;
680
+
681
+ for (const candidate of candidates) {
682
+ const absPath = path.isAbsolute(candidate) ? candidate : path.join(this.projectRoot, candidate);
683
+ if (fs.existsSync(absPath)) {
684
+ foundPath = absPath;
685
+ break;
686
+ }
687
+ }
688
+
689
+ const result = {
690
+ path: foundPath ? path.relative(this.projectRoot, foundPath) : null,
691
+ found: !!foundPath,
692
+ content: null,
693
+ projectName: '',
694
+ features: []
695
+ };
696
+
697
+ if (!foundPath) {
698
+ console.log(chalk.yellow('[Scanner] 未找到 PRD 文件'));
699
+ return result;
700
+ }
701
+
702
+ try {
703
+ const content = await fs.promises.readFile(foundPath, 'utf-8');
704
+ const ext = path.extname(foundPath).toLowerCase();
705
+
706
+ if (ext === '.json' || ext === '.prdx') {
707
+ // JSON 格式 PRD
708
+ try {
709
+ const prd = JSON.parse(content);
710
+ result.content = prd;
711
+ result.projectName = prd.project?.name || prd.name || prd.projectName || '';
712
+ result.features = Array.isArray(prd.features)
713
+ ? prd.features.map(f => ({
714
+ id: f.id || f.featureId,
715
+ name: f.name || f.title,
716
+ description: f.description || f.desc?.substring(0, 200),
717
+ priority: f.priority || 'P2',
718
+ status: f.status || f.stage || 'prd'
719
+ }))
720
+ : [];
721
+ } catch (parseError) {
722
+ console.warn(chalk.yellow(`[Scanner] PRD JSON 解析失败: ${parseError.message}`));
723
+ }
724
+ } else if (ext === '.md' || ext === '.prd') {
725
+ // Markdown 格式 PRD
726
+ result.content = { raw: content };
727
+
728
+ // 提取项目名
729
+ const titleMatch = content.match(/^#\s+(.+)$/m);
730
+ if (titleMatch) {
731
+ result.projectName = titleMatch[1].trim();
732
+ }
733
+
734
+ // 提取功能点列表(通常是 ## Features 或 ## 功能点 部分)
735
+ const featuresSection = content.match(
736
+ /(?:##\s*(?:Features|功能点|Feature\s*List)[\s\S]*?)(?=##[^#]|\Z)/i
737
+ );
738
+
739
+ if (featuresSection) {
740
+ // 匹配功能点条目
741
+ const featureItems = featuresSection[0].matchAll(
742
+ /^\s*(?:[-*+]|\d+[.)])\s+(.+?)(?:\n|$)/gm
743
+ );
744
+
745
+ for (const match of featureItems) {
746
+ const text = match[1].trim();
747
+ // 尝试从文本中解析 ID 和名称
748
+ const idMatch = text.match(/\(([a-zA-Z0-9_-]+)\)|\[([a-zA-Z0-9_-]+)\]/);
749
+ const name = text.replace(/\([^)]*\)|\[[^\]]*\]/g, '').trim();
750
+
751
+ result.features.push({
752
+ id: idMatch ? (idMatch[1] || idMatch[2]) : `feature-${result.features.length + 1}`,
753
+ name: name || text,
754
+ description: '',
755
+ priority: 'P2',
756
+ status: 'prd'
757
+ });
758
+ }
759
+ }
760
+ }
761
+
762
+ console.log(chalk.green(
763
+ `[Scanner] PRD 扫描完成: ${result.projectName || '(未命名)'}, ${result.features.length} 个功能点`
764
+ ));
765
+
766
+ } catch (error) {
767
+ console.error(chalk.red(`[Scanner] PRD 扫描错误: ${error.message}`));
768
+ }
769
+
770
+ return result;
771
+ }
772
+
773
+ /**
774
+ * 推断指定功能点的当前阶段
775
+ * 根据制品存在情况推断开发阶段
776
+ *
777
+ * 推断规则:
778
+ * - 有 PRD 无 spec → prd
779
+ * - 有 spec 无 code → extracted/spec
780
+ * - 有 code 在生成中 → implementing
781
+ * - 有验证报告 → verifying/done
782
+ *
783
+ * @param {string} featureId - 功能点 ID
784
+ * @returns {Promise<string>} 推断的阶段值 (StageEnum)
785
+ */
786
+ async inferFeatureStage(featureId) {
787
+ if (!featureId) {
788
+ return StageEnum.PRD;
789
+ }
790
+
791
+ // 收集各类型的制品存在情况
792
+ const artifacts = {
793
+ hasPRD: false,
794
+ hasSpec: false,
795
+ hasCode: false,
796
+ hasTest: false,
797
+ hasReport: false,
798
+ isImplementing: false
799
+ };
800
+
801
+ try {
802
+ // 1. 检查是否有对应的规格文件
803
+ const specs = await this.scanSpecs();
804
+ artifacts.hasSpec = specs.files.some(f =>
805
+ f.parsed?.featureId === featureId ||
806
+ f.path.toLowerCase().includes(featureId.toLowerCase())
807
+ );
808
+
809
+ // 2. 检查是否有对应的代码文件
810
+ const sourceCode = await this.scanSourceCode();
811
+ artifacts.hasCode = sourceCode.files.some(f =>
812
+ f.featureId === featureId ||
813
+ f.path.toLowerCase().includes(featureId.toLowerCase())
814
+ );
815
+
816
+ // 检查是否有正在生成的标记
817
+ artifacts.isImplementing = sourceCode.files.some(f => {
818
+ // 查找 .generating 或 .tmp 文件
819
+ return f.path.includes('.generating') || f.path.includes('.tmp');
820
+ }) || this._hasGeneratingMarker(featureId);
821
+
822
+ // 3. 检查是否有测试文件
823
+ artifacts.hasTest = sourceCode.files.some(f =>
824
+ (f.path.includes('.test.') || f.path.includes('.spec.') || f.path.includes('__tests__')) &&
825
+ (f.featureId === featureId || f.path.toLowerCase().includes(featureId.toLowerCase()))
826
+ );
827
+
828
+ // 4. 检查是否有验证报告
829
+ const reports = await this.scanReports();
830
+ artifacts.hasReport = reports.reports.some(r => {
831
+ if (r.parsed && typeof r.parsed === 'object') {
832
+ // 检查报告中是否包含该功能点的信息
833
+ const strContent = JSON.stringify(r.parsed);
834
+ return strContent.includes(featureId);
835
+ }
836
+ return r.path.toLowerCase().includes(featureId.toLowerCase());
837
+ });
838
+
839
+ // 5. 检查 PRD 中是否有记录
840
+ const prd = await this.scanPRD();
841
+ artifacts.hasPRD = prd.features.some(f => f.id === featureId);
842
+
843
+ } catch (error) {
844
+ console.warn(chalk.yellow(`[Scanner] 推断阶段时出错: ${error.message}`));
845
+ }
846
+
847
+ // 根据制品情况推断阶段
848
+ return this._determineStageFromArtifacts(artifacts);
849
+ }
850
+
851
+ /**
852
+ * 检查是否有正在生成的标记文件
853
+ * @param {string} featureId - 功能点 ID
854
+ * @returns {boolean} 是否有标记
855
+ * @private
856
+ */
857
+ _hasGeneratingMarker(featureId) {
858
+ const markerPaths = [
859
+ path.join(this.projectRoot, '.pdd-generating', `${featureId}.flag`),
860
+ path.join(this.projectRoot, '.pdd-vm', 'generating', `${featureId}.json`)
861
+ ];
862
+
863
+ return markerPaths.some(p => fs.existsSync(p));
864
+ }
865
+
866
+ /**
867
+ * 根据制品情况确定阶段
868
+ * @param {Object} artifacts - 制品状态
869
+ * @returns {string} 阶段值
870
+ * @private
871
+ */
872
+ _determineStageFromArtifacts(artifacts) {
873
+ // 最高优先级检查:有验证报告
874
+ if (artifacts.hasReport) {
875
+ // 如果同时有高质量评分,可能是 done
876
+ return StageEnum.VERIFYING; // 默认返回 verifying,需要更多信息才能判断 done
877
+ }
878
+
879
+ // 正在实现中
880
+ if (artifacts.isImplementing || (artifacts.hasCode && !artifacts.hasTest)) {
881
+ return StageEnum.IMPLEMENTING;
882
+ }
883
+
884
+ // 有代码和测试
885
+ if (artifacts.hasCode && artifacts.hasTest) {
886
+ return StageEnum.VERIFYING;
887
+ }
888
+
889
+ // 只有规格
890
+ if (artifacts.hasSpec && !artifacts.hasCode) {
891
+ return StageEnum.SPEC;
892
+ }
893
+
894
+ // 只有 PRD
895
+ if (artifacts.hasPRD && !artifacts.hasSpec) {
896
+ return StageEnum.PRD;
897
+ }
898
+
899
+ // 有规格但不确定其他状态
900
+ if (artifacts.hasSpec) {
901
+ return StageEnum.EXTRACTED;
902
+ }
903
+
904
+ // 默认返回 PRD 阶段
905
+ return StageEnum.PRD;
906
+ }
907
+
908
+ /**
909
+ * 执行完整的项目扫描
910
+ * 包含所有扫描类型的综合结果
911
+ *
912
+ * @returns {Promise<{
913
+ * specs: SpecScanResult,
914
+ * sourceCode: SourceCodeScanResult,
915
+ * reports: ReportScanResult,
916
+ * prd: PRDScanResult,
917
+ * timestamp: number,
918
+ * duration: number
919
+ * }>} 完整扫描结果
920
+ */
921
+ async fullScan() {
922
+ const startTime = Date.now();
923
+
924
+ console.log(chalk.blue('\n[Scanner] 开始全量项目扫描...\n'));
925
+
926
+ // 并行执行所有扫描任务
927
+ const [specs, sourceCode, reports, prd] = await Promise.all([
928
+ this.scanSpecs(),
929
+ this.scanSourceCode(),
930
+ this.scanReports(),
931
+ this.scanPRD()
932
+ ]);
933
+
934
+ const endTime = Date.now();
935
+ const duration = endTime - startTime;
936
+
937
+ const result = {
938
+ specs,
939
+ sourceCode,
940
+ reports,
941
+ prd,
942
+ timestamp: endTime,
943
+ duration
944
+ };
945
+
946
+ // 输出扫描摘要
947
+ console.log(chalk.blue('\n' + '='.repeat(50)));
948
+ console.log(chalk.bold('扫描摘要'));
949
+ console.log('='.repeat(50));
950
+ console.log(` 规格文件: ${chalk.cyan(String(specs.totalCount))} 个`);
951
+ console.log(` 代码文件: ${chalk.cyan(String(sourceCode.fileCount))} 个 (${sourceCode.totalLOC} LOC)`);
952
+ console.log(` 报告文件: ${chalk.cyan(String(reports.reports.length))} 个`);
953
+ console.log(` PRD: ${prd.found ? chalk.green('已找到') : chalk.gray('未找到')}`);
954
+ console.log(` 扫描耗时: ${chalk.yellow(`${duration}ms`)}`);
955
+ console.log('='.repeat(50) + '\n');
956
+
957
+ return result;
958
+ }
959
+
960
+ /**
961
+ * 清除扫描缓存
962
+ */
963
+ clearCache() {
964
+ this._cache.clear();
965
+ }
966
+
967
+ /**
968
+ * 获取扫描器配置
969
+ * @returns {Object} 当前配置副本
970
+ */
971
+ getConfig() {
972
+ return { ...this.config };
973
+ }
974
+
975
+ /**
976
+ * 更新扫描器配置
977
+ * @param {Object} updates - 配置更新项
978
+ */
979
+ updateConfig(updates) {
980
+ Object.assign(this.config, updates);
981
+ this.clearCache(); // 配置变更后清除缓存
982
+ }
983
+ }
984
+
985
+ /**
986
+ * 导出默认对象
987
+ */
988
+ export default ProjectScanner;