@tea-agent/loop-agent 0.1.0-fe-test.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 (1050) hide show
  1. package/AGENTS.md +114 -0
  2. package/CHANGELOG.md +2283 -0
  3. package/README.md +177 -0
  4. package/bin/agent-worker.js +22 -0
  5. package/bin/loop-agent.js +57 -0
  6. package/dist/adapters/aimax.js +91 -0
  7. package/dist/adapters/context.js +32 -0
  8. package/dist/adapters/index.js +29 -0
  9. package/dist/adapters/loop-agent.js +192 -0
  10. package/dist/adapters/types.js +1 -0
  11. package/dist/application/context-usage/skill-resolution-stats.js +263 -0
  12. package/dist/application/dag/args.js +486 -0
  13. package/dist/application/dag/generate-task-dag.js +408 -0
  14. package/dist/application/dag/report-dag.js +14 -0
  15. package/dist/application/dag/run-dag.js +109 -0
  16. package/dist/application/dag/validate-dag.js +166 -0
  17. package/dist/application/evaluation/alias.js +184 -0
  18. package/dist/application/evaluation/budget.js +192 -0
  19. package/dist/application/evaluation/campaign-hash.js +47 -0
  20. package/dist/application/evaluation/campaign-matrix.js +372 -0
  21. package/dist/application/evaluation/campaign-scorecard.js +135 -0
  22. package/dist/application/evaluation/campaign.js +370 -0
  23. package/dist/application/evaluation/candidate-hash.js +75 -0
  24. package/dist/application/evaluation/candidate.js +69 -0
  25. package/dist/application/evaluation/corpus-hash.js +38 -0
  26. package/dist/application/evaluation/corpus.js +56 -0
  27. package/dist/application/evaluation/experiment.js +294 -0
  28. package/dist/application/evaluation/ignition.js +198 -0
  29. package/dist/application/evaluation/integrity-audit.js +162 -0
  30. package/dist/application/evaluation/outer-loop.js +132 -0
  31. package/dist/application/evaluation/pi-cell-executor.js +39 -0
  32. package/dist/application/evaluation/private-verifier.js +46 -0
  33. package/dist/application/evaluation/promotion-policy.js +151 -0
  34. package/dist/application/evaluation/proposer.js +98 -0
  35. package/dist/application/evaluation/replay.js +289 -0
  36. package/dist/application/evaluation/types.js +652 -0
  37. package/dist/application/loop/run-action.js +19 -0
  38. package/dist/application/task-lifecycle/advance.js +1204 -0
  39. package/dist/application/task-lifecycle/gates.js +130 -0
  40. package/dist/application/task-lifecycle/index.js +7 -0
  41. package/dist/application/task-lifecycle/observe.js +424 -0
  42. package/dist/application/task-lifecycle/plan-transitions.js +251 -0
  43. package/dist/application/task-lifecycle/recommendations.js +180 -0
  44. package/dist/application/task-lifecycle/record.js +73 -0
  45. package/dist/application/task-lifecycle/types.js +1 -0
  46. package/dist/build-stamp.json +6 -0
  47. package/dist/cli/catalog.js +24 -0
  48. package/dist/cli/command-definitions.js +659 -0
  49. package/dist/cli/help.js +56 -0
  50. package/dist/cli/index.js +5 -0
  51. package/dist/cli/program.js +647 -0
  52. package/dist/cli/router.js +13 -0
  53. package/dist/cli/update/init-surface-notifier.js +167 -0
  54. package/dist/cli/update/notifier.js +117 -0
  55. package/dist/cli/update/npm-client.js +151 -0
  56. package/dist/cli/update/policy.js +92 -0
  57. package/dist/cli/update/runtime-activity.js +29 -0
  58. package/dist/cli/update/state.js +68 -0
  59. package/dist/cli-governance/active-residue-check.js +38 -0
  60. package/dist/cli.js +57 -0
  61. package/dist/commands/client-recovery.js +1373 -0
  62. package/dist/commands/closeout.js +13 -0
  63. package/dist/commands/coverage-audit.js +14 -0
  64. package/dist/commands/coverage-report.js +50 -0
  65. package/dist/commands/cursor-prompt.js +182 -0
  66. package/dist/commands/dag-approve.js +142 -0
  67. package/dist/commands/dag-final-verification.js +76 -0
  68. package/dist/commands/dag-init-hybrid.js +57 -0
  69. package/dist/commands/dag-reconcile-run.js +5 -0
  70. package/dist/commands/dag-reconcile-tasks.js +51 -0
  71. package/dist/commands/dag-reject.js +91 -0
  72. package/dist/commands/dag-report.js +76 -0
  73. package/dist/commands/dag-request-interrupt.js +20 -0
  74. package/dist/commands/dag-rerun-task.js +19 -0
  75. package/dist/commands/dag-rerun.js +111 -0
  76. package/dist/commands/dag-resume.js +35 -0
  77. package/dist/commands/dag-run-task.js +12 -0
  78. package/dist/commands/dag-validate.js +20 -0
  79. package/dist/commands/dag-workflow-compile.js +91 -0
  80. package/dist/commands/dag-workflow-plan.js +130 -0
  81. package/dist/commands/dag-workflow-validate.js +66 -0
  82. package/dist/commands/delegate.js +130 -0
  83. package/dist/commands/docs-archive.js +5 -0
  84. package/dist/commands/docs-audit.js +6 -0
  85. package/dist/commands/doctor.js +283 -0
  86. package/dist/commands/eval.js +1398 -0
  87. package/dist/commands/examples.js +90 -0
  88. package/dist/commands/goal.js +92 -0
  89. package/dist/commands/handoff-check.js +5 -0
  90. package/dist/commands/harvest.js +44 -0
  91. package/dist/commands/import-prd.js +81 -0
  92. package/dist/commands/init-upgrade.js +3085 -0
  93. package/dist/commands/init.js +3630 -0
  94. package/dist/commands/inspect.js +11 -0
  95. package/dist/commands/instructions.js +223 -0
  96. package/dist/commands/knowledge.js +162 -0
  97. package/dist/commands/loop-benchmark.js +72 -0
  98. package/dist/commands/loop.js +251 -0
  99. package/dist/commands/new-task.js +5 -0
  100. package/dist/commands/operator.js +44 -0
  101. package/dist/commands/pi-prompt.js +174 -0
  102. package/dist/commands/pi-reuse-benchmark.js +153 -0
  103. package/dist/commands/plan-list.js +5 -0
  104. package/dist/commands/plan.js +50 -0
  105. package/dist/commands/promote-run.js +29 -0
  106. package/dist/commands/reference-index.js +16 -0
  107. package/dist/commands/run-dag-progress.js +123 -0
  108. package/dist/commands/run-dag.js +22 -0
  109. package/dist/commands/spine.js +38 -0
  110. package/dist/commands/stats.js +113 -0
  111. package/dist/commands/status.js +57 -0
  112. package/dist/commands/study-init.js +192 -0
  113. package/dist/commands/task-advance.js +366 -0
  114. package/dist/commands/task-contract.js +269 -0
  115. package/dist/commands/task-source-prepare.js +479 -0
  116. package/dist/commands/task-status.js +133 -0
  117. package/dist/commands/workflow.js +259 -0
  118. package/dist/commands/worktree-create.js +31 -0
  119. package/dist/commands/worktree-list.js +5 -0
  120. package/dist/commands/worktree-remove.js +26 -0
  121. package/dist/executors/config-core.js +5 -0
  122. package/dist/executors/config.js +2 -0
  123. package/dist/executors/dag-pi-executor.js +1277 -0
  124. package/dist/executors/dag-static-executor.js +42 -0
  125. package/dist/executors/dag.js +3 -0
  126. package/dist/executors/index.js +6 -0
  127. package/dist/executors/model-routing.js +71 -0
  128. package/dist/executors/pi-defaults.js +9 -0
  129. package/dist/executors/pi-event-serializer.js +65 -0
  130. package/dist/executors/pi-executor.js +981 -0
  131. package/dist/executors/pi-playwright-cli-tool.js +1006 -0
  132. package/dist/executors/pi-prompt-transport.js +198 -0
  133. package/dist/executors/pi-reuse-benchmark.js +316 -0
  134. package/dist/executors/pi-runtime-reuse.js +29 -0
  135. package/dist/executors/pi-sdk-executor.js +722 -0
  136. package/dist/executors/pi-sdk.js +1 -0
  137. package/dist/executors/pi-writer-tool-policy.js +266 -0
  138. package/dist/executors/pi.js +3 -0
  139. package/dist/executors/playwright-cli-launcher.js +63 -0
  140. package/dist/executors/process-tree.js +33 -0
  141. package/dist/executors/shell-executor.js +3055 -0
  142. package/dist/executors/shell-presets.js +99 -0
  143. package/dist/executors/shell-verification.js +262 -0
  144. package/dist/executors/shell-write-guard.js +543 -0
  145. package/dist/executors/shell.js +3 -0
  146. package/dist/executors/static.js +1 -0
  147. package/dist/governance/checks.js +437 -0
  148. package/dist/governance/document-index-closure.js +164 -0
  149. package/dist/governance/exec-plans.js +549 -0
  150. package/dist/governance/harness.js +9 -0
  151. package/dist/governance/index.js +3 -0
  152. package/dist/governance/manifest-types.js +254 -0
  153. package/dist/governance/manifest.js +2 -0
  154. package/dist/governance/path-guard.js +69 -0
  155. package/dist/governance/path-guards.js +2 -0
  156. package/dist/governance/profiles.js +3 -0
  157. package/dist/governance/requirement-coverage.js +425 -0
  158. package/dist/governance/skill-safety.js +135 -0
  159. package/dist/governance/spine-audit.js +155 -0
  160. package/dist/infrastructure/evaluation/alias-store.js +199 -0
  161. package/dist/infrastructure/evaluation/campaign-store.js +154 -0
  162. package/dist/infrastructure/evaluation/candidate-store.js +439 -0
  163. package/dist/infrastructure/evaluation/corpus-store.js +181 -0
  164. package/dist/infrastructure/evaluation/experiment-store.js +124 -0
  165. package/dist/infrastructure/evaluation/ignition-store.js +82 -0
  166. package/dist/infrastructure/evaluation/private-verifier-store.js +145 -0
  167. package/dist/infrastructure/evaluation/proposer-store.js +78 -0
  168. package/dist/infrastructure/evaluation/store.js +40 -0
  169. package/dist/infrastructure/harness/active-residue-policy.js +73 -0
  170. package/dist/infrastructure/harness/artifact-store.js +72 -0
  171. package/dist/infrastructure/harness/atomic-write.js +50 -0
  172. package/dist/infrastructure/harness/completed-facts-guard.js +40 -0
  173. package/dist/infrastructure/harness/loop-action-store.js +20 -0
  174. package/dist/infrastructure/harness/loop-store.js +41 -0
  175. package/dist/infrastructure/harness/one-shot-run-store.js +94 -0
  176. package/dist/infrastructure/harness/task-store.js +108 -0
  177. package/dist/records/closeout.js +2 -0
  178. package/dist/records/harvest.js +215 -0
  179. package/dist/records/index.js +3 -0
  180. package/dist/records/one-shot-runs.js +386 -0
  181. package/dist/records/promotion.js +200 -0
  182. package/dist/shared/artifacts-core.js +107 -0
  183. package/dist/shared/artifacts.js +2 -0
  184. package/dist/shared/context-files.js +32 -0
  185. package/dist/shared/context.js +2 -0
  186. package/dist/shared/copy-dir.js +17 -0
  187. package/dist/shared/git-progress.js +172 -0
  188. package/dist/shared/index.js +5 -0
  189. package/dist/shared/logger.js +17 -0
  190. package/dist/shared/one-shot-prompt-args.js +98 -0
  191. package/dist/shared/openspec-spec.js +125 -0
  192. package/dist/shared/operator/capabilities.js +2659 -0
  193. package/dist/shared/operator/command-lifecycle.js +94 -0
  194. package/dist/shared/operator/envelope.js +59 -0
  195. package/dist/shared/operator/index.js +5 -0
  196. package/dist/shared/operator/registry.js +38 -0
  197. package/dist/shared/operator/types.js +5 -0
  198. package/dist/shared/output-truncation.js +37 -0
  199. package/dist/shared/package-metadata.js +530 -0
  200. package/dist/shared/path-refs.js +31 -0
  201. package/dist/shared/pi-retry-settings.js +23 -0
  202. package/dist/shared/playwright-cli-command-policy.js +41 -0
  203. package/dist/shared/preview.js +39 -0
  204. package/dist/shared/prompts.js +26 -0
  205. package/dist/shared/reference-context.js +264 -0
  206. package/dist/shared/resilient-git.js +133 -0
  207. package/dist/shared/runtime-activity.js +6 -0
  208. package/dist/shared/timeout-policy.js +24 -0
  209. package/dist/shared/timeout.js +1 -0
  210. package/dist/shared/types.js +5 -0
  211. package/dist/sidecars/cursor-prompt/executor.js +428 -0
  212. package/dist/sidecars/cursor-prompt/index.js +3 -0
  213. package/dist/sidecars/cursor-prompt/stream.js +121 -0
  214. package/dist/task/config-types.js +235 -0
  215. package/dist/task/config.js +2 -0
  216. package/dist/task/contract/adopt.js +166 -0
  217. package/dist/task/contract/apply.js +326 -0
  218. package/dist/task/contract/canonicalize.js +60 -0
  219. package/dist/task/contract/constants.js +30 -0
  220. package/dist/task/contract/diff.js +177 -0
  221. package/dist/task/contract/hash.js +42 -0
  222. package/dist/task/contract/import-revision.js +96 -0
  223. package/dist/task/contract/index.js +17 -0
  224. package/dist/task/contract/journal.js +155 -0
  225. package/dist/task/contract/lock.js +153 -0
  226. package/dist/task/contract/observe.js +296 -0
  227. package/dist/task/contract/paths.js +19 -0
  228. package/dist/task/contract/project.js +185 -0
  229. package/dist/task/contract/recover.js +312 -0
  230. package/dist/task/contract/request-ledger.js +37 -0
  231. package/dist/task/contract/schema.js +153 -0
  232. package/dist/task/contract/transaction.js +160 -0
  233. package/dist/task/contract/types.js +1 -0
  234. package/dist/task/contract/validate-draft.js +106 -0
  235. package/dist/task/dag-source-paths.js +50 -0
  236. package/dist/task/delegate.js +208 -0
  237. package/dist/task/frontend-preflight.js +131 -0
  238. package/dist/task/frontend-project-capability.js +570 -0
  239. package/dist/task/goal-audit.js +51 -0
  240. package/dist/task/goal-policy.js +8 -0
  241. package/dist/task/goal.js +3 -0
  242. package/dist/task/ids.js +1 -0
  243. package/dist/task/index.js +12 -0
  244. package/dist/task/lifecycle.js +1 -0
  245. package/dist/task/operator/capabilities.js +6 -0
  246. package/dist/task/operator/envelope.js +2 -0
  247. package/dist/task/operator/index.js +5 -0
  248. package/dist/task/operator/registry.js +2 -0
  249. package/dist/task/operator/types.js +1 -0
  250. package/dist/task/paths.js +1 -0
  251. package/dist/task/read-model.js +140 -0
  252. package/dist/task/runtime.js +708 -0
  253. package/dist/task/source-prepare/artifact-meta.js +137 -0
  254. package/dist/task/source-prepare/build-draft.js +232 -0
  255. package/dist/task/source-prepare/completeness.js +248 -0
  256. package/dist/task/source-prepare/index.js +10 -0
  257. package/dist/task/source-prepare/parse-intent.js +496 -0
  258. package/dist/task/source-prepare/path-policy.js +197 -0
  259. package/dist/task/source-prepare/placeholder-paths.js +77 -0
  260. package/dist/task/source-prepare/prepare.js +920 -0
  261. package/dist/task/source-prepare/reference-integrity.js +290 -0
  262. package/dist/task/source-prepare/semantic-intake.js +717 -0
  263. package/dist/task/source-prepare/types.js +7 -0
  264. package/dist/task/source-references.js +228 -0
  265. package/dist/task/source-state.js +1 -0
  266. package/dist/task/state.js +41 -0
  267. package/dist/task/subagent-guidance.js +1 -0
  268. package/dist/task/task-demand-routing.js +443 -0
  269. package/dist/task/workflow-state-types.js +92 -0
  270. package/dist/task/worktree-cleanup.js +140 -0
  271. package/dist/task/worktree.js +388 -0
  272. package/dist/verification/maven/cache.js +142 -0
  273. package/dist/verification/maven/index.js +120 -0
  274. package/dist/verification/maven/plan-commands.js +421 -0
  275. package/dist/verification/maven/pom-static.js +136 -0
  276. package/dist/verification/maven/scope-resolve.js +153 -0
  277. package/dist/verification/maven/stale.js +130 -0
  278. package/dist/verification/maven/types.js +23 -0
  279. package/dist/verification/maven/workspace-graph.js +322 -0
  280. package/dist/worker/cli.js +957 -0
  281. package/dist/worker/closeout/apply.js +73 -0
  282. package/dist/worker/closeout/preview.js +30 -0
  283. package/dist/worker/console/app-data.js +316 -0
  284. package/dist/worker/console/chat/artifact-card.js +30 -0
  285. package/dist/worker/console/chat/assistant-content.js +121 -0
  286. package/dist/worker/console/chat/chat-event-store.js +981 -0
  287. package/dist/worker/console/chat/chat-ui-policy.js +25 -0
  288. package/dist/worker/console/chat/compaction-errors.js +64 -0
  289. package/dist/worker/console/chat/composer-draft-store.js +45 -0
  290. package/dist/worker/console/chat/context-panel.js +54 -0
  291. package/dist/worker/console/chat/contract-apply-receipt-store.js +174 -0
  292. package/dist/worker/console/chat/explore-tools.js +299 -0
  293. package/dist/worker/console/chat/human-gate-card.js +45 -0
  294. package/dist/worker/console/chat/interview-adapter.js +149 -0
  295. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  296. package/dist/worker/console/chat/model-resolver.js +215 -0
  297. package/dist/worker/console/chat/operation-card.js +92 -0
  298. package/dist/worker/console/chat/pi-console-config.js +376 -0
  299. package/dist/worker/console/chat/pi-runtime.js +2446 -0
  300. package/dist/worker/console/chat/repo-browser.js +140 -0
  301. package/dist/worker/console/chat/repo-walk.js +125 -0
  302. package/dist/worker/console/chat/resource-loader.js +59 -0
  303. package/dist/worker/console/chat/resource-preferences-store.js +152 -0
  304. package/dist/worker/console/chat/routes.js +3137 -0
  305. package/dist/worker/console/chat/runtime-context.js +102 -0
  306. package/dist/worker/console/chat/runtime-selection.js +96 -0
  307. package/dist/worker/console/chat/semantic-activity.js +489 -0
  308. package/dist/worker/console/chat/session-store.js +484 -0
  309. package/dist/worker/console/chat/shortcuts.js +217 -0
  310. package/dist/worker/console/chat/tool-adapter.js +131 -0
  311. package/dist/worker/console/chat/tool-preview.js +247 -0
  312. package/dist/worker/console/chat/tools.js +212 -0
  313. package/dist/worker/console/chat/turn-execution-registry.js +82 -0
  314. package/dist/worker/console/chat/turn-process.js +346 -0
  315. package/dist/worker/console/chat/usage.js +165 -0
  316. package/dist/worker/console/chat/workspace-landing.js +103 -0
  317. package/dist/worker/console/dag-confirmation.js +347 -0
  318. package/dist/worker/console/dag-execution-receipt.js +393 -0
  319. package/dist/worker/console/doctor.js +230 -0
  320. package/dist/worker/console/draft-store.js +158 -0
  321. package/dist/worker/console/harness-pi-model-matrix.js +137 -0
  322. package/dist/worker/console/human-gate-token.js +130 -0
  323. package/dist/worker/console/index.js +17 -0
  324. package/dist/worker/console/inspect-split.js +82 -0
  325. package/dist/worker/console/interview/assessment.js +67 -0
  326. package/dist/worker/console/interview/grill-me.js +277 -0
  327. package/dist/worker/console/interview/session.js +100 -0
  328. package/dist/worker/console/interview/tools.js +109 -0
  329. package/dist/worker/console/loopback.js +16 -0
  330. package/dist/worker/console/mutation-gate-receipt-store.js +184 -0
  331. package/dist/worker/console/night-aux-ticker.js +146 -0
  332. package/dist/worker/console/observe-health-match.js +179 -0
  333. package/dist/worker/console/observe-link.js +42 -0
  334. package/dist/worker/console/open-browser.js +134 -0
  335. package/dist/worker/console/operation-run-facts.js +190 -0
  336. package/dist/worker/console/operation-runner.js +353 -0
  337. package/dist/worker/console/operation-sse.js +184 -0
  338. package/dist/worker/console/operation-store.js +169 -0
  339. package/dist/worker/console/operation-wait.js +355 -0
  340. package/dist/worker/console/operator-actions.js +3768 -0
  341. package/dist/worker/console/operator-selection.js +105 -0
  342. package/dist/worker/console/operator-surface-health.js +24 -0
  343. package/dist/worker/console/operator-user-error.js +45 -0
  344. package/dist/worker/console/pi-readiness.js +288 -0
  345. package/dist/worker/console/prd-identity.js +102 -0
  346. package/dist/worker/console/prd-intake-bridge.js +441 -0
  347. package/dist/worker/console/prd-reference-discovery.js +124 -0
  348. package/dist/worker/console/recovery-cta.js +320 -0
  349. package/dist/worker/console/recovery-error-copy.js +198 -0
  350. package/dist/worker/console/recovery-selection.js +107 -0
  351. package/dist/worker/console/repo-fingerprint.js +35 -0
  352. package/dist/worker/console/resolve-dag-run-for-task.js +115 -0
  353. package/dist/worker/console/resource-loader.js +95 -0
  354. package/dist/worker/console/routes.js +475 -0
  355. package/dist/worker/console/security.js +170 -0
  356. package/dist/worker/console/server.js +357 -0
  357. package/dist/worker/console/sibling-controller.js +33 -0
  358. package/dist/worker/console/static/assets/abnfDiagram-N423BO3Z-BmOEx0gA.js +1 -0
  359. package/dist/worker/console/static/assets/arc-BGiVVe8b.js +1 -0
  360. package/dist/worker/console/static/assets/architectureDiagram-T3A2C74G-nDdu4rgJ.js +36 -0
  361. package/dist/worker/console/static/assets/blockDiagram-VBNYF7ZC-Bb5u0BWf.js +132 -0
  362. package/dist/worker/console/static/assets/c4Diagram-5PPSVZJV-BlhC0j94.js +10 -0
  363. package/dist/worker/console/static/assets/channel-Tl-FkUqr.js +1 -0
  364. package/dist/worker/console/static/assets/chunk-2GRJ4B5K-Ai8_qQXI.js +1 -0
  365. package/dist/worker/console/static/assets/chunk-2Q5K7J3B-B9gyCjqX.js +1 -0
  366. package/dist/worker/console/static/assets/chunk-5RXB4S5H-Bzbj3zGH.js +231 -0
  367. package/dist/worker/console/static/assets/chunk-5VM5RSS4-BT415PvT.js +15 -0
  368. package/dist/worker/console/static/assets/chunk-6Q2QTUOP-D_A5_5QL.js +88 -0
  369. package/dist/worker/console/static/assets/chunk-GF5L2VYU-srJkaP3f.js +206 -0
  370. package/dist/worker/console/static/assets/chunk-JWPE2WC7-UYGvEP4l.js +1 -0
  371. package/dist/worker/console/static/assets/chunk-KBJHAD2P-B8luBrrz.js +1 -0
  372. package/dist/worker/console/static/assets/chunk-RYQCIY6F-DN7wXZSq.js +1 -0
  373. package/dist/worker/console/static/assets/chunk-XXDRQBXY-DXKyMuKS.js +1 -0
  374. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BaehtRk4.js +1 -0
  375. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BaehtRk4.js +1 -0
  376. package/dist/worker/console/static/assets/cose-bilkent-JH36ORCC-dqvoynXj.js +1 -0
  377. package/dist/worker/console/static/assets/cynefin-VYW2F7L2--PcyL34A.js +166 -0
  378. package/dist/worker/console/static/assets/cynefinDiagram-MW4NZA55-DvWs_hTY.js +62 -0
  379. package/dist/worker/console/static/assets/cytoscape.esm-yzknjiTM.js +321 -0
  380. package/dist/worker/console/static/assets/dagre-VZM6K2ZE-DxeIxbyO.js +4 -0
  381. package/dist/worker/console/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  382. package/dist/worker/console/static/assets/diagram-7IWD3JNH-DPCG61Of.js +30 -0
  383. package/dist/worker/console/static/assets/diagram-B4RE2ZJO-CmuU50-U.js +3 -0
  384. package/dist/worker/console/static/assets/diagram-LBJQPF4R-B61XLD_v.js +24 -0
  385. package/dist/worker/console/static/assets/diagram-Q27KOJAE-BB7HTRRY.js +24 -0
  386. package/dist/worker/console/static/assets/diagram-UB23O5K3-DvkboAIp.js +41 -0
  387. package/dist/worker/console/static/assets/ebnfDiagram-BXEA7PRR-BdJ_y1fL.js +1 -0
  388. package/dist/worker/console/static/assets/erDiagram-JOGREHBK-9bUBZTRi.js +85 -0
  389. package/dist/worker/console/static/assets/flowDiagram-UKHOOZJN-Dw4L4RBa.js +156 -0
  390. package/dist/worker/console/static/assets/ganttDiagram-PKOTCBZU-Jr82N_QP.js +292 -0
  391. package/dist/worker/console/static/assets/gitGraphDiagram-DS77QQ5N-DAz9wFLr.js +106 -0
  392. package/dist/worker/console/static/assets/graph-DOmOIIwC.js +1 -0
  393. package/dist/worker/console/static/assets/index-B7Y-0g3J.css +1 -0
  394. package/dist/worker/console/static/assets/index-DkCrqaYg.js +325 -0
  395. package/dist/worker/console/static/assets/infoDiagram-6WML65LV-CBB5Np7y.js +2 -0
  396. package/dist/worker/console/static/assets/init-Gi6I4Gst.js +1 -0
  397. package/dist/worker/console/static/assets/ishikawaDiagram-WSZJBQD7-D8IOp94y.js +70 -0
  398. package/dist/worker/console/static/assets/journeyDiagram-NVQOT4AX-Dgd3AHGG.js +139 -0
  399. package/dist/worker/console/static/assets/kanban-definition-27J2QSJJ-BmYOxLE9.js +89 -0
  400. package/dist/worker/console/static/assets/layout-D-LzfAck.js +1 -0
  401. package/dist/worker/console/static/assets/linear-DfYTEF6J.js +1 -0
  402. package/dist/worker/console/static/assets/map-DxJ2ADlA.js +1 -0
  403. package/dist/worker/console/static/assets/mermaid.core-BjuFPRqa.js +308 -0
  404. package/dist/worker/console/static/assets/mindmap-definition-FAOFIHXS-CLyUViGd.js +96 -0
  405. package/dist/worker/console/static/assets/ordinal-Cboi1Yqb.js +1 -0
  406. package/dist/worker/console/static/assets/pegDiagram-VL7TDLO6-BvX2Di4-.js +1 -0
  407. package/dist/worker/console/static/assets/pieDiagram-7S7Q4E2Y-BFj0_OLv.js +39 -0
  408. package/dist/worker/console/static/assets/quadrantDiagram-CIZ2JOQS-BIUrD_hx.js +7 -0
  409. package/dist/worker/console/static/assets/railroadDiagram-AXF67PYL-CHBB48fj.js +1 -0
  410. package/dist/worker/console/static/assets/requirementDiagram-LRYGKXZP-sJrYw2Sf.js +84 -0
  411. package/dist/worker/console/static/assets/sankeyDiagram-W5VNT64P-BzHH6fPR.js +40 -0
  412. package/dist/worker/console/static/assets/sequenceDiagram-SI44F4Z6-7QCnelCt.js +162 -0
  413. package/dist/worker/console/static/assets/sizeCapture-X5ZJPWSS-sDYSxn72.js +1 -0
  414. package/dist/worker/console/static/assets/stateDiagram-OKZ733FA-XpiUdwzm.js +1 -0
  415. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-eEhq2A8Y.js +1 -0
  416. package/dist/worker/console/static/assets/swimlanes-SLNWSIFB-DvqgNPzD.js +2 -0
  417. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-la8AC7fo.js +8 -0
  418. package/dist/worker/console/static/assets/timeline-definition-Z64GVDOM-2TyOYNx0.js +120 -0
  419. package/dist/worker/console/static/assets/vennDiagram-T6HMQDX7-B-nM6wM2.js +34 -0
  420. package/dist/worker/console/static/assets/wardleyDiagram-T6FBY63Y-B86wPdbr.js +78 -0
  421. package/dist/worker/console/static/assets/xychartDiagram-ELKLHX3M-Dr1C4AQz.js +7 -0
  422. package/dist/worker/console/static/favicon.svg +37 -0
  423. package/dist/worker/console/static/fonts/katex/KaTeX_AMS-Regular.woff2 +0 -0
  424. package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Bold.woff2 +0 -0
  425. package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Regular.woff2 +0 -0
  426. package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Bold.woff2 +0 -0
  427. package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Regular.woff2 +0 -0
  428. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Bold.woff2 +0 -0
  429. package/dist/worker/console/static/fonts/katex/KaTeX_Main-BoldItalic.woff2 +0 -0
  430. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Italic.woff2 +0 -0
  431. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Regular.woff2 +0 -0
  432. package/dist/worker/console/static/fonts/katex/KaTeX_Math-BoldItalic.woff2 +0 -0
  433. package/dist/worker/console/static/fonts/katex/KaTeX_Math-Italic.woff2 +0 -0
  434. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Bold.woff2 +0 -0
  435. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Italic.woff2 +0 -0
  436. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Regular.woff2 +0 -0
  437. package/dist/worker/console/static/fonts/katex/KaTeX_Script-Regular.woff2 +0 -0
  438. package/dist/worker/console/static/fonts/katex/KaTeX_Size1-Regular.woff2 +0 -0
  439. package/dist/worker/console/static/fonts/katex/KaTeX_Size2-Regular.woff2 +0 -0
  440. package/dist/worker/console/static/fonts/katex/KaTeX_Size3-Regular.woff2 +0 -0
  441. package/dist/worker/console/static/fonts/katex/KaTeX_Size4-Regular.woff2 +0 -0
  442. package/dist/worker/console/static/fonts/katex/KaTeX_Typewriter-Regular.woff2 +0 -0
  443. package/dist/worker/console/static/index.html +15 -0
  444. package/dist/worker/console/static-src/app/console-types.js +184 -0
  445. package/dist/worker/console/static-src/app/useConsoleShell.js +101 -0
  446. package/dist/worker/console/static-src/app/useOperatorActions.js +324 -0
  447. package/dist/worker/console/static-src/app/usePrdImport.js +172 -0
  448. package/dist/worker/console/static-src/app/useRecoveryActions.js +289 -0
  449. package/dist/worker/console/static-src/app/useRecoveryConsole.js +425 -0
  450. package/dist/worker/console/static-src/app/useTaskWizard.js +296 -0
  451. package/dist/worker/console/static-src/chat-markdown-security.js +38 -0
  452. package/dist/worker/console/static-src/chat-view-types.js +2 -0
  453. package/dist/worker/console/static-src/night/night-types.js +24 -0
  454. package/dist/worker/console/static-src/night/useNightBoard.js +94 -0
  455. package/dist/worker/console/static-src/night/useNightWizard.js +171 -0
  456. package/dist/worker/console/static-src/night-prepare-result.js +118 -0
  457. package/dist/worker/console/static-src/operator-chat/activity-journey.js +125 -0
  458. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +73 -0
  459. package/dist/worker/console/static-src/operator-chat/activity-references.js +20 -0
  460. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +420 -0
  461. package/dist/worker/console/static-src/operator-chat/format.js +71 -0
  462. package/dist/worker/console/static-src/operator-chat/landing-density.js +23 -0
  463. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  464. package/dist/worker/console/static-src/operator-chat/refs.js +86 -0
  465. package/dist/worker/console/static-src/operator-chat/resource-auto-invocation.js +91 -0
  466. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  467. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
  468. package/dist/worker/console/static-src/operator-chat/session-title-watcher.js +128 -0
  469. package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
  470. package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +24 -0
  471. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +141 -0
  472. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +38 -0
  473. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +77 -0
  474. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +690 -0
  475. package/dist/worker/console/static-src/operator-chat/turn-submission.js +158 -0
  476. package/dist/worker/console/static-src/operator-chat/types.js +1 -0
  477. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +59 -0
  478. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +495 -0
  479. package/dist/worker/console/static-src/operator-chat/useChatStream.js +780 -0
  480. package/dist/worker/console/static-src/operator-chat/useChatThread.js +280 -0
  481. package/dist/worker/console/static-src/operator-chat/useComposer.js +257 -0
  482. package/dist/worker/console/static-src/operator-chat/useInterview.js +108 -0
  483. package/dist/worker/console/static-src/operator-chat/useOverlayFocus.js +84 -0
  484. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +148 -0
  485. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +358 -0
  486. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
  487. package/dist/worker/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
  488. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +35 -0
  489. package/dist/worker/console/static-src/prd-file-import.js +218 -0
  490. package/dist/worker/console/vite.config.js +27 -0
  491. package/dist/worker/console/workflow-kinds.js +46 -0
  492. package/dist/worker/delivery/final-verification.js +302 -0
  493. package/dist/worker/delivery/git-transaction.js +497 -0
  494. package/dist/worker/delivery/package.js +537 -0
  495. package/dist/worker/delivery/verification-bundle.js +544 -0
  496. package/dist/worker/feature/acceptance-policy.js +227 -0
  497. package/dist/worker/feature/advance.js +301 -0
  498. package/dist/worker/feature/decision-loader.js +99 -0
  499. package/dist/worker/feature/discover.js +14 -0
  500. package/dist/worker/feature/doctor.js +223 -0
  501. package/dist/worker/feature/fullstack-validate.js +337 -0
  502. package/dist/worker/feature/next-action.js +140 -0
  503. package/dist/worker/feature/profile-schema.js +47 -0
  504. package/dist/worker/feature/ready-plan-projection.js +82 -0
  505. package/dist/worker/feature/reducer.js +137 -0
  506. package/dist/worker/feature/review.js +678 -0
  507. package/dist/worker/feature/run.js +390 -0
  508. package/dist/worker/feature/scaffold.js +812 -0
  509. package/dist/worker/feature/types.js +1 -0
  510. package/dist/worker/follow-up/approve.js +273 -0
  511. package/dist/worker/follow-up/factory.js +234 -0
  512. package/dist/worker/follow-up/paths.js +25 -0
  513. package/dist/worker/follow-up/policy.js +26 -0
  514. package/dist/worker/follow-up/schema.js +93 -0
  515. package/dist/worker/follow-up/store.js +96 -0
  516. package/dist/worker/loop-agent/command-result.js +1 -0
  517. package/dist/worker/loop-agent/controller-protocol.js +143 -0
  518. package/dist/worker/loop-agent/loop-agent-client.js +489 -0
  519. package/dist/worker/loop-agent/parse-json.js +14 -0
  520. package/dist/worker/materialize/harness-task-lifecycle-probe.js +126 -0
  521. package/dist/worker/materialize/harness-task-lineage.js +220 -0
  522. package/dist/worker/materialize/harness-task-materializer.js +648 -0
  523. package/dist/worker/metrics/projector.js +139 -0
  524. package/dist/worker/observability/dag-execution-trajectory.js +591 -0
  525. package/dist/worker/observability/event-history.js +216 -0
  526. package/dist/worker/observability/event-store.js +83 -0
  527. package/dist/worker/observability/events.js +79 -0
  528. package/dist/worker/observability/interrupt-eligibility.js +264 -0
  529. package/dist/worker/observability/progress-composite.js +34 -0
  530. package/dist/worker/observability/read-model.js +2499 -0
  531. package/dist/worker/observability/snapshot-store.js +43 -0
  532. package/dist/worker/observability/types.js +1 -0
  533. package/dist/worker/observe/dag-node-execution-output.js +180 -0
  534. package/dist/worker/observe/dag-run-artifacts.js +90 -0
  535. package/dist/worker/observe/health.js +58 -0
  536. package/dist/worker/observe/night-jobs.js +104 -0
  537. package/dist/worker/observe/node-input.js +441 -0
  538. package/dist/worker/observe/paths.js +174 -0
  539. package/dist/worker/observe/routes.js +1077 -0
  540. package/dist/worker/observe/server.js +117 -0
  541. package/dist/worker/observe/spec-evidence.js +398 -0
  542. package/dist/worker/observe/static/api.js +117 -0
  543. package/dist/worker/observe/static/app.js +169 -0
  544. package/dist/worker/observe/static/constants.js +182 -0
  545. package/dist/worker/observe/static/copy.js +67 -0
  546. package/dist/worker/observe/static/custom-select.js +567 -0
  547. package/dist/worker/observe/static/dag-edge-routing.js +368 -0
  548. package/dist/worker/observe/static/dag-helpers.js +161 -0
  549. package/dist/worker/observe/static/dag-history-labels.js +95 -0
  550. package/dist/worker/observe/static/dag-layout.d.ts +36 -0
  551. package/dist/worker/observe/static/dag-layout.js +163 -0
  552. package/dist/worker/observe/static/dag-model.js +145 -0
  553. package/dist/worker/observe/static/dom.js +220 -0
  554. package/dist/worker/observe/static/favicon.svg +37 -0
  555. package/dist/worker/observe/static/format-pool.d.ts +71 -0
  556. package/dist/worker/observe/static/format-pool.js +153 -0
  557. package/dist/worker/observe/static/format.js +347 -0
  558. package/dist/worker/observe/static/index.html +409 -0
  559. package/dist/worker/observe/static/kpi.js +78 -0
  560. package/dist/worker/observe/static/markdown-render.js +124 -0
  561. package/dist/worker/observe/static/operator-chrome.css +517 -0
  562. package/dist/worker/observe/static/operator-chrome.d.ts +83 -0
  563. package/dist/worker/observe/static/operator-chrome.js +623 -0
  564. package/dist/worker/observe/static/relations.js +133 -0
  565. package/dist/worker/observe/static/router.js +156 -0
  566. package/dist/worker/observe/static/run-processing.js +148 -0
  567. package/dist/worker/observe/static/shell-chrome.js +65 -0
  568. package/dist/worker/observe/static/state.js +496 -0
  569. package/dist/worker/observe/static/styles.css +3567 -0
  570. package/dist/worker/observe/static/views/batch.js +227 -0
  571. package/dist/worker/observe/static/views/dag-graph.js +508 -0
  572. package/dist/worker/observe/static/views/dag-inspector.js +1722 -0
  573. package/dist/worker/observe/static/views/dag-trajectory.js +313 -0
  574. package/dist/worker/observe/static/views/dag.js +483 -0
  575. package/dist/worker/observe/static/views/dags.js +877 -0
  576. package/dist/worker/observe/static/views/dashboard.js +747 -0
  577. package/dist/worker/observe/static/views/failures.js +143 -0
  578. package/dist/worker/observe/static/views/feature.js +492 -0
  579. package/dist/worker/observe/static/views/night.js +201 -0
  580. package/dist/worker/observe/static/views/pool.js +708 -0
  581. package/dist/worker/observe/static/views/run.js +453 -0
  582. package/dist/worker/observe/static/views/session-timeline.js +771 -0
  583. package/dist/worker/observe/static/views/shell.js +7 -0
  584. package/dist/worker/observe/static/views/task.js +315 -0
  585. package/dist/worker/observe/static/views/timeline.js +163 -0
  586. package/dist/worker/outcomes/adapters.js +153 -0
  587. package/dist/worker/outcomes/declared-artifacts.js +103 -0
  588. package/dist/worker/outcomes/evidence-tokens.js +29 -0
  589. package/dist/worker/outcomes/gate.js +40 -0
  590. package/dist/worker/outcomes/projector.js +227 -0
  591. package/dist/worker/outcomes/registry.js +1 -0
  592. package/dist/worker/outcomes/store.js +131 -0
  593. package/dist/worker/outcomes/types.js +79 -0
  594. package/dist/worker/pool/attempt-identity.js +41 -0
  595. package/dist/worker/pool/attempt-lease.js +184 -0
  596. package/dist/worker/pool/attempt-transition.js +210 -0
  597. package/dist/worker/pool/begin-attempt-with-lease.js +26 -0
  598. package/dist/worker/pool/begin-attempt.js +35 -0
  599. package/dist/worker/pool/doctor.js +165 -0
  600. package/dist/worker/pool/failure-routing.js +182 -0
  601. package/dist/worker/pool/migrate-state.js +303 -0
  602. package/dist/worker/pool/reconcile.js +285 -0
  603. package/dist/worker/pool/recovery-decision.js +163 -0
  604. package/dist/worker/pool/run-owner-store.js +126 -0
  605. package/dist/worker/pool/run-store.js +360 -0
  606. package/dist/worker/pool/runtime-reconcile-inventory.js +127 -0
  607. package/dist/worker/pool/state-projection.js +57 -0
  608. package/dist/worker/pool/types.js +17 -0
  609. package/dist/worker/pool/validation.js +144 -0
  610. package/dist/worker/preflight.js +157 -0
  611. package/dist/worker/profile-mapping.js +76 -0
  612. package/dist/worker/progress-reporter.js +63 -0
  613. package/dist/worker/report/morning-report.js +155 -0
  614. package/dist/worker/repos/repo-resolver.js +23 -0
  615. package/dist/worker/run-task/execute-prepared-task.js +178 -0
  616. package/dist/worker/run-task/run-task.js +912 -0
  617. package/dist/worker/runner/run-ready.js +561 -0
  618. package/dist/worker/runner/single-task-attempt.js +176 -0
  619. package/dist/worker/scheduler/admission.js +547 -0
  620. package/dist/worker/scheduler/auto-followup.js +99 -0
  621. package/dist/worker/scheduler/cli.js +644 -0
  622. package/dist/worker/scheduler/clock-install/darwin-launchd.js +238 -0
  623. package/dist/worker/scheduler/clock-install/linux-systemd-user.js +224 -0
  624. package/dist/worker/scheduler/clock-install/types.js +1 -0
  625. package/dist/worker/scheduler/clock-install/win32-schtasks.js +227 -0
  626. package/dist/worker/scheduler/clock-install.js +284 -0
  627. package/dist/worker/scheduler/clock.js +420 -0
  628. package/dist/worker/scheduler/dispatcher.js +503 -0
  629. package/dist/worker/scheduler/doctor.js +431 -0
  630. package/dist/worker/scheduler/evidence.js +170 -0
  631. package/dist/worker/scheduler/git-base.js +90 -0
  632. package/dist/worker/scheduler/index.js +25 -0
  633. package/dist/worker/scheduler/lease.js +114 -0
  634. package/dist/worker/scheduler/lifecycle.js +348 -0
  635. package/dist/worker/scheduler/lock.js +80 -0
  636. package/dist/worker/scheduler/morning-window.js +191 -0
  637. package/dist/worker/scheduler/night-git-finalizer.js +114 -0
  638. package/dist/worker/scheduler/night-harvest.js +412 -0
  639. package/dist/worker/scheduler/paths.js +92 -0
  640. package/dist/worker/scheduler/prepared-attempt-recovery.js +471 -0
  641. package/dist/worker/scheduler/recovery.js +277 -0
  642. package/dist/worker/scheduler/reservation.js +146 -0
  643. package/dist/worker/scheduler/retry.js +199 -0
  644. package/dist/worker/scheduler/scheduler-loop.js +272 -0
  645. package/dist/worker/scheduler/store.js +275 -0
  646. package/dist/worker/scheduler/traceability.js +54 -0
  647. package/dist/worker/scheduler/trigger.js +258 -0
  648. package/dist/worker/scheduler/types.js +369 -0
  649. package/dist/worker/scheduler/workspace-adapter.js +128 -0
  650. package/dist/worker/task-graph/acceptance-schema.js +40 -0
  651. package/dist/worker/task-graph/ready-planner.js +267 -0
  652. package/dist/worker/task-graph/ready-queue.js +23 -0
  653. package/dist/worker/task-graph/task-graph-schema.js +59 -0
  654. package/dist/worker/task-graph/types.js +1 -0
  655. package/dist/worker/task-graph/validate.js +293 -0
  656. package/dist/worker/task-spec/complexity-mapping.js +8 -0
  657. package/dist/worker/task-spec/schema.js +125 -0
  658. package/dist/worker/task-spec/types.js +1 -0
  659. package/dist/worker/task-spec/validate.js +391 -0
  660. package/dist/worker/task-spec/workflow-routing.js +149 -0
  661. package/dist/workflows/dag/authoring.js +8 -0
  662. package/dist/workflows/dag/authority-surface.js +138 -0
  663. package/dist/workflows/dag/backend-test-analysis-contract.js +507 -0
  664. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +1985 -0
  665. package/dist/workflows/dag/backend-test-case-manifest.js +638 -0
  666. package/dist/workflows/dag/backend-test-classification-contract.js +38 -0
  667. package/dist/workflows/dag/backend-test-contract-envelope.js +205 -0
  668. package/dist/workflows/dag/backend-test-coverage-contract.js +202 -0
  669. package/dist/workflows/dag/backend-test-execution-contract.js +715 -0
  670. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  671. package/dist/workflows/dag/backend-test-intake-context.js +178 -0
  672. package/dist/workflows/dag/backend-test-layout.js +133 -0
  673. package/dist/workflows/dag/backend-test-markdown-workflow.js +1984 -0
  674. package/dist/workflows/dag/backend-test-module-stem.js +26 -0
  675. package/dist/workflows/dag/backend-test-pytest-collection.js +859 -0
  676. package/dist/workflows/dag/backend-test-result-contract.js +1030 -0
  677. package/dist/workflows/dag/backend-test-scenario-param.js +1670 -0
  678. package/dist/workflows/dag/backend-test-scenario-partitions.js +249 -0
  679. package/dist/workflows/dag/backend-test-semantic-review-contract.js +36 -0
  680. package/dist/workflows/dag/backend-test-stability-contract.js +57 -0
  681. package/dist/workflows/dag/backend-test-writer-completeness.js +1058 -0
  682. package/dist/workflows/dag/budget-enforcement.js +67 -0
  683. package/dist/workflows/dag/canvas-observer.js +474 -0
  684. package/dist/workflows/dag/context-policy.js +137 -0
  685. package/dist/workflows/dag/contract-output-registry.js +14 -0
  686. package/dist/workflows/dag/contract-validator-registrations.js +10 -0
  687. package/dist/workflows/dag/controller-identity.js +104 -0
  688. package/dist/workflows/dag/convergence/controller.js +596 -0
  689. package/dist/workflows/dag/decision-envelope.js +557 -0
  690. package/dist/workflows/dag/decision-evidence.js +153 -0
  691. package/dist/workflows/dag/decision-gates.js +1 -0
  692. package/dist/workflows/dag/dynamic-runtime/condition.js +48 -0
  693. package/dist/workflows/dag/dynamic-runtime/loop-until.js +161 -0
  694. package/dist/workflows/dag/dynamic-runtime/map.js +435 -0
  695. package/dist/workflows/dag/dynamic-runtime/reduction.js +72 -0
  696. package/dist/workflows/dag/dynamic-runtime/shared.js +203 -0
  697. package/dist/workflows/dag/event-observer.js +132 -0
  698. package/dist/workflows/dag/executor-registry.js +23 -0
  699. package/dist/workflows/dag/facts.js +4 -0
  700. package/dist/workflows/dag/failure-category.js +118 -0
  701. package/dist/workflows/dag/failure-routing.js +105 -0
  702. package/dist/workflows/dag/final-verification.js +180 -0
  703. package/dist/workflows/dag/frontend-implementation-contract.js +1911 -0
  704. package/dist/workflows/dag/frontend-lint-baseline.js +474 -0
  705. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  706. package/dist/workflows/dag/frontend-prewrite-gate.js +1011 -0
  707. package/dist/workflows/dag/frontend-project-capability.js +1 -0
  708. package/dist/workflows/dag/frontend-recovery-plan.js +73 -0
  709. package/dist/workflows/dag/frontend-recovery-root-manifest.js +123 -0
  710. package/dist/workflows/dag/frontend-recovery-run.js +539 -0
  711. package/dist/workflows/dag/frontend-repair.js +541 -0
  712. package/dist/workflows/dag/frontend-review-context.js +116 -0
  713. package/dist/workflows/dag/frontend-risk.js +161 -0
  714. package/dist/workflows/dag/frontend-test-case-checklist.js +343 -0
  715. package/dist/workflows/dag/frontend-test-case-manifest.js +104 -0
  716. package/dist/workflows/dag/frontend-test-case-quality.js +105 -0
  717. package/dist/workflows/dag/frontend-test-html-report.js +174 -0
  718. package/dist/workflows/dag/frontend-test-l5-report.js +177 -0
  719. package/dist/workflows/dag/frontend-test-result-contract.js +487 -0
  720. package/dist/workflows/dag/frontend-verification-trace.js +275 -0
  721. package/dist/workflows/dag/frontend-worktree-diff.js +201 -0
  722. package/dist/workflows/dag/frontend-writer-recovery.js +106 -0
  723. package/dist/workflows/dag/frontend-writer-rollback.js +821 -0
  724. package/dist/workflows/dag/governance-constants.js +5 -0
  725. package/dist/workflows/dag/governance-profile.js +413 -0
  726. package/dist/workflows/dag/index.js +6 -0
  727. package/dist/workflows/dag/init-hybrid.js +7345 -0
  728. package/dist/workflows/dag/interrupt-request.js +559 -0
  729. package/dist/workflows/dag/knowledge-curator.js +165 -0
  730. package/dist/workflows/dag/l5-report-metrics.js +33 -0
  731. package/dist/workflows/dag/lifecycle.js +855 -0
  732. package/dist/workflows/dag/liveness-policy.js +251 -0
  733. package/dist/workflows/dag/node-execution.js +1081 -0
  734. package/dist/workflows/dag/observer-compose.js +52 -0
  735. package/dist/workflows/dag/output-protocol.js +356 -0
  736. package/dist/workflows/dag/project-governance-context.js +514 -0
  737. package/dist/workflows/dag/prompt-source.js +88 -0
  738. package/dist/workflows/dag/prompt.js +348 -0
  739. package/dist/workflows/dag/reconcile-run.js +145 -0
  740. package/dist/workflows/dag/reconcile-tasks.js +404 -0
  741. package/dist/workflows/dag/recovery-recommendation.js +329 -0
  742. package/dist/workflows/dag/repair-artifact.js +363 -0
  743. package/dist/workflows/dag/report.js +1229 -0
  744. package/dist/workflows/dag/rerun-plan.js +614 -0
  745. package/dist/workflows/dag/rerun-run.js +556 -0
  746. package/dist/workflows/dag/rerun-task.js +370 -0
  747. package/dist/workflows/dag/retry-policy.js +263 -0
  748. package/dist/workflows/dag/run-store.js +41 -0
  749. package/dist/workflows/dag/runner.js +1069 -0
  750. package/dist/workflows/dag/runtime-contract.js +87 -0
  751. package/dist/workflows/dag/runtime.js +5 -0
  752. package/dist/workflows/dag/scheduler.js +497 -0
  753. package/dist/workflows/dag/sdd-embedded.js +128 -0
  754. package/dist/workflows/dag/skill-instructions.js +478 -0
  755. package/dist/workflows/dag/skill-snapshot.js +553 -0
  756. package/dist/workflows/dag/skills.js +41 -0
  757. package/dist/workflows/dag/spec.js +3 -0
  758. package/dist/workflows/dag/task-contract-binding.js +138 -0
  759. package/dist/workflows/dag/task-demand-routing.js +1 -0
  760. package/dist/workflows/dag/topo.js +30 -0
  761. package/dist/workflows/dag/types.js +1145 -0
  762. package/dist/workflows/dag/upstream-artifacts.js +98 -0
  763. package/dist/workflows/dag/validate.js +998 -0
  764. package/dist/workflows/dag/workspace-checkpoint.js +186 -0
  765. package/dist/workflows/dynamic/artifacts.js +65 -0
  766. package/dist/workflows/dynamic/compile.js +371 -0
  767. package/dist/workflows/dynamic/compileTypes.js +1 -0
  768. package/dist/workflows/dynamic/errors.js +5 -0
  769. package/dist/workflows/dynamic/index.js +7 -0
  770. package/dist/workflows/dynamic/profiles.js +156 -0
  771. package/dist/workflows/dynamic/spec.js +115 -0
  772. package/dist/workflows/dynamic/validate.js +275 -0
  773. package/dist/workflows/loop/actions/dag-action.js +129 -0
  774. package/dist/workflows/loop/actions/pi-review.js +267 -0
  775. package/dist/workflows/loop/actions/shared.js +157 -0
  776. package/dist/workflows/loop/actions/shell-verify.js +82 -0
  777. package/dist/workflows/loop/actions/types.js +1 -0
  778. package/dist/workflows/loop/actions/workflow-action.js +255 -0
  779. package/dist/workflows/loop/actions.js +160 -0
  780. package/dist/workflows/loop/benchmark.js +510 -0
  781. package/dist/workflows/loop/closeout.js +135 -0
  782. package/dist/workflows/loop/context.js +47 -0
  783. package/dist/workflows/loop/events.js +26 -0
  784. package/dist/workflows/loop/hash.js +32 -0
  785. package/dist/workflows/loop/index.js +8 -0
  786. package/dist/workflows/loop/paths.js +17 -0
  787. package/dist/workflows/loop/policy/auto-policy.js +112 -0
  788. package/dist/workflows/loop/policy/path-patterns.js +13 -0
  789. package/dist/workflows/loop/rounds.js +81 -0
  790. package/dist/workflows/loop/signals.js +52 -0
  791. package/dist/workflows/loop/state.js +116 -0
  792. package/dist/workflows/loop/templates.js +54 -0
  793. package/dist/workflows/loop/types.js +28 -0
  794. package/docs/README.md +82 -0
  795. package/docs/architecture/README.md +35 -0
  796. package/docs/architecture/dag-execution.md +165 -0
  797. package/docs/architecture/evolution.md +108 -0
  798. package/docs/architecture/facts-and-state.md +74 -0
  799. package/docs/architecture/runtime-boundaries.md +226 -0
  800. package/docs/architecture/system-overview.md +100 -0
  801. package/docs/architecture/worker-and-feature.md +122 -0
  802. package/docs/governance/README.md +18 -0
  803. package/docs/governance/harness-methodology-debugging.md +177 -0
  804. package/docs/governance/harness-methodology-tdd.md +130 -0
  805. package/docs/governance/harness-methodology-verification.md +27 -0
  806. package/docs/init-surface.manifest.json +358 -0
  807. package/docs/operations/README.md +15 -0
  808. package/docs/operations/local-development-environment.md +77 -0
  809. package/docs/skills/README.md +7 -0
  810. package/docs/skills/vetted-skill-registry.md +49 -0
  811. package/docs/templates/README.md +67 -0
  812. package/docs/templates/adr.md +60 -0
  813. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -0
  814. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -0
  815. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -0
  816. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -0
  817. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -0
  818. package/docs/templates/agent-dag-report.schema.json +473 -0
  819. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -0
  820. package/docs/templates/agent-dag.base.json +190 -0
  821. package/docs/templates/agent-dag.final-verification.json +185 -0
  822. package/docs/templates/agent-dag.schema.json +590 -0
  823. package/docs/templates/agent-dag.supervised-implementation.json +640 -0
  824. package/docs/templates/agent-worker-production-readiness-checklist.md +47 -0
  825. package/docs/templates/backend-test-analysis.schema.json +37 -0
  826. package/docs/templates/backend-test-case-manifest.schema.json +223 -0
  827. package/docs/templates/backend-test-dag.classify.prompt.md +75 -0
  828. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +41 -0
  829. package/docs/templates/backend-test-dag.json +671 -0
  830. package/docs/templates/backend-test-dag.retrospect.prompt.md +165 -0
  831. package/docs/templates/backend-test-dag.review-cases.prompt.md +36 -0
  832. package/docs/templates/backend-test-execution.schema.json +138 -0
  833. package/docs/templates/backend-test-result.schema.json +99 -0
  834. package/docs/templates/branch-merge-report.md +115 -0
  835. package/docs/templates/evaluation/agents-map-slim-v1.candidate.json +9 -0
  836. package/docs/templates/evaluation/agents-map-slim-v1.md +87 -0
  837. package/docs/templates/evaluation/agents-map-verbose-v0.candidate.json +9 -0
  838. package/docs/templates/evaluation/agents-map-verbose-v0.md +153 -0
  839. package/docs/templates/evaluation/campaign-budget-v1.json +12 -0
  840. package/docs/templates/evaluation/campaign-dogfood-v0.json +24 -0
  841. package/docs/templates/evaluation/campaign-evidence-v1.json +44 -0
  842. package/docs/templates/evaluation/context-policy-baseline-v1.json +17 -0
  843. package/docs/templates/evaluation/context-policy-role-specialized-v1.json +28 -0
  844. package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +118 -0
  845. package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +21 -0
  846. package/docs/templates/evaluation/matrix-fixture-v1.json +10 -0
  847. package/docs/templates/evaluation/private-verifier-dogfood-v0.json +16 -0
  848. package/docs/templates/exec-plan.md +66 -0
  849. package/docs/templates/feature-spec.md +53 -0
  850. package/docs/templates/frontend-design-contract.md +68 -0
  851. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -0
  852. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -0
  853. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -0
  854. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -0
  855. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -0
  856. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -0
  857. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -0
  858. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -0
  859. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -0
  860. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -0
  861. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -0
  862. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -0
  863. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -0
  864. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -0
  865. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -0
  866. package/docs/templates/frontend-eval/metrics.md +138 -0
  867. package/docs/templates/frontend-eval/smoke-targets.md +53 -0
  868. package/docs/templates/frontend-implementation-contract.schema.json +35 -0
  869. package/docs/templates/frontend-task-constraints.md +62 -0
  870. package/docs/templates/frontend-task-requirement.md +104 -0
  871. package/docs/templates/frontend-test-case-checklist.md +44 -0
  872. package/docs/templates/frontend-test-case.example.md +143 -0
  873. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +63 -0
  874. package/docs/templates/frontend-test-dag.json +409 -0
  875. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +21 -0
  876. package/docs/templates/frontend-test-dag.retrospect.prompt.md +5 -0
  877. package/docs/templates/frontend-test-dag.review-cases.prompt.md +5 -0
  878. package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -0
  879. package/docs/templates/frontend-test-standard-scenarios.v1.json +159 -0
  880. package/docs/templates/harness.schema.json +351 -0
  881. package/docs/templates/hybrid-dag.json +188 -0
  882. package/docs/templates/init-evolution-review.md +35 -0
  883. package/docs/templates/init-managed-agents.md +157 -0
  884. package/docs/templates/interactive-ui-round2-experiment.md +66 -0
  885. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
  886. package/docs/templates/knowledge-sync-dag.json +178 -0
  887. package/docs/templates/knowledge-sync-draft.schema.json +71 -0
  888. package/docs/templates/product-line/AGENTS.md +9 -0
  889. package/docs/templates/product-line/README.md +42 -0
  890. package/docs/templates/product-line/acceptance.yaml +23 -0
  891. package/docs/templates/product-line/closeout.yaml +9 -0
  892. package/docs/templates/product-line/design.md +13 -0
  893. package/docs/templates/product-line/feature-scaffold-batch.example.yaml +31 -0
  894. package/docs/templates/product-line/feature.yaml +11 -0
  895. package/docs/templates/product-line/links.md +10 -0
  896. package/docs/templates/product-line/requirement.md +17 -0
  897. package/docs/templates/product-line/scaffold-samples/README.md +36 -0
  898. package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +16 -0
  899. package/docs/templates/product-line/scaffold-samples/backend-only/design.md +14 -0
  900. package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +14 -0
  901. package/docs/templates/product-line/scaffold-samples/backend-only/requirement.md +6 -0
  902. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/BE-IMPL-001.yaml +83 -0
  903. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/task-graph.yaml +14 -0
  904. package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +18 -0
  905. package/docs/templates/product-line/scaffold-samples/fe-with-api/design.md +18 -0
  906. package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +14 -0
  907. package/docs/templates/product-line/scaffold-samples/fe-with-api/requirement.md +6 -0
  908. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/BE-IMPL-001.yaml +84 -0
  909. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/CONTRACT-001.yaml +84 -0
  910. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/FE-IMPL-001.yaml +86 -0
  911. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/task-graph.yaml +40 -0
  912. package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +16 -0
  913. package/docs/templates/product-line/scaffold-samples/frontend-only/design.md +14 -0
  914. package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +14 -0
  915. package/docs/templates/product-line/scaffold-samples/frontend-only/requirement.md +6 -0
  916. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/FE-IMPL-001.yaml +85 -0
  917. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/task-graph.yaml +14 -0
  918. package/docs/templates/product-line/task-graph.yaml +23 -0
  919. package/docs/templates/product-line/task.yaml +68 -0
  920. package/docs/templates/product-line/test-plan.md +7 -0
  921. package/docs/templates/production-readiness-checklist.md +57 -0
  922. package/docs/templates/progress-log.md +27 -0
  923. package/docs/templates/project-start-checklist.md +9 -0
  924. package/docs/templates/qa-report.md +48 -0
  925. package/docs/templates/sprint-contract.md +29 -0
  926. package/docs/templates/worker-dogfood-evidence.md +80 -0
  927. package/docs/templates/worker-dogfood-setup.md +68 -0
  928. package/examples/decision-gate-agent-dag.json +177 -0
  929. package/examples/example-dag.json +46 -0
  930. package/examples/hybrid-loop-agent-dag.json +189 -0
  931. package/examples/l5-report-coms-process-definition.html +322 -0
  932. package/examples/l5-report-demo.html +79 -0
  933. package/harness.json +90 -0
  934. package/package.json +98 -0
  935. package/scripts/kb-bootstrap-init-skeleton.sh +241 -0
  936. package/scripts/kb-graph-incremental-prepare.mjs +386 -0
  937. package/scripts/kb-graph-materialize.mjs +105 -0
  938. package/scripts/kb-graph-promote.mjs +164 -0
  939. package/scripts/kb-query.mjs +554 -0
  940. package/skills/agent-worker/SKILL.md +48 -0
  941. package/skills/agent-worker/references/agent-worker-operator.md +159 -0
  942. package/skills/ai-engineering-context/SKILL.md +48 -0
  943. package/skills/analyze-product-dependencies/SKILL.md +108 -0
  944. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +35 -0
  945. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +38 -0
  946. package/skills/analyze-product-dependencies/references/example.md +76 -0
  947. package/skills/analyze-product-dependencies/references/forward-test-cases.md +110 -0
  948. package/skills/analyze-product-dependencies/references/input-contract.md +34 -0
  949. package/skills/analyze-product-dependencies/references/kb-integration.md +64 -0
  950. package/skills/analyze-product-dependencies/references/scouting-rules.md +76 -0
  951. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +460 -0
  952. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +194 -0
  953. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +200 -0
  954. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +90 -0
  955. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +138 -0
  956. package/skills/analyze-product-requirements/SKILL.md +145 -0
  957. package/skills/analyze-product-requirements/references/acceptance-criteria.md +87 -0
  958. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +70 -0
  959. package/skills/analyze-product-requirements/references/example.md +104 -0
  960. package/skills/analyze-product-requirements/references/forward-test-cases.md +144 -0
  961. package/skills/analyze-product-requirements/references/kb-integration.md +56 -0
  962. package/skills/analyze-product-requirements/references/product-analysis-schema.md +41 -0
  963. package/skills/analyze-product-requirements/references/product-requirement-schema.md +42 -0
  964. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +66 -0
  965. package/skills/analyze-product-requirements/scripts/compute-source-identity.mjs +35 -0
  966. package/skills/analyze-product-requirements/scripts/test-validators.mjs +501 -0
  967. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +100 -0
  968. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +109 -0
  969. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +109 -0
  970. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +175 -0
  971. package/skills/browser-tools/SKILL.md +196 -0
  972. package/skills/browser-tools/browser-content.js +103 -0
  973. package/skills/browser-tools/browser-cookies.js +35 -0
  974. package/skills/browser-tools/browser-eval.js +53 -0
  975. package/skills/browser-tools/browser-hn-scraper.js +108 -0
  976. package/skills/browser-tools/browser-nav.js +44 -0
  977. package/skills/browser-tools/browser-pick.js +162 -0
  978. package/skills/browser-tools/browser-screenshot.js +34 -0
  979. package/skills/browser-tools/browser-start.js +86 -0
  980. package/skills/browser-tools/package-lock.json +2556 -0
  981. package/skills/browser-tools/package.json +19 -0
  982. package/skills/code-review-core/SKILL.md +20 -0
  983. package/skills/codebase-scout/SKILL.md +19 -0
  984. package/skills/frontend-bounded-implement/SKILL.md +53 -0
  985. package/skills/frontend-design-review/SKILL.md +79 -0
  986. package/skills/frontend-design-review/references/review-checklist.md +43 -0
  987. package/skills/frontend-implementation/SKILL.md +52 -0
  988. package/skills/frontend-implementation/references/code-standards.md +33 -0
  989. package/skills/frontend-implementation/references/design-spec.md +56 -0
  990. package/skills/frontend-implementation/references/node-contracts.md +31 -0
  991. package/skills/frontend-review/SKILL.md +55 -0
  992. package/skills/frontend-review/references/review-findings.md +50 -0
  993. package/skills/frontend-verification/SKILL.md +61 -0
  994. package/skills/frontend-verification/references/verification-checklist.md +48 -0
  995. package/skills/grill-me/SKILL.md +10 -0
  996. package/skills/grill-with-docs/SKILL.md +80 -0
  997. package/skills/grill-with-docs/adr-format.md +58 -0
  998. package/skills/grill-with-docs/context-format.md +52 -0
  999. package/skills/init-capability-evolution/SKILL.md +70 -0
  1000. package/skills/local-jacoco-coverage/SKILL.md +281 -0
  1001. package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
  1002. package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
  1003. package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
  1004. package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
  1005. package/skills/loop-agent/SKILL.md +68 -0
  1006. package/skills/loop-agent/references/README.md +67 -0
  1007. package/skills/loop-agent/references/command-reference.md +645 -0
  1008. package/skills/loop-agent/references/docs-converge.md +126 -0
  1009. package/skills/loop-agent/references/harness-policy.md +289 -0
  1010. package/skills/loop-agent/references/hybrid-dag.md +245 -0
  1011. package/skills/loop-agent/references/learned/README.md +21 -0
  1012. package/skills/loop-agent/references/long-running-loop.md +57 -0
  1013. package/skills/loop-agent/references/model-routing.md +45 -0
  1014. package/skills/loop-agent/references/multi-worktree.md +54 -0
  1015. package/skills/loop-agent/references/one-shot-runs.md +84 -0
  1016. package/skills/loop-agent/references/orchestrator-and-interventions.md +175 -0
  1017. package/skills/loop-agent/references/pi-prompt.md +23 -0
  1018. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -0
  1019. package/skills/loop-agent/references/post-implementation-and-patterns.md +54 -0
  1020. package/skills/loop-agent/references/source-and-plan-practice.md +169 -0
  1021. package/skills/loop-agent/references/task-workflow.md +104 -0
  1022. package/skills/loop-agent/references/verification-and-failure-handling.md +148 -0
  1023. package/skills/playwright-cli/SKILL.md +93 -0
  1024. package/skills/playwright-cli/references/element-attributes.md +23 -0
  1025. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  1026. package/skills/playwright-cli/references/request-mocking.md +87 -0
  1027. package/skills/playwright-cli/references/running-code.md +241 -0
  1028. package/skills/playwright-cli/references/session-management.md +225 -0
  1029. package/skills/playwright-cli/references/storage-state.md +275 -0
  1030. package/skills/playwright-cli/references/test-generation.md +433 -0
  1031. package/skills/playwright-cli/references/tracing.md +5 -0
  1032. package/skills/playwright-cli/references/video-recording.md +5 -0
  1033. package/skills/playwright-cli-case-generator/SKILL.md +108 -0
  1034. package/skills/requesting-code-review/SKILL.md +101 -0
  1035. package/skills/requesting-code-review/code-reviewer.md +168 -0
  1036. package/skills/systematic-debugging/CREATION-LOG.md +119 -0
  1037. package/skills/systematic-debugging/SKILL.md +312 -0
  1038. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  1039. package/skills/systematic-debugging/condition-based-waiting.md +115 -0
  1040. package/skills/systematic-debugging/defense-in-depth.md +122 -0
  1041. package/skills/systematic-debugging/find-polluter.sh +63 -0
  1042. package/skills/systematic-debugging/root-cause-tracing.md +169 -0
  1043. package/skills/systematic-debugging/test-academic.md +14 -0
  1044. package/skills/systematic-debugging/test-pressure-1.md +58 -0
  1045. package/skills/systematic-debugging/test-pressure-2.md +68 -0
  1046. package/skills/systematic-debugging/test-pressure-3.md +69 -0
  1047. package/skills/test-driven-development/SKILL.md +27 -0
  1048. package/skills/using-git-worktrees/SKILL.md +215 -0
  1049. package/skills/verification-before-completion/SKILL.md +154 -0
  1050. package/skills/webapp-testing/SKILL.md +19 -0
