@chenguangyao/devflow-kit 0.1.43

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 (198) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/LICENSE +21 -0
  3. package/README.md +539 -0
  4. package/bin/devflow.js +9 -0
  5. package/docs/RFC-001-devflow-kit.md +617 -0
  6. package/docs/RFC-002-workflow-kernel.md +134 -0
  7. package/docs/enterprise-integration-supplement.md +274 -0
  8. package/docs/internal-gitlab-setup.md +426 -0
  9. package/docs/marketplace-skills.md +231 -0
  10. package/docs/migration-from-arb.md +232 -0
  11. package/docs/tooling-overview.md +774 -0
  12. package/docs/workflow-orchestration.md +695 -0
  13. package/docs/workflow-ui-prototype.html +271 -0
  14. package/package.json +52 -0
  15. package/schemas/config.schema.json +51 -0
  16. package/schemas/delta.schema.json +22 -0
  17. package/schemas/state.schema.json +130 -0
  18. package/schemas/status-surface.schema.json +197 -0
  19. package/schemas/workflow-confirmation-surface.schema.json +70 -0
  20. package/schemas/workflow-picker.schema.json +94 -0
  21. package/scripts/postinstall.js +101 -0
  22. package/scripts/render-workflow-ui-prototype.js +271 -0
  23. package/skills/apply/SKILL.md +313 -0
  24. package/skills/apply/references/discipline-checklist.md +145 -0
  25. package/skills/apply/references/subagent-implementer-prompt.md +113 -0
  26. package/skills/apply/references/subagent-orchestration.md +150 -0
  27. package/skills/apply/references/subagent-reviewer-prompt.md +180 -0
  28. package/skills/apply/references/tdd-loop.md +287 -0
  29. package/skills/apply/references/when-plan-is-wrong.md +279 -0
  30. package/skills/apply/references/worktree-swarm.md +292 -0
  31. package/skills/archive/SKILL.md +229 -0
  32. package/skills/archive/references/conflict-resolution.md +336 -0
  33. package/skills/archive/references/knowledge-deposit.md +381 -0
  34. package/skills/archive/references/spec-merge.md +365 -0
  35. package/skills/brainstorm/SKILL.md +123 -0
  36. package/skills/brainstorm/references/proposal-template.md +244 -0
  37. package/skills/brainstorm/references/question-catalog.md +168 -0
  38. package/skills/brainstorm/references/session-template.md +184 -0
  39. package/skills/ci-fix/SKILL.md +63 -0
  40. package/skills/ci-fix/references/loop.md +25 -0
  41. package/skills/code-review/SKILL.md +279 -0
  42. package/skills/code-review/references/escalation-playbook.md +192 -0
  43. package/skills/code-review/references/language-cheatsheets/go.md +175 -0
  44. package/skills/code-review/references/language-cheatsheets/java-spring-mybatis.md +246 -0
  45. package/skills/code-review/references/language-cheatsheets/python.md +170 -0
  46. package/skills/code-review/references/language-cheatsheets/vue.md +199 -0
  47. package/skills/code-review/references/output-template.md +275 -0
  48. package/skills/code-review/references/review-checklist.md +251 -0
  49. package/skills/complexity-grading/SKILL.md +259 -0
  50. package/skills/deliver/SKILL.md +271 -0
  51. package/skills/deliver/references/delivery-modes.md +299 -0
  52. package/skills/deliver/references/notify.md +359 -0
  53. package/skills/deliver/references/pr-description.md +319 -0
  54. package/skills/dependency-upgrade/SKILL.md +57 -0
  55. package/skills/dependency-upgrade/references/risk-matrix.md +38 -0
  56. package/skills/df-orchestrator/SKILL.md +407 -0
  57. package/skills/df-orchestrator/references/complexity-grading.md +177 -0
  58. package/skills/df-orchestrator/references/escalation-matrix.md +191 -0
  59. package/skills/df-orchestrator/references/routing-rules.md +290 -0
  60. package/skills/df-orchestrator/references/workflow-state-machine.md +208 -0
  61. package/skills/frontend-quality/SKILL.md +61 -0
  62. package/skills/frontend-quality/references/checklist.md +35 -0
  63. package/skills/handoff-resume/SKILL.md +59 -0
  64. package/skills/handoff-resume/references/handoff-template.md +54 -0
  65. package/skills/plan/SKILL.md +166 -0
  66. package/skills/plan/references/task-breakdown.md +207 -0
  67. package/skills/plan/references/task-sequencing.md +143 -0
  68. package/skills/plan/references/task-template.md +248 -0
  69. package/skills/requirement-analysis/SKILL.md +499 -0
  70. package/skills/requirement-analysis/references/acceptance-criteria.md +183 -0
  71. package/skills/requirement-analysis/references/code-recon.md +151 -0
  72. package/skills/requirement-analysis/references/edge-case-catalog.md +164 -0
  73. package/skills/requirement-analysis/references/requirement-template.md +339 -0
  74. package/skills/requirement-analysis/references/scope-negotiation.md +162 -0
  75. package/skills/security-hardening/SKILL.md +60 -0
  76. package/skills/security-hardening/references/checklist.md +42 -0
  77. package/skills/tech-spec/SKILL.md +388 -0
  78. package/skills/tech-spec/references/api-contract-design.md +172 -0
  79. package/skills/tech-spec/references/decision-records.md +110 -0
  80. package/skills/tech-spec/references/design-template.md +301 -0
  81. package/skills/tech-spec/references/rollout-and-rollback.md +203 -0
  82. package/skills/tech-spec/references/spec-delta-conventions.md +250 -0
  83. package/skills/tech-spec/references/transaction-patterns.md +212 -0
  84. package/skills/test-spec/SKILL.md +219 -0
  85. package/skills/test-spec/references/coverage-strategy.md +218 -0
  86. package/skills/test-spec/references/edge-case-to-test.md +143 -0
  87. package/skills/test-spec/references/test-case-template.md +276 -0
  88. package/skills/verify/SKILL.md +232 -0
  89. package/skills/verify/references/nfr-verification.md +292 -0
  90. package/skills/verify/references/report-templates.md +510 -0
  91. package/skills/verify/references/self-test-guide.md +240 -0
  92. package/skills/verify/references/verify-rollback-map.md +247 -0
  93. package/src/cli/commands/_helpers.js +108 -0
  94. package/src/cli/commands/_submit.js +718 -0
  95. package/src/cli/commands/apply.js +198 -0
  96. package/src/cli/commands/archive.js +180 -0
  97. package/src/cli/commands/checkpoint.js +113 -0
  98. package/src/cli/commands/deliver.js +377 -0
  99. package/src/cli/commands/deploy.js +504 -0
  100. package/src/cli/commands/design.js +158 -0
  101. package/src/cli/commands/disable.js +21 -0
  102. package/src/cli/commands/doctor.js +178 -0
  103. package/src/cli/commands/enable.js +21 -0
  104. package/src/cli/commands/flow.js +645 -0
  105. package/src/cli/commands/help.js +93 -0
  106. package/src/cli/commands/ingest.js +602 -0
  107. package/src/cli/commands/init.js +341 -0
  108. package/src/cli/commands/knowledge.js +523 -0
  109. package/src/cli/commands/logs.js +43 -0
  110. package/src/cli/commands/new.js +202 -0
  111. package/src/cli/commands/plan.js +49 -0
  112. package/src/cli/commands/propose.js +27 -0
  113. package/src/cli/commands/provider.js +698 -0
  114. package/src/cli/commands/report.js +143 -0
  115. package/src/cli/commands/requirement.js +227 -0
  116. package/src/cli/commands/review.js +301 -0
  117. package/src/cli/commands/skills.js +457 -0
  118. package/src/cli/commands/status.js +925 -0
  119. package/src/cli/commands/switch.js +27 -0
  120. package/src/cli/commands/sync.js +47 -0
  121. package/src/cli/commands/test.js +366 -0
  122. package/src/cli/commands/uninstall.js +32 -0
  123. package/src/cli/commands/update.js +74 -0
  124. package/src/cli/commands/verify.js +354 -0
  125. package/src/cli/commands/worktree.js +78 -0
  126. package/src/cli/index.js +72 -0
  127. package/src/cli/parse-args.js +102 -0
  128. package/src/core/autodetect.js +271 -0
  129. package/src/core/change.js +208 -0
  130. package/src/core/checkpoint.js +217 -0
  131. package/src/core/config.js +60 -0
  132. package/src/core/delta.js +290 -0
  133. package/src/core/markers.js +59 -0
  134. package/src/core/paths.js +173 -0
  135. package/src/core/plan-tasks.js +36 -0
  136. package/src/core/project-routing.js +285 -0
  137. package/src/core/projects.js +200 -0
  138. package/src/core/state.js +200 -0
  139. package/src/core/workflow-check.js +177 -0
  140. package/src/core/workflow-init.js +34 -0
  141. package/src/core/workflow-picker.js +154 -0
  142. package/src/core/workflow-policy.js +119 -0
  143. package/src/core/workflow-suggest.js +181 -0
  144. package/src/core/workflow-verify.js +88 -0
  145. package/src/core/workflow.js +433 -0
  146. package/src/core/worktree.js +241 -0
  147. package/src/knowledge/categories.js +107 -0
  148. package/src/knowledge/classify.js +125 -0
  149. package/src/knowledge/deposit.js +414 -0
  150. package/src/knowledge/migrate.js +149 -0
  151. package/src/knowledge/mr.js +219 -0
  152. package/src/knowledge/query.js +131 -0
  153. package/src/knowledge/registry.js +151 -0
  154. package/src/knowledge/sync.js +179 -0
  155. package/src/providers/base.js +74 -0
  156. package/src/providers/drivers/api-yapi.js +78 -0
  157. package/src/providers/drivers/ci-jenkins.js +109 -0
  158. package/src/providers/drivers/intake-confluence.js +544 -0
  159. package/src/providers/drivers/kb-git.js +549 -0
  160. package/src/providers/drivers/kb-weknora.js +472 -0
  161. package/src/providers/drivers/notify-smtp.js +515 -0
  162. package/src/providers/drivers/observability-oss.js +43 -0
  163. package/src/providers/drivers/observability-sls.js +50 -0
  164. package/src/providers/lifecycle.js +135 -0
  165. package/src/providers/loader.js +132 -0
  166. package/src/providers/local.js +190 -0
  167. package/src/providers/userconfig.js +283 -0
  168. package/src/reports/aggregate.js +185 -0
  169. package/src/reports/coverage.js +163 -0
  170. package/src/reports/detect.js +143 -0
  171. package/src/reports/parse.js +236 -0
  172. package/src/templates/files/ci/github.yml +38 -0
  173. package/src/templates/files/ci/gitlab.yml +27 -0
  174. package/src/templates/files/design.md +63 -0
  175. package/src/templates/files/ide/devflow-workflow.md +58 -0
  176. package/src/templates/files/ide/project-overview-reference.md +1 -0
  177. package/src/templates/files/ide/project-overview.md +27 -0
  178. package/src/templates/files/knowledge-index.json +17 -0
  179. package/src/templates/files/knowledge.md +28 -0
  180. package/src/templates/files/meta.json +8 -0
  181. package/src/templates/files/plan.md +38 -0
  182. package/src/templates/files/proposal.md +33 -0
  183. package/src/templates/files/reports/contract-test.md +40 -0
  184. package/src/templates/files/reports/e2e-test.md +30 -0
  185. package/src/templates/files/reports/integration-test.md +36 -0
  186. package/src/templates/files/reports/joint-test.md +58 -0
  187. package/src/templates/files/reports/perf.md +24 -0
  188. package/src/templates/files/reports/regression.md +20 -0
  189. package/src/templates/files/reports/remote-test.md +55 -0
  190. package/src/templates/files/reports/self-test.md +43 -0
  191. package/src/templates/files/reports/smoke-test.md +22 -0
  192. package/src/templates/files/reports/unit-test.md +36 -0
  193. package/src/templates/files/requirement.md +51 -0
  194. package/src/templates/files/review.md +38 -0
  195. package/src/templates/files/tests.md +36 -0
  196. package/src/templates/files/verify.md +32 -0
  197. package/src/templates/index.js +21 -0
  198. package/src/utils/log.js +37 -0
@@ -0,0 +1,283 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const fsp = require('fs/promises');
4
+ const path = require('path');
5
+ const paths = require('../core/paths.js');
6
+
7
+ /**
8
+ * User-level config bootstrap.
9
+ *
10
+ * ~/.devflow/
11
+ * ├── providers.json (chmod 0600) ← single source of truth for ALL providers
12
+ * ├── providers.example.json ← filled provider examples, no real secrets
13
+ * ├── projects.json (chmod 0600) ← non-secret project/deploy/test profiles
14
+ * └── README.md ← usage notes (committed once on first install)
15
+ *
16
+ * Why a single file:
17
+ * - All credentials live in one place → easy to back up, rotate, lock down
18
+ * - File is shared across every project on the same machine
19
+ * - Project-level <repo>/devflow/providers.json may override individual entries
20
+ *
21
+ * Security notes:
22
+ * - Permissions: dir 0700, providers.json 0600 (we re-apply on every call so an
23
+ * accidental chmod gets corrected). Other readable-by-group setups would let
24
+ * other Unix users on the same host read tokens.
25
+ * - Prefer `${env:VAR}` placeholders over inline secrets in providers.json so
26
+ * tokens never sit on disk in plaintext when avoidable.
27
+ */
28
+
29
+ const DEFAULT_PROVIDERS_JSON = '{}\n';
30
+ const DEFAULT_PROVIDERS_EXAMPLE = {
31
+ _instructions: [
32
+ 'Copy the providers you need into ~/.devflow/providers.json.',
33
+ 'Public/shared endpoints are filled in; personal usernames, tokens, passwords, and webhook URLs should use ${env:VAR} placeholders.',
34
+ 'Never commit providers.json or paste real secrets into reports, PR descriptions, or chat logs.'
35
+ ],
36
+ 'intake.confluence': {
37
+ type: 'intake',
38
+ driver: 'confluence',
39
+ config: {
40
+ baseUrl: 'https://wiki.qingsonghealth.com',
41
+ token: '${env:CONFLUENCE_TOKEN}',
42
+ user: '${env:CONFLUENCE_USER}'
43
+ }
44
+ },
45
+ 'ci.jenkins': {
46
+ type: 'ci',
47
+ driver: 'jenkins',
48
+ config: {
49
+ baseUrl: 'https://test-jenkins.qingsonghealth.com',
50
+ user: '${env:JENKINS_USER}',
51
+ token: '${env:JENKINS_TOKEN}',
52
+ triggerMethod: 'post'
53
+ }
54
+ },
55
+ 'notify.smtp': {
56
+ type: 'notify',
57
+ driver: 'smtp',
58
+ config: {
59
+ host: 'smtp.exmail.qq.com',
60
+ port: 465,
61
+ secure: true,
62
+ user: '${env:SMTP_USER}',
63
+ pass: '${env:SMTP_PASS}',
64
+ from: '${env:SMTP_FROM}',
65
+ defaults: {
66
+ to: ['qa@example.com'],
67
+ cc: ['dev-team@example.com']
68
+ }
69
+ }
70
+ },
71
+ 'kb.weknora': {
72
+ type: 'kb',
73
+ driver: 'weknora',
74
+ config: {
75
+ endpoint: '${env:WEKNORA_ENDPOINT}',
76
+ kb_id: '${env:WEKNORA_KB_ID}',
77
+ token: '${env:WEKNORA_API_KEY}'
78
+ }
79
+ },
80
+ 'api.yapi': {
81
+ type: 'api',
82
+ driver: 'yapi',
83
+ config: {
84
+ baseUrl: 'https://yapi.qingsonghealth.com',
85
+ token: '${env:YAPI_TOKEN}',
86
+ projectId: '${env:YAPI_PROJECT_ID}',
87
+ syncPath: '/api/open/import_data'
88
+ }
89
+ },
90
+ 'observability.sls': {
91
+ type: 'observability',
92
+ driver: 'sls',
93
+ config: {
94
+ endpoint: '${env:SLS_ENDPOINT}',
95
+ project: '${env:SLS_PROJECT}',
96
+ logstore: '${env:SLS_LOGSTORE}',
97
+ accessKeyId: '${env:ALIYUN_ACCESS_KEY_ID}',
98
+ accessKeySecret: '${env:ALIYUN_ACCESS_KEY_SECRET}'
99
+ }
100
+ },
101
+ 'observability.oss': {
102
+ type: 'observability',
103
+ driver: 'oss',
104
+ config: {
105
+ endpoint: '${env:OSS_ENDPOINT}',
106
+ bucket: '${env:OSS_BUCKET}',
107
+ prefix: 'logs/',
108
+ accessKeyId: '${env:ALIYUN_ACCESS_KEY_ID}',
109
+ accessKeySecret: '${env:ALIYUN_ACCESS_KEY_SECRET}'
110
+ }
111
+ }
112
+ };
113
+
114
+ const README_TEMPLATE = `# devflow user config
115
+
116
+ Files in this directory:
117
+
118
+ | File | Purpose |
119
+ | ---- | ------- |
120
+ | \`providers.json\` | Single source of truth for all provider configs (chmod 0600) |
121
+ | \`providers.example.json\` | Filled examples with public endpoints and env-var placeholders for personal secrets |
122
+ | \`projects.json\` | Project deploy/test profiles imported from tools like publish-code (chmod 0600) |
123
+
124
+ ## providers.json schema
125
+
126
+ \`\`\`jsonc
127
+ {
128
+ // Key format: "<type>.<name>"
129
+ // - type: intake | issue | vcs | ci | notify | kb | api | observability
130
+ // - name: short id you pick (typically the driver name or an env qualifier)
131
+ "intake.confluence": {
132
+ "type": "intake",
133
+ "driver": "confluence",
134
+ "config": {
135
+ "baseUrl": "https://wiki.qingsonghealth.com",
136
+ "token": "\${env:CONFLUENCE_TOKEN}",
137
+ "user": "\${env:CONFLUENCE_USER}"
138
+ }
139
+ },
140
+ "ci.jenkins": {
141
+ "type": "ci",
142
+ "driver": "jenkins",
143
+ "config": {
144
+ "baseUrl": "https://test-jenkins.qingsonghealth.com",
145
+ "user": "\${env:JENKINS_USER}",
146
+ "token": "\${env:JENKINS_TOKEN}",
147
+ "triggerMethod": "post"
148
+ }
149
+ },
150
+ "notify.smtp": {
151
+ "type": "notify",
152
+ "driver": "smtp",
153
+ "config": {
154
+ "host": "smtp.example.com",
155
+ "port": 587,
156
+ "secure": false,
157
+ "user": "ci@example.com",
158
+ "pass": "\${env:SMTP_PASS}",
159
+ "from": "ci@example.com",
160
+ "defaults": {
161
+ "to": ["qa@example.com"],
162
+ "cc": ["dev-team@example.com"]
163
+ }
164
+ }
165
+ },
166
+ "kb.weknora": {
167
+ "type": "kb",
168
+ "driver": "weknora",
169
+ "config": {
170
+ "baseUrl": "https://weknora.example.com",
171
+ "kbId": "your-kb-id",
172
+ "auth": { "type": "bearer", "tokenEnv": "WEKNORA_TOKEN" }
173
+ }
174
+ }
175
+ }
176
+ \`\`\`
177
+
178
+ ## projects.json schema
179
+
180
+ \`\`\`jsonc
181
+ {
182
+ "version": 1,
183
+ "environments": {
184
+ "test": { "apiBaseUrl": "https://test-api.example.com" }
185
+ },
186
+ "projects": {
187
+ "go-member": {
188
+ "repoUrl": "https://code.example.com/health/go-member",
189
+ "gitlabProjectId": "3678",
190
+ "jenkins": {
191
+ "test": "https://jenkins.example.com/job/health_go-member-test/build?delay=0sec"
192
+ },
193
+ "tests": {
194
+ "smoke": { "command": "curl -fsS \${apiBaseUrl}/health" }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ \`\`\`
200
+
201
+ ## Useful commands
202
+
203
+ \`\`\`bash
204
+ devflow provider list # list configured providers (merges user + project)
205
+ devflow provider status # connectivity & auth check for each
206
+ devflow provider add <name> # interactive add
207
+ devflow provider add --from <file> # bulk import from JSON snippet
208
+ devflow provider import publish-code --from <file> # import project/Jenkins profiles
209
+ devflow provider validate <n> # config schema check
210
+ devflow provider rotate <n> # rotate token / cookie
211
+ devflow provider audit # security audit (plaintext tokens, file modes)
212
+ devflow doctor --scope cred # same audit + actionable hints
213
+ \`\`\`
214
+
215
+ ## Resolution order (highest priority first)
216
+
217
+ 1. \`<repo>/devflow/providers.json\` (project override — checked into the repo if non-sensitive)
218
+ 2. \`~/.devflow/providers.json\` (this file — user-wide, 0600)
219
+ 3. Built-in \`local\` driver (zero-dep fallback for intake/issue/vcs/ci/notify/kb)
220
+
221
+ ## Secret hygiene
222
+
223
+ - Never check \`~/.devflow/providers.json\` into git.
224
+ - Prefer \`"\${env:VAR_NAME}"\` over inline secrets — devflow expands them at load time.
225
+ - \`devflow provider audit\` flags fields that look like inline tokens (≥20 chars,
226
+ high-entropy) and warns you to switch to env-var references.
227
+ `;
228
+
229
+ /**
230
+ * Ensure ~/.devflow/ exists with an empty providers.json (0600), a populated
231
+ * providers.example.json, and README.
232
+ *
233
+ * Idempotent: never overwrites existing files. Always re-applies tight perms in
234
+ * case the user (or a tool) loosened them.
235
+ *
236
+ * Returns { dir, file, readme, created: { dir, providers, readme } } so callers
237
+ * can decide whether to print "created new config" vs "config already present".
238
+ */
239
+ async function ensureUserConfig() {
240
+ const dir = paths.userConfigDir();
241
+ const file = paths.userProvidersFile();
242
+ const example = paths.userProvidersExampleFile();
243
+ const readme = path.join(dir, 'README.md');
244
+ const created = { dir: false, providers: false, example: false, readme: false };
245
+
246
+ if (!fs.existsSync(dir)) {
247
+ await fsp.mkdir(dir, { recursive: true, mode: 0o700 });
248
+ created.dir = true;
249
+ }
250
+ // Re-apply (idempotent) tight perms even if dir pre-existed
251
+ try { await fsp.chmod(dir, 0o700); } catch { /* not all FS support chmod */ }
252
+
253
+ if (!fs.existsSync(file)) {
254
+ // Open with O_CREAT | O_WRONLY | O_EXCL semantics so we don't race
255
+ const legacy = paths.legacyUserProvidersFile && paths.legacyUserProvidersFile();
256
+ if (legacy && fs.existsSync(legacy)) {
257
+ await fsp.copyFile(legacy, file);
258
+ } else {
259
+ await fsp.writeFile(file, DEFAULT_PROVIDERS_JSON, { mode: 0o600 });
260
+ }
261
+ created.providers = true;
262
+ }
263
+ try { await fsp.chmod(file, 0o600); } catch { /* ignore */ }
264
+
265
+ if (!fs.existsSync(example)) {
266
+ await fsp.writeFile(example, JSON.stringify(DEFAULT_PROVIDERS_EXAMPLE, null, 2) + '\n', 'utf8');
267
+ created.example = true;
268
+ }
269
+
270
+ if (!fs.existsSync(readme)) {
271
+ await fsp.writeFile(readme, README_TEMPLATE);
272
+ created.readme = true;
273
+ }
274
+
275
+ return { dir, file, example, readme, created };
276
+ }
277
+
278
+ module.exports = {
279
+ ensureUserConfig,
280
+ DEFAULT_PROVIDERS_JSON,
281
+ DEFAULT_PROVIDERS_EXAMPLE,
282
+ README_TEMPLATE,
283
+ };
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs/promises');
4
+ const fsSync = require('fs');
5
+ const path = require('path');
6
+
7
+ const ORDER = ['deploy', 'unit', 'integration', 'contract', 'remote', 'e2e', 'joint', 'smoke', 'self-test', 'regression', 'perf'];
8
+ const LABELS = {
9
+ deploy: '部署',
10
+ unit: '单元测试',
11
+ integration: '集成测试',
12
+ contract: '调用链一致性',
13
+ remote: '远程 API',
14
+ e2e: '端到端',
15
+ joint: '联调测试',
16
+ smoke: '冒烟测试',
17
+ 'self-test': '自测',
18
+ regression: '回归测试',
19
+ perf: '性能测试',
20
+ };
21
+
22
+ async function upsertReport({ reportsDir, slug, title, kind, body, meta = {} }) {
23
+ await fs.mkdir(reportsDir, { recursive: true });
24
+ const current = readAggregate(reportsDir);
25
+ const reports = current.reports.filter((r) => r.kind !== kind);
26
+ reports.push({
27
+ kind,
28
+ status: meta.status || 'pending',
29
+ total: meta.total == null ? null : meta.total,
30
+ passed: meta.passed == null ? null : meta.passed,
31
+ failed: meta.failed == null ? null : meta.failed,
32
+ coverage: meta.coverage || null,
33
+ failureType: meta.failureType || meta.failure_type || null,
34
+ ts: meta.ts || new Date().toISOString(),
35
+ body: stripFrontmatter(body || '').trim(),
36
+ });
37
+ const md = renderAggregate({ slug, title: title || current.title || slug, reports });
38
+ const file = path.join(reportsDir, 'test-report.md');
39
+ await fs.writeFile(file, md, 'utf8');
40
+ return file;
41
+ }
42
+
43
+ function readAggregate(reportsDir) {
44
+ const file = path.join(reportsDir, 'test-report.md');
45
+ let text = '';
46
+ try { text = fsSync.readFileSync(file, 'utf8'); } catch (_) { return { title: '', reports: [] }; }
47
+ const title = ((text.match(/^#\s+(.+)$/m) || [])[1] || '').replace(/^测试报告\s*-\s*/, '').trim();
48
+ const reports = [];
49
+ const re = /<!-- devflow:report ([a-z0-9_-]+):start -->\n<!-- devflow:report-meta ([\s\S]*?) -->\n([\s\S]*?)\n<!-- devflow:report \1:end -->/g;
50
+ let m;
51
+ while ((m = re.exec(text))) {
52
+ let meta = {};
53
+ try { meta = JSON.parse(m[2]); } catch (_) { meta = {}; }
54
+ reports.push({
55
+ kind: meta.kind || m[1],
56
+ status: meta.status || 'pending',
57
+ total: numberOrNull(meta.total),
58
+ passed: numberOrNull(meta.passed),
59
+ failed: numberOrNull(meta.failed),
60
+ coverage: meta.coverage || null,
61
+ failureType: meta.failureType || meta.failure_type || null,
62
+ ts: meta.ts || null,
63
+ file: 'test-report.md',
64
+ section: m[1],
65
+ body: (m[3] || '').trim(),
66
+ });
67
+ }
68
+ return { title, reports };
69
+ }
70
+
71
+ function renderAggregate({ slug, title, reports }) {
72
+ const sorted = sortReports(reports);
73
+ const status = overallStatus(sorted);
74
+ const totals = sumTotals(sorted);
75
+ const lines = [
76
+ '---',
77
+ `slug: ${slug || '-'}`,
78
+ 'kind: test-report',
79
+ `status: ${status}`,
80
+ `total: ${totals.total == null ? '-' : totals.total}`,
81
+ `passed: ${totals.passed == null ? '-' : totals.passed}`,
82
+ `failed: ${totals.failed == null ? '-' : totals.failed}`,
83
+ `ts: ${new Date().toISOString()}`,
84
+ '---',
85
+ '',
86
+ `# 测试报告 - ${title || slug || '-'}`,
87
+ '',
88
+ '## 测试覆盖汇总',
89
+ '',
90
+ '| 类型 | 状态 | 用例 | 通过 | 失败 | 覆盖率 | 失败类型 |',
91
+ '| --- | --- | --- | --- | --- | --- | --- |',
92
+ ];
93
+ for (const r of sorted) {
94
+ lines.push(`| ${labelKind(r.kind)} | ${statusIcon(r.status)} | ${fmt(r.total)} | ${fmt(r.passed)} | ${fmt(r.failed)} | ${r.coverage || '-'} | ${r.failureType || '-'} |`);
95
+ }
96
+ lines.push('', '## 详细报告', '');
97
+ for (const r of sorted) {
98
+ const meta = {
99
+ kind: r.kind,
100
+ status: r.status || 'pending',
101
+ total: r.total,
102
+ passed: r.passed,
103
+ failed: r.failed,
104
+ coverage: r.coverage,
105
+ failureType: r.failureType,
106
+ ts: r.ts || new Date().toISOString(),
107
+ };
108
+ lines.push(`<!-- devflow:report ${r.kind}:start -->`);
109
+ lines.push(`<!-- devflow:report-meta ${JSON.stringify(meta)} -->`);
110
+ lines.push(normalizeSection(r));
111
+ lines.push(`<!-- devflow:report ${r.kind}:end -->`, '');
112
+ }
113
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
114
+ }
115
+
116
+ function normalizeSection(r) {
117
+ const body = (r.body || '').trim();
118
+ if (!body) return `### ${labelKind(r.kind)}\n\n_(暂无详情)_`;
119
+ if (/^#{2,4}\s+/m.test(body)) return body;
120
+ if (/^#\s+/m.test(body)) return body.replace(/^#\s+/, '### ');
121
+ return `### ${labelKind(r.kind)}\n\n${body}`;
122
+ }
123
+
124
+ function stripFrontmatter(text) {
125
+ const s = String(text || '');
126
+ if (!s.startsWith('---')) return s;
127
+ const end = s.indexOf('\n---', 3);
128
+ return end >= 0 ? s.slice(end + 4).replace(/^\s+/, '') : s;
129
+ }
130
+
131
+ function sortReports(reports) {
132
+ return [...reports].sort((a, b) => {
133
+ const ai = ORDER.indexOf(a.kind);
134
+ const bi = ORDER.indexOf(b.kind);
135
+ return (ai < 0 ? 999 : ai) - (bi < 0 ? 999 : bi) || String(a.kind).localeCompare(String(b.kind));
136
+ });
137
+ }
138
+
139
+ function overallStatus(reports) {
140
+ if (!reports.length) return 'pending';
141
+ if (reports.some((r) => r.status === 'fail')) return 'fail';
142
+ if (reports.some((r) => r.status === 'blocked')) return 'blocked';
143
+ if (reports.some((r) => !r.status || r.status === 'pending' || r.status === 'draft')) return 'pending';
144
+ if (reports.some((r) => r.status === 'partial')) return 'partial';
145
+ return 'pass';
146
+ }
147
+
148
+ function sumTotals(reports) {
149
+ let seen = false;
150
+ const out = { total: 0, passed: 0, failed: 0 };
151
+ for (const r of reports) {
152
+ if (r.total != null || r.passed != null || r.failed != null) seen = true;
153
+ out.total += Number.isFinite(Number(r.total)) ? Number(r.total) : 0;
154
+ out.passed += Number.isFinite(Number(r.passed)) ? Number(r.passed) : 0;
155
+ out.failed += Number.isFinite(Number(r.failed)) ? Number(r.failed) : 0;
156
+ }
157
+ return seen ? out : { total: null, passed: null, failed: null };
158
+ }
159
+
160
+ function labelKind(k) { return LABELS[k] || k; }
161
+ function statusIcon(s) {
162
+ if (s === 'pass') return '✅ 通过';
163
+ if (s === 'fail') return '❌ 失败';
164
+ if (s === 'blocked') return '⛔ 阻塞';
165
+ if (s === 'pending' || s === 'draft') return '⏳ 待补充';
166
+ if (s === 'partial') return '⚠️ 部分通过';
167
+ return s || '-';
168
+ }
169
+ function fmt(v) { return v == null || v === '' ? '-' : String(v); }
170
+ function numberOrNull(v) {
171
+ if (v == null || v === '-') return null;
172
+ const n = Number(v);
173
+ return Number.isFinite(n) ? n : null;
174
+ }
175
+
176
+ module.exports = {
177
+ upsertReport,
178
+ readAggregate,
179
+ renderAggregate,
180
+ stripFrontmatter,
181
+ overallStatus,
182
+ sumTotals,
183
+ labelKind,
184
+ statusIcon,
185
+ };
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ /**
6
+ * Parse coverage from common formats.
7
+ * Returns: { format, lines: { pct, covered, total }, statements?, branches?, functions? }
8
+ *
9
+ * Supported:
10
+ * - 'json-summary' istanbul / nyc / jest / vitest (coverage-summary.json)
11
+ * - 'lcov' lcov.info
12
+ * - 'cobertura' coverage.xml (pytest --cov-report=xml, jacoco-cobertura, etc.)
13
+ * - 'go-cover-out' go's coverage.out (mode + per-file count)
14
+ * - 'jacoco-xml' target/site/jacoco/jacoco.xml
15
+ */
16
+ function parseFile(format, file) {
17
+ if (!fs.existsSync(file)) return null;
18
+ const text = fs.readFileSync(file, 'utf8');
19
+ return parseText(format, text);
20
+ }
21
+
22
+ function parseText(format, text) {
23
+ switch (format) {
24
+ case 'json-summary': return parseJsonSummary(text);
25
+ case 'lcov': return parseLcov(text);
26
+ case 'cobertura': return parseCobertura(text);
27
+ case 'go-cover-out': return parseGoCoverOut(text);
28
+ case 'jacoco-xml': return parseJacoco(text);
29
+ default: return null;
30
+ }
31
+ }
32
+
33
+ function parseJsonSummary(text) {
34
+ let json; try { json = JSON.parse(text); } catch { return null; }
35
+ const total = json.total;
36
+ if (!total) return null;
37
+ return {
38
+ format: 'json-summary',
39
+ lines: pick(total.lines),
40
+ statements: pick(total.statements),
41
+ branches: pick(total.branches),
42
+ functions: pick(total.functions),
43
+ };
44
+ }
45
+
46
+ function pick(o) {
47
+ if (!o) return null;
48
+ return { pct: o.pct, covered: o.covered, total: o.total };
49
+ }
50
+
51
+ function parseLcov(text) {
52
+ let LF = 0, LH = 0, BF = 0, BH = 0, FNF = 0, FNH = 0;
53
+ for (const line of text.split(/\r?\n/)) {
54
+ const i = line.indexOf(':');
55
+ if (i < 0) continue;
56
+ const k = line.slice(0, i);
57
+ const v = parseInt(line.slice(i + 1), 10);
58
+ if (Number.isNaN(v)) continue;
59
+ if (k === 'LF') LF += v;
60
+ else if (k === 'LH') LH += v;
61
+ else if (k === 'BRF') BF += v;
62
+ else if (k === 'BRH') BH += v;
63
+ else if (k === 'FNF') FNF += v;
64
+ else if (k === 'FNH') FNH += v;
65
+ }
66
+ if (LF === 0) return null;
67
+ return {
68
+ format: 'lcov',
69
+ lines: { pct: pct(LH, LF), covered: LH, total: LF },
70
+ branches: BF > 0 ? { pct: pct(BH, BF), covered: BH, total: BF } : null,
71
+ functions: FNF > 0 ? { pct: pct(FNH, FNF), covered: FNH, total: FNF } : null,
72
+ };
73
+ }
74
+
75
+ function parseCobertura(text) {
76
+ // Look for top-level <coverage line-rate="0.85" ...> ... <coverage>...</coverage>
77
+ const m = text.match(/<coverage[^>]*line-rate="([\d.]+)"[^>]*>/);
78
+ if (!m) return null;
79
+ const lineRate = parseFloat(m[1]);
80
+ const branchM = text.match(/<coverage[^>]*branch-rate="([\d.]+)"[^>]*>/);
81
+ return {
82
+ format: 'cobertura',
83
+ lines: { pct: round1(lineRate * 100), covered: null, total: null },
84
+ branches: branchM ? { pct: round1(parseFloat(branchM[1]) * 100), covered: null, total: null } : null,
85
+ };
86
+ }
87
+
88
+ function parseGoCoverOut(text) {
89
+ // Lines: "<file>:<startLine>.<col>,<endLine>.<col> <numStmt> <count>"
90
+ let total = 0, covered = 0;
91
+ for (const line of text.split(/\r?\n/)) {
92
+ if (!line || line.startsWith('mode:')) continue;
93
+ const parts = line.trim().split(/\s+/);
94
+ if (parts.length < 3) continue;
95
+ const numStmt = parseInt(parts[parts.length - 2], 10);
96
+ const count = parseInt(parts[parts.length - 1], 10);
97
+ if (!Number.isFinite(numStmt) || !Number.isFinite(count)) continue;
98
+ total += numStmt;
99
+ if (count > 0) covered += numStmt;
100
+ }
101
+ if (total === 0) return null;
102
+ return {
103
+ format: 'go-cover-out',
104
+ statements: { pct: pct(covered, total), covered, total },
105
+ lines: { pct: pct(covered, total), covered, total }, // approximate
106
+ };
107
+ }
108
+
109
+ function parseJacoco(text) {
110
+ // Jacoco's root element has multiple <counter type="..." missed=".." covered=".."/>
111
+ const counters = {};
112
+ const re = /<counter\s+type="(\w+)"\s+missed="(\d+)"\s+covered="(\d+)"\s*\/?>/g;
113
+ // Restrict to top level: pick the LAST occurrence per type which is the project totals.
114
+ let m;
115
+ while ((m = re.exec(text)) !== null) {
116
+ counters[m[1]] = { missed: parseInt(m[2], 10), covered: parseInt(m[3], 10) };
117
+ }
118
+ const sec = (key) => {
119
+ const c = counters[key];
120
+ if (!c) return null;
121
+ const total = c.missed + c.covered;
122
+ return total === 0 ? null : { pct: pct(c.covered, total), covered: c.covered, total };
123
+ };
124
+ const lines = sec('LINE');
125
+ if (!lines) return null;
126
+ return {
127
+ format: 'jacoco-xml',
128
+ lines,
129
+ statements: sec('INSTRUCTION'),
130
+ branches: sec('BRANCH'),
131
+ functions: sec('METHOD'),
132
+ };
133
+ }
134
+
135
+ function pct(num, denom) {
136
+ if (!denom) return 0;
137
+ return round1((num / denom) * 100);
138
+ }
139
+ function round1(n) { return Math.round(n * 10) / 10; }
140
+
141
+ /**
142
+ * Best-effort discovery: try the explicit hint, then a list of common locations.
143
+ */
144
+ function discover(root, hint) {
145
+ const candidates = [];
146
+ if (hint && hint.file) candidates.push({ format: hint.format, file: path.join(root, hint.file) });
147
+ candidates.push(
148
+ { format: 'json-summary', file: path.join(root, 'coverage', 'coverage-summary.json') },
149
+ { format: 'lcov', file: path.join(root, 'coverage', 'lcov.info') },
150
+ { format: 'cobertura', file: path.join(root, 'coverage.xml') },
151
+ { format: 'go-cover-out', file: path.join(root, 'coverage.out') },
152
+ { format: 'jacoco-xml', file: path.join(root, 'target', 'site', 'jacoco', 'jacoco.xml') },
153
+ );
154
+ for (const c of candidates) {
155
+ if (fs.existsSync(c.file)) {
156
+ const r = parseFile(c.format, c.file);
157
+ if (r) return r;
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+
163
+ module.exports = { parseFile, parseText, discover };