@@ -0,0 +1,1984 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { connect } from "node:net";
4
+ import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
5
+ import { createWriteStream } from "node:fs";
6
+ import path from "node:path";
7
+ import { promisify } from "node:util";
8
+ import { isPriorityOnlyBackendTestModuleStem } from "./backend-test-module-stem.js";
9
+ import { resolveBackendTestLayout, } from "./backend-test-layout.js";
10
+ import { parseJacocoXml, } from "./backend-test-coverage-contract.js";
11
+ import { backendTestCaseManifestSchema, computeCaseManifestCoverageSummary, } from "./backend-test-case-manifest.js";
12
+ const execFileAsync = promisify(execFile);
13
+ // Canonical contract is three digits (BE-MODULE-001). Readers accept legacy/
14
+ // model-drift two-digit suffixes and canonicalize them in memory so one bad
15
+ // heading width cannot collapse the whole manifest/report to zero cases.
16
+ const CASE_ID = /\bBE-[A-Z0-9_-]+-\d{2,3}\b/g;
17
+ const CANONICAL_CASE_ID = /\bBE-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}\b/;
18
+ const SHARED_SETUP_REF_LINE = /^\s*引用前置\s*:\s*(\S+)\s*#\s*([A-Za-z0-9_.\-\u4e00-\u9fa5]+)\s*$/;
19
+ const sharedSetupAnchorRe = /^SS-[A-Z0-9][A-Z0-9._-]*$/i;
20
+ export function extractBackendSharedSetupRefs(markdown) {
21
+ const refs = [];
22
+ for (const line of markdown.split("\n")) {
23
+ const match = SHARED_SETUP_REF_LINE.exec(line);
24
+ if (match) {
25
+ refs.push({ raw: match[0].trim(), path: match[1], anchor: match[2] });
26
+ }
27
+ }
28
+ return refs;
29
+ }
30
+ const AC_ID = /\bAC-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g;
31
+ const SECRET = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|authorization)\s*[:=]\s*\S+/i;
32
+ const SECRET_ASSIGNMENT = /\b(password|passwd|secret|token|api[_-]?key|private[_-]?key|authorization|cookie|set-cookie|credential)\b\s*[:=]\s*([^\r\n,;]+)/gi;
33
+ const SECRET_VALUE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b|\bsk[-_][-_A-Za-z0-9]{10,}\b/i;
34
+ const PLACEHOLDER_SECRET_VALUE = /^(?:<[^>]+>|\$\{[^}]+\}|\*+|x+|redacted|masked|example(?:[-_].*)?|sample(?:[-_].*)?|dummy(?:[-_].*)?|test(?:[-_].*)?|your[-_].*)$/i;
35
+ const PLACEHOLDER = /\b(?:TODO|TBD|FIXME)\b|后续补充|暂不实现|接口正常|结果正确|按实际情况处理/i;
36
+ const CASE_SECTION_ALIASES = {
37
+ acceptanceCriteria: ["Acceptance Criteria", "验收标准"],
38
+ sourceReferences: ["Source References", "需求依据"],
39
+ preconditions: ["Preconditions", "前置条件"],
40
+ testData: ["Test Data", "测试数据"],
41
+ steps: ["Steps", "操作步骤"],
42
+ expectedResults: ["Expected Results", "预期结果"],
43
+ automationNotes: ["Automation Notes", "自动化映射", "自动化说明"],
44
+ testPurpose: ["Test Purpose", "测试目的", "测试场景"],
45
+ };
46
+ const REQUIRED_CASE_SECTIONS = [
47
+ CASE_SECTION_ALIASES.preconditions,
48
+ CASE_SECTION_ALIASES.steps,
49
+ CASE_SECTION_ALIASES.expectedResults,
50
+ ];
51
+ async function exists(filePath) {
52
+ try {
53
+ await access(filePath);
54
+ return true;
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ async function mappedScriptExists(filePath) {
61
+ try {
62
+ await access(filePath);
63
+ return true;
64
+ }
65
+ catch (error) {
66
+ if (error.code === "ENOENT")
67
+ return false;
68
+ throw error;
69
+ }
70
+ }
71
+ async function existingPaths(root, candidates) {
72
+ const found = [];
73
+ for (const candidate of candidates) {
74
+ if (await exists(path.join(root, candidate)))
75
+ found.push(candidate);
76
+ }
77
+ return found;
78
+ }
79
+ function unique(values) {
80
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right));
81
+ }
82
+ export function requiredBackendMarkdownCaseAcIds(criteria) {
83
+ const downstreamEvidence = /(?:pytest|junit|html|stdout|stderr|traceability|markdown|readme|human-readable|skip|xfail|吞(?:断言|异常)|生产代码|生产配置|配置写入|写入边界|中文(?:用例|文档|结构|展示)?|用例(?:索引|结构|文档)|测试目的|需求依据|操作步骤|预期结果|自动化映射|追溯|一一映射|一一追溯|执行并产生|执行结果|测试报告|失败分类|证据)/i;
84
+ return unique([...criteria]
85
+ .filter((criterion) => !downstreamEvidence.test(criterion))
86
+ .flatMap((criterion) => criterion.match(/\bAC-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g) ?? []));
87
+ }
88
+ function extractFixtures(markdown) {
89
+ return unique([
90
+ ...markdown.matchAll(/(?:@pytest\.fixture(?:\([^)]*\))?\s*(?:\r?\n)+\s*def\s+|def\s+)([A-Za-z_][A-Za-z0-9_]*)\s*\(/g),
91
+ ]
92
+ .map((match) => match[1])
93
+ .filter(Boolean));
94
+ }
95
+ export async function inspectBackendTestEnvironment(input) {
96
+ const pytestConfigs = await existingPaths(input.workspaceRoot, [
97
+ "pytest.ini",
98
+ "pyproject.toml",
99
+ "setup.cfg",
100
+ "tox.ini",
101
+ ]);
102
+ const testRoots = await existingPaths(input.workspaceRoot, [
103
+ "testcase",
104
+ "tests",
105
+ "test",
106
+ "src/tests",
107
+ "app/tests",
108
+ ]);
109
+ const conftestFiles = await existingPaths(input.workspaceRoot, [
110
+ "conftest.py",
111
+ "testcase/conftest.py",
112
+ "tests/conftest.py",
113
+ "test/conftest.py",
114
+ ]);
115
+ const serverEntries = await existingPaths(input.workspaceRoot, [
116
+ "server.py",
117
+ "app.py",
118
+ "main.py",
119
+ "manage.py",
120
+ "src/server.py",
121
+ "src/main.py",
122
+ "app/main.py",
123
+ "server.js",
124
+ "server.ts",
125
+ "src/server.js",
126
+ "src/server.ts",
127
+ ]);
128
+ const fixtures = [];
129
+ for (const relativePath of conftestFiles) {
130
+ const content = await readFile(path.join(input.workspaceRoot, relativePath), "utf8");
131
+ if (SECRET.test(content)) {
132
+ // Do not copy conftest content into evidence. Fixture names are still safe to discover.
133
+ }
134
+ fixtures.push(...extractFixtures(content));
135
+ }
136
+ const hasPytestHtml = /--html\b/.test(input.pytestHelp);
137
+ const htmlRenderer = hasPytestHtml
138
+ ? "pytest-html --self-contained-html"
139
+ : "missing pytest-html (required: pip install pytest-html)";
140
+ const warnings = [
141
+ ...(pytestConfigs.length === 0
142
+ ? ["No explicit pytest configuration file was found."]
143
+ : []),
144
+ ...(!hasPytestHtml
145
+ ? [
146
+ "pytest-html is not installed; the backend-test pipeline requires it to render the self-contained HTML report. Install with: pip install pytest-html",
147
+ ]
148
+ : []),
149
+ ...(conftestFiles.length === 0
150
+ ? ["No bounded conftest.py candidate was found."]
151
+ : []),
152
+ ...(serverEntries.length === 0
153
+ ? [
154
+ "No bounded server entry candidate was found; Markdown cases must document an external or library target explicitly.",
155
+ ]
156
+ : []),
157
+ ];
158
+ const lines = [
159
+ "# Backend Test Environment",
160
+ "",
161
+ "## Status",
162
+ "",
163
+ "PASS",
164
+ "",
165
+ "## Runtime",
166
+ "",
167
+ `- Python: ${input.pythonVersion.trim()}`,
168
+ `- Pytest: ${input.pytestVersion.trim()}`,
169
+ "- Shell environment: clean",
170
+ "",
171
+ "## Project Discovery",
172
+ "",
173
+ `- Pytest config: ${pytestConfigs.join(", ") || "not found"}`,
174
+ `- Existing test roots: ${testRoots.join(", ") || "not found (testcase will be generated)"}`,
175
+ `- Conftest files: ${conftestFiles.join(", ") || "not found"}`,
176
+ `- Server entry candidates: ${serverEntries.join(", ") || "not found"}`,
177
+ `- Fixtures: ${unique(fixtures).join(", ") || "none discovered"}`,
178
+ "",
179
+ "## HTML Report",
180
+ "",
181
+ `- Selected renderer: ${htmlRenderer}`,
182
+ "",
183
+ "## Bounded Discovery Policy",
184
+ "",
185
+ "- No .env value, credential file, dependency tree, build output, coverage output, or runtime directory was inspected.",
186
+ "- External readiness must be represented by an explicitly configured verify command or a documented safe health endpoint; secret values are never printed.",
187
+ "",
188
+ "## Warnings",
189
+ "",
190
+ ...(warnings.length > 0
191
+ ? warnings.map((warning) => `- ${warning}`)
192
+ : ["- None"]),
193
+ ];
194
+ return {
195
+ markdown: `${lines.join("\n")}\n`,
196
+ fixtures: unique(fixtures),
197
+ pytestConfigs,
198
+ testRoots,
199
+ conftestFiles,
200
+ serverEntries,
201
+ };
202
+ }
203
+ export async function markdownFiles(root, layout) {
204
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
205
+ const directory = path.join(root, ...resolved.markdownDir.split("/"));
206
+ const entries = await readdir(directory, { withFileTypes: true });
207
+ return entries
208
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
209
+ .map((entry) => path.join(directory, entry.name))
210
+ .sort();
211
+ }
212
+ function canonicalBackendCaseId(value) {
213
+ const upper = value.toUpperCase();
214
+ const match = upper.match(/^(BE-[A-Z0-9_-]+)-(\d{2,3})$/);
215
+ if (!match)
216
+ return upper;
217
+ const modulePart = match[1]
218
+ .replaceAll("_", "-")
219
+ .replace(/-+/g, "-")
220
+ .replace(/-$/, "");
221
+ return `${modulePart}-${match[2].padStart(3, "0")}`;
222
+ }
223
+ function caseIdsInText(value) {
224
+ return unique((value.match(CASE_ID) ?? []).map(canonicalBackendCaseId));
225
+ }
226
+ function splitCases(markdown) {
227
+ const headings = [
228
+ ...markdown.matchAll(/^##\s+(BE-[A-Z0-9_-]+-\d{2,3})\b.*$/gm),
229
+ ];
230
+ return headings.map((match, index) => ({
231
+ id: canonicalBackendCaseId(match[1]),
232
+ rawId: match[1],
233
+ body: markdown.slice(match.index, headings[index + 1]?.index ?? markdown.length),
234
+ }));
235
+ }
236
+ function sectionBody(body, headings) {
237
+ const headingPattern = headings.map(escapeRegExp).join("|");
238
+ const marker = new RegExp(`^###\\s+(?:${headingPattern})\\s*$`, "mi");
239
+ const match = marker.exec(body);
240
+ if (!match)
241
+ return "";
242
+ const remainder = body.slice(match.index + match[0].length);
243
+ const next = /^###\s+/m.exec(remainder);
244
+ return remainder.slice(0, next?.index ?? remainder.length);
245
+ }
246
+ function escapeRegExp(value) {
247
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
248
+ }
249
+ function isTaskRelativeSourcePath(value) {
250
+ return /^source\/(?:需求\.md|执行约束\.md|references\/[^\s`'"()[\]{}()。,;:#]+)$/.test(value);
251
+ }
252
+ function extractLegacySourceReferences(sourceSection) {
253
+ const inlineCode = [
254
+ ...sourceSection.matchAll(/`(source\/(?:需求\.md|执行约束\.md|references\/[^`\r\n]+))`/g),
255
+ ].map((match) => match[1].trim());
256
+ const plain = [
257
+ ...sourceSection.matchAll(/(?:^|[\s('"(])(source\/(?:需求\.md|执行约束\.md|references\/[^\s`'"()[\]{}()。,;:#]+))(?=$|[\s`'"()()\],。;:#])/gm),
258
+ ].map((match) => match[1]);
259
+ return unique([...inlineCode, ...plain].filter(isTaskRelativeSourcePath));
260
+ }
261
+ function extractSourceReferences(input) {
262
+ const citedPaths = extractLegacySourceReferences(input.sourceSection);
263
+ if (!input.sourceBinding) {
264
+ return citedPaths;
265
+ }
266
+ const boundMatches = input.sourceBinding.sources
267
+ .map((source) => source.path)
268
+ .filter(isTaskRelativeSourcePath)
269
+ .filter((sourcePath) => {
270
+ const escaped = escapeRegExp(sourcePath);
271
+ return new RegExp(`(?:^|[\\s(\`'\"(])${escaped}(?=$|[\\s)\`'\")\\],。;:#])`, "m").test(input.sourceSection);
272
+ });
273
+ // Preserve explicitly cited unknown paths so resolveBoundSourceReference()
274
+ // rejects them instead of silently accepting a nearby bound prefix.
275
+ return unique([...boundMatches, ...citedPaths]);
276
+ }
277
+ function hasStructuredMarkdownTable(value, headerPattern) {
278
+ const rows = value.split(/\r?\n/).filter((line) => /^\s*\|.*\|\s*$/.test(line));
279
+ if (rows.length < 3 || !headerPattern.test(rows[0]))
280
+ return false;
281
+ return /^\s*\|(?:\s*:?-{3,}:?\s*\|)+\s*$/i.test(rows[1]) && rows.slice(2).some((row) => row.split("|").some((cell) => cell.trim().length > 0));
282
+ }
283
+ function hasNumberedListItem(value) {
284
+ return (/^\s*\d+[.)]\s+\S+/m.test(value) ||
285
+ /^\s*\|\s*\d+\s*\|\s*\S+/m.test(value) ||
286
+ hasStructuredMarkdownTable(value, /(?:步骤|操作|测试点|参数|step|action|operation|test point|parameter)/i));
287
+ }
288
+ function hasAssertableExpectedResult(value) {
289
+ return (/^\s*(?:\d+[.)]|[-*+])\s+\S+/m.test(value) ||
290
+ hasStructuredMarkdownTable(value, /(?:预期|期望|断言|状态|结果|expected|assert|status|result)/i));
291
+ }
292
+ function resolveBoundSourceReference(input) {
293
+ if (!input.sourceBinding) {
294
+ return path.join(input.workspaceRoot, input.sourceRef);
295
+ }
296
+ const member = input.sourceBinding.sources.find((source) => source.path === input.sourceRef);
297
+ if (!member) {
298
+ throw new Error(`source reference is not present in DAG sourceBinding: ${input.sourceRef}`);
299
+ }
300
+ const taskRoot = path.join(input.workspaceRoot, ".harness", "tasks", input.sourceBinding.taskId);
301
+ const resolved = path.resolve(taskRoot, member.path);
302
+ const relative = path.relative(taskRoot, resolved);
303
+ if (relative.startsWith("..") ||
304
+ path.isAbsolute(relative)) {
305
+ throw new Error(`unsafe task-relative source path: ${input.sourceRef}`);
306
+ }
307
+ return resolved;
308
+ }
309
+ function leadingWhitespace(value) {
310
+ return value.match(/^\s*/)?.[0].length ?? 0;
311
+ }
312
+ function isCleanupStatement(value) {
313
+ const trimmed = value.trim();
314
+ if (!trimmed || trimmed.startsWith("#"))
315
+ return true;
316
+ if (/^(?:with|if)\b/.test(trimmed))
317
+ return false;
318
+ if (/\b(?:assert|pytest\.fail|raise)\b/.test(trimmed))
319
+ return false;
320
+ if (/\b[A-Za-z_][A-Za-z0-9_.]*request\s*\(\s*["']DELETE["']/i.test(trimmed))
321
+ return true;
322
+ return /(?:^|[._])(?:cleanup|clean_up|teardown|delete|remove|close|release|rollback|disconnect|dispose|unlink)(?:[_(.]|$)/i.test(trimmed);
323
+ }
324
+ function findUnsafeSwallowedException(source) {
325
+ const lines = source.split(/\r?\n/);
326
+ for (let index = 0; index < lines.length; index += 1) {
327
+ const exceptMatch = lines[index].match(/^(\s*)except(?:\s+([^:]+))?\s*:\s*(?:(pass)\s*)?(?:#.*)?$/);
328
+ if (!exceptMatch)
329
+ continue;
330
+ const indent = exceptMatch[1].length;
331
+ const inlinePass = exceptMatch[3] === "pass";
332
+ let handlerIndex = index + 1;
333
+ while (!inlinePass &&
334
+ handlerIndex < lines.length &&
335
+ (!lines[handlerIndex].trim() || lines[handlerIndex].trim().startsWith("#"))) {
336
+ handlerIndex += 1;
337
+ }
338
+ if (!inlinePass &&
339
+ (handlerIndex >= lines.length ||
340
+ leadingWhitespace(lines[handlerIndex]) <= indent ||
341
+ lines[handlerIndex].trim() !== "pass")) {
342
+ continue;
343
+ }
344
+ const exceptionType = exceptMatch[2]?.trim() || "bare except";
345
+ const narrowDecodeTypes = exceptionType
346
+ .replace(/[()]/g, "")
347
+ .split(",")
348
+ .map((value) => value.trim())
349
+ .filter(Boolean);
350
+ const precedingDecodeRegion = lines.slice(Math.max(0, index - 8), index).join("\n");
351
+ if (narrowDecodeTypes.length > 0 &&
352
+ narrowDecodeTypes.every((value) => value === "UnicodeDecodeError" || value === "json.JSONDecodeError") &&
353
+ /\.json\s*\(\s*\)/.test(precedingDecodeRegion)) {
354
+ continue;
355
+ }
356
+ if (!new Set(["requests.RequestException", "urllib.error.URLError", "httpx.RequestError"]).has(exceptionType)) {
357
+ return exceptionType;
358
+ }
359
+ let tryIndex = index - 1;
360
+ while (tryIndex >= 0) {
361
+ const candidate = lines[tryIndex];
362
+ if (!candidate.trim() || candidate.trim().startsWith("#")) {
363
+ tryIndex -= 1;
364
+ continue;
365
+ }
366
+ if (leadingWhitespace(candidate) === indent &&
367
+ candidate.trim() === "try:") {
368
+ break;
369
+ }
370
+ if (leadingWhitespace(candidate) <= indent)
371
+ break;
372
+ tryIndex -= 1;
373
+ }
374
+ if (tryIndex < 0 || lines[tryIndex].trim() !== "try:") {
375
+ return exceptionType;
376
+ }
377
+ const tryBody = lines.slice(tryIndex + 1, index);
378
+ const executable = tryBody.filter((line) => line.trim() && !line.trim().startsWith("#"));
379
+ if (executable.length === 0 ||
380
+ executable.some((line) => leadingWhitespace(line) <= indent || !isCleanupStatement(line))) {
381
+ return exceptionType;
382
+ }
383
+ }
384
+ return null;
385
+ }
386
+ function resolvesSharedLoggingHelper(source) {
387
+ return /\b(?:LoggedHttpClient|LoggedApiClient|LoggingHttpClient|HttpLoggingClient)\b/.test(source);
388
+ }
389
+ function httpLoggingFindings(relativeScript, source) {
390
+ if (resolvesSharedLoggingHelper(source))
391
+ return [];
392
+ const hasHttpRequest = /\b(?:requests\.(?:get|post|put|patch|delete|request)|httpx\.(?:get|post|put|patch|delete|request)|(?:client|session|api_client)\.(?:get|post|put|patch|delete|request))\s*\(/i.test(source);
393
+ if (!hasHttpRequest)
394
+ return [];
395
+ const findings = [];
396
+ const hasRequestLog = /(?:print|log(?:ger)?\.(?:debug|info|warning|error))\s*\([^\n]*(?:request|请求)[^\n]*(?:method|url|path|params|query|json|body|payload|参数)/i.test(source)
397
+ || /["']record["']\s*:\s*["']request["'][\s\S]{0,600}["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source)
398
+ || /\b[A-Za-z_][A-Za-z0-9_]*log[A-Za-z0-9_]*\s*\(\s*["'](?:HTTP|API)_REQUEST["'][\s\S]{0,600}["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source)
399
+ || /print\s*\(\s*f?["'](?:HTTP|API)(?:_|\s+)REQUEST\b/i.test(source) && /["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source)
400
+ || /\b[A-Za-z_][A-Za-z0-9_]*log\s*\(\s*["']REQUEST["'][\s\S]{0,600}["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source);
401
+ const hasResponseLog = /(?:print|log(?:ger)?\.(?:debug|info|warning|error))\s*\([^\n]*(?:response|响应)[^\n]*(?:status|status_code|body|json|text|result|结果)/i.test(source)
402
+ || /["']record["']\s*:\s*["']response["'][\s\S]{0,600}["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source)
403
+ || /\b[A-Za-z_][A-Za-z0-9_]*log[A-Za-z0-9_]*\s*\(\s*["'](?:HTTP|API)_RESPONSE["'][\s\S]{0,600}["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source)
404
+ || /print\s*\(\s*f?["'](?:HTTP|API)(?:_|\s+)RESPONSE\b/i.test(source) && /["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source)
405
+ || /\b[A-Za-z_][A-Za-z0-9_]*log\s*\(\s*["']RESPONSE["'][\s\S]{0,600}["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source);
406
+ const hasSanitizer = /\b(?:sanitize|sanitise|redact|mask)[A-Za-z0-9_]*\s*\(|\b[A-Za-z0-9_]*(?:sanitize|sanitise|redact|mask)[A-Za-z0-9_]*\s*\(/i.test(source)
407
+ || /(?:SENSITIVE_KEYS|sensitive_keys)[\s\S]{0,1600}["']<redacted>["']/i.test(source);
408
+ const hasBoundedOutput = /(?:MAX_[A-Z_]*(?:LOG|BODY|RESPONSE)|max_(?:log|body|response)|truncate|truncat|\[:\s*\d{3,}\s*\])/i.test(source);
409
+ if (!hasRequestLog)
410
+ findings.push(`request logging evidence is missing in mapped backend pytest: ${relativeScript}`);
411
+ if (!hasResponseLog)
412
+ findings.push(`response logging evidence is missing in mapped backend pytest: ${relativeScript}`);
413
+ if (!hasSanitizer)
414
+ findings.push(`recursive redaction evidence is missing in mapped backend pytest: ${relativeScript}`);
415
+ if (!hasBoundedOutput)
416
+ findings.push(`bounded log truncation evidence is missing in mapped backend pytest: ${relativeScript}`);
417
+ return findings;
418
+ }
419
+ function markdownSecretFindings(relativeFile, markdown) {
420
+ const findings = [];
421
+ if (SECRET_VALUE.test(markdown)) {
422
+ findings.push(`secret-shaped value detected in ${relativeFile}`);
423
+ }
424
+ for (const match of markdown.matchAll(SECRET_ASSIGNMENT)) {
425
+ const value = match[2]
426
+ .trim()
427
+ .replace(/^bearer\s+/i, "")
428
+ .replace(/^['"`]|['"`]$/g, "");
429
+ if (!PLACEHOLDER_SECRET_VALUE.test(value)) {
430
+ findings.push(`secret-shaped ${match[1].toLowerCase()} value detected in ${relativeFile}`);
431
+ }
432
+ }
433
+ return unique(findings);
434
+ }
435
+ export function hasBlockingBackendMarkdownSafetyFindings(report) {
436
+ return /^## Safety Status\s*\r?\n\s*BLOCKED\s*$/m.test(report);
437
+ }
438
+ export function hasBlockingBackendMarkdownModuleStemFindings(report) {
439
+ return /^## Module Stem Status\s*\r?\n\s*BLOCKED\s*$/m.test(report);
440
+ }
441
+ export async function validateBackendMarkdownCases(input) {
442
+ const layout = input.layout ?? resolveBackendTestLayout(undefined);
443
+ const findings = [];
444
+ const safetyFindings = [];
445
+ const moduleStemFindings = [];
446
+ let files = [];
447
+ try {
448
+ files = await markdownFiles(input.workspaceRoot, layout);
449
+ }
450
+ catch {
451
+ findings.push(`missing ${layout.markdownDir} directory`);
452
+ }
453
+ const readme = path.join(input.workspaceRoot, ...layout.readmePath.split("/"));
454
+ if (!(await exists(readme)))
455
+ findings.push(`missing ${layout.readmePath}`);
456
+ const moduleFiles = files.filter((file) => path.basename(file).toLowerCase() !== "readme.md");
457
+ if (moduleFiles.length === 0)
458
+ findings.push(`no ${layout.markdownDir} module case file found`);
459
+ const seen = new Map();
460
+ const coveredAc = new Set();
461
+ let caseCount = 0;
462
+ for (const file of moduleFiles) {
463
+ const relativeFile = path.relative(input.workspaceRoot, file).replaceAll(path.sep, "/");
464
+ if (isPriorityOnlyBackendTestModuleStem(path.basename(file, path.extname(file)))) {
465
+ moduleStemFindings.push(`priority-only-module-stem: ${relativeFile}; regenerate the README Module Index with a stable business resource/domain stem`);
466
+ }
467
+ const markdown = await readFile(file, "utf8");
468
+ safetyFindings.push(...markdownSecretFindings(relativeFile, markdown));
469
+ if (!markdown.trim()) {
470
+ findings.push(`empty Markdown case file: ${relativeFile}`);
471
+ continue;
472
+ }
473
+ if (PLACEHOLDER.test(markdown)) {
474
+ findings.push(`placeholder or non-assertable wording in ${relativeFile}`);
475
+ }
476
+ const sharedSetupRefs = extractBackendSharedSetupRefs(markdown);
477
+ for (const ref of sharedSetupRefs) {
478
+ if (!input.sharedSetup) {
479
+ findings.push(`shared setup reference without a bound document: ${ref.raw} in ${relativeFile}`);
480
+ }
481
+ else if (ref.path !== input.sharedSetup.path) {
482
+ findings.push(`shared setup reference cites unbound path ${ref.path} (bound: ${input.sharedSetup.path}) in ${relativeFile}`);
483
+ }
484
+ else if (ref.anchor !== "SS-DEFAULT" && !sharedSetupAnchorRe.test(ref.anchor)) {
485
+ findings.push(`shared setup reference anchor is not a stable SS ID and not SS-DEFAULT: ${ref.raw} in ${relativeFile}`);
486
+ }
487
+ }
488
+ const cases = splitCases(markdown);
489
+ if (cases.length === 0) {
490
+ findings.push(`no BE-<MODULE>-<NNN> case heading in ${relativeFile}`);
491
+ continue;
492
+ }
493
+ for (const testCase of cases) {
494
+ caseCount += 1;
495
+ if (!CANONICAL_CASE_ID.test(testCase.rawId)) {
496
+ findings.push(`${testCase.rawId} uses a non-canonical Case ID; use ${testCase.id} (hyphen-separated module, three-digit sequence)`);
497
+ }
498
+ const previous = seen.get(testCase.id);
499
+ if (previous) {
500
+ findings.push(`duplicate case id ${testCase.id}: ${previous} and ${relativeFile}`);
501
+ }
502
+ else {
503
+ seen.set(testCase.id, relativeFile);
504
+ }
505
+ for (const sectionAliases of REQUIRED_CASE_SECTIONS) {
506
+ const sectionPattern = sectionAliases.map(escapeRegExp).join("|");
507
+ if (!new RegExp(`^###\\s+(?:${sectionPattern})\\s*$`, "mi").test(testCase.body)) {
508
+ findings.push(`${testCase.id} missing section: ${sectionAliases.join(" or ")}`);
509
+ }
510
+ }
511
+ const bindingFinding = markdownTestPointBindingFinding(testCase.id, testCase.body);
512
+ if (bindingFinding)
513
+ findings.push(bindingFinding);
514
+ const payloadLabelFinding = markdownPayloadLabelFinding(testCase.id, testCase.body);
515
+ if (payloadLabelFinding)
516
+ findings.push(payloadLabelFinding);
517
+ const scenarioIntentFinding = markdownScenarioIntentFinding(testCase.id, testCase.body);
518
+ if (scenarioIntentFinding)
519
+ findings.push(scenarioIntentFinding);
520
+ const acIds = unique(testCase.body.match(AC_ID) ?? []);
521
+ acIds.forEach((id) => coveredAc.add(id));
522
+ const steps = sectionBody(testCase.body, CASE_SECTION_ALIASES.steps);
523
+ const expected = sectionBody(testCase.body, CASE_SECTION_ALIASES.expectedResults);
524
+ if (!hasNumberedListItem(steps)) {
525
+ findings.push(`${testCase.id} has no numbered executable step`);
526
+ }
527
+ if (!hasAssertableExpectedResult(expected)) {
528
+ findings.push(`${testCase.id} has no structured expected result`);
529
+ }
530
+ const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile, layout);
531
+ const mappedScripts = extractMappedPytestScripts(testCase.body);
532
+ if (isEvidenceOnlyMarkdownCaseBody(testCase.body)) {
533
+ // Evidence-only meta cases intentionally declare 脚本/primary symbol as 无 and keep only assertion/cross-cutting TPs.
534
+ }
535
+ else if (mappedScripts.length === 0) {
536
+ findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射 (expected ${expectedScript} from module ${path.basename(relativeFile)})`);
537
+ }
538
+ else {
539
+ for (const script of mappedScripts) {
540
+ if (script !== expectedScript) {
541
+ findings.push(`${testCase.id} automation mapping ${script} must equal module one-to-one path ${expectedScript} (from ${path.basename(relativeFile)})`);
542
+ }
543
+ }
544
+ }
545
+ }
546
+ }
547
+ const missing = input.requiredRequirementIds.filter((id) => !coveredAc.has(id));
548
+ if (missing.length > 0) {
549
+ findings.push(`required AC IDs are not covered by final Markdown cases: ${missing.join(", ")}`);
550
+ }
551
+ findings.push(...moduleStemFindings);
552
+ const status = findings.length === 0 ? "PASS" : "FAIL";
553
+ const report = [
554
+ "# Backend Markdown Case Validation",
555
+ "",
556
+ "## Status",
557
+ "",
558
+ status,
559
+ "",
560
+ `- Markdown files: ${files.length}`,
561
+ `- Module files: ${moduleFiles.length}`,
562
+ `- Cases: ${caseCount}`,
563
+ `- Covered AC IDs: ${[...coveredAc].sort().join(", ") || "none"}`,
564
+ `- Environment evidence present: ${input.environmentMarkdown.trim() ? "yes" : "no"}`,
565
+ `- Findings: ${findings.length}`,
566
+ "- Required sections: Preconditions/前置条件, Steps/操作步骤, Expected Results/预期结果",
567
+ "- Source-reference validity check: not applied",
568
+ "- Markdown quality sensitive-information finding: not advisory; see Safety Status",
569
+ "",
570
+ "## Module Stem Status",
571
+ "",
572
+ moduleStemFindings.length > 0 ? "BLOCKED" : "PASS",
573
+ "",
574
+ ...(moduleStemFindings.length > 0
575
+ ? moduleStemFindings.map((finding) => `- ${finding}`)
576
+ : ["- All Markdown modules use non-priority business stems."]),
577
+ "",
578
+ "## Safety Status",
579
+ "",
580
+ safetyFindings.length > 0 ? "BLOCKED" : "PASS",
581
+ "",
582
+ ...(safetyFindings.length > 0
583
+ ? safetyFindings.map((finding) => `- ${finding}`)
584
+ : ["- No secret-shaped values detected."]),
585
+ "",
586
+ "## Findings",
587
+ "",
588
+ ...(findings.length > 0 ? findings.map((finding) => `- ${finding}`) : ["- None"]),
589
+ ];
590
+ return `${report.join("\n")}\n`;
591
+ }
592
+ function collectMarkdownCaseIds(markdown) {
593
+ return splitCases(markdown).map((testCase) => testCase.id);
594
+ }
595
+ /**
596
+ * Derive the stable module stem used for Markdown ↔ pytest one-to-one mapping.
597
+ * `testcase/md/<module>.md` → stem → `testcase/test_<stem>.py`.
598
+ * Example: `BE-HEALTH.md` / `be-health.md` / `order-api.md` → `be_health` / `order_api`.
599
+ */
600
+ export function normalizeBackendTestModuleStem(moduleFileName) {
601
+ const base = path.basename(moduleFileName).replace(/\.md$/i, "");
602
+ const stem = base
603
+ .toLowerCase()
604
+ .replace(/[^a-z0-9]+/g, "_")
605
+ .replace(/^_+|_+$/g, "")
606
+ .replace(/_+/g, "_");
607
+ if (!stem) {
608
+ throw new Error(`cannot derive backend-test module stem from: ${moduleFileName}`);
609
+ }
610
+ return stem;
611
+ }
612
+ /** Expected pytest script path for a Markdown module file (basename or relative path). */
613
+ export function expectedBackendTestPytestScriptForMarkdownModule(moduleFileName, layout) {
614
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
615
+ return `${resolved.scriptDir}/test_${normalizeBackendTestModuleStem(moduleFileName)}.py`;
616
+ }
617
+ function extractMappedPytestScripts(testCaseBody) {
618
+ const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
619
+ if (!automation.trim())
620
+ return [];
621
+ return unique([...automation.matchAll(/`?(testcase\/[A-Za-z0-9_./-]*test_[A-Za-z0-9_.-]*\.py)`?/gi)]
622
+ .map((match) => match[1].replaceAll("\\", "/"))
623
+ .filter((value) => !value.includes("..")));
624
+ }
625
+ function markdownScenarioIntentFinding(testCaseId, testCaseBody) {
626
+ const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
627
+ if (!automation.trim())
628
+ return null;
629
+ const variantLine = /^(?:\s*[-*+]\s*)?(?:变体测试点|Variant Test Points)\s*[::]\s*(.*)$/im.exec(automation)?.[1] ?? "";
630
+ const variants = unique(variantLine.match(/\bTP-[A-Z0-9-]+\b/g) ?? []);
631
+ if (variants.length === 0)
632
+ return null;
633
+ const localIntentIds = unique([...testCaseBody.matchAll(/(?:场景意图|Scenario Intent)\s*[::]\s*(TP-[A-Z0-9-]+)/gi)].map((match) => match[1].toUpperCase()));
634
+ const missing = variants.filter((id) => !localIntentIds.includes(id.toUpperCase()));
635
+ return missing.length > 0
636
+ ? `${testCaseId} variant Test Points require a local Scenario Intent line inside the same Case: missing=${missing.join(", ")}`
637
+ : null;
638
+ }
639
+ function markdownPayloadLabelFinding(testCaseId, testCaseBody) {
640
+ const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
641
+ if (!automation.trim())
642
+ return null;
643
+ const payloadLabel = /Payload (?:Contract|Required Paths|Allowed Paths|Enum)\s*[::]/gi;
644
+ for (const line of automation.split(/\r?\n/)) {
645
+ const matches = line.match(payloadLabel) ?? [];
646
+ if (matches.length === 0)
647
+ continue;
648
+ if (matches.length !== 1 || !/^\s*[-*+]\s*Payload (?:Contract|Required Paths|Allowed Paths|Enum)\s*[::]/i.test(line)) {
649
+ return `${testCaseId} payload contract labels must each occupy one independent machine-readable list line`;
650
+ }
651
+ }
652
+ return null;
653
+ }
654
+ function markdownTestPointBindingFinding(testCaseId, testCaseBody) {
655
+ const declaredSection = sectionBody(testCaseBody, ["测试点", "Test Points"]);
656
+ const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
657
+ if (!declaredSection.trim() || !automation.trim())
658
+ return null;
659
+ const declared = unique(declaredSection.match(/\bTP-[A-Z0-9-]+\b/g) ?? []);
660
+ const bindingLines = [
661
+ /^(?:\s*[-*+]\s*)?(?:变体测试点|Variant Test Points)\s*[::]\s*(.*)$/gim,
662
+ /^(?:\s*[-*+]\s*)?(?:场景断言测试点|Assertion Test Points)\s*[::]\s*(.*)$/gim,
663
+ /^(?:\s*[-*+]\s*)?(?:横切证据测试点|Cross-Cutting Test Points)\s*[::]\s*(.*)$/gim,
664
+ ];
665
+ const bound = unique(bindingLines.flatMap((pattern) => [...automation.matchAll(pattern)].flatMap((match) => match[1]?.match(/\bTP-[A-Z0-9-]+\b/g) ?? [])));
666
+ const missingBindings = declared.filter((id) => !bound.includes(id));
667
+ const undeclaredBindings = bound.filter((id) => !declared.includes(id));
668
+ if (missingBindings.length === 0 && undeclaredBindings.length === 0)
669
+ return null;
670
+ return `${testCaseId} Test Point set must exactly equal variant/assertion/cross-cutting binding union: unclassified=${missingBindings.join(", ") || "none"}; undeclared-bindings=${undeclaredBindings.join(", ") || "none"}`;
671
+ }
672
+ function isEvidenceOnlyMarkdownCaseBody(testCaseBody) {
673
+ const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
674
+ if (!automation.trim())
675
+ return false;
676
+ const scriptLine = /^\s*[-*+]\s*(?:脚本|script)\s*[::]\s*(.*)$/im.exec(automation)?.[1]?.replaceAll("`", "").trim() ??
677
+ "";
678
+ const primaryLine = /^\s*[-*+]\s*(?:primary symbol|Primary Symbol|主测试符号)\s*[::]\s*(.*)$/im.exec(automation)?.[1]
679
+ ?.replaceAll("`", "")
680
+ .trim() ?? "";
681
+ const noneLike = /^(?:none|无|n\/a|not-applicable)(?:\s*[((].*[))])?$/i;
682
+ const scriptIsNone = noneLike.test(scriptLine) || scriptLine.length === 0;
683
+ const primaryIsNone = noneLike.test(primaryLine) || primaryLine.length === 0;
684
+ const hasMappedScript = extractMappedPytestScripts(testCaseBody).length > 0;
685
+ const hasPrimary = /\btest_[A-Za-z0-9_]+\b/.test(primaryLine);
686
+ const hasVariant = /(?:变体测试点|Variant Test Points)\s*[::]\s*(?!\s*(?:无|none)\b)[^\n]*\bTP-[A-Z0-9-]+/i.test(automation);
687
+ const hasAssertionOrCross = /(?:场景断言测试点|Assertion Test Points|横切证据测试点|Cross-Cutting Test Points)\s*[::]\s*(?!\s*(?:无|none)\b)[^\n]*\bTP-[A-Z0-9-]+/i.test(automation);
688
+ return scriptIsNone && primaryIsNone && !hasMappedScript && !hasPrimary && !hasVariant && hasAssertionOrCross;
689
+ }
690
+ function isSafeBackendPytestScript(script, workspaceRoot, layout) {
691
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
692
+ const absolute = path.resolve(workspaceRoot, script);
693
+ const relative = path.relative(workspaceRoot, absolute).replaceAll(path.sep, "/");
694
+ const allowedRoots = [`${resolved.testRoot}/`];
695
+ return (!relative.startsWith("..") &&
696
+ !path.isAbsolute(relative) &&
697
+ allowedRoots.some((prefix) => script.startsWith(prefix)) &&
698
+ !script.includes("..") &&
699
+ /^test_.*\.py$/i.test(path.basename(script)));
700
+ }
701
+ function testFunctionRegion(input) {
702
+ const functionLineStart = input.source.lastIndexOf("\n", input.functionIndex - 1) + 1;
703
+ const functionLine = input.source.slice(functionLineStart, input.functionHeaderEnd);
704
+ const indent = functionLine.match(/^\s*/)?.[0] ?? "";
705
+ let regionStart = functionLineStart;
706
+ let cursor = functionLineStart;
707
+ while (cursor > 0) {
708
+ const previousEnd = cursor - 1;
709
+ const previousStart = input.source.lastIndexOf("\n", previousEnd - 1) + 1;
710
+ const previousLine = input.source.slice(previousStart, previousEnd).replace(/\r$/, "");
711
+ if (!previousLine.trim())
712
+ break;
713
+ if (new RegExp(`^${escapeRegExp(indent)}@`).test(previousLine)) {
714
+ regionStart = previousStart;
715
+ cursor = previousStart;
716
+ continue;
717
+ }
718
+ break;
719
+ }
720
+ const lines = input.source.slice(input.functionHeaderEnd).split(/\r?\n/);
721
+ let consumed = 0;
722
+ for (const line of lines) {
723
+ const boundary = line.match(/^(\s*)(?:(?:async\s+)?def|class)\s+[A-Za-z_][A-Za-z0-9_]*\b/);
724
+ if (boundary && boundary[1].length <= indent.length)
725
+ break;
726
+ consumed += line.length + 1;
727
+ }
728
+ const regionEnd = Math.min(input.source.length, input.functionHeaderEnd + consumed);
729
+ return input.source
730
+ .slice(regionStart, regionEnd)
731
+ .split(/\r?\n/)
732
+ .filter((line) => !line.trimStart().startsWith("#"))
733
+ .join("\n");
734
+ }
735
+ function symbolCaseId(symbol) {
736
+ const match = symbol.match(/^test_(BE(?:_[A-Z0-9]+)+?_\d{2,3})(?:_|$)/i);
737
+ return match
738
+ ? canonicalBackendCaseId(match[1].replaceAll("_", "-"))
739
+ : undefined;
740
+ }
741
+ export async function validateBackendMarkdownTraceability(workspaceRoot, layout) {
742
+ const files = await markdownFiles(workspaceRoot, layout);
743
+ const markdownIds = new Set();
744
+ const expectedIdsByScript = new Map();
745
+ const findings = [];
746
+ for (const file of files) {
747
+ if (path.basename(file).toLowerCase() === "readme.md")
748
+ continue;
749
+ const content = await readFile(file, "utf8");
750
+ for (const testCase of splitCases(content)) {
751
+ const scripts = extractMappedPytestScripts(testCase.body);
752
+ if (isEvidenceOnlyMarkdownCaseBody(testCase.body)) {
753
+ continue;
754
+ }
755
+ markdownIds.add(testCase.id);
756
+ if (scripts.length === 0) {
757
+ findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射`);
758
+ continue;
759
+ }
760
+ for (const script of scripts) {
761
+ const bucket = expectedIdsByScript.get(script) ?? new Set();
762
+ bucket.add(testCase.id);
763
+ expectedIdsByScript.set(script, bucket);
764
+ }
765
+ }
766
+ }
767
+ const resolvedLayout = layout ?? resolveBackendTestLayout(undefined);
768
+ const mappings = new Map();
769
+ const automationIds = new Set();
770
+ let requestResponseLoggingFindingCount = 0;
771
+ for (const [relativeScript, expectedIds] of expectedIdsByScript) {
772
+ const absolute = path.resolve(workspaceRoot, relativeScript);
773
+ const relative = path.relative(workspaceRoot, absolute);
774
+ if (relative.startsWith("..") ||
775
+ path.isAbsolute(relative) ||
776
+ !relativeScript.startsWith(`${resolvedLayout.testRoot}/`) ||
777
+ !/^test_.*\.py$/i.test(path.basename(relativeScript))) {
778
+ findings.push(`unsafe mapped pytest script: ${relativeScript}`);
779
+ continue;
780
+ }
781
+ if (!(await exists(absolute))) {
782
+ findings.push(`mapped pytest script is missing: ${relativeScript}`);
783
+ continue;
784
+ }
785
+ const content = await readFile(absolute, "utf8");
786
+ const loggingFindings = httpLoggingFindings(relativeScript, content);
787
+ requestResponseLoggingFindingCount += loggingFindings.length;
788
+ findings.push(...loggingFindings);
789
+ if (/pytest\.(?:skip|xfail)\s*\(|@pytest\.mark\.(?:skip|xfail)\b/.test(content)) {
790
+ findings.push(`skip/xfail is forbidden in mapped backend pytest: ${relativeScript}`);
791
+ }
792
+ const unsafeSwallowedException = findUnsafeSwallowedException(content);
793
+ if (unsafeSwallowedException) {
794
+ findings.push(`swallowed exception is forbidden in mapped backend pytest: ${relativeScript} (${unsafeSwallowedException})`);
795
+ }
796
+ for (const match of content.matchAll(/^(\s*)(?:async\s+)?def\s+(test_[A-Za-z0-9_]+)\s*\([^)]*\)\s*(?:->\s*[^:\r\n]+)?\s*:/gm)) {
797
+ const symbol = match[2];
798
+ const region = testFunctionRegion({
799
+ source: content,
800
+ functionIndex: match.index,
801
+ functionHeaderEnd: match.index + match[0].length,
802
+ });
803
+ const associatedIds = new Set(caseIdsInText(region));
804
+ const fromSymbol = symbolCaseId(symbol);
805
+ if (fromSymbol)
806
+ associatedIds.add(fromSymbol);
807
+ for (const id of associatedIds) {
808
+ automationIds.add(id);
809
+ if (!expectedIds.has(id))
810
+ continue;
811
+ const bucket = mappings.get(id) ?? [];
812
+ bucket.push(`${relativeScript}#${symbol}`);
813
+ mappings.set(id, bucket);
814
+ }
815
+ }
816
+ }
817
+ const missing = [...markdownIds].filter((id) => !mappings.has(id));
818
+ const multiple = [...mappings]
819
+ .filter(([, refs]) => refs.length > 1)
820
+ .map(([id]) => id);
821
+ const extra = [...automationIds].filter((id) => !markdownIds.has(id));
822
+ if (missing.length > 0) {
823
+ findings.push(`Markdown Case IDs without pytest mapping: ${missing.join(", ")}`);
824
+ }
825
+ const mappingCount = [...mappings.values()].reduce((total, refs) => total + refs.length, 0);
826
+ return ([
827
+ "# Backend Test Traceability",
828
+ "",
829
+ "## Status",
830
+ "",
831
+ findings.length === 0 ? "PASS" : "FAIL",
832
+ "",
833
+ `- Markdown Case IDs: ${markdownIds.size}`,
834
+ `- Covered Markdown Case IDs: ${mappings.size}`,
835
+ `- Pytest mappings: ${mappingCount}`,
836
+ `- Scanned pytest scripts: ${expectedIdsByScript.size}`,
837
+ `- Missing mappings: ${missing.length}${missing.length ? ` (${missing.join(", ")})` : ""}`,
838
+ `- Multiple mappings: ${multiple.length}${multiple.length ? ` (${multiple.join(", ")})` : ""}`,
839
+ `- Extra automation Case IDs: ${extra.length}${extra.length ? ` (${extra.join(", ")})` : ""}`,
840
+ `- Request/response logging findings: ${requestResponseLoggingFindingCount}`,
841
+ "- Logging scope: mapped scripts with detected HTTP client calls; findings are advisory",
842
+ "- Scan scope: pytest scripts explicitly mapped by Markdown cases only",
843
+ "",
844
+ "## Findings",
845
+ "",
846
+ ...(findings.length > 0 ? findings.map((finding) => `- ${finding}`) : ["- None"]),
847
+ ].join("\n") + "\n");
848
+ }
849
+ export async function resolveBackendTestMappedPytestScripts(workspaceRoot, layout) {
850
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
851
+ const files = await markdownFiles(workspaceRoot, resolved);
852
+ const expectedScripts = new Set();
853
+ const existingScripts = new Set();
854
+ const missingScripts = new Set();
855
+ const scriptOwners = new Map();
856
+ const recordScript = (script, owner, present) => {
857
+ const previousOwner = scriptOwners.get(script);
858
+ if (previousOwner && previousOwner !== owner) {
859
+ throw new Error(`conflicting Markdown pytest mapping: ${previousOwner} and ${owner} both map ${script}`);
860
+ }
861
+ scriptOwners.set(script, owner);
862
+ expectedScripts.add(script);
863
+ if (present)
864
+ existingScripts.add(script);
865
+ else
866
+ missingScripts.add(script);
867
+ };
868
+ for (const file of files) {
869
+ if (path.basename(file).toLowerCase() === "readme.md")
870
+ continue;
871
+ const relativeFile = path.relative(workspaceRoot, file).replaceAll(path.sep, "/");
872
+ const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile, resolved);
873
+ const markdown = await readFile(file, "utf8");
874
+ const mappedFromModule = new Set();
875
+ for (const testCase of splitCases(markdown)) {
876
+ for (const script of extractMappedPytestScripts(testCase.body)) {
877
+ mappedFromModule.add(script);
878
+ }
879
+ }
880
+ // Prefer explicit mappings, but recover to the deterministic module stem path
881
+ // when the model wrote the correct one-to-one file while Markdown still names
882
+ // a drifted script (common: health.md maps test_health.py but writer emitted
883
+ // test_be_health.py for BE-HEALTH.md, or the reverse). If neither exists, the
884
+ // deterministic module path is the only path a bounded repair may create.
885
+ if (mappedFromModule.size === 0) {
886
+ if (await mappedScriptExists(path.resolve(workspaceRoot, expectedScript))) {
887
+ recordScript(expectedScript, relativeFile, true);
888
+ }
889
+ continue;
890
+ }
891
+ for (const script of mappedFromModule) {
892
+ if (!isSafeBackendPytestScript(script, workspaceRoot, resolved)) {
893
+ throw new Error(`unsafe mapped pytest script: ${script}`);
894
+ }
895
+ if (await mappedScriptExists(path.resolve(workspaceRoot, script))) {
896
+ recordScript(script, relativeFile, true);
897
+ continue;
898
+ }
899
+ if (!isSafeBackendPytestScript(expectedScript, workspaceRoot, resolved)) {
900
+ throw new Error(`unsafe mapped pytest script: ${expectedScript}`);
901
+ }
902
+ recordScript(expectedScript, relativeFile, await mappedScriptExists(path.resolve(workspaceRoot, expectedScript)));
903
+ }
904
+ }
905
+ if (expectedScripts.size === 0) {
906
+ throw new Error("no pytest scripts are mapped by final Markdown cases");
907
+ }
908
+ return {
909
+ expectedScripts: unique(expectedScripts),
910
+ existingScripts: unique(existingScripts),
911
+ missingScripts: unique(missingScripts),
912
+ };
913
+ }
914
+ export async function collectBackendTestMappedPytestScripts(workspaceRoot, layout) {
915
+ const resolution = await resolveBackendTestMappedPytestScripts(workspaceRoot, layout);
916
+ if (resolution.missingScripts.length > 0) {
917
+ throw new Error(`mapped pytest script is missing: ${resolution.missingScripts[0]}`);
918
+ }
919
+ return resolution.existingScripts;
920
+ }
921
+ function cleanCaseTitle(rawHeading, id) {
922
+ return rawHeading
923
+ .replace(/^##\s+/, "")
924
+ // Strip either canonical or compatible two-digit heading ID.
925
+ .replace(/^BE-[A-Z0-9_-]+-\d{2,3}\s*(?:[||—–-]\s*)?/i, "")
926
+ .trim() || id;
927
+ }
928
+ export async function collectBackendTestHumanCaseCatalog(workspaceRoot, layout) {
929
+ const files = await markdownFiles(workspaceRoot, layout);
930
+ const catalog = new Map();
931
+ for (const file of files) {
932
+ if (path.basename(file).toLowerCase() === "readme.md")
933
+ continue;
934
+ const markdown = await readFile(file, "utf8");
935
+ for (const testCase of splitCases(markdown)) {
936
+ const heading = testCase.body.match(/^##\s+.*$/m)?.[0] ?? testCase.id;
937
+ const purpose = sectionBody(testCase.body, CASE_SECTION_ALIASES.testPurpose).trim();
938
+ const fallbackScenario = sectionBody(testCase.body, CASE_SECTION_ALIASES.expectedResults)
939
+ .replace(/^\s*(?:\d+[.)]|[-*+])\s+/gm, "")
940
+ .replace(/\s+/g, " ")
941
+ .trim();
942
+ catalog.set(testCase.id, {
943
+ id: testCase.id,
944
+ title: cleanCaseTitle(heading, testCase.id),
945
+ scenario: purpose.replace(/\s+/g, " ").trim() || fallbackScenario || "详见 Markdown 用例步骤与预期结果。",
946
+ markdownPath: path.relative(workspaceRoot, file).replaceAll(path.sep, "/"),
947
+ testFunctions: [],
948
+ });
949
+ }
950
+ }
951
+ const resolvedLayout = layout ?? resolveBackendTestLayout(undefined);
952
+ const testcaseRoot = path.join(workspaceRoot, ...resolvedLayout.testRoot.split("/"));
953
+ const pythonFiles = [];
954
+ async function walk(directory) {
955
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
956
+ const absolute = path.join(directory, entry.name);
957
+ if (entry.isDirectory())
958
+ await walk(absolute);
959
+ else if (/^test_.*\.py$/i.test(entry.name))
960
+ pythonFiles.push(absolute);
961
+ }
962
+ }
963
+ if (await exists(testcaseRoot))
964
+ await walk(testcaseRoot);
965
+ for (const file of pythonFiles) {
966
+ const content = await readFile(file, "utf8");
967
+ for (const match of content.matchAll(/^(\s*)(?:async\s+)?def\s+(test_[A-Za-z0-9_]+)\s*\([^)]*\)\s*(?:->\s*[^:\r\n]+)?\s*:/gm)) {
968
+ const symbol = match[2];
969
+ const region = testFunctionRegion({ source: content, functionIndex: match.index, functionHeaderEnd: match.index + match[0].length });
970
+ const ids = new Set(caseIdsInText(region));
971
+ const fromSymbol = symbolCaseId(symbol);
972
+ if (fromSymbol)
973
+ ids.add(fromSymbol);
974
+ for (const id of ids) {
975
+ const item = catalog.get(id);
976
+ if (!item)
977
+ continue;
978
+ item.scriptPath = path.relative(workspaceRoot, file).replaceAll(path.sep, "/");
979
+ if (!item.testFunctions.includes(symbol))
980
+ item.testFunctions.push(symbol);
981
+ }
982
+ }
983
+ }
984
+ return [...catalog.values()].sort((left, right) => left.id.localeCompare(right.id));
985
+ }
986
+ function junitCaseId(name) {
987
+ return symbolCaseId(name) ?? caseIdsInText(name)[0];
988
+ }
989
+ /** Prefer function-name Case ID; fall back to first BE-* in free text (docstring/log). */
990
+ function extractCaseIdFromText(value) {
991
+ if (!value?.trim())
992
+ return undefined;
993
+ return junitCaseId(value) ?? caseIdsInText(value)[0];
994
+ }
995
+ function resolveReportCaseId(result, catalog) {
996
+ const fromName = extractCaseIdFromText(result.name);
997
+ if (fromName && catalog.has(fromName))
998
+ return fromName;
999
+ if (fromName)
1000
+ return fromName;
1001
+ // Match catalog by pytest function/method name when Case ID is only in docstring/md.
1002
+ const byFunction = [...catalog.values()].find((item) => item.testFunctions.includes(result.name));
1003
+ if (byFunction)
1004
+ return byFunction.id;
1005
+ const fromStdout = extractCaseIdFromText(result.stdout);
1006
+ if (fromStdout && catalog.has(fromStdout))
1007
+ return fromStdout;
1008
+ if (fromStdout)
1009
+ return fromStdout;
1010
+ const fromDetails = extractCaseIdFromText(result.details);
1011
+ if (fromDetails && catalog.has(fromDetails))
1012
+ return fromDetails;
1013
+ return "未关联";
1014
+ }
1015
+ function humanStatus(status) {
1016
+ return status === "passed" ? "通过" : status === "failure" ? "失败" : status === "error" ? "错误" : "跳过";
1017
+ }
1018
+ function formatDuration(durationMs) {
1019
+ return durationMs === undefined ? "未记录" : `${(durationMs / 1000).toFixed(3)} 秒`;
1020
+ }
1021
+ function inferredScriptPath(classname, filePath) {
1022
+ if (filePath?.trim()) {
1023
+ return filePath.replaceAll("\\", "/").replace(/^\.\//, "");
1024
+ }
1025
+ const moduleName = classname
1026
+ .split(".")
1027
+ .filter((part) => part && !/^Test[A-Z_]/.test(part) && part !== "Test")
1028
+ .join("/");
1029
+ if (!moduleName || moduleName === "unknown")
1030
+ return "unknown.py";
1031
+ return moduleName.endsWith(".py") ? moduleName : `${moduleName}.py`;
1032
+ }
1033
+ function resolveReportScriptPath(result, item) {
1034
+ if (item?.scriptPath)
1035
+ return item.scriptPath;
1036
+ if (result.filePath?.trim()) {
1037
+ return result.filePath.replaceAll("\\", "/").replace(/^\.\//, "");
1038
+ }
1039
+ return inferredScriptPath(result.classname, result.filePath);
1040
+ }
1041
+ export function redactBackendTestOutput(value) {
1042
+ return value
1043
+ .replace(/((?:password|passwd|secret|token|api[_-]?key|authorization)\s*[:=]\s*)\S+/gi, "$1[REDACTED]")
1044
+ .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]");
1045
+ }
1046
+ function escapeHtml(value) {
1047
+ return value
1048
+ .replaceAll("&", "&amp;")
1049
+ .replaceAll("<", "&lt;")
1050
+ .replaceAll(">", "&gt;")
1051
+ .replaceAll('"', "&quot;");
1052
+ }
1053
+ function reportOutputLines(value, prefix) {
1054
+ if (!value?.trim())
1055
+ return [];
1056
+ // Accept both the underscore form ("HTTP_REQUEST") and the space-separated
1057
+ // form ("HTTP REQUEST") that some generated pytest helpers emit. Keep the
1058
+ // original line text so downstream parsers can read the kv payload.
1059
+ const underscorePrefix = prefix;
1060
+ const spacePrefix = prefix.replaceAll("_", " ");
1061
+ return value
1062
+ .split(/\r?\n/)
1063
+ .map((line) => line.trim())
1064
+ .filter((line) => line.startsWith(underscorePrefix) || line.startsWith(spacePrefix))
1065
+ .map((line) => {
1066
+ const trimmed = line.startsWith(underscorePrefix)
1067
+ ? line.slice(underscorePrefix.length)
1068
+ : line.slice(spacePrefix.length);
1069
+ // Preserve a complete single-line JSON payload through parsing. Bounding
1070
+ // happens after structural extraction; slicing here can corrupt valid JSON
1071
+ // and make a large passed response appear as an empty body.
1072
+ return redactBackendTestOutput(trimmed.trim());
1073
+ });
1074
+ }
1075
+ /**
1076
+ * Normalize alternate HTTP log dialects into HTTP_REQUEST / HTTP_RESPONSE lines.
1077
+ * Supports common project helpers that emit [REQ]/[RESP] block style instead of
1078
+ * the contract prefixes (keeps original HTTP_* lines unchanged).
1079
+ */
1080
+ export function normalizeBackendTestHttpLogText(value) {
1081
+ if (!value.trim())
1082
+ return value;
1083
+ const lines = value.split(/\r?\n/);
1084
+ const out = [];
1085
+ let i = 0;
1086
+ while (i < lines.length) {
1087
+ const raw = lines[i];
1088
+ const line = raw.trim();
1089
+ // Canonical and compatible single-line forms. Normalize case, separator,
1090
+ // and an optional colon so downstream parsing only consumes the canonical
1091
+ // HTTP_REQUEST / HTTP_RESPONSE protocol. Examples accepted here include
1092
+ // `HTTP_REQUEST {...}`, `HTTP REQUEST method=...`, and the generated-helper
1093
+ // dialect `HTTP request: {...}`.
1094
+ const protocolLine = line.match(/^HTTP(?:_|\s+)(REQUEST|RESPONSE)\b\s*:?\s*(.*)$/i);
1095
+ if (protocolLine) {
1096
+ const canonicalPrefix = `HTTP_${protocolLine[1].toUpperCase()}`;
1097
+ const payload = protocolLine[2]?.trim() ?? "";
1098
+ out.push(payload ? `${canonicalPrefix} ${payload}` : canonicalPrefix);
1099
+ i += 1;
1100
+ continue;
1101
+ }
1102
+ // [REQ] METHOD path OR [REQ] METHOD url
1103
+ const reqHead = line.match(/^\[REQ\]\s+([A-Z]+)\s+(\S+)\s*$/i);
1104
+ if (reqHead) {
1105
+ const method = reqHead[1].toUpperCase();
1106
+ const url = reqHead[2];
1107
+ let body = "";
1108
+ let j = i + 1;
1109
+ // Optional [REQ body] then JSON/text until separator or next tag.
1110
+ if (j < lines.length && /^\[REQ\s*body\]/i.test(lines[j].trim())) {
1111
+ j += 1;
1112
+ const bodyLines = [];
1113
+ while (j < lines.length &&
1114
+ !/^[=-]{3,}\s*$/.test(lines[j].trim()) &&
1115
+ !/^\[(?:REQ|RESP)/i.test(lines[j].trim()) &&
1116
+ !/^(HTTP_REQUEST|HTTP_RESPONSE|HTTP REQUEST|HTTP RESPONSE)\b/i.test(lines[j].trim())) {
1117
+ bodyLines.push(lines[j]);
1118
+ j += 1;
1119
+ }
1120
+ body = bodyLines.join("\n").trim();
1121
+ }
1122
+ const payload = { method, url };
1123
+ if (body) {
1124
+ try {
1125
+ payload.json = JSON.parse(body);
1126
+ }
1127
+ catch {
1128
+ payload.body = body.slice(0, 4000);
1129
+ }
1130
+ }
1131
+ out.push(`HTTP_REQUEST ${JSON.stringify(payload)}`);
1132
+ i = j;
1133
+ continue;
1134
+ }
1135
+ // [RESP status] 200 OR [RESP] 200
1136
+ const respHead = line.match(/^\[RESP(?:\s+status)?\]\s+(\d{3})\b/i);
1137
+ if (respHead) {
1138
+ const status_code = Number(respHead[1]);
1139
+ let body = "";
1140
+ let j = i + 1;
1141
+ if (j < lines.length && /^\[RESP\s*body\]/i.test(lines[j].trim())) {
1142
+ j += 1;
1143
+ const bodyLines = [];
1144
+ while (j < lines.length &&
1145
+ !/^[=-]{3,}\s*$/.test(lines[j].trim()) &&
1146
+ !/^\[(?:REQ|RESP)/i.test(lines[j].trim()) &&
1147
+ !/^(HTTP_REQUEST|HTTP_RESPONSE|HTTP REQUEST|HTTP RESPONSE)\b/i.test(lines[j].trim())) {
1148
+ bodyLines.push(lines[j]);
1149
+ j += 1;
1150
+ }
1151
+ body = bodyLines.join("\n").trim();
1152
+ }
1153
+ const payload = { status_code };
1154
+ if (body) {
1155
+ try {
1156
+ payload.result = JSON.parse(body);
1157
+ }
1158
+ catch {
1159
+ payload.result = body.slice(0, 4000);
1160
+ }
1161
+ }
1162
+ out.push(`HTTP_RESPONSE ${JSON.stringify(payload)}`);
1163
+ i = j;
1164
+ continue;
1165
+ }
1166
+ out.push(raw);
1167
+ i += 1;
1168
+ }
1169
+ return out.join("\n");
1170
+ }
1171
+ function requestResponseSummary(result) {
1172
+ const combined = normalizeBackendTestHttpLogText([result.stdout, result.stderr].filter(Boolean).join("\n"));
1173
+ return {
1174
+ requests: reportOutputLines(combined, "HTTP_REQUEST"),
1175
+ responses: reportOutputLines(combined, "HTTP_RESPONSE"),
1176
+ };
1177
+ }
1178
+ const HTTP_STATUS_TEXT = {
1179
+ 200: "OK",
1180
+ 201: "Created",
1181
+ 204: "No Content",
1182
+ 400: "Bad Request",
1183
+ 401: "Unauthorized",
1184
+ 403: "Forbidden",
1185
+ 404: "Not Found",
1186
+ 409: "Conflict",
1187
+ 500: "Internal Server Error",
1188
+ };
1189
+ /**
1190
+ * Parse the kv payload of an HTTP log line into a structured entry.
1191
+ *
1192
+ * Accepts two shapes emitted by generated pytest helpers:
1193
+ * - legacy: a bare JSON object ({"method":"POST",...})
1194
+ * - current: kv pairs (method=POST url=... parameters={...} / status_code=201 result={...})
1195
+ */
1196
+ function parseHttpLogPayload(line, kind) {
1197
+ const trimmed = line.trim();
1198
+ if (!trimmed)
1199
+ return {};
1200
+ let parsedJson;
1201
+ let kv = {};
1202
+ // Try JSON first (legacy HTTP_REQUEST {"method":...} shape).
1203
+ if (trimmed.startsWith("{")) {
1204
+ try {
1205
+ parsedJson = JSON.parse(trimmed);
1206
+ for (const [key, value] of Object.entries(parsedJson)) {
1207
+ kv[key.toLowerCase()] = typeof value === "string" ? value : safeStringify(value);
1208
+ }
1209
+ }
1210
+ catch {
1211
+ // Fall through to kv extraction below.
1212
+ }
1213
+ }
1214
+ if (Object.keys(kv).length === 0) {
1215
+ kv = extractKvPairs(trimmed);
1216
+ }
1217
+ if (kind === "request") {
1218
+ const queryEnvelope = asHttpLogRecord(parsedJson?.query);
1219
+ const method = kv.method ?? httpLogScalar(queryEnvelope?.method);
1220
+ const url = kv.url ?? kv.path ?? httpLogScalar(queryEnvelope?.url) ?? httpLogScalar(queryEnvelope?.path);
1221
+ let body = kv.parameters ?? kv.payload ?? kv.data;
1222
+ if (!body && parsedJson) {
1223
+ const parameters = {};
1224
+ if (queryEnvelope) {
1225
+ const businessQuery = Object.fromEntries(Object.entries(queryEnvelope).filter(([key]) => !["method", "url", "path"].includes(key)));
1226
+ parameters.query = businessQuery;
1227
+ }
1228
+ else if (Object.hasOwn(parsedJson, "query")) {
1229
+ parameters.query = parsedJson.query;
1230
+ }
1231
+ for (const key of ["body", "json"]) {
1232
+ if (Object.hasOwn(parsedJson, key))
1233
+ parameters[key] = parsedJson[key];
1234
+ }
1235
+ if (Object.keys(parameters).length > 0)
1236
+ body = boundedHttpLogBody(parameters);
1237
+ }
1238
+ return {
1239
+ method,
1240
+ url,
1241
+ body: boundedHttpLogBody(body ?? kv.body ?? kv.json ?? kv.query),
1242
+ };
1243
+ }
1244
+ const nestedJson = asHttpLogRecord(parsedJson?.json);
1245
+ const nestedTransport = !kv.status_code && !kv.status
1246
+ && Boolean(nestedJson && (Object.hasOwn(nestedJson, "status_code") || Object.hasOwn(nestedJson, "body")));
1247
+ const status = kv.status_code ?? kv.status
1248
+ ?? (nestedTransport ? httpLogScalar(nestedJson?.status_code ?? nestedJson?.status) : undefined);
1249
+ const body = kv.result
1250
+ ?? (nestedTransport ? boundedHttpLogBody(nestedJson?.body ?? nestedJson) : kv.json)
1251
+ ?? kv.body
1252
+ ?? kv.text;
1253
+ return {
1254
+ method: kv.method,
1255
+ url: kv.url ?? kv.path,
1256
+ status,
1257
+ statusText: kv.status_text ?? (status ? HTTP_STATUS_TEXT[status] : undefined),
1258
+ body: boundedHttpLogBody(body),
1259
+ };
1260
+ }
1261
+ function asHttpLogRecord(value) {
1262
+ return value !== null && typeof value === "object" && !Array.isArray(value)
1263
+ ? value
1264
+ : undefined;
1265
+ }
1266
+ function httpLogScalar(value) {
1267
+ return typeof value === "string" || typeof value === "number"
1268
+ ? String(value)
1269
+ : undefined;
1270
+ }
1271
+ function boundedHttpLogBody(value) {
1272
+ if (value === undefined)
1273
+ return undefined;
1274
+ const text = typeof value === "string" ? value : safeStringify(value);
1275
+ const maxChars = 4000;
1276
+ return text.length <= maxChars
1277
+ ? text
1278
+ : `${text.slice(0, maxChars)}\n… [truncated ${text.length - maxChars} chars]`;
1279
+ }
1280
+ function extractKvPairs(value) {
1281
+ const result = {};
1282
+ // Split on top-level spaces but keep {...}/[...] payloads intact.
1283
+ let depth = 0;
1284
+ let buffer = "";
1285
+ const flush = () => {
1286
+ const segment = buffer.trim();
1287
+ buffer = "";
1288
+ if (!segment)
1289
+ return;
1290
+ const eq = segment.indexOf("=");
1291
+ if (eq <= 0)
1292
+ return;
1293
+ const key = segment.slice(0, eq).trim();
1294
+ const val = segment.slice(eq + 1).trim();
1295
+ if (key)
1296
+ result[key] = val;
1297
+ };
1298
+ for (const ch of value) {
1299
+ if (ch === "{" || ch === "[")
1300
+ depth += 1;
1301
+ else if (ch === "}" || ch === "]")
1302
+ depth = Math.max(0, depth - 1);
1303
+ if (ch === " " && depth === 0) {
1304
+ flush();
1305
+ }
1306
+ else {
1307
+ buffer += ch;
1308
+ }
1309
+ }
1310
+ flush();
1311
+ return result;
1312
+ }
1313
+ function safeStringify(value) {
1314
+ try {
1315
+ return JSON.stringify(value, null, 2);
1316
+ }
1317
+ catch {
1318
+ return String(value);
1319
+ }
1320
+ }
1321
+ function reportDetails(title, values, emptyText) {
1322
+ const body = values.length > 0
1323
+ ? values.map((value) => `<pre>${escapeHtml(value)}</pre>`).join("")
1324
+ : `<p class="muted">${escapeHtml(emptyText)}</p>`;
1325
+ return `<details class="io-details" open><summary>${escapeHtml(title)} <span>${values.length}</span></summary>${body}</details>`;
1326
+ }
1327
+ function parseAdvisorySummary(markdown) {
1328
+ if (!markdown?.trim())
1329
+ return { status: "Unavailable", findings: undefined, firstFinding: "未提供证据" };
1330
+ const explicitStatus = markdown.match(/^## Status\s*\r?\n\s*(PASS|FAIL)\s*$/mi)?.[1]
1331
+ ?? markdown.trim().match(/^(PASS|FAIL)$/i)?.[1]?.toUpperCase();
1332
+ const status = explicitStatus;
1333
+ const findings = Number(markdown.match(/^- Findings:\s*(\d+)\s*$/mi)?.[1]);
1334
+ const findingLines = markdown
1335
+ .split(/\r?\n/)
1336
+ .slice(markdown.split(/\r?\n/).findIndex((line) => /^## Findings\s*$/.test(line.trim())) + 1)
1337
+ .map((line) => line.trim())
1338
+ .filter((line) => /^-\s+/.test(line) && !/^- None$/i.test(line));
1339
+ const resolvedFindings = Number.isFinite(findings) ? findings : findingLines.length;
1340
+ return {
1341
+ status: status ?? "Unavailable",
1342
+ findings: resolvedFindings,
1343
+ firstFinding: findingLines[0]?.replace(/^-\s+/, "") ?? "无",
1344
+ };
1345
+ }
1346
+ function advisoryBadge(status) {
1347
+ const className = status === "PASS" ? "passed" : status === "FAIL" ? "failure" : "skipped";
1348
+ return `<span class="status ${className}">${status === "Unavailable" ? "不可用" : status}</span>`;
1349
+ }
1350
+ function conclusionIcon(failed) {
1351
+ return failed
1352
+ ? `<svg class="conclusion-icon" viewBox="0 0 48 48" role="img" aria-label="测试未通过"><circle cx="24" cy="24" r="20"/><path d="M24 14v13"/><circle cx="24" cy="34" r="1.5" class="icon-dot"/></svg>`
1353
+ : `<svg class="conclusion-icon" viewBox="0 0 48 48" role="img" aria-label="测试通过"><circle cx="24" cy="24" r="20"/><path d="m14 24 7 7 14-15"/></svg>`;
1354
+ }
1355
+ function reportDetailSection(title, content, open = false) {
1356
+ return `<details class="report-detail"${open ? " open" : ""}><summary>${escapeHtml(title)}</summary><div class="report-detail-body">${content}</div></details>`;
1357
+ }
1358
+ /** Render a pretty "prettified" body block: if it looks like JSON/kv, reformat; otherwise show raw. */
1359
+ function renderLogBody(raw, emptyText) {
1360
+ const text = (raw ?? "").trim();
1361
+ if (!text || text === "{}" || text === "null") {
1362
+ return `<pre style="white-space:pre-wrap;background:#f6f8fa;color:#1f2937;padding:10px 12px;border-radius:8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.78rem;margin-top:6px;border:1px solid #eceef2;line-height:1.6">${escapeHtml(emptyText)}</pre>`;
1363
+ }
1364
+ let pretty = text;
1365
+ // Best-effort: pretty-print JSON payloads.
1366
+ if (text.startsWith("{") || text.startsWith("[")) {
1367
+ try {
1368
+ pretty = JSON.stringify(JSON.parse(text), null, 2);
1369
+ }
1370
+ catch {
1371
+ // Keep original kv text.
1372
+ }
1373
+ }
1374
+ else if (text.includes(":") || text.includes("=")) {
1375
+ // kv-style "key: value" or "key=value" → one per line.
1376
+ pretty = text
1377
+ .replace(/\s+(?=[A-Za-z_]+[:=])/g, "\n")
1378
+ .trim();
1379
+ }
1380
+ return `<pre style="white-space:pre-wrap;background:#f6f8fa;color:#1f2937;padding:10px 12px;border-radius:8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.78rem;margin-top:6px;border:1px solid #eceef2;line-height:1.6">${escapeHtml(pretty)}</pre>`;
1381
+ }
1382
+ function statusColor(status) {
1383
+ if (status === "passed")
1384
+ return { fg: "#067647", bg: "#ecfdf3" };
1385
+ if (status === "skipped")
1386
+ return { fg: "#9a6700", bg: "#fffaeb" };
1387
+ return { fg: "#b42318", bg: "#fef3f2" };
1388
+ }
1389
+ function methodBadge(method) {
1390
+ const m = (method ?? "?").toUpperCase();
1391
+ const bg = m === "GET" ? "#2563eb" : m === "POST" ? "#16a34a" : m === "DELETE" ? "#b42318" : m === "PUT" || m === "PATCH" ? "#d97706" : "#475467";
1392
+ return `<span style="display:inline-block;padding:2px 8px;border-radius:5px;background:${bg};color:#fff;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.72rem;font-weight:700">${escapeHtml(m)}</span>`;
1393
+ }
1394
+ function statusPill(status, fallback) {
1395
+ const code = Number(status);
1396
+ if (!Number.isFinite(code) || code === 0) {
1397
+ return `<span style="color:#98a2b3;font-size:0.74rem">${escapeHtml(fallback)}</span>`;
1398
+ }
1399
+ const color = code >= 200 && code < 300 ? { fg: "#067647", bg: "#ecfdf3" } : code >= 400 ? { fg: "#b42318", bg: "#fef3f2" } : { fg: "#9a6700", bg: "#fffaeb" };
1400
+ return `<span style="display:inline-block;padding:2px 9px;border-radius:999px;font-weight:700;font-size:0.72rem;color:${color.fg};background:${color.bg}">${code}</span>`;
1401
+ }
1402
+ /** Pair request lines with their matching response lines (by method+url or order). */
1403
+ function pairHttpCalls(requests, responses) {
1404
+ const parsedRequests = requests.map((line) => parseHttpLogPayload(line, "request"));
1405
+ const parsedResponses = responses.map((line) => parseHttpLogPayload(line, "response"));
1406
+ const used = new Set();
1407
+ return parsedRequests.map((request) => {
1408
+ let matchIndex = -1;
1409
+ for (let i = 0; i < parsedResponses.length; i += 1) {
1410
+ if (used.has(i))
1411
+ continue;
1412
+ const candidate = parsedResponses[i];
1413
+ if (candidate.method && request.method && candidate.method.toUpperCase() === request.method.toUpperCase()
1414
+ && candidate.url && request.url && candidate.url === request.url) {
1415
+ matchIndex = i;
1416
+ break;
1417
+ }
1418
+ }
1419
+ // Fallback: first unused response in order.
1420
+ if (matchIndex < 0) {
1421
+ for (let i = 0; i < parsedResponses.length; i += 1) {
1422
+ if (!used.has(i)) {
1423
+ matchIndex = i;
1424
+ break;
1425
+ }
1426
+ }
1427
+ }
1428
+ if (matchIndex >= 0) {
1429
+ used.add(matchIndex);
1430
+ return { request, response: parsedResponses[matchIndex] };
1431
+ }
1432
+ return { request };
1433
+ });
1434
+ }
1435
+ function renderHttpCallCard(call) {
1436
+ // method/url come from the request line; status/result come from the response.
1437
+ // Some log formats (HTTP RESPONSE status=... result=...) omit method/url on
1438
+ // the response line, so prefer the request for those fields.
1439
+ const method = call.request.method ?? call.response?.method;
1440
+ const url = call.request.url ?? call.response?.url ?? "未知 URL";
1441
+ const header = `<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">${methodBadge(method)}<code style="font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.78rem;color:#344054">${escapeHtml(url)}</code><span style="flex:1"></span>${call.response ? statusPill(call.response.status, call.response.statusText ?? "") : "<span style=\"color:#98a2b3;font-size:0.74rem\">无响应</span>"}${call.response?.statusText ? `<span style="color:#98a2b3;font-size:0.74rem">${escapeHtml(call.response.statusText)}</span>` : ""}</div>`;
1442
+ const requestBlock = `<div style="color:#667085;font-size:0.74rem;font-weight:600;margin-bottom:3px;margin-top:10px">请求参数 / 请求体</div>${renderLogBody(call.request.body, "无")}`;
1443
+ const responseBlock = call.response
1444
+ ? `<div style="color:#667085;font-size:0.74rem;font-weight:600;margin-bottom:3px;margin-top:10px">响应结果</div>${renderLogBody(call.response.body, "无")}`
1445
+ : "";
1446
+ return `<div style="border:1px solid #eef0f3;border-radius:10px;padding:12px 14px;margin-top:10px;background:#fcfdff">${header}${requestBlock}${responseBlock}</div>`;
1447
+ }
1448
+ function analyzeBackendTestFailure(result) {
1449
+ const evidence = [result.message, result.details].filter(Boolean).join("\n");
1450
+ const statusPatterns = [
1451
+ { pattern: /实际(?:\s*HTTP)?\s*状态码\s*[::]?\s*(\d{3})[\s\S]{0,80}?预期(?:\s*HTTP)?\s*状态码?\s*[::]?\s*(\d{3})/i, actualGroup: 1, expectedGroup: 2 },
1452
+ { pattern: /预期(?:\s*HTTP)?\s*状态码\s*[::]?\s*(\d{3})[\s\S]{0,80}?实际(?:\s*HTTP)?\s*状态码?\s*[::]?\s*(\d{3})/i, actualGroup: 2, expectedGroup: 1 },
1453
+ { pattern: /assert\s+(\d{3})\s*==\s*(\d{3})/i, actualGroup: 1, expectedGroup: 2 },
1454
+ { pattern: /expected\s*[:=]?\s*(\d{3})[\s\S]{0,80}?(?:actual|received|got)\s*[:=]?\s*(\d{3})/i, actualGroup: 2, expectedGroup: 1 },
1455
+ ];
1456
+ for (const candidate of statusPatterns) {
1457
+ const match = evidence.match(candidate.pattern);
1458
+ if (match) {
1459
+ const actual = match[candidate.actualGroup];
1460
+ const expected = match[candidate.expectedGroup];
1461
+ if (actual && expected) {
1462
+ return `HTTP 状态码不符合预期:期望 ${expected},实际 ${actual}。建议核对接口错误码契约、异常映射与测试前置数据。`;
1463
+ }
1464
+ }
1465
+ }
1466
+ const contentTypeMatch = evidence.match(/assert\s+(['"])(.*?)\1\.startswith\(\s*(['"])application\/json\3\s*\)/i);
1467
+ if (contentTypeMatch) {
1468
+ const actual = contentTypeMatch[2];
1469
+ const actualLabel = actual ? `为 ${actual}` : "为空值";
1470
+ return `响应 Content-Type 不符合预期:期望以 application/json 开头,实际${actualLabel}。建议核对接口响应头设置。`;
1471
+ }
1472
+ const assertionEvidence = (result.details ?? "")
1473
+ .split(/\r?\n/)
1474
+ .map((line) => line.trim().replace(/^E\s+/, ""))
1475
+ .find((line) => /^assert\b/i.test(line));
1476
+ const rawMessage = /^\d+:\s*AssertionError\s*$/i.test(result.message?.trim() ?? "") && assertionEvidence
1477
+ ? assertionEvidence
1478
+ : result.message || assertionEvidence || humanStatus(result.status);
1479
+ const message = rawMessage.replace(/\s+/g, " ").trim().slice(0, 240);
1480
+ if (result.status === "error") {
1481
+ return `测试执行发生错误:${message}。建议优先检查 fixture、依赖导入、环境连接与运行时异常。`;
1482
+ }
1483
+ return `断言未满足:${message}。建议结合失败详情及请求/响应证据核对接口行为与用例预期。`;
1484
+ }
1485
+ export function renderBackendTestHtml(input) {
1486
+ const failed = input.parsed.failed + input.parsed.errors > 0;
1487
+ const passRate = input.parsed.tests > 0 ? input.parsed.passed / input.parsed.tests : 0;
1488
+ const catalog = new Map((input.cases ?? []).map((item) => [item.id, item]));
1489
+ const caseValidation = parseAdvisorySummary(input.caseValidationSummary);
1490
+ const caseCoverage = parseAdvisorySummary(input.caseCoverageSummary);
1491
+ const traceability = parseAdvisorySummary(input.traceabilitySummary);
1492
+ const correspondence = parseAdvisorySummary(input.correspondenceSummary);
1493
+ const failures = input.parsed.cases.filter((result) => result.status !== "passed");
1494
+ const headColor = failed ? { fg: "#b42318", bg: "linear-gradient(135deg,#fef3f2 0,#fee4e2 100%)", border: "#fda29b", icon: "✗" } : { fg: "#067647", bg: "linear-gradient(135deg,#ecfdf3 0,#d1fadf 100%)", border: "#abefc6", icon: "✓" };
1495
+ const orderedCases = input.parsed.cases
1496
+ .map((result, index) => ({ result, index }))
1497
+ .sort((left, right) => {
1498
+ const leftFailed = left.result.status === "failure" || left.result.status === "error" ? 0 : 1;
1499
+ const rightFailed = right.result.status === "failure" || right.result.status === "error" ? 0 : 1;
1500
+ return leftFailed - rightFailed || left.index - right.index;
1501
+ })
1502
+ .map(({ result }) => result);
1503
+ const caseCards = orderedCases.map((result) => {
1504
+ const caseId = resolveReportCaseId(result, catalog);
1505
+ const item = caseId === "未关联" ? undefined : catalog.get(caseId);
1506
+ const io = requestResponseSummary(result);
1507
+ const calls = pairHttpCalls(io.requests, io.responses);
1508
+ const statusColors = statusColor(result.status);
1509
+ const logSection = calls.length > 0
1510
+ ? `<div style="margin-top:12px;display:flex;align-items:baseline;gap:8px"><span style="color:#17365d;font-size:0.9rem;font-weight:700">执行日志</span><span style="color:#98a2b3;font-size:0.76rem">共 ${calls.length} 次接口调用</span></div>${calls.map(renderHttpCallCard).join("")}`
1511
+ : "";
1512
+ const failureBlock = result.status === "passed"
1513
+ ? ""
1514
+ : `<div style="margin-top:12px;padding:10px 12px;border-radius:8px;background:#fef3f2;border:1px solid #fda29b;color:#b42318;font-size:0.84rem"><strong>${escapeHtml(result.message || humanStatus(result.status))}</strong>${result.details ? `<details style="margin-top:6px"><summary style="cursor:pointer;color:#b42318;font-size:0.78rem">失败详情</summary><pre style="white-space:pre-wrap;background:#f6f8fa;color:#1f2937;padding:10px 12px;border-radius:8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.78rem;margin-top:6px;border:1px solid #eceef2;line-height:1.6">${escapeHtml(result.details)}</pre></details>` : ""}</div>`;
1515
+ const scriptPath = resolveReportScriptPath(result, item);
1516
+ return `<details style="background:#fff;border:1px solid #e6eaf0;border-radius:12px;margin-top:10px"><summary style="display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;list-style:none"><span style="background:#e8edf5;color:#2b3a55;padding:2px 9px;border-radius:6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.76rem;font-weight:600;white-space:nowrap">${escapeHtml(caseId)}</span><div style="flex:1;min-width:0"><div style="font-weight:600;color:#172033;font-size:0.92rem">${escapeHtml(item?.title ?? result.name)}</div><code style="font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.74rem;color:#98a2b3">${escapeHtml(result.name)}</code></div><span style="color:#98a2b3;font-size:0.8rem;white-space:nowrap">${formatDuration(result.durationMs)}</span><span style="display:inline-block;padding:2px 10px;border-radius:999px;font-weight:700;font-size:0.72rem;color:${statusColors.fg};background:${statusColors.bg}">${humanStatus(result.status)}</span><span style="color:#aab2bf;font-size:0.8rem">▾</span></summary><div style="padding:4px 16px 14px 16px;border-top:1px solid #f0f3f7"><div style="margin-top:10px"><div style="color:#667085;font-size:0.74rem;font-weight:600;margin-bottom:3px">自动化脚本</div><code style="background:#eef2f7;color:#344054;padding:1px 6px;border-radius:5px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.8rem">${escapeHtml(scriptPath)}</code></div>${item?.scenario ? `<div style="margin-top:12px"><div style="color:#667085;font-size:0.74rem;font-weight:600;margin-bottom:3px">验收场景</div><div style="color:#475467;font-size:0.86rem;line-height:1.75">${escapeHtml(item.scenario)}</div></div>` : ""}${logSection}${failureBlock}</div></details>`;
1517
+ }).join("");
1518
+ const metricCard = (label, value, valueColor = "#172033") => `<div style="padding:14px 16px;border:1px solid #e3e8ef;border-radius:12px;background:#fbfcfe"><span style="color:#667085;font-size:0.78rem">${escapeHtml(label)}</span><div style="font-size:22px;font-weight:700;color:${valueColor}">${value}</div></div>`;
1519
+ const metrics = `<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px;margin-top:14px">${metricCard("用例总数", input.parsed.tests)}${metricCard("通过", input.parsed.passed, "#067647")}${metricCard("失败", input.parsed.failed)}${metricCard("错误", input.parsed.errors)}${metricCard("跳过", input.parsed.skipped)}<div style="padding:14px 16px;border:1px solid #e3e8ef;border-radius:12px;background:#fbfcfe"><span style="color:#667085;font-size:0.78rem">通过率</span><div style="font-size:22px;font-weight:700;color:#067647">${(passRate * 100).toFixed(2)}%</div><span style="color:#667085;font-size:0.78rem">耗时 ${formatDuration(input.parsed.durationMs)}</span></div></div>`;
1520
+ const summaryBanner = `<div style="display:flex;align-items:center;gap:16px;padding:16px 18px;border-radius:14px;background:${headColor.bg};border:1px solid ${headColor.border}"><div style="flex:0 0 auto;width:44px;height:44px;border-radius:999px;background:${headColor.fg};display:flex;align-items:center;justify-content:center;color:#fff;font-size:24px;font-weight:900;box-shadow:0 4px 12px ${headColor.fg}4d">${headColor.icon}</div><div><div style="color:${headColor.fg};font-size:1.05rem;font-weight:800;line-height:1.3">${failed ? "本轮测试未通过" : "本轮测试通过"}</div><div style="color:${headColor.fg};font-size:0.88rem;margin-top:2px">${failed ? `${input.parsed.failed + input.parsed.errors} 条用例失败或错误` : `${input.parsed.passed} 条用例全部执行成功`}</div></div></div>`;
1521
+ const qualityBlock = `<div style="margin-top:18px"><div style="color:#17365d;font-size:16px;font-weight:700;margin:0 4px 6px">质量校验</div><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;margin-top:8px">${renderQualityCard("Markdown 用例校验", caseValidation, input.caseValidationSummary)}${renderQualityCard("用例场景覆盖分析", caseCoverage, input.caseCoverageSummary)}${renderQualityCard("Markdown → pytest 追溯", traceability, input.traceabilitySummary)}${renderQualityCard("Markdown → pytest 一一对应", correspondence, input.correspondenceSummary)}</div></div>`;
1522
+ const failureOverview = failures.length > 0
1523
+ ? `<div style="margin-top:18px"><div style="color:#17365d;font-size:16px;font-weight:700;margin:0 4px 6px">失败概览</div><div style="display:grid;gap:10px;margin-top:8px">${failures.map((result) => {
1524
+ const caseId = resolveReportCaseId(result, catalog);
1525
+ const c = statusColor(result.status);
1526
+ return `<div style="border-left:4px solid ${c.fg};background:${c.bg};padding:10px 12px;border-radius:8px"><div style="font-weight:600;color:#172033;font-size:0.88rem">${escapeHtml(caseId)} · ${escapeHtml(catalog.get(caseId)?.title ?? result.name)}</div><div style="color:${c.fg};font-size:0.82rem;margin-top:4px"><strong>原始失败:</strong>${escapeHtml(result.message || humanStatus(result.status))}</div><div style="color:#475467;font-size:0.82rem;margin-top:5px"><strong style="color:#17365d">原因分析:</strong>${escapeHtml(analyzeBackendTestFailure(result))}</div></div>`;
1527
+ }).join("")}</div></div>`
1528
+ : "";
1529
+ return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(input.title)}</title></head><body style="margin:0;background:linear-gradient(180deg,#eef2f7 0,#f7f9fc 40rem);color:#172033;font-family:system-ui,'Microsoft YaHei','PingFang SC',sans-serif;line-height:1.6"><div style="max-width:1000px;margin:0 auto;padding:32px 22px 56px"><div style="padding:8px 0 4px"><div style="color:#315f93;font-weight:700;letter-spacing:0.06em;font-size:0.76rem">后端自动化测试报告</div><h1 style="margin:6px 0 0;color:#17365d;font-size:24px;line-height:1.35">${escapeHtml(input.title)}</h1><p style="color:#667085;margin:8px 0 0;font-size:0.9rem">执行事实来自同一次 pytest 执行;用例默认折叠,点击任意用例可展开执行日志(接口、请求参数、响应结果)与检查点。</p></div><div style="background:#fff;border:1px solid #e3e8ef;border-radius:16px;padding:18px 20px;margin-top:16px;box-shadow:0 1px 2px rgba(16,24,40,0.04)">${summaryBanner}${metrics}</div><div style="background:#fff;border:1px solid #e3e8ef;border-radius:16px;padding:18px 20px;margin-top:16px;box-shadow:0 1px 2px rgba(16,24,40,0.04)">${qualityBlock}<div style="margin-top:12px;padding:10px 12px;background:#f6f8fa;border-radius:8px;font-size:0.82rem;color:#475467"><strong style="color:#667085">执行环境</strong>:${escapeHtml((input.environmentSummary || "不可用").split("\n").slice(0, 3).join(" ").slice(0, 200))}</div></div>${failureOverview}<div style="margin-top:18px"><div style="display:flex;align-items:baseline;justify-content:space-between;margin:0 4px 6px"><span style="color:#17365d;font-size:16px;font-weight:700">用例执行明细</span><span style="color:#98a2b3;font-size:0.8rem">共 ${input.parsed.cases.length} 条 · 点击展开</span></div>${caseCards || '<div style="padding:16px;border:1px dashed #e3e8ef;border-radius:12px;color:#667085;background:#fbfcfe;text-align:center">未发现可展示的用例。</div>'}</div></div></body></html>`;
1530
+ }
1531
+ function renderQualityCard(title, summary, rawSummary) {
1532
+ const colors = summary.status === "PASS" ? { fg: "#067647", bg: "#ecfdf3" } : summary.status === "FAIL" ? { fg: "#b42318", bg: "#fef3f2" } : { fg: "#9a6700", bg: "#fffaeb" };
1533
+ const label = summary.status === "Unavailable" ? "不可用" : summary.status;
1534
+ const details = rawSummary?.trim()
1535
+ ? `<details style="margin-top:9px;padding-top:8px;border-top:1px solid #e3e8ef"><summary style="cursor:pointer;color:#315f93;font-size:0.78rem;font-weight:700">查看详情 · ${escapeHtml(title)}详情</summary><pre style="white-space:pre-wrap;overflow-wrap:anywhere;background:#f6f8fa;color:#1f2937;padding:10px 12px;border-radius:8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.74rem;margin:7px 0 0;border:1px solid #eceef2;line-height:1.55">${escapeHtml(rawSummary.trim())}</pre></details>`
1536
+ : "";
1537
+ return `<div style="padding:12px 14px;border:1px solid #e3e8ef;border-radius:10px;background:#fbfcfe"><div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px"><strong style="color:#17365d;font-size:0.88rem">${escapeHtml(title)}</strong><span style="display:inline-block;padding:2px 9px;border-radius:999px;font-weight:700;font-size:0.72rem;color:${colors.fg};background:${colors.bg}">${label}</span></div><div style="color:#667085;font-size:0.78rem">Findings:${summary.findings ?? "不可用"}</div><div style="color:#475467;font-size:0.8rem;margin-top:2px">${escapeHtml(summary.firstFinding)}</div>${details}</div>`;
1538
+ }
1539
+ /**
1540
+ * Dump JaCoCo execution data over TCP from a JaCoCo tcpserver agent, convert
1541
+ * .exec → jacoco.xml via jacococli.jar, then parse into a code-coverage-v1
1542
+ * contract. Designed for Java services started with
1543
+ * `-javaagent:jacocoagent.jar=output=tcpserver,port=6300,append=false`.
1544
+ *
1545
+ * Failure-safe: any transport/conversion/parse error returns null so the L-5
1546
+ * dashboard degrades coverage to unavailable instead of blocking the run.
1547
+ */
1548
+ export async function collectJacocoCoverage(input) {
1549
+ const [host, portStr] = input.endpoint.split(":");
1550
+ const port = Number(portStr);
1551
+ if (!host || !Number.isInteger(port) || port <= 0 || port > 65535) {
1552
+ return null;
1553
+ }
1554
+ const execPath = path.join(input.workDir, "jacoco.exec");
1555
+ const xmlPath = path.join(input.workDir, "jacoco.xml");
1556
+ // Step 1: TCP dump of JaCoCo execution data. The protocol is JaCoCo's
1557
+ // binary "dump" command: a magic header + command byte. We send the dump
1558
+ // request and stream the response into jacoco.exec.
1559
+ try {
1560
+ await dumpJacocoExec(host, port, execPath, input.connectTimeoutMs);
1561
+ }
1562
+ catch {
1563
+ return null;
1564
+ }
1565
+ // Step 2: jacococli.jar report .exec → jacoco.xml
1566
+ try {
1567
+ await execFileAsync("java", [
1568
+ "-jar", input.cliJarPath,
1569
+ "report", execPath,
1570
+ "--xml", xmlPath,
1571
+ "--includes", input.includes,
1572
+ ], { timeout: 60000, windowsHide: true });
1573
+ }
1574
+ catch {
1575
+ return null;
1576
+ }
1577
+ // Step 3: parse jacoco.xml into a code-coverage-v1 contract.
1578
+ try {
1579
+ const xml = await readFile(xmlPath, "utf8");
1580
+ const artifactSha256 = createHash("sha256").update(xml).digest("hex");
1581
+ return parseJacocoXml(xml, {
1582
+ sourceScope: input.sourceScope,
1583
+ commitSha: input.commitSha,
1584
+ artifactPath: "reports/jacoco.xml",
1585
+ artifactSha256,
1586
+ });
1587
+ }
1588
+ catch {
1589
+ return null;
1590
+ }
1591
+ }
1592
+ /**
1593
+ * Send a JaCoCo TCP dump command and stream the response into destPath.
1594
+ * JaCoCo tcpserver protocol: connect, send dump command header, read until close.
1595
+ */
1596
+ function dumpJacocoExec(host, port, destPath, connectTimeoutMs) {
1597
+ return new Promise((resolve, reject) => {
1598
+ const socket = connect({ host, port });
1599
+ const writeStream = createWriteStream(destPath);
1600
+ const timer = setTimeout(() => {
1601
+ socket.destroy();
1602
+ reject(new Error("jacoco-tcp-connect-timeout"));
1603
+ }, connectTimeoutMs);
1604
+ socket.on("connect", () => {
1605
+ // JaCoCo TCP "dump" command: magic 0xC0FFEE + cmd=0x40 (dump)
1606
+ // Format: <magic 3 bytes><cmd id 1 byte>
1607
+ // The server responds with the exec data and closes.
1608
+ const magic = Buffer.from([0xC0, 0xFF, 0xEE]);
1609
+ const dumpCmd = Buffer.from([0x40]);
1610
+ socket.end(Buffer.concat([magic, dumpCmd]));
1611
+ });
1612
+ socket.on("error", (error) => {
1613
+ clearTimeout(timer);
1614
+ reject(error);
1615
+ });
1616
+ socket.on("close", () => {
1617
+ clearTimeout(timer);
1618
+ writeStream.end(() => {
1619
+ // Verify we got non-empty exec data.
1620
+ readFile(destPath).then((buf) => {
1621
+ if (buf.length === 0)
1622
+ reject(new Error("jacoco-dump-empty"));
1623
+ else
1624
+ resolve();
1625
+ }).catch(reject);
1626
+ });
1627
+ });
1628
+ socket.pipe(writeStream);
1629
+ });
1630
+ }
1631
+ /**
1632
+ * Deterministically derive a Backend Test Case Manifest v1 from final
1633
+ * Markdown cases (testcase/md/**) and generated pytest scripts
1634
+ * (testcase/test_*.py). No model involvement: AC IDs come from the case
1635
+ * 验收标准/Acceptance Criteria section, automation status comes from whether
1636
+ * a pytest test function references the Case ID. The output feeds
1637
+ * computeCaseManifestCoverageSummary so the L-5 dashboard shows real
1638
+ * AC/automation coverage instead of unavailable.
1639
+ */
1640
+ export async function deriveBackendTestCaseManifest(input) {
1641
+ const files = await markdownFiles(input.workspaceRoot);
1642
+ const manifestCases = [];
1643
+ for (const file of files) {
1644
+ if (path.basename(file).toLowerCase() === "readme.md")
1645
+ continue;
1646
+ const content = await readFile(file, "utf8");
1647
+ for (const testCase of splitCases(content)) {
1648
+ const acIds = unique(testCase.body.match(AC_ID) ?? []);
1649
+ if (acIds.length === 0)
1650
+ continue; // skip cases without AC binding
1651
+ const mappedScripts = extractMappedPytestScripts(testCase.body);
1652
+ const category = inferCaseCategory(testCase.body);
1653
+ let generated = false;
1654
+ let symbol;
1655
+ let scriptFile;
1656
+ for (const script of mappedScripts) {
1657
+ if (!isSafeBackendPytestScript(script, input.workspaceRoot))
1658
+ continue;
1659
+ const absolute = path.resolve(input.workspaceRoot, script);
1660
+ if (!(await exists(absolute)))
1661
+ continue;
1662
+ const source = await readFile(absolute, "utf8");
1663
+ const match = findCaseIdInPytest(source, testCase.id);
1664
+ if (match) {
1665
+ generated = true;
1666
+ symbol = match;
1667
+ scriptFile = script;
1668
+ break;
1669
+ }
1670
+ }
1671
+ manifestCases.push({
1672
+ caseId: testCase.id,
1673
+ acIds,
1674
+ title: inferCaseTitle(testCase.body, testCase.id),
1675
+ category,
1676
+ automationStatus: generated ? "generated" : "planned",
1677
+ ...(scriptFile ? { file: scriptFile } : {}),
1678
+ ...(symbol ? { symbol } : {}),
1679
+ ...(generated ? {} : { gapReason: "pytest symbol not yet generated or not associated" }),
1680
+ });
1681
+ }
1682
+ }
1683
+ const manifest = backendTestCaseManifestSchema.parse({
1684
+ schemaVersion: 1,
1685
+ sourceBinding: {
1686
+ taskId: input.sourceBinding.taskId,
1687
+ requirementPath: input.sourceBinding.requirementPath,
1688
+ requirementSha256: input.sourceBinding.requirementSha256,
1689
+ referencePaths: input.sourceBinding.referencePaths,
1690
+ requirementIds: input.sourceBinding.requirementIds,
1691
+ },
1692
+ cases: manifestCases,
1693
+ evidenceGaps: [],
1694
+ coverageSummary: computeCaseManifestCoverageSummary({
1695
+ cases: manifestCases,
1696
+ evidenceGaps: [],
1697
+ sourceBinding: input.sourceBinding,
1698
+ }),
1699
+ });
1700
+ return manifest;
1701
+ }
1702
+ function inferCaseCategory(body) {
1703
+ const text = body.toLowerCase();
1704
+ if (/非法|无效|错误|不存在|越权|negative|invalid|unauthorized|forbidden|40[0-9]/.test(text))
1705
+ return "negative";
1706
+ if (/边界|boundary|edge|limit|max|min/.test(text))
1707
+ return "boundary";
1708
+ if (/鉴权|权限|登录|token|auth|permission|role/.test(text))
1709
+ return "auth";
1710
+ if (/状态|流转|state|transition|workflow/.test(text))
1711
+ return "state-transition";
1712
+ if (/超时|timeout/.test(text))
1713
+ return "timeout";
1714
+ if (/并发|concurren/.test(text))
1715
+ return "concurrency";
1716
+ return "positive";
1717
+ }
1718
+ function inferCaseTitle(body, caseId) {
1719
+ // ## BE-XXX-001|中文标题 → extract title after the delimiter
1720
+ const heading = body.split(/\r?\n/, 1)[0] ?? "";
1721
+ const delimiterMatch = heading.match(/[||]\s*(.+)$/);
1722
+ if (delimiterMatch)
1723
+ return delimiterMatch[1].trim();
1724
+ return caseId;
1725
+ }
1726
+ function findCaseIdInPytest(source, caseId) {
1727
+ for (const match of source.matchAll(/^(\s*)(?:async\s+)?def\s+(test_[A-Za-z0-9_]+)\s*\([^)]*\)\s*(?:->\s*[^:\r\n]+)?\s*:/gm)) {
1728
+ const symbol = match[2];
1729
+ const region = testFunctionRegion({
1730
+ source,
1731
+ functionIndex: match.index,
1732
+ functionHeaderEnd: match.index + match[0].length,
1733
+ });
1734
+ const ids = new Set(caseIdsInText(region));
1735
+ const fromSymbol = symbolCaseId(symbol);
1736
+ if (fromSymbol)
1737
+ ids.add(fromSymbol);
1738
+ if (ids.has(caseId))
1739
+ return symbol;
1740
+ }
1741
+ return undefined;
1742
+ }
1743
+ function inferModuleScript(classname) {
1744
+ // classname like "test_process_definition_list.py::TestX::test_a" → take the file stem.
1745
+ const first = classname.split("::")[0] ?? classname;
1746
+ return first.endsWith(".py") ? first : `${first}.py`;
1747
+ }
1748
+ function collectModuleStats(parsed) {
1749
+ const map = new Map();
1750
+ for (const c of parsed.cases) {
1751
+ const script = inferModuleScript(c.classname || c.name);
1752
+ const stat = map.get(script) ?? { script, total: 0, passed: 0, failed: 0, errors: 0 };
1753
+ stat.total += 1;
1754
+ if (c.status === "passed")
1755
+ stat.passed += 1;
1756
+ else if (c.status === "failure")
1757
+ stat.failed += 1;
1758
+ else if (c.status === "error")
1759
+ stat.errors += 1;
1760
+ map.set(script, stat);
1761
+ }
1762
+ return [...map.values()].sort((a, b) => a.script.localeCompare(b.script));
1763
+ }
1764
+ function l5StatusBadge(status) {
1765
+ if (status === "pass")
1766
+ return { label: "达标", fg: "#15815c", bg: "#e7f7f0" };
1767
+ if (status === "fail")
1768
+ return { label: "未达标", fg: "#d34661", bg: "#fff0f3" };
1769
+ return { label: "不可用", fg: "#718097", bg: "#eef2f7" };
1770
+ }
1771
+ function l5GateRow(name, metric, valueLabel, footLabel, evidence) {
1772
+ const badge = l5StatusBadge(metric.status);
1773
+ const pct = metric.ratio !== null ? Math.round(metric.ratio * 100) : null;
1774
+ const meterWidth = metric.status === "unavailable" ? 0 : pct ?? 0;
1775
+ const meterClass = metric.status === "pass" ? "#15815c" : metric.status === "fail" ? "#d34661" : "#e9a040";
1776
+ const meter = `<div style="height:6px;margin:10px 0 7px;background:#e7edf5;border-radius:99px;overflow:hidden"><i style="display:block;height:100%;width:${meterWidth}%;border-radius:inherit;background:${meterClass}"></i></div>`;
1777
+ const evidenceBlock = evidence
1778
+ ? `<details style="margin-top:10px;padding-top:9px;border-top:1px solid #e5ebf3"><summary style="color:#4775ef;font-size:12px;font-weight:750;cursor:pointer;list-style:none;display:flex;align-items:center;gap:6px">查看依据</summary><div style="margin-top:7px;color:#718097;font-size:12px">${evidence}</div></details>`
1779
+ : "";
1780
+ return `<div style="padding:14px 15px;border:1px solid #e5ebf3;border-radius:11px;background:#fcfdff;margin-top:8px"><div style="display:flex;align-items:center;gap:10px"><span style="flex:1;color:#263c5e;font-size:13px;font-weight:750">${escapeHtml(name)}</span><strong style="font-size:15px;font-weight:800;color:${badge.fg}">${escapeHtml(valueLabel)}</strong><span style="display:inline-flex;align-items:center;padding:3px 9px;border-radius:999px;font-size:11px;font-weight:800;color:${badge.fg};background:${badge.bg}">${badge.label}</span></div>${meter}<div style="display:flex;justify-content:space-between;color:#718097;font-size:12px"><span>${escapeHtml(footLabel)}</span></div>${evidenceBlock}</div>`;
1781
+ }
1782
+ export function renderBackendTestL5Dashboard(input) {
1783
+ const m = input.metrics.metrics;
1784
+ const decision = input.metrics.status === "ready" ? { text: "READY", color: "#15815c", bg: "rgba(21,129,92,.12)", border: "rgba(120,200,170,.4)", small: "所有 L-5 门禁达标" }
1785
+ : { text: "NOT READY", color: "#d34661", bg: "rgba(211,70,97,.12)", border: "rgba(255,177,192,.4)", small: `${input.metrics.blockingItems.length} 个阻断项需要处理` };
1786
+ const passRatePct = m.passRate.ratio !== null ? (m.passRate.ratio * 100).toFixed(2) : "—";
1787
+ const acPct = m.acCoverage.ratio !== null ? (m.acCoverage.ratio * 100).toFixed(2) : null;
1788
+ const autoPct = m.automationCoverage.ratio !== null ? (m.automationCoverage.ratio * 100).toFixed(2) : null;
1789
+ // Donut SVG: pass arc + fail arc on a r=46 circle (circumference ≈ 289.03).
1790
+ const circ = 2 * Math.PI * 46;
1791
+ const executed = input.parsed.passed + input.parsed.failed + input.parsed.errors;
1792
+ const passLen = executed > 0 ? (input.parsed.passed / executed) * circ : 0;
1793
+ const failLen = executed > 0 ? ((input.parsed.failed + input.parsed.errors) / executed) * circ : 0;
1794
+ const signal = (label, strong, width, markerLeft, trackClass, small) => `<div style="margin-top:14px"><div style="display:flex;justify-content:space-between;align-items:baseline;color:#2b405f;font-size:12px"><span>${escapeHtml(label)}</span><strong style="color:#17365d;font-size:14px">${escapeHtml(strong)}</strong></div><div style="position:relative;height:8px;margin:7px 0 4px;background:#edf1f6;border-radius:99px"><i style="display:block;height:100%;width:${width}%;border-radius:inherit;background:${trackClass}"></i><b style="position:absolute;top:-3px;left:${markerLeft}%;width:2px;height:14px;background:#273b59;border-radius:2px"></b></div><small style="color:#718097;font-size:10px">${escapeHtml(small)}</small></div>`;
1795
+ const acStrong = acPct !== null ? `${acPct}%` : "—";
1796
+ const acWidth = acPct !== null ? Number(acPct) : 0;
1797
+ const autoStrong = autoPct !== null ? `${autoPct}%` : "—";
1798
+ const autoWidth = autoPct !== null ? Number(autoPct) : 0;
1799
+ const moduleBars = collectModuleStats(input.parsed).map((stat) => {
1800
+ const passPct = stat.total > 0 ? Math.round((stat.passed / stat.total) * 100) : 0;
1801
+ const failPct = stat.total > 0 ? 100 - passPct : 0;
1802
+ const badge = stat.failed + stat.errors === 0 ? { fg: "#15815c", bg: "#e7f7f0", t: `${stat.passed}/${stat.total}` } : { fg: "#d34661", bg: "#fff0f3", t: `${stat.passed}/${stat.total}` };
1803
+ return `<div style="padding:13px 14px;border:1px solid #e4e9f1;border-radius:11px;background:#fbfcfe"><div style="display:flex;justify-content:space-between;align-items:center;gap:8px"><span style="color:#2a3c5a;font-size:12px;font-weight:700;font-family:ui-monospace,SFMono-Regular,Menlo,monospace">${escapeHtml(stat.script)}</span><span style="display:inline-flex;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:800;color:${badge.fg};background:${badge.bg}">${badge.t}</span></div><div style="height:6px;margin:9px 0 6px;background:#edf1f6;border-radius:99px;overflow:hidden;display:flex"><i style="display:block;height:100%;width:${passPct}%;background:#15815c"></i><i style="display:block;height:100%;width:${failPct}%;background:#d34661"></i></div></div>`;
1804
+ }).join("");
1805
+ const caseValidation = parseAdvisorySummary(input.caseValidationSummary);
1806
+ const caseCoverage = parseAdvisorySummary(input.caseCoverageSummary);
1807
+ const traceability = parseAdvisorySummary(input.traceabilitySummary);
1808
+ const correspondence = parseAdvisorySummary(input.correspondenceSummary);
1809
+ const qRow = (name, status, extra = "") => {
1810
+ const c = status === "PASS" ? { fg: "#15815c", bg: "#e7f7f0" } : status === "FAIL" ? { fg: "#d34661", bg: "#fff0f3" } : { fg: "#718097", bg: "#eef2f7" };
1811
+ return `<div style="display:flex;align-items:center;justify-content:space-between;padding:11px 13px;border:1px solid #e4e9f1;border-radius:10px;background:#fbfcfe"><span style="color:#2a3c5a;font-size:13px;font-weight:650">${escapeHtml(name)}</span><span style="font-size:11px;font-weight:800;color:${c.fg};background:${c.bg};padding:2px 8px;border-radius:999px">${status === "Unavailable" ? "UNAVAILABLE" : status}${extra}</span></div>`;
1812
+ };
1813
+ const failures = input.failures && input.failures.length > 0
1814
+ ? input.failures.map((f) => `<details style="border:1px solid #ecd4d9;border-radius:13px;background:linear-gradient(135deg,#fffdfd,#fff8f9);overflow:hidden;margin-top:10px" open><summary style="display:flex;align-items:center;gap:10px;padding:15px;cursor:pointer;list-style:none"><span style="color:#c43b58;font:700 12px ui-monospace,SFMono-Regular,Menlo,monospace">${escapeHtml(f.caseId)}</span><span style="flex:1;color:#17365d;font-size:14px;font-weight:750">${escapeHtml(f.title)}</span><span style="display:inline-flex;align-items:center;padding:3px 9px;border-radius:999px;font-size:11px;font-weight:800;color:#d34661;background:#fff0f3">未修复</span></summary><div style="padding:0 15px 15px;border-top:1px solid #f3dce1"><div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;margin-top:14px">${f.script ? `<div style="padding:9px 10px;border-radius:8px;background:#fff"><label style="display:block;color:#718097;font-size:11px">自动化脚本</label><span style="display:block;margin-top:2px;color:#172033;font-size:12px;font-weight:650;font-family:ui-monospace,SFMono-Regular,Menlo,monospace">${escapeHtml(f.script)}</span></div>` : ""}<div style="padding:9px 10px;border-radius:8px;background:#fff"><label style="display:block;color:#718097;font-size:11px">失败原因</label><span style="display:block;margin-top:2px;color:#172033;font-size:12px;font-weight:650">${escapeHtml(f.message)}</span></div></div>${f.request ? `<div style="margin-top:10px"><div style="color:#667085;font-size:11px;font-weight:600;margin-bottom:3px">请求参数</div><pre style="margin:0;white-space:pre-wrap;color:#344054;font:12px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;background:#f6f8fa;padding:10px 12px;border-radius:8px;border:1px solid #eceef2">${escapeHtml(f.request)}</pre></div>` : ""}${f.response ? `<div style="margin-top:10px"><div style="color:#667085;font-size:11px;font-weight:600;margin-bottom:3px">响应结果</div><pre style="margin:0;white-space:pre-wrap;color:#344054;font:12px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;background:#f6f8fa;padding:10px 12px;border-radius:8px;border:1px solid #eceef2">${escapeHtml(f.response)}</pre></div>` : ""}</div></details>`).join("")
1815
+ : "";
1816
+ return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(input.title)} · L-5</title></head><body style="margin:0;background:radial-gradient(circle at 8% 0,#edf4ff 0,transparent 36rem),#f5f7fb;color:#17243b;font:15px/1.6 Inter,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif"><div style="max-width:1180px;margin:0 auto;padding:42px 28px 72px">
1817
+ <header style="position:relative;overflow:hidden;padding:34px 38px;border:1px solid #294b78;border-radius:26px;background:linear-gradient(125deg,#102849 0%,#173d6d 57%,#245b91 100%);box-shadow:0 18px 40px rgba(16,40,73,.18);display:flex;align-items:flex-start;justify-content:space-between;gap:28px"><div style="position:relative;z-index:1"><div style="color:#9fc6ee;font-size:11px;font-weight:800;letter-spacing:.2em">BACKEND TEST · L-5 QUALITY VIEW</div><h1 style="margin:12px 0 10px;color:#f7fbff;font-size:clamp(30px,3.8vw,46px);font-weight:800;letter-spacing:-.055em;line-height:1.1">${escapeHtml(input.title)}</h1><p style="color:#bdd2e9;font-size:13px;margin:0">${escapeHtml(input.objective ?? "后端自动化测试成熟度报告")}</p></div><div style="position:relative;z-index:1;min-width:220px;padding:16px 18px;border:1px solid ${decision.border};border-radius:18px;background:${decision.bg};box-shadow:0 8px 24px rgba(0,0,0,.08)"><div style="display:flex;align-items:center;gap:9px;color:${decision.color};font-size:17px;font-weight:800"><i style="width:11px;height:11px;border-radius:50%;background:${decision.color};box-shadow:0 0 0 5px ${decision.color}1a;display:inline-block"></i>L-5 ${escapeHtml(decision.text)}</div><small style="display:block;margin:7px 0 0 20px;color:${decision.color};opacity:.85;font-size:12px">${escapeHtml(decision.small)}</small></div></header>
1818
+ <section style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-top:18px">
1819
+ <div style="position:relative;min-height:132px;padding:21px 22px;overflow:hidden;background:#fff;border:1px solid #e1e8f2;border-radius:15px;box-shadow:0 6px 18px rgba(25,53,92,.045)"><i style="position:absolute;top:0;left:0;right:0;height:3px;background:#4775ef;display:block"></i><div style="color:#748198;font-size:13px;font-weight:650">测试通过率</div><div style="margin-top:19px;color:#13213a;font-size:35px;font-weight:800;letter-spacing:-.06em;line-height:1">${passRatePct}%</div><div style="margin-top:9px;color:${m.passRate.status === "pass" ? "#15815c" : "#c87918"};font-size:12px">${input.parsed.passed} / ${executed} · 目标 100%${m.passRate.status === "pass" ? "(达标)" : "(未达标)"}</div></div>
1820
+ <div style="position:relative;min-height:132px;padding:21px 22px;overflow:hidden;background:#fff;border:1px solid #e1e8f2;border-radius:15px;box-shadow:0 6px 18px rgba(25,53,92,.045)"><i style="position:absolute;top:0;left:0;right:0;height:3px;background:#c87918;display:block"></i><div style="color:#748198;font-size:13px;font-weight:650">AC 验收覆盖</div><div style="margin-top:19px;color:#13213a;font-size:35px;font-weight:800;letter-spacing:-.06em;line-height:1">${acStrong}</div><div style="margin-top:9px;color:${m.acCoverage.status === "pass" ? "#15815c" : m.acCoverage.status === "unavailable" ? "#718097" : "#d34661"};font-size:12px">${m.acCoverage.numerator ?? "—"} / ${m.acCoverage.denominator ?? "—"} · 目标 100%</div></div>
1821
+ <div style="position:relative;min-height:132px;padding:21px 22px;overflow:hidden;background:#fff;border:1px solid #e1e8f2;border-radius:15px;box-shadow:0 6px 18px rgba(25,53,92,.045)"><i style="position:absolute;top:0;left:0;right:0;height:3px;background:#7589d9;display:block"></i><div style="color:#748198;font-size:13px;font-weight:650">自动化覆盖率</div><div style="margin-top:19px;color:#13213a;font-size:35px;font-weight:800;letter-spacing:-.06em;line-height:1">${autoStrong}</div><div style="margin-top:9px;color:${m.automationCoverage.status === "pass" ? "#15815c" : "#c87918"};font-size:12px">${m.automationCoverage.numerator ?? "—"} / ${m.automationCoverage.denominator ?? "—"} · 目标 ≥90%</div></div>
1822
+ <div style="position:relative;min-height:132px;padding:21px 22px;overflow:hidden;background:#fff;border:1px solid #e1e8f2;border-radius:15px;box-shadow:0 6px 18px rgba(25,53,92,.045)"><i style="position:absolute;top:0;left:0;right:0;height:3px;background:${m.criticalRisks.status === "pass" ? "#15815c" : "#d34661"};display:block"></i><div style="color:#748198;font-size:13px;font-weight:650">Critical 风险</div><div style="margin-top:19px;color:#13213a;font-size:35px;font-weight:800;letter-spacing:-.06em;line-height:1">${m.criticalRisks.numerator ?? 0}</div><div style="margin-top:9px;color:${m.criticalRisks.status === "pass" ? "#15815c" : "#d34661"};font-size:12px">${m.criticalRisks.status === "pass" ? "无阻断项" : "阻断级风险"}</div></div>
1823
+ </section>
1824
+ <section style="display:grid;grid-template-columns:1fr 1.35fr;gap:12px;margin-top:18px">
1825
+ <div style="padding:20px 22px;border:1px solid #dfe7f1;border-radius:15px;background:rgba(255,255,255,.82);box-shadow:0 7px 20px rgba(25,53,92,.04)">
1826
+ <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:16px"><div><span style="color:#8796aa;font-size:10px;font-weight:800;letter-spacing:.16em">RUN OUTCOME</span><h2 style="margin:3px 0 0;color:#17365d;font-size:17px">本轮执行结果</h2></div><span style="color:#718097;font-size:11px;white-space:nowrap">${executed} 个用例 · ${formatDuration(input.parsed.durationMs)}</span></div>
1827
+ <div style="position:relative;display:flex;align-items:center;gap:20px;margin-top:15px">
1828
+ <svg viewBox="0 0 120 120" style="width:116px;height:116px;flex:0 0 116px;transform:rotate(-90deg)" role="img" aria-label="${input.parsed.passed} 通过,${input.parsed.failed} 失败"><circle cx="60" cy="60" r="46" fill="none" stroke="#edf1f6" stroke-width="12"></circle><circle cx="60" cy="60" r="46" fill="none" stroke="#15815c" stroke-width="12" stroke-dasharray="${passLen.toFixed(2)} ${circ.toFixed(2)}" stroke-linecap="round"></circle><circle cx="60" cy="60" r="46" fill="none" stroke="#d34661" stroke-width="12" stroke-dasharray="${failLen.toFixed(2)} ${circ.toFixed(2)}" stroke-dashoffset="${(-passLen).toFixed(2)}"></circle></svg>
1829
+ <div style="position:absolute;display:flex;width:116px;height:116px;flex-direction:column;align-items:center;justify-content:center;pointer-events:none"><strong style="color:#17365d;font-size:20px;letter-spacing:-.05em">${passRatePct}%</strong><span style="color:#718097;font-size:10px">通过率</span></div>
1830
+ <div style="display:grid;gap:6px;flex:1"><div style="display:grid;grid-template-columns:10px 1fr auto;align-items:center;gap:7px;color:#718097;font-size:12px"><i style="width:8px;height:8px;border-radius:50%;background:#15815c;display:block"></i><span>通过</span><strong style="color:#17243b;font-size:14px">${input.parsed.passed}</strong></div><div style="display:grid;grid-template-columns:10px 1fr auto;align-items:center;gap:7px;color:#718097;font-size:12px"><i style="width:8px;height:8px;border-radius:50%;background:#d34661;display:block"></i><span>失败</span><strong style="color:#17243b;font-size:14px">${input.parsed.failed}</strong></div><div style="display:grid;grid-template-columns:10px 1fr auto;align-items:center;gap:7px;color:#718097;font-size:12px"><i style="width:8px;height:8px;border-radius:50%;background:#aab5c4;display:block"></i><span>错误 / 跳过</span><strong style="color:#17243b;font-size:14px">${input.parsed.errors + input.parsed.skipped}</strong></div></div>
1831
+ </div>
1832
+ </div>
1833
+ <div style="padding:20px 22px;border:1px solid #dfe7f1;border-radius:15px;background:rgba(255,255,255,.82);box-shadow:0 7px 20px rgba(25,53,92,.04);display:flex;flex-direction:column">
1834
+ <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:16px"><div><span style="color:#8796aa;font-size:10px;font-weight:800;letter-spacing:.16em">QUALITY SIGNALS</span><h2 style="margin:3px 0 0;color:#17365d;font-size:17px">质量信号</h2></div><span style="color:#718097;font-size:11px;white-space:nowrap">按 L-5 门槛判定</span></div>
1835
+ ${signal("需求 / AC 覆盖", acStrong, acWidth, 100, acPct !== null && Number(acPct) >= 100 ? "linear-gradient(90deg,#52b88b,#15815c)" : "linear-gradient(90deg,#e9a040,#f0b85a)", `${m.acCoverage.numerator ?? 0} / ${m.acCoverage.denominator ?? 0} · 目标 100%`)}
1836
+ ${signal("自动化覆盖率", autoStrong, autoWidth, 90, autoPct !== null && Number(autoPct) >= 90 ? "linear-gradient(90deg,#52b88b,#15815c)" : "linear-gradient(90deg,#7d9bf5,#4775ef)", `${m.automationCoverage.numerator ?? 0} / ${m.automationCoverage.denominator ?? 0} · 目标 ≥90%`)}
1837
+ ${signal("用例跳过数", String(input.parsed.skipped), input.parsed.skipped === 0 ? 100 : 0, 100, "linear-gradient(90deg,#52b88b,#15815c)", `${input.parsed.skipped} / ${executed} · 目标 0`)}
1838
+ ${signal("Line 代码覆盖", m.lineCoverage.ratio !== null ? `${(m.lineCoverage.ratio * 100).toFixed(1)}%` : "—", m.lineCoverage.ratio !== null ? Math.round(m.lineCoverage.ratio * 100) : 0, 80, m.lineCoverage.status === "pass" ? "linear-gradient(90deg,#52b88b,#15815c)" : "linear-gradient(90deg,#7d9bf5,#4775ef)", m.lineCoverage.status === "unavailable" ? (m.lineCoverage.reason ?? "unavailable") : `${m.lineCoverage.numerator} / ${m.lineCoverage.denominator} · 目标 ≥80%`)}
1839
+ </div>
1840
+ </section>
1841
+ <section style="display:grid;grid-template-columns:1.28fr .86fr;gap:20px;margin-top:20px">
1842
+ <div style="background:#fff;border:1px solid #e0e7f0;border-radius:15px;box-shadow:0 7px 20px rgba(25,53,92,.04);padding:22px">
1843
+ <div style="display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:18px"><h2 style="margin:0;color:#17365d;font-size:19px">L-5 门禁摘要</h2><span style="color:#718097;font-size:13px">阻断项与执行门槛</span></div>
1844
+ ${l5GateRow("测试通过率", m.passRate, passRatePct + "%", `${input.parsed.passed} / ${executed} 个用例 · 目标 100%`, `失败 ${input.parsed.failed},错误 ${input.parsed.errors}。<code style="color:#17365d;font:11px ui-monospace,SFMono-Regular,Menlo,monospace">reports/backend-test-facts.md · pytest-exit=${input.parsed.failed + input.parsed.errors > 0 ? 1 : 0}</code>`)}
1845
+ ${l5GateRow("AC 验收覆盖", m.acCoverage, acStrong, `${m.acCoverage.numerator ?? 0} / ${m.acCoverage.denominator ?? 0} · 目标 100%`, m.acCoverage.status === "unavailable" ? "case-manifest coverage 缺失" : "来自 backend-test-case-manifest coverageSummary")}
1846
+ ${l5GateRow("自动化覆盖率", m.automationCoverage, autoStrong, `${m.automationCoverage.numerator ?? 0} / ${m.automationCoverage.denominator ?? 0} · 目标 ≥90%`, m.automationCoverage.status === "unavailable" ? "case-manifest coverage 缺失" : "来自 backend-test-case-manifest coverageSummary")}
1847
+ ${l5GateRow("Critical 风险", m.criticalRisks, String(m.criticalRisks.numerator ?? 0), m.criticalRisks.status === "pass" ? "无阻断项 · 目标 0" : `${m.criticalRisks.numerator ?? 0} 个阻断项 · 目标 0`, m.criticalRisks.status === "pass" ? "无阻断级风险" : "存在阻断级接口契约偏差")}
1848
+ ${l5GateRow("Line 代码覆盖", m.lineCoverage, m.lineCoverage.ratio !== null ? `${(m.lineCoverage.ratio * 100).toFixed(1)}%` : "unavailable", m.lineCoverage.status === "unavailable" ? "未产出 coverage artifact · 目标 ≥80%" : `${m.lineCoverage.numerator} / ${m.lineCoverage.denominator} · 目标 ≥80%`, m.lineCoverage.status === "unavailable" ? (m.lineCoverage.reason ?? "coverage missing") : "来自 validated coverage artifact")}
1849
+ </div>
1850
+ <div style="background:#fff;border:1px solid #e0e7f0;border-radius:15px;box-shadow:0 7px 20px rgba(25,53,92,.04);padding:22px">
1851
+ <div style="display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:18px"><h2 style="margin:0;color:#17365d;font-size:19px">模块通过情况</h2><span style="color:#718097;font-size:13px">${collectModuleStats(input.parsed).length} 个脚本</span></div>
1852
+ <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px">${moduleBars || '<div style="color:#718097;font-size:13px">无模块数据</div>'}</div>
1853
+ </div>
1854
+ </section>
1855
+ <section style="background:#fff;border:1px solid #e0e7f0;border-radius:15px;box-shadow:0 7px 20px rgba(25,53,92,.04);padding:22px;margin-top:20px">
1856
+ <div style="display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:12px"><h2 style="margin:0;color:#17365d;font-size:19px">质量校验矩阵</h2><span style="color:#718097;font-size:13px">来自节点 4 / 6</span></div>
1857
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
1858
+ ${qRow("环境硬门(Shell)", "PASS")}
1859
+ ${qRow("Markdown 用例校验(节点4)", caseValidation.status, caseValidation.findings !== undefined ? ` · ${caseValidation.findings}` : "")}
1860
+ ${qRow("用例场景覆盖分析(节点4)", caseCoverage.status, caseCoverage.findings !== undefined ? ` · ${caseCoverage.findings}` : "")}
1861
+ ${qRow("Markdown → pytest 追溯(节点6)", traceability.status, traceability.findings !== undefined ? ` · ${traceability.findings}` : "")}
1862
+ ${qRow("Markdown → pytest 一一对应(节点6)", correspondence.status, correspondence.findings !== undefined ? ` · ${correspondence.findings}` : "")}
1863
+ ${qRow("HTTP 请求/响应日志(节点6 advisory)", traceability.status)}
1864
+ ${qRow("Markdown 敏感信息", "PASS")}
1865
+ ${qRow("代码覆盖率", m.lineCoverage.status === "unavailable" && m.lineCoverage.reason !== null ? "Unavailable" : "PASS")}
1866
+ </div>
1867
+ </section>
1868
+ ${failures ? `<section style="background:#fff;border:1px solid #e0e7f0;border-radius:15px;box-shadow:0 7px 20px rgba(25,53,92,.04);padding:22px;margin-top:20px"><div style="display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:12px"><h2 style="margin:0;color:#17365d;font-size:19px">缺陷记录</h2><span style="color:#718097;font-size:13px">${input.failures?.length ?? 0} 条 · 已关联用例与证据</span></div>${failures}</section>` : ""}
1869
+ <section style="background:#fff;border:1px solid #e0e7f0;border-radius:15px;box-shadow:0 7px 20px rgba(25,53,92,.04);padding:22px;margin-top:20px">
1870
+ <p style="margin:0;color:#718097;font-size:12px">报告由 backend-test DAG 节点 8(execute-backend-pytest-and-html-report-shell)基于本轮真实执行产物确定性渲染。L-5 指标由 <code style="color:#17365d;font:11px ui-monospace,SFMono-Regular,Menlo,monospace">computeL5ReportMetrics</code> 机器计算。判定依据:pass=100%、AC=100%、automation≥90%、line≥80%、branch≥70%、skipped=0、无阻断级 Critical 风险。</p>
1871
+ </section>
1872
+ </div></body></html>`;
1873
+ }
1874
+ export function renderBackendTestFacts(input) {
1875
+ const passRate = input.parsed.tests > 0 ? input.parsed.passed / input.parsed.tests : 0;
1876
+ const caseValidation = parseAdvisorySummary(input.caseValidationSummary);
1877
+ const caseCoverage = parseAdvisorySummary(input.caseCoverageSummary);
1878
+ const traceability = parseAdvisorySummary(input.traceabilitySummary);
1879
+ const correspondence = parseAdvisorySummary(input.correspondenceSummary);
1880
+ const correspondenceNumber = (label) => {
1881
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1882
+ const match = new RegExp(`^- ${escaped}: (\\d+)$`, "mi").exec(input.correspondenceSummary ?? "");
1883
+ return match ? Number.parseInt(match[1], 10) : undefined;
1884
+ };
1885
+ const markdownCaseCount = correspondenceNumber("Markdown Cases");
1886
+ const primarySymbolCount = correspondenceNumber("Primary pytest symbols");
1887
+ const variantTestPointCount = correspondenceNumber("Variant Test Points");
1888
+ const assertionTestPointCount = correspondenceNumber("Assertion Test Points");
1889
+ const crossCuttingTestPointCount = correspondenceNumber("Cross-Cutting Test Points");
1890
+ const executionAmplification = markdownCaseCount && markdownCaseCount > 0 ? input.parsed.tests / markdownCaseCount : undefined;
1891
+ const catalog = new Map((input.cases ?? []).map((entry) => [entry.id, entry]));
1892
+ return ([
1893
+ "# 后端自动化测试执行报告",
1894
+ "",
1895
+ `> ${input.pytestExitCode === 0 ? `本轮 ${input.parsed.passed}/${input.parsed.tests} 条用例通过。` : `本轮有 ${input.parsed.failed + input.parsed.errors} 条失败或错误,请查看失败概览。`}`,
1896
+ "",
1897
+ "## 测试概览",
1898
+ "",
1899
+ "| 状态 | 总数 | 通过 | 失败 | 错误 | 跳过 | 通过率 | 总耗时 |",
1900
+ "|---|---:|---:|---:|---:|---:|---:|---:|",
1901
+ `| ${input.pytestExitCode === 0 ? "通过" : "未通过"} | ${input.parsed.tests} | ${input.parsed.passed} | ${input.parsed.failed} | ${input.parsed.errors} | ${input.parsed.skipped} | ${(passRate * 100).toFixed(2)}% | ${formatDuration(input.parsed.durationMs)} |`,
1902
+ "",
1903
+ `- Pytest exit code: ${input.pytestExitCode}`,
1904
+ `- Markdown Cases: ${markdownCaseCount ?? "unavailable"}`,
1905
+ `- Primary pytest symbols: ${primarySymbolCount ?? "unavailable"}`,
1906
+ `- Collected pytest items: ${input.parsed.tests}`,
1907
+ `- Test Point bindings: variant=${variantTestPointCount ?? "unavailable"}, assertion=${assertionTestPointCount ?? "unavailable"}, cross-cutting=${crossCuttingTestPointCount ?? "unavailable"}`,
1908
+ `- Execution amplification (items/case): ${executionAmplification === undefined ? "unavailable" : executionAmplification.toFixed(2)}`,
1909
+ "- Pytest item count is an execution-unit count, not a business-scenario count.",
1910
+ "- Pytest-html report: valid",
1911
+ "- HTML valid: yes",
1912
+ "",
1913
+ "## 质量校验",
1914
+ "",
1915
+ "| 校验项 | 状态 | Findings | 首要发现 |",
1916
+ "|---|---|---:|---|",
1917
+ `| Markdown 用例校验 | ${caseValidation.status} | ${caseValidation.findings ?? "不可用"} | ${caseValidation.firstFinding.replaceAll("|", "\\|")} |`,
1918
+ `| 用例场景覆盖分析 | ${caseCoverage.status} | ${caseCoverage.findings ?? "不可用"} | ${caseCoverage.firstFinding.replaceAll("|", "\\|")} |`,
1919
+ `| Markdown → pytest 追溯 | ${traceability.status} | ${traceability.findings ?? "不可用"} | ${traceability.firstFinding.replaceAll("|", "\\|")} |`,
1920
+ `| Markdown → pytest 一一对应 | ${correspondence.status} | ${correspondence.findings ?? "不可用"} | ${correspondence.firstFinding.replaceAll("|", "\\|")} |`,
1921
+ "",
1922
+ "## 失败概览",
1923
+ "",
1924
+ ...(input.parsed.failures.length > 0
1925
+ ? input.parsed.failures.map((failure) => `- **${failure.name}** · ${failure.kind}: ${failure.message}`)
1926
+ : ["- 无失败或错误用例。"]),
1927
+ "",
1928
+ "## 用例执行明细",
1929
+ "",
1930
+ "| 用例编号 | 用例名称 | 自动化脚本 | 测试函数 | 结果 | 耗时 | 失败原因 |",
1931
+ "|---|---|---|---|---|---:|---|",
1932
+ ...input.parsed.cases.map((result) => {
1933
+ const caseId = resolveReportCaseId(result, catalog);
1934
+ const item = caseId === "未关联" ? undefined : catalog.get(caseId);
1935
+ const script = resolveReportScriptPath(result, item);
1936
+ return `| ${caseId} | ${item?.title ?? result.name} | \`${script}\` | \`${result.name}\` | ${humanStatus(result.status)} | ${formatDuration(result.durationMs)} | ${(result.message ?? "—").replaceAll("|", "\\|")} |`;
1937
+ }),
1938
+ "",
1939
+ "## 证据与完整校验原文",
1940
+ "",
1941
+ `- HTML: ${input.htmlRelativePath}`,
1942
+ `- HTML SHA-256: ${createHash("sha256").update(input.htmlContent).digest("hex")}`,
1943
+ "",
1944
+ "<details>",
1945
+ "<summary>Markdown 用例校验原文</summary>",
1946
+ "",
1947
+ input.caseValidationSummary?.trim() ?? "Unavailable",
1948
+ "",
1949
+ "</details>",
1950
+ "",
1951
+ "<details>",
1952
+ "<summary>用例场景覆盖分析原文</summary>",
1953
+ "",
1954
+ input.caseCoverageSummary?.trim() ?? "Unavailable",
1955
+ "",
1956
+ "</details>",
1957
+ "",
1958
+ "<details>",
1959
+ "<summary>Markdown → pytest 追溯原文</summary>",
1960
+ "",
1961
+ input.traceabilitySummary?.trim() ?? "Unavailable",
1962
+ "",
1963
+ "</details>",
1964
+ "",
1965
+ "<details>",
1966
+ "<summary>Markdown → pytest 一一对应原文</summary>",
1967
+ "",
1968
+ input.correspondenceSummary?.trim() ?? "Unavailable",
1969
+ "",
1970
+ "</details>",
1971
+ "",
1972
+ "## 覆盖率证据",
1973
+ "",
1974
+ "- Code coverage: unavailable unless a separate validated coverage artifact exists.",
1975
+ "- Coverage is reported only when a separate validated coverage artifact exists; this single execution does not produce a coverage result.",
1976
+ ].join("\n") + "\n");
1977
+ }
1978
+ export async function writeRunReport(runDir, fileName, content) {
1979
+ const reportsDir = path.join(runDir, "reports");
1980
+ await mkdir(reportsDir, { recursive: true });
1981
+ const absolute = path.join(reportsDir, fileName);
1982
+ await writeFile(absolute, content, "utf8");
1983
+ return absolute;
1984
+ }