@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,1985 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { z } from "zod";
6
+ import YAML from "yaml";
7
+ import { resolveBackendTestLayout, } from "./backend-test-layout.js";
8
+ import { assessScenarioPartitionCoverage, parseScenarioPartitions, } from "./backend-test-scenario-partitions.js";
9
+ import { backendTestCaseManifestSchema, computeCaseManifestCoverageSummary, } from "./backend-test-case-manifest.js";
10
+ const CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-\d{3})\b.*$/gm;
11
+ const CASE_ID_IN_TEXT = /\bBE-[A-Z0-9_-]+-\d{3}\b/g;
12
+ const NON_CANONICAL_CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-(?:\d{2}|\d{2,3}[A-Z]+))\b.*$/gm;
13
+ const AC_ID_IN_TEXT = /\bAC-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g;
14
+ const SOURCE_RULE_ID_IN_TEXT = /\b(?:AC|REQ|BR)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g;
15
+ const SOURCE_ERROR_CODE_IN_TEXT = /\b[A-Z][0-9]{5,}\b/g;
16
+ const JSON_FIELD_IN_TEXT = /["']([A-Za-z][A-Za-z0-9_]*)["']\s*:/g;
17
+ const DTO_FIELD_IN_TEXT = /\b(?:private|public|protected)\s+(?:static\s+)?(?:final\s+)?[A-Za-z_$][A-Za-z0-9_$<>,.?\[\] ]*\s+([a-z][A-Za-z0-9_]*)\s*[;=]/g;
18
+ const NATURAL_BUSINESS_RULE_KEYWORDS = /(?:事务|原子性|回滚|幂等|唯一性|唯一|状态转换|状态流转|权限|角色|租户|跨字段|transaction|atomic|rollback|idempoten|unique|state transition|authorization|permission|tenant|cross-field)/i;
19
+ const NATURAL_BUSINESS_RULE_NORMATIVE = /(?:必须|不得|禁止|仅当|应当|需要|不能|保证|must|shall|required|must not|only when|guarantee)/i;
20
+ // Token form is used for section scanning; anchored form validates full cells.
21
+ const RULE_KEY_TOKEN = /(?:AC|REQ|BR|API|GET|POST|PUT|PATCH|DELETE)-[A-Z0-9]+(?:-[A-Z0-9]+)*|[A-Z][0-9]{5,}/;
22
+ const RULE_KEY = new RegExp(`^(?:${RULE_KEY_TOKEN.source})$`);
23
+ const TEST_POINT = /^TP-[A-Z0-9]+(?:-[A-Z0-9]+)*$/;
24
+ const COVERAGE_HEADERS = [
25
+ "Rule Key",
26
+ "Priority",
27
+ "Source",
28
+ "Endpoint/Field",
29
+ "Dimension",
30
+ "Rule",
31
+ "Required Test Points",
32
+ "Case IDs",
33
+ "Status",
34
+ ];
35
+ const CASE_SECTION_ALIASES = {
36
+ acceptance: ["验收标准", "Acceptance Criteria"],
37
+ rules: ["覆盖规则", "Coverage Rules"],
38
+ testPoints: ["测试点", "Test Points"],
39
+ scenarioTypes: ["场景类型", "Scenario Types", "Scenario Type"],
40
+ automation: ["自动化映射", "自动化说明", "Automation Notes"],
41
+ };
42
+ const TEST_POINT_BINDING_MODES = ["variant", "assertion", "cross-cutting"];
43
+ const CHANGE_CLASSIFICATIONS = [
44
+ "new-operation",
45
+ "contract-change",
46
+ "behavior-change",
47
+ "bugfix",
48
+ "implementation-optimization",
49
+ ];
50
+ const COVERAGE_POLICIES = [
51
+ "full-contract",
52
+ "affected-contract-full",
53
+ "affected-behavior-full",
54
+ "reproduction-plus-neighbors",
55
+ "change-focused-plus-regression-floor",
56
+ ];
57
+ const COVERAGE_POLICY_BY_CLASSIFICATION = {
58
+ "new-operation": "full-contract",
59
+ "contract-change": "affected-contract-full",
60
+ "behavior-change": "affected-behavior-full",
61
+ bugfix: "reproduction-plus-neighbors",
62
+ "implementation-optimization": "change-focused-plus-regression-floor",
63
+ };
64
+ const REQUIRED_REGRESSION_FLOOR = {
65
+ "new-operation": [],
66
+ "contract-change": ["main-success-path", "unchanged-response-shape", "changed-contract-boundaries"],
67
+ "behavior-change": ["main-success-path", "unchanged-response-shape", "affected-state-transition"],
68
+ bugfix: ["main-success-path", "unchanged-response-shape", "defect-reproduction", "adjacent-boundary"],
69
+ "implementation-optimization": ["main-success-path", "unchanged-response-shape"],
70
+ };
71
+ const AUTOMATION_BINDING_LABELS = {
72
+ variant: ["变体测试点", "Variant Test Points"],
73
+ assertion: ["场景断言测试点", "Assertion Test Points"],
74
+ "cross-cutting": ["横切证据测试点", "Cross-Cutting Test Points"],
75
+ };
76
+ const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
77
+ const inputFileSchema = z.object({ path: z.string().min(1), sha256: sha256Schema }).strict();
78
+ const sourceBindingSchema = z.object({
79
+ taskId: z.string().min(1),
80
+ requirementPath: z.string().min(1),
81
+ requirementSha256: sha256Schema,
82
+ referencePaths: z.array(z.string().min(1)),
83
+ requirementIds: z.array(z.string().min(1)),
84
+ }).strict();
85
+ const coverageCaseSchema = z.object({
86
+ caseId: z.string().min(1),
87
+ title: z.string().min(1),
88
+ markdownPath: z.string().min(1),
89
+ acIds: z.array(z.string()),
90
+ ruleKeys: z.array(z.string()),
91
+ testPoints: z.array(z.string()),
92
+ testPointBindings: z.array(z.object({
93
+ testPoint: z.string(),
94
+ mode: z.enum(TEST_POINT_BINDING_MODES),
95
+ }).strict()),
96
+ unclassifiedTestPoints: z.array(z.string()),
97
+ duplicateBindingTestPoints: z.array(z.string()),
98
+ scenarioTypes: z.array(z.string()),
99
+ declaredScripts: z.array(z.string()),
100
+ declaredPrimarySymbols: z.array(z.string()),
101
+ payloadContract: z.object({
102
+ declared: z.boolean(),
103
+ noPayload: z.boolean(),
104
+ requiredPaths: z.array(z.string()),
105
+ allowedPaths: z.array(z.string()),
106
+ enumValues: z.record(z.string(), z.array(z.string())),
107
+ }).strict(),
108
+ }).strict();
109
+ const coverageRuleSchema = z.object({
110
+ ruleKey: z.string().min(1),
111
+ priority: z.enum(["P0", "P1", "P2"]),
112
+ source: z.string().min(1),
113
+ endpointField: z.string().min(1),
114
+ dimension: z.string().min(1),
115
+ rule: z.string().min(1),
116
+ requiredTestPoints: z.array(z.string()),
117
+ caseIds: z.array(z.string()),
118
+ declaredStatus: z.enum(["COVERED", "PARTIAL", "GAP", "CONFLICT"]),
119
+ coveredTestPoints: z.array(z.string()),
120
+ missingTestPoints: z.array(z.string()),
121
+ status: z.enum(["COVERED", "PARTIAL", "GAP", "CONFLICT"]),
122
+ }).strict();
123
+ const sourceCoverageFactSchema = z.object({
124
+ kind: z.enum(["requirement-id", "error-code", "dto-field", "business-rule"]),
125
+ key: z.string().min(1),
126
+ sourcePath: z.string().min(1),
127
+ sourceSha256: sha256Schema,
128
+ lineStart: z.number().int().positive(),
129
+ lineEnd: z.number().int().positive(),
130
+ quote: z.string().min(1).max(1000),
131
+ covered: z.boolean(),
132
+ }).strict();
133
+ const sourceCoverageSchema = z.object({
134
+ sourceFactCount: z.number().int().min(0),
135
+ coveredSourceFactCount: z.number().int().min(0),
136
+ residualSourceFactCount: z.number().int().min(0),
137
+ facts: z.array(sourceCoverageFactSchema),
138
+ residual: z.array(sourceCoverageFactSchema),
139
+ }).strict();
140
+ const coverageScopeSchema = z.object({
141
+ changeClassification: z.enum(CHANGE_CLASSIFICATIONS),
142
+ coveragePolicy: z.enum(COVERAGE_POLICIES),
143
+ affectedOperations: z.array(z.string().min(1)),
144
+ affectedRuleKeys: z.array(z.string().min(1)),
145
+ regressionFloor: z.array(z.string().min(1)),
146
+ scopeEvidence: z.array(z.string().min(1)),
147
+ completenessClaim: z.enum(["affected-operations-full", "affected-scope"]),
148
+ }).strict();
149
+ export const backendTestCaseCoverageFactsSchema = z.object({
150
+ schemaId: z.literal("backend-test-case-coverage-facts-v4"),
151
+ schemaVersion: z.literal(4),
152
+ taskId: z.string().min(1),
153
+ status: z.enum(["PASS", "FAIL", "UNAVAILABLE"]),
154
+ sourceBinding: sourceBindingSchema,
155
+ inputFiles: z.array(inputFileSchema),
156
+ coverageScope: coverageScopeSchema,
157
+ sourceCoverage: sourceCoverageSchema,
158
+ summary: z.object({
159
+ explicitAcCount: z.number().int().min(0),
160
+ coveredAcCount: z.number().int().min(0),
161
+ ruleCount: z.number().int().min(0),
162
+ coveredRuleCount: z.number().int().min(0),
163
+ testPointCount: z.number().int().min(0),
164
+ coveredTestPointCount: z.number().int().min(0),
165
+ enumValueCount: z.number().int().min(0),
166
+ coveredEnumValueCount: z.number().int().min(0),
167
+ invalidEquivalenceClassCount: z.number().int().min(0),
168
+ coveredInvalidEquivalenceClassCount: z.number().int().min(0),
169
+ boundaryPointCount: z.number().int().min(0),
170
+ coveredBoundaryPointCount: z.number().int().min(0),
171
+ formatClassCount: z.number().int().min(0),
172
+ coveredFormatClassCount: z.number().int().min(0),
173
+ businessStateCount: z.number().int().min(0),
174
+ coveredBusinessStateCount: z.number().int().min(0),
175
+ gapCount: z.number().int().min(0),
176
+ conflictCount: z.number().int().min(0),
177
+ variantTestPointCount: z.number().int().min(0),
178
+ assertionTestPointCount: z.number().int().min(0),
179
+ crossCuttingTestPointCount: z.number().int().min(0),
180
+ unclassifiedTestPointCount: z.number().int().min(0),
181
+ duplicateBindingTestPointCount: z.number().int().min(0),
182
+ }).strict(),
183
+ cases: z.array(coverageCaseSchema),
184
+ rules: z.array(coverageRuleSchema),
185
+ /** Plan C: declared query/filter axis partitions and their slot coverage. */
186
+ scenarioPartitions: z.object({
187
+ partitions: z.array(z.object({
188
+ partitionId: z.string(),
189
+ operation: z.string(),
190
+ axis: z.string(),
191
+ domain: z.array(z.string()),
192
+ optional: z.boolean(),
193
+ complementExpected: z.string(),
194
+ bindRule: z.string(),
195
+ slotIds: z.array(z.string()),
196
+ })),
197
+ slots: z.array(z.object({
198
+ slotId: z.string(),
199
+ partitionId: z.string(),
200
+ kind: z.string(),
201
+ intent: z.string(),
202
+ expected: z.string(),
203
+ })),
204
+ missingSlots: z.array(z.string()),
205
+ }),
206
+ findings: z.array(z.string()),
207
+ evidenceGaps: z.array(z.string()),
208
+ conflicts: z.array(z.string()),
209
+ }).strict();
210
+ const correspondenceEntrySchema = z.object({
211
+ markdownModule: z.string().min(1),
212
+ caseId: z.string().optional(),
213
+ testPoints: z.array(z.string()),
214
+ variantTestPoints: z.array(z.string()),
215
+ assertionTestPoints: z.array(z.string()),
216
+ crossCuttingTestPoints: z.array(z.string()),
217
+ mappedTestPoints: z.array(z.string()),
218
+ declaredScript: z.string().optional(),
219
+ declaredPrimarySymbol: z.string().optional(),
220
+ expectedScript: z.string().min(1),
221
+ actualScripts: z.array(z.string()),
222
+ pytestSymbols: z.array(z.string()),
223
+ parameterIds: z.array(z.string()),
224
+ cardinality: z.enum(["1:1", "1:0", "1:N", "0:1"]),
225
+ status: z.enum([
226
+ "EXACT_1_TO_1",
227
+ "MISSING_PYTEST",
228
+ "MULTIPLE_PYTEST",
229
+ "EXTRA_PYTEST",
230
+ "SCRIPT_MISMATCH",
231
+ "SYMBOL_MISSING_CASE_ID",
232
+ "VARIANT_PARAMETER_MISSING",
233
+ "ASSERTION_BINDING_MISSING",
234
+ "CROSS_CUTTING_EVIDENCE_MISSING",
235
+ "TEST_POINT_BINDING_DUPLICATE",
236
+ "TEST_POINT_BINDING_EXTRA",
237
+ "PAYLOAD_CONTRACT_UNAVAILABLE",
238
+ "PAYLOAD_SHAPE_MISMATCH",
239
+ ]),
240
+ payloadAssessment: z.object({
241
+ status: z.enum(["SAFE", "UNSAFE", "UNAVAILABLE"]),
242
+ requiredPaths: z.array(z.string()),
243
+ allowedPaths: z.array(z.string()),
244
+ observedPaths: z.array(z.string()),
245
+ missingPaths: z.array(z.string()),
246
+ unexpectedPaths: z.array(z.string()),
247
+ enumMismatches: z.array(z.string()),
248
+ }).strict(),
249
+ executionSignature: z.string().min(1).optional(),
250
+ findings: z.array(z.string()),
251
+ }).strict();
252
+ export const backendTestMarkdownPytestCorrespondenceFactsSchema = z.object({
253
+ schemaId: z.literal("backend-test-markdown-pytest-correspondence-facts-v3"),
254
+ schemaVersion: z.literal(3),
255
+ taskId: z.string().min(1),
256
+ status: z.enum(["PASS", "FAIL", "UNAVAILABLE"]),
257
+ inputFiles: z.array(inputFileSchema),
258
+ summary: z.object({
259
+ markdownModuleCount: z.number().int().min(0),
260
+ exactModuleCount: z.number().int().min(0),
261
+ markdownCaseCount: z.number().int().min(0),
262
+ exactCorrespondenceCount: z.number().int().min(0),
263
+ primarySymbolCount: z.number().int().min(0),
264
+ missingPytestCount: z.number().int().min(0),
265
+ multiplePytestCount: z.number().int().min(0),
266
+ extraPytestCount: z.number().int().min(0),
267
+ scriptMismatchCount: z.number().int().min(0),
268
+ testPointCount: z.number().int().min(0),
269
+ mappedTestPointCount: z.number().int().min(0),
270
+ variantTestPointCount: z.number().int().min(0),
271
+ assertionTestPointCount: z.number().int().min(0),
272
+ crossCuttingTestPointCount: z.number().int().min(0),
273
+ unclassifiedTestPointCount: z.number().int().min(0),
274
+ duplicateBindingTestPointCount: z.number().int().min(0),
275
+ payloadSafeCount: z.number().int().min(0),
276
+ payloadUnsafeCount: z.number().int().min(0),
277
+ payloadUnavailableCount: z.number().int().min(0),
278
+ duplicateExecutionSignatureCount: z.number().int().min(0),
279
+ }).strict(),
280
+ entries: z.array(correspondenceEntrySchema),
281
+ findings: z.array(z.string()),
282
+ }).strict();
283
+ function isRecord(value) {
284
+ return typeof value === "object" && value !== null && !Array.isArray(value);
285
+ }
286
+ function openApiToken(value) {
287
+ return value.replace(/[{}]/g, "").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toUpperCase();
288
+ }
289
+ function resolveOpenApiSchema(document, value) {
290
+ if (!isRecord(value))
291
+ return undefined;
292
+ if (typeof value.$ref !== "string" || !value.$ref.startsWith("#/"))
293
+ return value;
294
+ let current = document;
295
+ for (const segment of value.$ref.slice(2).split("/")) {
296
+ if (!isRecord(current))
297
+ return undefined;
298
+ current = current[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
299
+ }
300
+ return isRecord(current) ? current : undefined;
301
+ }
302
+ function openApiTestPoints(field, dimension, values = []) {
303
+ const prefix = `TP-${openApiToken(field)}`;
304
+ if (dimension === "enum")
305
+ return [...values.map((value) => `${prefix}-ENUM-${openApiToken(value)}`), `${prefix}-ENUM-UNKNOWN`, `${prefix}-ENUM-CASE-VARIANT`, `${prefix}-ENUM-WHITESPACE`, `${prefix}-ENUM-EMPTY`, `${prefix}-ENUM-NULL`, `${prefix}-ENUM-WRONG-TYPE`];
306
+ if (dimension === "boundary")
307
+ return [`${prefix}-MIN-1`, `${prefix}-MIN`, `${prefix}-NOMINAL`, `${prefix}-MAX`, `${prefix}-MAX-PLUS-1`];
308
+ if (dimension === "format")
309
+ return [`${prefix}-VALID-CLASS`, `${prefix}-VALID-MIXED`, `${prefix}-UPPERCASE`, `${prefix}-WHITESPACE`, `${prefix}-PUNCTUATION`, `${prefix}-SLASH`, `${prefix}-EMOJI`, `${prefix}-CONTROL`];
310
+ return [`${prefix}-PRESENT`, `${prefix}-MISSING`, `${prefix}-NULL`, `${prefix}-WRONG-TYPE`];
311
+ }
312
+ async function collectSourceCoverageFacts(input) {
313
+ const paths = orderedUnique([
314
+ input.sourceBinding.requirementPath,
315
+ ...input.sourceBinding.referencePaths,
316
+ ]);
317
+ const facts = [];
318
+ const seen = new Set();
319
+ for (const sourcePath of paths) {
320
+ const physicalPath = resolveBoundSourcePath(input.workspaceRoot, input.sourceBinding.taskId, sourcePath);
321
+ let text;
322
+ try {
323
+ text = await readFile(physicalPath, "utf8");
324
+ }
325
+ catch {
326
+ continue;
327
+ }
328
+ const sourceSha256 = createHash("sha256").update(await readFile(physicalPath)).digest("hex");
329
+ const lines = text.split(/\r?\n/);
330
+ const add = (kind, key, lineIndex, quote) => {
331
+ const normalizedQuote = quote.trim().slice(0, 1000);
332
+ if (!normalizedQuote)
333
+ return;
334
+ const identity = `${kind}:${key}:${sourcePath}:${lineIndex + 1}`;
335
+ if (seen.has(identity))
336
+ return;
337
+ seen.add(identity);
338
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
339
+ const covered = kind === "business-rule"
340
+ ? input.generatedMarkdown.includes(normalizedQuote)
341
+ : new RegExp(`\\b${escaped}\\b`).test(input.generatedMarkdown);
342
+ facts.push({
343
+ kind,
344
+ key,
345
+ sourcePath,
346
+ sourceSha256,
347
+ lineStart: lineIndex + 1,
348
+ lineEnd: lineIndex + 1,
349
+ quote: normalizedQuote,
350
+ covered,
351
+ });
352
+ };
353
+ lines.forEach((line, lineIndex) => {
354
+ for (const key of line.match(SOURCE_RULE_ID_IN_TEXT) ?? [])
355
+ add("requirement-id", key, lineIndex, line);
356
+ for (const key of line.match(SOURCE_ERROR_CODE_IN_TEXT) ?? [])
357
+ add("error-code", key, lineIndex, line);
358
+ for (const match of line.matchAll(JSON_FIELD_IN_TEXT))
359
+ add("dto-field", match[1], lineIndex, line);
360
+ for (const match of line.matchAll(DTO_FIELD_IN_TEXT))
361
+ add("dto-field", match[1], lineIndex, line);
362
+ const trimmed = line.trim();
363
+ if (trimmed.length >= 8 && trimmed.length <= 1000 && !/^\s*(?:#|\||[-:]{3,})/.test(trimmed) && NATURAL_BUSINESS_RULE_KEYWORDS.test(trimmed) && NATURAL_BUSINESS_RULE_NORMATIVE.test(trimmed)) {
364
+ add("business-rule", `BR-SOURCE-${sourceSha256.slice(0, 8).toUpperCase()}-L${lineIndex + 1}`, lineIndex, trimmed);
365
+ }
366
+ });
367
+ }
368
+ return facts.sort((left, right) => left.kind.localeCompare(right.kind) || left.key.localeCompare(right.key) || left.sourcePath.localeCompare(right.sourcePath) || left.lineStart - right.lineStart);
369
+ }
370
+ function resolveBoundSourcePath(workspaceRoot, taskId, sourcePath) {
371
+ if (path.isAbsolute(sourcePath))
372
+ return sourcePath;
373
+ const normalized = sourcePath.replaceAll("\\", "/").replace(/^\.\//, "");
374
+ const boundPrefix = `.harness/tasks/${taskId}/`;
375
+ return normalized.startsWith(boundPrefix)
376
+ ? path.join(workspaceRoot, ...normalized.split("/"))
377
+ : path.join(workspaceRoot, ".harness", "tasks", taskId, ...normalized.split("/"));
378
+ }
379
+ export async function extractBackendTestOpenApiRules(input) {
380
+ const rules = [];
381
+ for (const referencePath of input.sourceBinding.referencePaths) {
382
+ const physicalPath = resolveBoundSourcePath(input.workspaceRoot, input.sourceBinding.taskId, referencePath);
383
+ let document;
384
+ try {
385
+ const parsed = YAML.parse(await readFile(physicalPath, "utf8"));
386
+ if (!isRecord(parsed) || !isRecord(parsed.paths))
387
+ continue;
388
+ document = parsed;
389
+ }
390
+ catch {
391
+ continue;
392
+ }
393
+ for (const [apiPath, pathItem] of Object.entries(document.paths)) {
394
+ if (!isRecord(pathItem))
395
+ continue;
396
+ for (const method of ["get", "post", "put", "patch", "delete"]) {
397
+ const operation = pathItem[method];
398
+ if (!isRecord(operation))
399
+ continue;
400
+ const operationBase = `API-${method.toUpperCase()}-${openApiToken(apiPath)}`;
401
+ const responses = isRecord(operation.responses) ? operation.responses : {};
402
+ const responseStatuses = Object.keys(responses).filter((status) => /^(?:[1-5]\d\d|default)$/i.test(status)).sort();
403
+ if (responseStatuses.length > 0) {
404
+ rules.push({
405
+ ruleKey: `${operationBase}-RESPONSE-STATUS`,
406
+ priority: "P1",
407
+ source: referencePath,
408
+ endpointField: `${method.toUpperCase()} ${apiPath} / response status`,
409
+ dimension: "response-status",
410
+ rule: `documented response statuses: ${responseStatuses.join(", ")}`,
411
+ requiredTestPoints: responseStatuses.map((status) => `TP-RESPONSE-STATUS-${openApiToken(status)}`),
412
+ });
413
+ }
414
+ const parameterValues = [
415
+ ...(Array.isArray(pathItem.parameters) ? pathItem.parameters : []),
416
+ ...(Array.isArray(operation.parameters) ? operation.parameters : []),
417
+ ];
418
+ for (const rawParameter of parameterValues) {
419
+ const parameter = resolveOpenApiSchema(document, rawParameter);
420
+ if (!parameter || typeof parameter.name !== "string")
421
+ continue;
422
+ const field = parameter.name;
423
+ const property = resolveOpenApiSchema(document, parameter.schema);
424
+ if (!property)
425
+ continue;
426
+ const base = `${operationBase}-${openApiToken(field)}`;
427
+ const endpointField = `${method.toUpperCase()} ${apiPath} / ${String(parameter.in ?? "parameter")}:${field}`;
428
+ const add = (suffix, dimension, rule, points = openApiTestPoints(field, dimension)) => {
429
+ rules.push({ ruleKey: `${base}-${suffix}`, priority: "P1", source: referencePath, endpointField, dimension, rule, requiredTestPoints: points });
430
+ };
431
+ if (parameter.required === true)
432
+ add("REQUIRED", "requiredness", `${field} parameter is required`);
433
+ if (Array.isArray(property.enum)) {
434
+ const values = property.enum.map(String);
435
+ add("ENUM", "enum", `${field} enum: ${values.join(", ")}`, openApiTestPoints(field, "enum", values));
436
+ }
437
+ const boundaryPoints = openApiTestPoints(field, "boundary");
438
+ if (typeof property.minimum === "number")
439
+ add("MINIMUM", "boundary", `${field} minimum=${property.minimum}`, boundaryPoints.filter((point) => !point.includes("MAX")));
440
+ if (typeof property.maximum === "number")
441
+ add("MAXIMUM", "boundary", `${field} maximum=${property.maximum}`, boundaryPoints.filter((point) => !point.includes("MIN-")));
442
+ if (typeof property.pattern === "string")
443
+ add("PATTERN", "format", `${field} pattern=${property.pattern}`);
444
+ if (typeof property.format === "string")
445
+ add("FORMAT", "format", `${field} format=${property.format}`);
446
+ }
447
+ const requestBody = isRecord(operation.requestBody) ? operation.requestBody : undefined;
448
+ const content = requestBody && isRecord(requestBody.content) ? requestBody.content : undefined;
449
+ const media = content && (content["application/json"] ?? Object.values(content)[0]);
450
+ const schema = resolveOpenApiSchema(document, isRecord(media) ? media.schema : undefined);
451
+ if (!schema)
452
+ continue;
453
+ const required = new Set(Array.isArray(schema.required) ? schema.required.filter((value) => typeof value === "string") : []);
454
+ const properties = isRecord(schema.properties) ? schema.properties : {};
455
+ for (const [field, rawProperty] of Object.entries(properties)) {
456
+ const property = resolveOpenApiSchema(document, rawProperty);
457
+ if (!property)
458
+ continue;
459
+ const base = `${operationBase}-${openApiToken(field)}`;
460
+ const endpointField = `${method.toUpperCase()} ${apiPath} / ${field}`;
461
+ const add = (suffix, dimension, rule, points = openApiTestPoints(field, dimension)) => {
462
+ rules.push({ ruleKey: `${base}-${suffix}`, priority: "P1", source: referencePath, endpointField, dimension, rule, requiredTestPoints: points });
463
+ };
464
+ if (required.has(field))
465
+ add("REQUIRED", "requiredness", `${field} is required`);
466
+ if (Array.isArray(property.enum)) {
467
+ const values = property.enum.map(String);
468
+ add("ENUM", "enum", `${field} enum: ${values.join(", ")}`, openApiTestPoints(field, "enum", values));
469
+ }
470
+ const boundaryPoints = openApiTestPoints(field, "boundary");
471
+ if (typeof property.minLength === "number")
472
+ add("MIN-LENGTH", "boundary", `${field} minLength=${property.minLength}`, boundaryPoints.filter((point) => !point.includes("MAX")));
473
+ if (typeof property.maxLength === "number")
474
+ add("MAX-LENGTH", "boundary", `${field} maxLength=${property.maxLength}`, boundaryPoints.filter((point) => !point.includes("MIN-")));
475
+ if (typeof property.minimum === "number")
476
+ add("MINIMUM", "boundary", `${field} minimum=${property.minimum}`, boundaryPoints.filter((point) => !point.includes("MAX")));
477
+ if (typeof property.maximum === "number")
478
+ add("MAXIMUM", "boundary", `${field} maximum=${property.maximum}`, boundaryPoints.filter((point) => !point.includes("MIN-")));
479
+ if (typeof property.pattern === "string")
480
+ add("PATTERN", "format", `${field} pattern=${property.pattern}`);
481
+ if (typeof property.format === "string")
482
+ add("FORMAT", "format", `${field} format=${property.format}`);
483
+ }
484
+ }
485
+ }
486
+ }
487
+ return rules.sort((left, right) => left.ruleKey.localeCompare(right.ruleKey));
488
+ }
489
+ function orderedUnique(values) {
490
+ const result = [];
491
+ const seen = new Set();
492
+ for (const value of values) {
493
+ if (!value || seen.has(value))
494
+ continue;
495
+ seen.add(value);
496
+ result.push(value);
497
+ }
498
+ return result;
499
+ }
500
+ function canonicalCaseId(value) {
501
+ const upper = value.toUpperCase().replaceAll("_", "-").replace(/-+/g, "-");
502
+ return /^(BE-[A-Z0-9]+(?:-[A-Z0-9]+)*)-\d{3}$/.test(upper) ? upper : value.toUpperCase();
503
+ }
504
+ function caseIds(value) {
505
+ return orderedUnique((value.match(CASE_ID_IN_TEXT) ?? []).map(canonicalCaseId));
506
+ }
507
+ function sectionBody(body, aliases) {
508
+ const escaped = aliases.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
509
+ const marker = new RegExp(`^###\\s+(?:${escaped})\\s*$`, "mi");
510
+ const match = marker.exec(body);
511
+ if (!match)
512
+ return "";
513
+ const rest = body.slice(match.index + match[0].length);
514
+ const next = /^###\s+/m.exec(rest);
515
+ return rest.slice(0, next?.index ?? rest.length);
516
+ }
517
+ function listTokens(body, aliases, pattern) {
518
+ const section = sectionBody(body, aliases).replaceAll("`", "");
519
+ // Prefer dedicated token patterns; never strip ^/$ from alternation-based anchors
520
+ // (that would turn `^A$|^B$` into `A$|^B` and break scanning).
521
+ const tokenSource = pattern === RULE_KEY
522
+ ? RULE_KEY_TOKEN.source
523
+ : pattern === TEST_POINT
524
+ ? TEST_POINT.source.replace(/^\^/, "").replace(/\$$/, "")
525
+ : pattern.source.replace(/^\^/, "").replace(/\$$/, "");
526
+ return orderedUnique(section.match(new RegExp(tokenSource, "g")) ?? []);
527
+ }
528
+ function scenarioTypes(body) {
529
+ return orderedUnique(sectionBody(body, CASE_SECTION_ALIASES.scenarioTypes)
530
+ .split(/\r?\n/)
531
+ .map((line) => line.replace(/^\s*(?:[-*+] |\d+[.)]\s*)/, "").replaceAll("`", "").trim())
532
+ .flatMap((line) => line.split(/[;,,、]/).map((item) => item.trim().toLowerCase()))
533
+ .filter(Boolean));
534
+ }
535
+ function declaredScripts(body) {
536
+ const automation = sectionBody(body, CASE_SECTION_ALIASES.automation);
537
+ return orderedUnique([...automation.matchAll(/testcase\/[A-Za-z0-9_./-]*test_[A-Za-z0-9_.-]*\.py/gi)]
538
+ .map((match) => match[0].replaceAll("\\", "/")));
539
+ }
540
+ function automationLabelBody(body, labels) {
541
+ const automation = sectionBody(body, CASE_SECTION_ALIASES.automation);
542
+ const escaped = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
543
+ const match = new RegExp(`^\\s*[-*+]\\s*(?:${escaped})\\s*[::]\\s*(.*)$`, "mi").exec(automation);
544
+ return match?.[1]?.replaceAll("`", "").trim() ?? "";
545
+ }
546
+ function testPointBindingFacts(body, testPoints) {
547
+ const bindings = TEST_POINT_BINDING_MODES.flatMap((mode) => {
548
+ const value = automationLabelBody(body, AUTOMATION_BINDING_LABELS[mode]);
549
+ return orderedUnique(value.match(/\bTP-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g) ?? [])
550
+ .map((testPoint) => ({ testPoint, mode }));
551
+ });
552
+ const counts = new Map();
553
+ for (const binding of bindings)
554
+ counts.set(binding.testPoint, (counts.get(binding.testPoint) ?? 0) + 1);
555
+ return {
556
+ testPointBindings: bindings,
557
+ unclassifiedTestPoints: testPoints.filter((testPoint) => !counts.has(testPoint)),
558
+ duplicateBindingTestPoints: testPoints.filter((testPoint) => (counts.get(testPoint) ?? 0) > 1),
559
+ };
560
+ }
561
+ function declaredPrimarySymbols(body) {
562
+ const value = automationLabelBody(body, ["primary symbol", "Primary Symbol", "主测试符号"]);
563
+ if (/^(?:none|无|n\/a|not-applicable)$/i.test(value.trim()))
564
+ return [];
565
+ return orderedUnique(value.match(/\btest_[A-Za-z0-9_]+\b/g) ?? []);
566
+ }
567
+ function isEvidenceOnlyMarkdownCase(testCase) {
568
+ const hasVariant = testCase.testPointBindings.some((item) => item.mode === "variant");
569
+ const hasNonVariant = testCase.testPointBindings.some((item) => item.mode === "assertion" || item.mode === "cross-cutting");
570
+ return (testCase.declaredScripts.length === 0 &&
571
+ testCase.declaredPrimarySymbols.length === 0 &&
572
+ !hasVariant &&
573
+ hasNonVariant);
574
+ }
575
+ function payloadContract(body) {
576
+ const contractValue = automationLabelBody(body, ["Payload Contract", "载荷合同"]);
577
+ const requiredValue = automationLabelBody(body, ["Payload Required Paths", "载荷必需路径"]);
578
+ const allowedValue = automationLabelBody(body, ["Payload Allowed Paths", "载荷允许路径"]);
579
+ const enumValue = automationLabelBody(body, ["Payload Enum", "载荷枚举"]);
580
+ const tokens = (value) => orderedUnique(value.split(/[,;,;]/).map((item) => item.trim()).filter((item) => /^[A-Za-z_][A-Za-z0-9_.]*$/.test(item)));
581
+ const enumValues = {};
582
+ for (const assignment of enumValue.split(/[,;,;]/).map((item) => item.trim()).filter(Boolean)) {
583
+ const match = /^([A-Za-z_][A-Za-z0-9_.]*)\s*(?:=|:)\s*(.+)$/.exec(assignment);
584
+ if (!match)
585
+ continue;
586
+ enumValues[match[1]] = orderedUnique(match[2].split(/\||\//).map((item) => item.trim().replace(/[((].*$/, "").trim().replace(/^['"]|['"]$/g, "")).filter(Boolean));
587
+ }
588
+ const noPayload = /^(?:none|无|not-applicable|n\/a)(?:\s|[((]|$)/i.test(contractValue.trim());
589
+ return {
590
+ declared: noPayload || requiredValue.length > 0 || allowedValue.length > 0 || enumValue.length > 0,
591
+ noPayload,
592
+ requiredPaths: tokens(requiredValue),
593
+ allowedPaths: tokens(allowedValue),
594
+ enumValues,
595
+ };
596
+ }
597
+ function cleanTitle(heading, id) {
598
+ return heading.replace(/^##\s+/, "").replace(/^BE-[A-Z0-9_-]+-\d{2,3}\s*(?:[||—–-]\s*)?/i, "").trim() || id;
599
+ }
600
+ async function exists(filePath) {
601
+ try {
602
+ await access(filePath);
603
+ return true;
604
+ }
605
+ catch {
606
+ return false;
607
+ }
608
+ }
609
+ async function markdownModuleFiles(workspaceRoot, layout) {
610
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
611
+ const dir = path.join(workspaceRoot, ...resolved.markdownDir.split("/"));
612
+ const entries = await readdir(dir, { withFileTypes: true });
613
+ return entries
614
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md") && entry.name.toLowerCase() !== "readme.md")
615
+ .map((entry) => path.join(dir, entry.name))
616
+ .sort();
617
+ }
618
+ async function parseMarkdownCases(workspaceRoot, layout) {
619
+ const result = [];
620
+ for (const file of await markdownModuleFiles(workspaceRoot, layout)) {
621
+ const markdown = await readFile(file, "utf8");
622
+ const headings = [...markdown.matchAll(CASE_HEADING)];
623
+ for (let index = 0; index < headings.length; index += 1) {
624
+ const heading = headings[index];
625
+ const id = canonicalCaseId(heading[1]);
626
+ const body = markdown.slice(heading.index, headings[index + 1]?.index ?? markdown.length);
627
+ const testPoints = listTokens(body, CASE_SECTION_ALIASES.testPoints, TEST_POINT);
628
+ result.push({
629
+ caseId: id,
630
+ title: cleanTitle(heading[0], id),
631
+ markdownPath: path.relative(workspaceRoot, file).replaceAll(path.sep, "/"),
632
+ acIds: orderedUnique(body.match(AC_ID_IN_TEXT) ?? []),
633
+ ruleKeys: listTokens(body, CASE_SECTION_ALIASES.rules, RULE_KEY),
634
+ testPoints,
635
+ ...testPointBindingFacts(body, testPoints),
636
+ scenarioTypes: scenarioTypes(body),
637
+ declaredScripts: declaredScripts(body),
638
+ declaredPrimarySymbols: declaredPrimarySymbols(body),
639
+ payloadContract: payloadContract(body),
640
+ body,
641
+ });
642
+ }
643
+ }
644
+ return result;
645
+ }
646
+ function parseTableRow(line) {
647
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
648
+ const cells = [];
649
+ let current = "";
650
+ for (let index = 0; index < trimmed.length; index += 1) {
651
+ const char = trimmed[index];
652
+ if (char === "\\" && trimmed[index + 1] === "|") {
653
+ current += "|";
654
+ index += 1;
655
+ }
656
+ else if (char === "|") {
657
+ const prev = trimmed[index - 1] ?? "";
658
+ const next = trimmed[index + 1] ?? "";
659
+ // In-cell enum/value pipes look like ACTIVE|ARCHIVED (alnum on both sides).
660
+ // Column separators use " | " or touch markdown separator dashes (---|---).
661
+ if (/[A-Za-z0-9_]/.test(prev) && /[A-Za-z0-9_]/.test(next)) {
662
+ current += "|";
663
+ }
664
+ else {
665
+ cells.push(current.trim());
666
+ current = "";
667
+ }
668
+ }
669
+ else {
670
+ current += char;
671
+ }
672
+ }
673
+ cells.push(current.trim());
674
+ return cells;
675
+ }
676
+ function splitCellTokens(value, pattern) {
677
+ const values = orderedUnique(value.split(/;|<br\s*\/?\s*>|,|、/i).map((item) => item.replaceAll("`", "").trim()).filter(Boolean));
678
+ return pattern ? values.filter((item) => pattern.test(item)) : values;
679
+ }
680
+ function parseCoverageScope(readme) {
681
+ const findings = [];
682
+ const fallback = {
683
+ changeClassification: "new-operation",
684
+ coveragePolicy: "full-contract",
685
+ affectedOperations: [],
686
+ affectedRuleKeys: [],
687
+ regressionFloor: [],
688
+ scopeEvidence: [],
689
+ completenessClaim: "affected-operations-full",
690
+ };
691
+ const marker = /^##\s+Coverage Scope\s*$/mi.exec(readme);
692
+ if (!marker)
693
+ return { scope: fallback, findings: ["README is missing required ## Coverage Scope section; defaulted to full-contract"] };
694
+ const rest = readme.slice(marker.index + marker[0].length);
695
+ const nextHeading = /^##\s+/m.exec(rest);
696
+ const section = rest.slice(0, nextHeading?.index ?? rest.length);
697
+ const lines = section.split(/\r?\n/).filter((line) => line.trim().startsWith("|"));
698
+ if (lines.length < 2 || JSON.stringify(parseTableRow(lines[0])) !== JSON.stringify(["Field", "Value"])) {
699
+ return { scope: fallback, findings: ["Coverage Scope table headers must be exactly: Field | Value"] };
700
+ }
701
+ const separatorCells = lines[1] ? parseTableRow(lines[1]) : [];
702
+ const hasSeparator = separatorCells.length === 2 && separatorCells.every((cell) => /^:?-{3,}:?$/.test(cell));
703
+ if (!hasSeparator)
704
+ findings.push("Coverage Scope table is missing the required |---|---| separator row");
705
+ const values = new Map();
706
+ for (const line of lines.slice(hasSeparator ? 2 : 1)) {
707
+ const cells = parseTableRow(line);
708
+ if (cells.length !== 2) {
709
+ findings.push(`Coverage Scope row must have exactly 2 columns: ${line.slice(0, 160)}`);
710
+ continue;
711
+ }
712
+ const field = cells[0].trim();
713
+ if (values.has(field))
714
+ findings.push(`duplicate Coverage Scope field: ${field}`);
715
+ values.set(field, cells[1].trim());
716
+ }
717
+ const classificationRaw = (values.get("Change Classification") ?? "").replaceAll("`", "").trim();
718
+ const policyRaw = (values.get("Coverage Policy") ?? "").replaceAll("`", "").trim();
719
+ const changeClassification = CHANGE_CLASSIFICATIONS.includes(classificationRaw)
720
+ ? classificationRaw
721
+ : fallback.changeClassification;
722
+ const coveragePolicy = COVERAGE_POLICIES.includes(policyRaw)
723
+ ? policyRaw
724
+ : fallback.coveragePolicy;
725
+ if (!CHANGE_CLASSIFICATIONS.includes(classificationRaw))
726
+ findings.push(`invalid or missing Change Classification: ${classificationRaw || "<empty>"}`);
727
+ if (!COVERAGE_POLICIES.includes(policyRaw))
728
+ findings.push(`invalid or missing Coverage Policy: ${policyRaw || "<empty>"}`);
729
+ const expectedPolicy = COVERAGE_POLICY_BY_CLASSIFICATION[changeClassification];
730
+ if (coveragePolicy !== expectedPolicy)
731
+ findings.push(`Change Classification ${changeClassification} requires Coverage Policy ${expectedPolicy}, received ${coveragePolicy}`);
732
+ const affectedOperations = splitCellTokens(values.get("Affected Operations") ?? "").map((value) => value.replace(/\s+/g, " ").trim());
733
+ const affectedRuleKeys = splitCellTokens(values.get("Affected Rule Keys") ?? "", RULE_KEY);
734
+ const regressionFloor = splitCellTokens(values.get("Regression Floor") ?? "").map((value) => value.toLowerCase());
735
+ const scopeEvidence = splitCellTokens(values.get("Scope Evidence") ?? "");
736
+ if (affectedOperations.length === 0)
737
+ findings.push("Coverage Scope requires at least one Affected Operations entry");
738
+ if (affectedRuleKeys.length === 0)
739
+ findings.push("Coverage Scope requires at least one Affected Rule Keys entry");
740
+ if (scopeEvidence.length === 0)
741
+ findings.push("Coverage Scope requires non-empty Scope Evidence");
742
+ for (const required of REQUIRED_REGRESSION_FLOOR[changeClassification]) {
743
+ if (!regressionFloor.includes(required))
744
+ findings.push(`Coverage Scope regression floor is missing required entry: ${required}`);
745
+ }
746
+ return {
747
+ scope: {
748
+ changeClassification,
749
+ coveragePolicy,
750
+ affectedOperations,
751
+ affectedRuleKeys,
752
+ regressionFloor,
753
+ scopeEvidence,
754
+ completenessClaim: coveragePolicy === "full-contract" ? "affected-operations-full" : "affected-scope",
755
+ },
756
+ findings,
757
+ };
758
+ }
759
+ function operationFromEndpointField(endpointField) {
760
+ const match = /^([A-Z]+)\s+(\/\S+)/.exec(endpointField.trim());
761
+ return match ? `${match[1]} ${match[2]}` : undefined;
762
+ }
763
+ function parseCoverageMatrix(readme) {
764
+ const findings = [];
765
+ const marker = /^##\s+(?:(?:\d+(?:\.\d+)*[.)]?\s+)?(?:Coverage Matrix|覆盖矩阵))\s*$/mi.exec(readme);
766
+ if (!marker)
767
+ return { rows: [], findings: ["README is missing required ## Coverage Matrix section"] };
768
+ if (!/^##\s+Coverage Matrix\s*$/i.test(marker[0])) {
769
+ findings.push(`Coverage Matrix should use the exact canonical heading \"## Coverage Matrix\"; accepted compatibility heading: ${marker[0].trim()}`);
770
+ }
771
+ const rest = readme.slice(marker.index + marker[0].length);
772
+ const nextHeading = /^##\s+/m.exec(rest);
773
+ const section = rest.slice(0, nextHeading?.index ?? rest.length);
774
+ const lines = section.split(/\r?\n/).filter((line) => line.trim().startsWith("|"));
775
+ if (lines.length < 2)
776
+ return { rows: [], findings: ["Coverage Matrix table is missing or incomplete"] };
777
+ const headers = parseTableRow(lines[0]);
778
+ if (JSON.stringify(headers) !== JSON.stringify(COVERAGE_HEADERS)) {
779
+ findings.push(`Coverage Matrix headers must be exactly: ${COVERAGE_HEADERS.join(" | ")}`);
780
+ return { rows: [], findings };
781
+ }
782
+ const rows = [];
783
+ for (const line of lines.slice(2)) {
784
+ const cells = parseTableRow(line);
785
+ if (cells.length !== COVERAGE_HEADERS.length) {
786
+ findings.push(`Coverage Matrix row has ${cells.length} columns instead of ${COVERAGE_HEADERS.length}: ${line.slice(0, 160)}`);
787
+ continue;
788
+ }
789
+ rows.push(Object.fromEntries(COVERAGE_HEADERS.map((header, index) => [header, cells[index] ?? ""])));
790
+ }
791
+ if (rows.length === 0)
792
+ findings.push("Coverage Matrix contains no rule rows");
793
+ return { rows, findings };
794
+ }
795
+ async function fileHash(filePath) {
796
+ return createHash("sha256").update(await readFile(filePath)).digest("hex");
797
+ }
798
+ async function inputFileFacts(workspaceRoot, files) {
799
+ const result = [];
800
+ for (const file of orderedUnique(files).sort()) {
801
+ result.push({ path: path.relative(workspaceRoot, file).replaceAll(path.sep, "/"), sha256: await fileHash(file) });
802
+ }
803
+ return result;
804
+ }
805
+ function dimensionCounts(rules, matcher) {
806
+ const selected = rules.filter((rule) => matcher.test(rule.dimension));
807
+ return [
808
+ selected.reduce((sum, rule) => sum + rule.requiredTestPoints.length, 0),
809
+ selected.reduce((sum, rule) => sum + rule.coveredTestPoints.length, 0),
810
+ ];
811
+ }
812
+ function renderCoverageTable(rules) {
813
+ return [
814
+ "| Rule Key | Priority | Dimension | Required | Covered | Missing | Status | Case IDs |",
815
+ "|---|---|---|---:|---:|---|---|---|",
816
+ ...rules.map((rule) => `| ${rule.ruleKey} | ${rule.priority} | ${rule.dimension} | ${rule.requiredTestPoints.length} | ${rule.coveredTestPoints.length} | ${rule.missingTestPoints.join(", ") || "—"} | ${rule.status} | ${rule.caseIds.join(", ") || "—"} |`),
817
+ ];
818
+ }
819
+ export async function analyzeBackendTestCaseCoverage(input) {
820
+ const layout = input.layout ?? resolveBackendTestLayout(undefined);
821
+ const findings = [];
822
+ const readmePath = path.join(input.workspaceRoot, ...layout.readmePath.split("/"));
823
+ const readme = (await exists(readmePath)) ? await readFile(readmePath, "utf8") : "";
824
+ const coverageScope = parseCoverageScope(readme);
825
+ findings.push(...coverageScope.findings);
826
+ const matrix = parseCoverageMatrix(readme);
827
+ findings.push(...matrix.findings);
828
+ const cases = await parseMarkdownCases(input.workspaceRoot, layout);
829
+ for (const file of await markdownModuleFiles(input.workspaceRoot, layout)) {
830
+ const markdown = await readFile(file, "utf8");
831
+ for (const match of markdown.matchAll(NON_CANONICAL_CASE_HEADING)) {
832
+ findings.push(`non-canonical Case ID heading must use BE-<MODULE>-<NNN>: ${match[1]}`);
833
+ }
834
+ }
835
+ const caseById = new Map(cases.map((item) => [item.caseId, item]));
836
+ const rules = [];
837
+ const evidenceGaps = [];
838
+ const conflicts = [];
839
+ const seenRules = new Set();
840
+ for (const row of matrix.rows) {
841
+ const ruleKey = row["Rule Key"].replaceAll("`", "").trim();
842
+ const priority = row.Priority.trim().toUpperCase();
843
+ const declaredStatus = row.Status.trim().toUpperCase();
844
+ if (!RULE_KEY.test(ruleKey))
845
+ findings.push(`invalid Rule Key: ${ruleKey || "<empty>"}`);
846
+ if (seenRules.has(ruleKey))
847
+ findings.push(`duplicate Coverage Matrix Rule Key: ${ruleKey}`);
848
+ seenRules.add(ruleKey);
849
+ if (!/^(?:P0|P1|P2)$/.test(priority))
850
+ findings.push(`${ruleKey} has invalid priority: ${priority}`);
851
+ if (!/^(?:COVERED|PARTIAL|GAP|CONFLICT)$/.test(declaredStatus))
852
+ findings.push(`${ruleKey} has invalid status: ${declaredStatus}`);
853
+ const requiredTestPoints = splitCellTokens(row["Required Test Points"], TEST_POINT);
854
+ const rowCaseIds = splitCellTokens(row["Case IDs"]).map(canonicalCaseId);
855
+ if (requiredTestPoints.length === 0 && declaredStatus === "COVERED")
856
+ findings.push(`${ruleKey} declares COVERED without Required Test Points`);
857
+ const declaredPoints = orderedUnique(rowCaseIds.flatMap((id) => caseById.get(id)?.testPoints ?? []));
858
+ const coveredTestPoints = requiredTestPoints.filter((point) => declaredPoints.includes(point));
859
+ const missingTestPoints = requiredTestPoints.filter((point) => !declaredPoints.includes(point));
860
+ for (const id of rowCaseIds) {
861
+ const testCase = caseById.get(id);
862
+ if (!testCase)
863
+ findings.push(`${ruleKey} references missing Markdown Case: ${id}`);
864
+ else if (!testCase.ruleKeys.includes(ruleKey))
865
+ findings.push(`${id} does not reference Coverage Matrix rule ${ruleKey}`);
866
+ }
867
+ for (const testCase of cases.filter((item) => item.ruleKeys.includes(ruleKey))) {
868
+ if (!rowCaseIds.includes(testCase.caseId))
869
+ findings.push(`${testCase.caseId} references ${ruleKey} but is absent from its Coverage Matrix Case IDs`);
870
+ }
871
+ const normalizedDeclaredStatus = /^(?:COVERED|PARTIAL|GAP|CONFLICT)$/.test(declaredStatus)
872
+ ? declaredStatus
873
+ : "PARTIAL";
874
+ let status = normalizedDeclaredStatus;
875
+ if (status === "COVERED" && missingTestPoints.length > 0)
876
+ status = "PARTIAL";
877
+ if (missingTestPoints.length > 0)
878
+ findings.push(`${ruleKey} is missing required test points: ${missingTestPoints.join(", ")}`);
879
+ if (status === "GAP")
880
+ evidenceGaps.push(`${ruleKey}: ${row.Rule}`);
881
+ if (status === "CONFLICT")
882
+ conflicts.push(`${ruleKey}: ${row.Rule}`);
883
+ if (status !== "COVERED")
884
+ findings.push(`${ruleKey} coverage status is ${status}`);
885
+ rules.push({
886
+ ruleKey,
887
+ priority: /^(?:P0|P1|P2)$/.test(priority) ? priority : "P2",
888
+ source: row.Source || "unavailable",
889
+ endpointField: row["Endpoint/Field"] || "unavailable",
890
+ dimension: row.Dimension.trim().toLowerCase() || "other",
891
+ rule: row.Rule || "unavailable",
892
+ requiredTestPoints,
893
+ caseIds: rowCaseIds,
894
+ declaredStatus: normalizedDeclaredStatus,
895
+ coveredTestPoints,
896
+ missingTestPoints,
897
+ status,
898
+ });
899
+ }
900
+ const extractedOpenApiRules = await extractBackendTestOpenApiRules(input);
901
+ const knownOperations = orderedUnique(extractedOpenApiRules.map((rule) => operationFromEndpointField(rule.endpointField)).filter((value) => Boolean(value)));
902
+ for (const affectedOperation of coverageScope.scope.affectedOperations) {
903
+ if (knownOperations.length > 0 && !knownOperations.includes(affectedOperation))
904
+ findings.push(`Coverage Scope Affected Operation is absent from bound OpenAPI evidence: ${affectedOperation}`);
905
+ }
906
+ const requiredOpenApiRules = extractedOpenApiRules.filter((rule) => {
907
+ const operation = operationFromEndpointField(rule.endpointField);
908
+ return Boolean(operation && coverageScope.scope.affectedOperations.includes(operation));
909
+ });
910
+ for (const extracted of requiredOpenApiRules) {
911
+ if (seenRules.has(extracted.ruleKey))
912
+ continue;
913
+ findings.push(`Coverage Matrix is missing documented in-scope OpenAPI rule: ${extracted.ruleKey} (${extracted.source})`);
914
+ seenRules.add(extracted.ruleKey);
915
+ rules.push({
916
+ ...extracted,
917
+ caseIds: [],
918
+ declaredStatus: "PARTIAL",
919
+ coveredTestPoints: [],
920
+ missingTestPoints: extracted.requiredTestPoints,
921
+ status: "PARTIAL",
922
+ });
923
+ }
924
+ for (const affectedRuleKey of coverageScope.scope.affectedRuleKeys) {
925
+ if (!seenRules.has(affectedRuleKey))
926
+ findings.push(`Coverage Scope Affected Rule Key is absent from Coverage Matrix: ${affectedRuleKey}`);
927
+ }
928
+ const nonCrossCuttingOwners = new Map();
929
+ for (const testCase of cases) {
930
+ if (testCase.ruleKeys.length === 0)
931
+ findings.push(`${testCase.caseId} missing section or entries: 覆盖规则`);
932
+ if (testCase.testPoints.length === 0)
933
+ findings.push(`${testCase.caseId} missing section or entries: 测试点`);
934
+ if (testCase.scenarioTypes.length === 0)
935
+ findings.push(`${testCase.caseId} missing section or entries: 场景类型`);
936
+ if (isEvidenceOnlyMarkdownCase(testCase)) {
937
+ // Evidence-only meta cases intentionally declare 脚本/primary symbol as 无 and only keep assertion/cross-cutting TPs.
938
+ }
939
+ else if (testCase.declaredPrimarySymbols.length !== 1) {
940
+ findings.push(`${testCase.caseId} must declare exactly one primary symbol`);
941
+ }
942
+ if (testCase.unclassifiedTestPoints.length > 0)
943
+ findings.push(`${testCase.caseId} has unclassified Test Points: ${testCase.unclassifiedTestPoints.join(", ")}`);
944
+ if (testCase.duplicateBindingTestPoints.length > 0)
945
+ findings.push(`${testCase.caseId} has duplicate Test Point bindings: ${testCase.duplicateBindingTestPoints.join(", ")}`);
946
+ const extraBindings = testCase.testPointBindings.filter((binding) => !testCase.testPoints.includes(binding.testPoint));
947
+ if (extraBindings.length > 0)
948
+ findings.push(`${testCase.caseId} binds undeclared Test Points: ${orderedUnique(extraBindings.map((binding) => binding.testPoint)).join(", ")}`);
949
+ for (const binding of testCase.testPointBindings.filter((item) => item.mode !== "cross-cutting")) {
950
+ const owners = nonCrossCuttingOwners.get(binding.testPoint) ?? [];
951
+ owners.push(testCase.caseId);
952
+ nonCrossCuttingOwners.set(binding.testPoint, owners);
953
+ }
954
+ for (const ruleKey of testCase.ruleKeys)
955
+ if (!seenRules.has(ruleKey))
956
+ findings.push(`${testCase.caseId} references unknown Rule Key: ${ruleKey}`);
957
+ }
958
+ for (const [testPoint, owners] of nonCrossCuttingOwners) {
959
+ const uniqueOwners = orderedUnique(owners);
960
+ if (uniqueOwners.length > 1)
961
+ findings.push(`${testPoint} is bound as a non-cross-cutting Test Point by multiple Cases: ${uniqueOwners.join(", ")}`);
962
+ }
963
+ const generatedMarkdown = [readme, ...cases.map((item) => item.body)].join("\n\n");
964
+ const sourceCoverageFacts = await collectSourceCoverageFacts({
965
+ workspaceRoot: input.workspaceRoot,
966
+ sourceBinding: input.sourceBinding,
967
+ generatedMarkdown,
968
+ });
969
+ const residualSourceFacts = sourceCoverageFacts.filter((fact) => !fact.covered);
970
+ for (const fact of residualSourceFacts) {
971
+ findings.push(`source coverage is missing ${fact.kind} ${fact.key} from ${fact.sourcePath}`);
972
+ }
973
+ const sourceRequirementIds = sourceCoverageFacts
974
+ .filter((fact) => fact.kind === "requirement-id")
975
+ .map((fact) => fact.key);
976
+ const explicitAc = orderedUnique([
977
+ ...input.sourceBinding.requirementIds.filter((id) => id.startsWith("AC-")),
978
+ ...sourceRequirementIds.filter((id) => id.startsWith("AC-")),
979
+ ]);
980
+ const coveredAc = new Set(cases.flatMap((item) => item.acIds));
981
+ for (const acId of explicitAc) {
982
+ if (!coveredAc.has(acId))
983
+ findings.push(`explicit source AC is missing from Markdown Cases: ${acId}`);
984
+ }
985
+ // Plan C: deterministic query/filter axis partitions (fail-closed on missing slots).
986
+ const parsedPartitions = parseScenarioPartitions(readme);
987
+ findings.push(...parsedPartitions.issues);
988
+ const declaredPointUniverse = new Set(cases.flatMap((item) => item.testPoints));
989
+ const partitionAssessment = assessScenarioPartitionCoverage(parsedPartitions.partitions, declaredPointUniverse);
990
+ findings.push(...partitionAssessment.findings);
991
+ const [enumValueCount, coveredEnumValueCount] = dimensionCounts(rules, /enum/);
992
+ const [boundaryPointCount, coveredBoundaryPointCount] = dimensionCounts(rules, /boundary|length|numeric/);
993
+ const [formatClassCount, coveredFormatClassCount] = dimensionCounts(rules, /format|pattern|charset/);
994
+ const [businessStateCount, coveredBusinessStateCount] = dimensionCounts(rules, /business-state|state-transition|uniqueness|lifecycle/);
995
+ const invalidPointPattern = /(?:UNKNOWN|CASE-VARIANT|WHITESPACE|EMPTY|NULL|WRONG-TYPE|MIN-1|MAX-PLUS-1|UPPERCASE|PUNCTUATION|SLASH|EMOJI|CONTROL)$/;
996
+ const invalidEquivalenceClassCount = rules.reduce((sum, rule) => sum + rule.requiredTestPoints.filter((point) => invalidPointPattern.test(point)).length, 0);
997
+ const coveredInvalidEquivalenceClassCount = rules.reduce((sum, rule) => sum + rule.coveredTestPoints.filter((point) => invalidPointPattern.test(point)).length, 0);
998
+ const facts = backendTestCaseCoverageFactsSchema.parse({
999
+ schemaId: "backend-test-case-coverage-facts-v4",
1000
+ schemaVersion: 4,
1001
+ taskId: input.sourceBinding.taskId,
1002
+ status: findings.length === 0 ? "PASS" : "FAIL",
1003
+ sourceBinding: input.sourceBinding,
1004
+ inputFiles: await inputFileFacts(input.workspaceRoot, [
1005
+ ...((await exists(readmePath)) ? [readmePath] : []),
1006
+ ...(await markdownModuleFiles(input.workspaceRoot)),
1007
+ ...(await Promise.all(input.sourceBinding.referencePaths.map(async (referencePath) => {
1008
+ const physicalPath = resolveBoundSourcePath(input.workspaceRoot, input.sourceBinding.taskId, referencePath);
1009
+ return (await exists(physicalPath)) ? physicalPath : "";
1010
+ }))).filter(Boolean),
1011
+ ]),
1012
+ coverageScope: coverageScope.scope,
1013
+ sourceCoverage: {
1014
+ sourceFactCount: sourceCoverageFacts.length,
1015
+ coveredSourceFactCount: sourceCoverageFacts.length - residualSourceFacts.length,
1016
+ residualSourceFactCount: residualSourceFacts.length,
1017
+ facts: sourceCoverageFacts,
1018
+ residual: residualSourceFacts,
1019
+ },
1020
+ summary: {
1021
+ explicitAcCount: explicitAc.length,
1022
+ coveredAcCount: explicitAc.filter((id) => coveredAc.has(id)).length,
1023
+ ruleCount: rules.length,
1024
+ coveredRuleCount: rules.filter((rule) => rule.status === "COVERED").length,
1025
+ testPointCount: rules.reduce((sum, rule) => sum + rule.requiredTestPoints.length, 0),
1026
+ coveredTestPointCount: rules.reduce((sum, rule) => sum + rule.coveredTestPoints.length, 0),
1027
+ enumValueCount,
1028
+ coveredEnumValueCount,
1029
+ invalidEquivalenceClassCount,
1030
+ coveredInvalidEquivalenceClassCount,
1031
+ boundaryPointCount,
1032
+ coveredBoundaryPointCount,
1033
+ formatClassCount,
1034
+ coveredFormatClassCount,
1035
+ businessStateCount,
1036
+ coveredBusinessStateCount,
1037
+ gapCount: evidenceGaps.length,
1038
+ conflictCount: conflicts.length,
1039
+ variantTestPointCount: cases.reduce((sum, item) => sum + item.testPointBindings.filter((binding) => binding.mode === "variant").length, 0),
1040
+ assertionTestPointCount: cases.reduce((sum, item) => sum + item.testPointBindings.filter((binding) => binding.mode === "assertion").length, 0),
1041
+ crossCuttingTestPointCount: cases.reduce((sum, item) => sum + item.testPointBindings.filter((binding) => binding.mode === "cross-cutting").length, 0),
1042
+ unclassifiedTestPointCount: cases.reduce((sum, item) => sum + item.unclassifiedTestPoints.length, 0),
1043
+ duplicateBindingTestPointCount: cases.reduce((sum, item) => sum + item.duplicateBindingTestPoints.length, 0),
1044
+ },
1045
+ cases: cases.map(({ body: _body, ...item }) => item),
1046
+ rules,
1047
+ scenarioPartitions: partitionAssessment.facts,
1048
+ findings: orderedUnique(findings),
1049
+ evidenceGaps,
1050
+ conflicts,
1051
+ });
1052
+ const summary = facts.summary;
1053
+ const markdown = [
1054
+ "# Backend Test Case Coverage Analysis",
1055
+ "",
1056
+ "## Status",
1057
+ "",
1058
+ facts.status,
1059
+ "",
1060
+ "## Coverage Scope",
1061
+ "",
1062
+ `- Change Classification: ${facts.coverageScope.changeClassification}`,
1063
+ `- Coverage Policy: ${facts.coverageScope.coveragePolicy}`,
1064
+ `- Completeness Claim: ${facts.coverageScope.completenessClaim}`,
1065
+ `- Affected Operations: ${facts.coverageScope.affectedOperations.join(", ") || "Unavailable"}`,
1066
+ `- Affected Rule Keys: ${facts.coverageScope.affectedRuleKeys.join(", ") || "Unavailable"}`,
1067
+ `- Regression Floor: ${facts.coverageScope.regressionFloor.join(", ") || "Unavailable"}`,
1068
+ `- Scope Evidence: ${facts.coverageScope.scopeEvidence.join(", ") || "Unavailable"}`,
1069
+ ...(facts.coverageScope.completenessClaim === "affected-scope" ? ["- Coverage is scoped to affected operations; it is not whole-API completeness."] : ["- Coverage fully evaluates the declared affected operations; it is not whole-API completeness unless every operation is explicitly listed."]),
1070
+ "",
1071
+ "## Source Coverage Inventory",
1072
+ "",
1073
+ `- Source facts: ${facts.sourceCoverage.sourceFactCount}`,
1074
+ `- Covered source facts: ${facts.sourceCoverage.coveredSourceFactCount}`,
1075
+ `- Residual source facts: ${facts.sourceCoverage.residualSourceFactCount}`,
1076
+ "",
1077
+ "| Kind | Key | Source Anchor | Source SHA-256 | Exact Quote | Status |",
1078
+ "|---|---|---|---|---|---|",
1079
+ ...facts.sourceCoverage.facts.map((fact) => `| ${fact.kind} | ${fact.key} | ${fact.sourcePath}:L${fact.lineStart} | ${fact.sourceSha256} | ${fact.quote.replaceAll("|", "\\|")} | ${fact.covered ? "COVERED" : "MISSING_AFTER_SYNC"} |`),
1080
+ "",
1081
+ "## Residual Missing Source Scenarios",
1082
+ "",
1083
+ ...(facts.sourceCoverage.residual.length > 0
1084
+ ? facts.sourceCoverage.residual.map((fact) => `- ${fact.kind} \`${fact.key}\` from \`${fact.sourcePath}:L${fact.lineStart}\` (sha256 \`${fact.sourceSha256}\`), quote: “${fact.quote}”: missing after the bounded Markdown synchronization round; no full-coverage claim is allowed.`)
1085
+ : ["- None"]),
1086
+ "",
1087
+ "## Coverage Summary",
1088
+ "",
1089
+ "| Dimension | Total | Covered |",
1090
+ "|---|---:|---:|",
1091
+ `| Product AC | ${summary.explicitAcCount} | ${summary.coveredAcCount} |`,
1092
+ `| Rules | ${summary.ruleCount} | ${summary.coveredRuleCount} |`,
1093
+ `| Required Test Points | ${summary.testPointCount} | ${summary.coveredTestPointCount} |`,
1094
+ `| Enum | ${summary.enumValueCount} | ${summary.coveredEnumValueCount} |`,
1095
+ `| Boundary | ${summary.boundaryPointCount} | ${summary.coveredBoundaryPointCount} |`,
1096
+ `| Format | ${summary.formatClassCount} | ${summary.coveredFormatClassCount} |`,
1097
+ `| Business State | ${summary.businessStateCount} | ${summary.coveredBusinessStateCount} |`,
1098
+ `| Variant bindings | ${summary.variantTestPointCount} | ${summary.variantTestPointCount} |`,
1099
+ `| Assertion bindings | ${summary.assertionTestPointCount} | ${summary.assertionTestPointCount} |`,
1100
+ `| Cross-cutting bindings | ${summary.crossCuttingTestPointCount} | ${summary.crossCuttingTestPointCount} |`,
1101
+ `| Unclassified bindings | ${summary.unclassifiedTestPointCount} | 0 |`,
1102
+ `| Duplicate bindings | ${summary.duplicateBindingTestPointCount} | 0 |`,
1103
+ "",
1104
+ "## Product Requirement Coverage",
1105
+ "",
1106
+ ...renderCoverageTable(rules.filter((rule) => rule.priority === "P0" || rule.ruleKey.startsWith("AC-"))),
1107
+ "",
1108
+ "## API Operation and Field Rule Coverage",
1109
+ "",
1110
+ ...renderCoverageTable(rules.filter((rule) => rule.priority !== "P0")),
1111
+ "",
1112
+ "## Business State Coverage",
1113
+ "",
1114
+ ...renderCoverageTable(rules.filter((rule) => /business-state|state-transition|uniqueness|lifecycle/.test(rule.dimension))),
1115
+ "",
1116
+ "## Enum Coverage",
1117
+ "",
1118
+ ...renderCoverageTable(rules.filter((rule) => /enum/.test(rule.dimension))),
1119
+ "",
1120
+ "## Boundary and Format Coverage",
1121
+ "",
1122
+ ...renderCoverageTable(rules.filter((rule) => /boundary|length|numeric|format|pattern|charset/.test(rule.dimension))),
1123
+ "",
1124
+ "## Uncovered/Partial Rules",
1125
+ "",
1126
+ ...(rules.filter((rule) => rule.status !== "COVERED").map((rule) => `- ${rule.ruleKey}: ${rule.status}; missing=${rule.missingTestPoints.join(", ") || "none"}`)),
1127
+ ...(rules.every((rule) => rule.status === "COVERED") ? ["- None"] : []),
1128
+ "",
1129
+ "## Conflicts and Evidence Gaps",
1130
+ "",
1131
+ ...([...evidenceGaps, ...conflicts].length ? [...evidenceGaps, ...conflicts].map((item) => `- ${item}`) : ["- None"]),
1132
+ "",
1133
+ "## Matrix/Case Consistency Findings",
1134
+ "",
1135
+ ...(facts.findings.length ? facts.findings.map((item) => `- ${item}`) : ["- None"]),
1136
+ "",
1137
+ ].join("\n");
1138
+ return { facts, markdown };
1139
+ }
1140
+ function normalizeModuleStem(markdownPath) {
1141
+ return path.posix.basename(markdownPath).replace(/\.md$/i, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
1142
+ }
1143
+ function expectedScript(markdownPath, layout) {
1144
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
1145
+ return `${resolved.scriptDir}/test_${normalizeModuleStem(markdownPath)}.py`;
1146
+ }
1147
+ function symbolCaseId(symbol) {
1148
+ const match = symbol.match(/^test_(BE(?:_[A-Z0-9]+)+?_\d{2,3})(?:_|$)/i);
1149
+ return match ? canonicalCaseId(match[1].replaceAll("_", "-")) : undefined;
1150
+ }
1151
+ function pythonPayloadShapes(source) {
1152
+ const program = String.raw `
1153
+ import ast,json,sys
1154
+ source=sys.stdin.buffer.read().decode('utf-8')
1155
+ tree=ast.parse(source)
1156
+ GLOBAL_PAYLOADS={}
1157
+ GLOBAL_VALUES={}
1158
+ for top in tree.body:
1159
+ if isinstance(top,(ast.Assign,ast.AnnAssign)):
1160
+ names=([target.id for target in top.targets if isinstance(target,ast.Name)] if isinstance(top,ast.Assign) else ([top.target.id] if isinstance(top.target,ast.Name) else []))
1161
+ if top.value is None: continue
1162
+ candidates=[]
1163
+ for node in ast.walk(top.value):
1164
+ if isinstance(node,ast.Call) and isinstance(node.func,ast.Attribute) and node.func.attr == 'param' and node.args and isinstance(node.args[0],ast.Dict): candidates.append(node.args[0])
1165
+ if isinstance(top.value,ast.Dict) and any(name.lower() in {'payload','body','request_body','request_json','dto'} for name in names): candidates.append(top.value)
1166
+ for name in names:
1167
+ GLOBAL_PAYLOADS[name]=candidates
1168
+ GLOBAL_VALUES[name]=top.value
1169
+ FUNCTION_PAYLOADS={}
1170
+ FUNCTION_TRANSPORT_SHAPES={}
1171
+ TRANSPORT_PAYLOAD_PARAMS={}
1172
+ for top in ast.walk(tree):
1173
+ if isinstance(top,(ast.FunctionDef,ast.AsyncFunctionDef)) and not top.name.startswith('test_'):
1174
+ candidates=[]
1175
+ assigned={}
1176
+ mutated={}
1177
+ returned_names=set()
1178
+ positional=[arg.arg for arg in [*top.args.posonlyargs,*top.args.args]]
1179
+ call_offset=1 if positional and positional[0] in {'self','cls'} else 0
1180
+ transport=[]
1181
+ for node in ast.walk(top):
1182
+ if isinstance(node,(ast.Assign,ast.AnnAssign)):
1183
+ targets=node.targets if isinstance(node,ast.Assign) else [node.target]
1184
+ if node.value is None: continue
1185
+ for target in targets:
1186
+ if isinstance(target,ast.Name): assigned[target.id]=node.value
1187
+ for target in targets:
1188
+ if isinstance(target,ast.Subscript) and isinstance(target.value,ast.Name) and isinstance(target.slice,ast.Constant) and isinstance(target.slice.value,str):
1189
+ mutated.setdefault(target.value.id,[]).append((target.slice.value,node.value))
1190
+ if isinstance(node,ast.Return):
1191
+ if isinstance(node.value,ast.Dict): candidates.append(node.value)
1192
+ elif isinstance(node.value,ast.Name) and node.value.id in assigned:
1193
+ returned=assigned[node.value.id]
1194
+ if isinstance(returned,ast.Dict): candidates.append(returned)
1195
+ elif isinstance(returned,ast.Call) and isinstance(returned.func,ast.Name) and returned.func.id in FUNCTION_PAYLOADS:
1196
+ candidates.extend(FUNCTION_PAYLOADS[returned.func.id])
1197
+ returned_names.add(node.value.id)
1198
+ if isinstance(node,ast.Call):
1199
+ for keyword in node.keywords:
1200
+ if keyword.arg in {'payload','body','request_body','request_json','dto','json','data','json_body'} and isinstance(keyword.value,ast.Name) and keyword.value.id in positional:
1201
+ transport.append((keyword.value.id,positional.index(keyword.value.id)-call_offset))
1202
+ if isinstance(node.func,ast.Attribute) and node.func.attr.lower() in {'post','put','patch'} and len(node.args) >= 2:
1203
+ candidate=node.args[1]
1204
+ if isinstance(candidate,ast.Name) and candidate.id in positional:
1205
+ transport.append((candidate.id,positional.index(candidate.id)-call_offset))
1206
+ callee=None
1207
+ if isinstance(node.func,ast.Name): callee=node.func.id
1208
+ elif isinstance(node.func,ast.Attribute): callee=node.func.attr
1209
+ for _nested_name,nested_position in TRANSPORT_PAYLOAD_PARAMS.get(callee,[]):
1210
+ matched=node.args[nested_position] if nested_position >= 0 and nested_position < len(node.args) else None
1211
+ if isinstance(matched,ast.Name) and matched.id in positional:
1212
+ transport.append((matched.id,positional.index(matched.id)-call_offset))
1213
+ for returned_name in returned_names:
1214
+ entries=mutated.get(returned_name,[])
1215
+ if entries:
1216
+ candidates.append(ast.Dict(keys=[ast.Constant(key) for key,_value in entries],values=[value for _key,value in entries]))
1217
+ transport_shapes=[]
1218
+ for node in ast.walk(top):
1219
+ if not isinstance(node,ast.Call): continue
1220
+ for keyword in node.keywords:
1221
+ if keyword.arg in {'payload','body','request_body','request_json','dto','json','data','json_body'} and isinstance(keyword.value,ast.Name) and keyword.value.id in assigned:
1222
+ transport_shapes.append(assigned[keyword.value.id])
1223
+ if candidates: FUNCTION_PAYLOADS[top.name]=candidates
1224
+ if transport_shapes: FUNCTION_TRANSPORT_SHAPES[top.name]=transport_shapes
1225
+ if transport: TRANSPORT_PAYLOAD_PARAMS[top.name]=list(dict.fromkeys(transport))
1226
+ # Propagate transport payload positions through local wrapper chains after all
1227
+ # direct transport methods/functions are known. ast.walk may visit module-level
1228
+ # wrappers before class methods, so one-pass discovery is order-sensitive.
1229
+ for _round in range(6):
1230
+ changed=False
1231
+ for top in ast.walk(tree):
1232
+ if not isinstance(top,(ast.FunctionDef,ast.AsyncFunctionDef)) or top.name.startswith('test_'): continue
1233
+ positional=[arg.arg for arg in [*top.args.posonlyargs,*top.args.args]]
1234
+ call_offset=1 if positional and positional[0] in {'self','cls'} else 0
1235
+ current=list(TRANSPORT_PAYLOAD_PARAMS.get(top.name,[]))
1236
+ for node in ast.walk(top):
1237
+ if not isinstance(node,ast.Call): continue
1238
+ callee=node.func.id if isinstance(node.func,ast.Name) else (node.func.attr if isinstance(node.func,ast.Attribute) else None)
1239
+ for _nested_name,nested_position in TRANSPORT_PAYLOAD_PARAMS.get(callee,[]):
1240
+ matched=node.args[nested_position] if nested_position >= 0 and nested_position < len(node.args) else None
1241
+ if isinstance(matched,ast.Name) and matched.id in positional:
1242
+ item=(matched.id,positional.index(matched.id)-call_offset)
1243
+ if item not in current:
1244
+ current.append(item); changed=True
1245
+ if current: TRANSPORT_PAYLOAD_PARAMS[top.name]=current
1246
+ if not changed: break
1247
+
1248
+ # A local row factory may return pytest.param(_payload_builder(...), ..., id=tp).
1249
+ # Treat its first positional payload builder as shape evidence while keeping the
1250
+ # writer contract free to require direct pytest.param rows for scenario checks.
1251
+ for top in ast.walk(tree):
1252
+ if not isinstance(top,(ast.FunctionDef,ast.AsyncFunctionDef)) or top.name.startswith('test_'): continue
1253
+ for node in ast.walk(top):
1254
+ if not isinstance(node,ast.Return) or not isinstance(node.value,ast.Call): continue
1255
+ returned=node.value
1256
+ if not isinstance(returned.func,ast.Attribute) or returned.func.attr != 'param' or not returned.args: continue
1257
+ payload=returned.args[0]
1258
+ if isinstance(payload,ast.Call) and isinstance(payload.func,ast.Name) and payload.func.id in FUNCTION_PAYLOADS:
1259
+ FUNCTION_PAYLOADS[top.name]=list(FUNCTION_PAYLOADS[payload.func.id])
1260
+ # Resolve module-level parameter collections after local payload/row factories
1261
+ # are known (for example CASE_ROWS = [_post_case(...) , ...]).
1262
+ for top in tree.body:
1263
+ if not isinstance(top,ast.Assign): continue
1264
+ names=[target.id for target in top.targets if isinstance(target,ast.Name)]
1265
+ resolved=[]
1266
+ for node in ast.walk(top.value):
1267
+ if isinstance(node,ast.Call) and isinstance(node.func,ast.Name) and node.func.id in FUNCTION_PAYLOADS:
1268
+ resolved.extend(FUNCTION_PAYLOADS[node.func.id])
1269
+ for name in names:
1270
+ if resolved: GLOBAL_PAYLOADS[name]=list(GLOBAL_PAYLOADS.get(name,[]))+resolved
1271
+
1272
+ def literal(node):
1273
+ if isinstance(node, ast.Constant) and isinstance(node.value, (str,int,float,bool,type(None))):
1274
+ return repr(node.value)
1275
+ return None
1276
+
1277
+ def collect_dict(node, prefix, paths, values):
1278
+ if not isinstance(node, ast.Dict): return
1279
+ for key,value in zip(node.keys,node.values):
1280
+ if not isinstance(key, ast.Constant) or not isinstance(key.value,str): continue
1281
+ # Ignore writer helper/meta keys that are not DTO request fields.
1282
+ if key.value in {'expect','expected','expected_status','expected_code','echo_title','echo_content','echo_status'} or key.value.startswith('echo_') or key.value.startswith('expected_'):
1283
+ continue
1284
+ current = key.value if not prefix else prefix + '.' + key.value
1285
+ paths.add(current)
1286
+ lv=literal(value)
1287
+ if lv is not None: values.setdefault(current,set()).add(lv)
1288
+ if isinstance(value, ast.Dict): collect_dict(value,current,paths,values)
1289
+ elif isinstance(value,(ast.List,ast.Tuple)):
1290
+ for item in value.elts:
1291
+ if isinstance(item,ast.Dict): collect_dict(item,current,paths,values)
1292
+
1293
+ PAYLOAD_NAMES={'payload','body','request_body','request_json','dto','json','data','json_body'}
1294
+
1295
+ def shape_only(node):
1296
+ if isinstance(node,ast.Dict):
1297
+ values=[]
1298
+ for value in node.values:
1299
+ if isinstance(value,(ast.Dict,ast.List,ast.Tuple)): values.append(shape_only(value))
1300
+ else: values.append(ast.Name(id='_PAYLOAD_VALUE_',ctx=ast.Load()))
1301
+ return ast.Dict(keys=node.keys,values=values)
1302
+ if isinstance(node,(ast.List,ast.Tuple)):
1303
+ items=[shape_only(item) if isinstance(item,(ast.Dict,ast.List,ast.Tuple)) else ast.Name(id='_PAYLOAD_VALUE_',ctx=ast.Load()) for item in node.elts]
1304
+ return ast.List(elts=items,ctx=ast.Load()) if isinstance(node,ast.List) else ast.Tuple(elts=items,ctx=ast.Load())
1305
+ return node
1306
+
1307
+ def payload_expr_nodes(expr, assigned, parameters, seen=None):
1308
+ seen=set() if seen is None else seen
1309
+ if isinstance(expr,ast.Dict): return [expr]
1310
+ if isinstance(expr,ast.Name):
1311
+ if expr.id in seen: return []
1312
+ seen.add(expr.id)
1313
+ if expr.id in assigned: return payload_expr_nodes(assigned[expr.id],assigned,parameters,seen)
1314
+ if expr.id in parameters:
1315
+ result=[]
1316
+ for value in parameters[expr.id]: result.extend(payload_expr_nodes(value,assigned,parameters,set(seen)))
1317
+ return result
1318
+ return list(GLOBAL_PAYLOADS.get(expr.id,[]))
1319
+ if isinstance(expr,ast.Subscript) and isinstance(expr.value,ast.Name) and isinstance(expr.slice,ast.Constant) and isinstance(expr.slice.value,str):
1320
+ result=[]
1321
+ for candidate in parameters.get(expr.value.id,[]):
1322
+ if not isinstance(candidate,ast.Dict): continue
1323
+ for key,value in zip(candidate.keys,candidate.values):
1324
+ if isinstance(key,ast.Constant) and key.value == expr.slice.value:
1325
+ result.extend(shape_only(item) for item in payload_expr_nodes(value,assigned,parameters,set(seen)))
1326
+ return result
1327
+ if isinstance(expr,ast.Call):
1328
+ result=[]
1329
+ if isinstance(expr.func,ast.Name) and expr.func.id in FUNCTION_PAYLOADS:
1330
+ result.extend(FUNCTION_PAYLOADS[expr.func.id])
1331
+ override_keys=[]; override_values=[]
1332
+ for keyword in expr.keywords:
1333
+ if keyword.arg:
1334
+ override_keys.append(ast.Constant(keyword.arg)); override_values.append(keyword.value)
1335
+ if override_keys: result.append(ast.Dict(keys=override_keys,values=override_values))
1336
+ # A transport body may be assembled by a local combinator such as
1337
+ # _apply_patch(_base_body(...), patch). Because this traversal starts
1338
+ # only from an identified transport-body position, recursively merging
1339
+ # argument shapes is narrower than scanning arbitrary helper calls.
1340
+ for argument in expr.args:
1341
+ result.extend(payload_expr_nodes(argument,assigned,parameters,set(seen)))
1342
+ for keyword in expr.keywords:
1343
+ result.extend(payload_expr_nodes(keyword.value,assigned,parameters,set(seen)))
1344
+ return result
1345
+ return []
1346
+
1347
+ def decorator_parameter_values(dec):
1348
+ result={}
1349
+ if not isinstance(dec,ast.Call) or not isinstance(dec.func,ast.Attribute) or dec.func.attr != 'parametrize' or len(dec.args) < 2: return result
1350
+ if not isinstance(dec.args[0],ast.Constant) or not isinstance(dec.args[0].value,str): return result
1351
+ names=[item.strip() for item in dec.args[0].value.split(',')]
1352
+ rows=dec.args[1]
1353
+ if isinstance(rows,ast.Name) and rows.id in GLOBAL_VALUES: rows=GLOBAL_VALUES[rows.id]
1354
+ for node in ast.walk(rows):
1355
+ if not isinstance(node,ast.Call) or not isinstance(node.func,ast.Attribute) or node.func.attr != 'param': continue
1356
+ for index,name in enumerate(names):
1357
+ if index < len(node.args): result.setdefault(name,[]).append(node.args[index])
1358
+ return result
1359
+
1360
+ def decorator_payload_nodes(parameters,assigned):
1361
+ result=[]
1362
+ for name,values in parameters.items():
1363
+ if name.lower() not in PAYLOAD_NAMES: continue
1364
+ for value in values: result.extend(payload_expr_nodes(value,assigned,parameters))
1365
+ return result
1366
+
1367
+ def fallback_payload_nodes(fn):
1368
+ result=[]
1369
+ assigned={}
1370
+ for node in ast.walk(fn):
1371
+ if isinstance(node,(ast.Assign,ast.AnnAssign)):
1372
+ if node.value is None: continue
1373
+ targets=node.targets if isinstance(node,ast.Assign) else [node.target]
1374
+ for target in targets:
1375
+ if isinstance(target,ast.Name): assigned[target.id]=node.value
1376
+ parameters={}
1377
+ for dec in fn.decorator_list:
1378
+ for name,values in decorator_parameter_values(dec).items(): parameters.setdefault(name,[]).extend(values)
1379
+ for node in ast.walk(fn):
1380
+ if not isinstance(node,ast.Call): continue
1381
+ callee=None
1382
+ if isinstance(node.func,ast.Name): callee=node.func.id
1383
+ elif isinstance(node.func,ast.Attribute): callee=node.func.attr
1384
+ result.extend(FUNCTION_TRANSPORT_SHAPES.get(callee,[]))
1385
+ # Wrapper payloads are fallback evidence: they are useful when the Case
1386
+ # contract requires a body, but must not contaminate GET/DELETE targets
1387
+ # merely because setup creates a resource first.
1388
+ for parameter_name,position in TRANSPORT_PAYLOAD_PARAMS.get(callee,[]):
1389
+ matched=next((keyword.value for keyword in node.keywords if keyword.arg == parameter_name),None)
1390
+ if matched is None and position >= 0 and position < len(node.args): matched=node.args[position]
1391
+ if matched is not None: result.extend(payload_expr_nodes(matched,assigned,parameters))
1392
+ return result
1393
+
1394
+ def is_direct_transport_call(node):
1395
+ if not isinstance(node,ast.Call) or not isinstance(node.func,ast.Attribute): return False
1396
+ if node.func.attr.lower() not in {'get','post','put','patch','delete','request'}: return False
1397
+ receiver_tokens=[]
1398
+ current=node.func.value
1399
+ while isinstance(current,ast.Attribute):
1400
+ receiver_tokens.append(current.attr.lower())
1401
+ current=current.value
1402
+ if isinstance(current,ast.Name): receiver_tokens.append(current.id.lower())
1403
+ return any(token in {'requests','httpx','client','session','api_client','http_client'} or token.endswith('_client') for token in receiver_tokens)
1404
+
1405
+ def payload_nodes(fn):
1406
+ result=[]
1407
+ assigned={}
1408
+ for node in ast.walk(fn):
1409
+ if isinstance(node,(ast.Assign,ast.AnnAssign)):
1410
+ if node.value is None: continue
1411
+ targets=node.targets if isinstance(node,ast.Assign) else [node.target]
1412
+ for target in targets:
1413
+ if isinstance(target,ast.Name): assigned[target.id]=node.value
1414
+ parameters={}
1415
+ for dec in fn.decorator_list:
1416
+ for name,values in decorator_parameter_values(dec).items(): parameters.setdefault(name,[]).extend(values)
1417
+ result.extend(decorator_payload_nodes(parameters,assigned))
1418
+ for dec in fn.decorator_list:
1419
+ for node in ast.walk(dec):
1420
+ if isinstance(node,ast.Call) and isinstance(node.func,ast.Name) and node.func.id in FUNCTION_PAYLOADS:
1421
+ result.extend(payload_expr_nodes(node,assigned,parameters))
1422
+ elif isinstance(node,ast.Name) and node.id in GLOBAL_PAYLOADS:
1423
+ result.extend(GLOBAL_PAYLOADS[node.id])
1424
+ # Observe local payload/body variables only when they flow into a direct
1425
+ # HTTP transport call. Arbitrary setup variables are not target requests.
1426
+ for node in ast.walk(fn):
1427
+ if not is_direct_transport_call(node): continue
1428
+ for keyword in node.keywords:
1429
+ if keyword.arg in PAYLOAD_NAMES: result.extend(payload_expr_nodes(keyword.value,assigned,parameters))
1430
+ return result
1431
+
1432
+ out={}
1433
+ for fn in ast.walk(tree):
1434
+ if isinstance(fn,(ast.FunctionDef,ast.AsyncFunctionDef)) and fn.name.startswith('test_'):
1435
+ paths=set(); values={}
1436
+ fallback_paths=set(); fallback_values={}
1437
+ for node in payload_nodes(fn): collect_dict(node,'',paths,values)
1438
+ for node in fallback_payload_nodes(fn): collect_dict(node,'',fallback_paths,fallback_values)
1439
+ out[fn.name]={
1440
+ 'paths':sorted(paths),
1441
+ 'values':{key:sorted(items) for key,items in sorted(values.items())},
1442
+ 'fallbackPaths':sorted(fallback_paths),
1443
+ 'fallbackValues':{key:sorted(items) for key,items in sorted(fallback_values.items())},
1444
+ }
1445
+ print(json.dumps(out,ensure_ascii=True))
1446
+ `;
1447
+ // Pass bytes and decode stdin explicitly as UTF-8 in Python. Clean-env
1448
+ // Windows Python may expose stdin as GBK; decoding generated Chinese/emoji
1449
+ // source through that locale can create surrogateescape code points and make
1450
+ // ast.parse fail with "surrogates not allowed".
1451
+ const result = spawnSync("python", ["-c", program], { input: Buffer.from(source, "utf8"), encoding: "utf8", maxBuffer: 4 * 1024 * 1024, windowsHide: true });
1452
+ if (result.status !== 0 || !result.stdout.trim())
1453
+ return new Map();
1454
+ try {
1455
+ const parsed = JSON.parse(result.stdout);
1456
+ return new Map(Object.entries(parsed));
1457
+ }
1458
+ catch {
1459
+ return new Map();
1460
+ }
1461
+ }
1462
+ function metadataCaseIds(region) {
1463
+ return orderedUnique([...region.matchAll(/^\s*Case-ID\s*:\s*(BE-[A-Z0-9]+(?:-[A-Z0-9]+)*)\s*$/gmi)]
1464
+ .map((match) => canonicalCaseId(match[1])));
1465
+ }
1466
+ function metadataTestPoints(region, label) {
1467
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1468
+ const match = new RegExp(`^\\s*${escaped}\\s*:\\s*(.*)$`, "mi").exec(region);
1469
+ return orderedUnique(match?.[1]?.match(/\bTP-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g) ?? []);
1470
+ }
1471
+ function pytestFunctionRegionStart(source, functionIndex) {
1472
+ let regionStart = source.lastIndexOf("\n", functionIndex - 1) + 1;
1473
+ let cursor = regionStart;
1474
+ while (cursor > 0) {
1475
+ const previousEnd = cursor - 1;
1476
+ const previousStart = source.lastIndexOf("\n", previousEnd - 1) + 1;
1477
+ const previous = source.slice(previousStart, previousEnd).replace(/\r$/, "");
1478
+ if (!previous.trim())
1479
+ break;
1480
+ regionStart = previousStart;
1481
+ cursor = previousStart;
1482
+ }
1483
+ return regionStart;
1484
+ }
1485
+ function pytestFunctionBodyEnd(source, signatureEnd, functionIndent) {
1486
+ let cursor = source.indexOf("\n", signatureEnd);
1487
+ if (cursor < 0)
1488
+ return source.length;
1489
+ cursor += 1;
1490
+ const baseIndent = functionIndent.replaceAll("\t", " ").length;
1491
+ while (cursor < source.length) {
1492
+ const nextLine = source.indexOf("\n", cursor);
1493
+ const lineEnd = nextLine < 0 ? source.length : nextLine;
1494
+ const line = source.slice(cursor, lineEnd).replace(/\r$/, "");
1495
+ if (line.trim()) {
1496
+ const indentation = line.match(/^[ \t]*/)?.[0]?.replaceAll("\t", " ").length ?? 0;
1497
+ if (indentation <= baseIndent)
1498
+ return cursor;
1499
+ }
1500
+ if (nextLine < 0)
1501
+ return source.length;
1502
+ cursor = nextLine + 1;
1503
+ }
1504
+ return source.length;
1505
+ }
1506
+ function pytestParameterCollections(source) {
1507
+ const collections = new Map();
1508
+ for (const match of source.matchAll(/^(_?[A-Z][A-Z0-9_]*)\s*=\s*\[(?![^\r\n]*\])([\s\S]*?)^\]/gm)) {
1509
+ const explicitIds = [...match[2].matchAll(/\bid\s*=\s*["'](TP-[A-Z0-9-]+)["']/g)].map((item) => item[1]);
1510
+ const factoryIds = explicitIds.length === 0
1511
+ ? [...match[2].matchAll(/["'](TP-[A-Z0-9-]+)["']/g)].map((item) => item[1])
1512
+ : [];
1513
+ collections.set(match[1], orderedUnique([...explicitIds, ...factoryIds]));
1514
+ }
1515
+ return collections;
1516
+ }
1517
+ function pytestSymbols(script, source) {
1518
+ const matches = [...source.matchAll(/^([ \t]*)(?:async\s+)?def\s+(test_[A-Za-z0-9_]+)\s*\([^)]*\)\s*(?:->\s*[^:\r\n]+)?\s*:/gm)];
1519
+ const regionStarts = matches.map((match) => pytestFunctionRegionStart(source, match.index));
1520
+ const parameterCollections = pytestParameterCollections(source);
1521
+ const payloadShapes = pythonPayloadShapes(source);
1522
+ return matches.map((match, index) => {
1523
+ const regionStart = regionStarts[index];
1524
+ const regionEnd = pytestFunctionBodyEnd(source, match.index + match[0].length, match[1]);
1525
+ const region = source.slice(match.index, regionEnd);
1526
+ const decorators = source.slice(regionStart, match.index);
1527
+ const ids = metadataCaseIds(region);
1528
+ const fromSymbol = symbolCaseId(match[2]);
1529
+ if (fromSymbol)
1530
+ ids.unshift(fromSymbol);
1531
+ const explicitParameterIds = [...decorators.matchAll(/\bid\s*=\s*["'](TP-[A-Z0-9-]+)["']/g)].map((item) => item[1]);
1532
+ const collectionParameterIds = [...parameterCollections.entries()].flatMap(([name, values]) => new RegExp(`\\b${name}\\b`).test(decorators) ? values : []);
1533
+ const factoryParameterIds = explicitParameterIds.length === 0 && collectionParameterIds.length === 0
1534
+ ? [...decorators.matchAll(/["'](TP-[A-Z0-9-]+)["']/g)].map((item) => item[1])
1535
+ : [];
1536
+ const parameterIds = orderedUnique([
1537
+ ...explicitParameterIds,
1538
+ ...collectionParameterIds,
1539
+ ...factoryParameterIds,
1540
+ ]);
1541
+ const assertionTestPoints = metadataTestPoints(region, "Assertion-Test-Points");
1542
+ const crossCuttingTestPoints = metadataTestPoints(region, "Cross-Cutting-Test-Points");
1543
+ const payloadShape = payloadShapes.get(match[2]) ?? { paths: [], values: {} };
1544
+ return {
1545
+ script,
1546
+ symbol: match[2],
1547
+ caseIds: orderedUnique(ids),
1548
+ testPoints: orderedUnique([...parameterIds, ...assertionTestPoints, ...crossCuttingTestPoints]),
1549
+ parameterIds,
1550
+ assertionTestPoints,
1551
+ crossCuttingTestPoints,
1552
+ payloadPaths: payloadShape.paths,
1553
+ payloadValues: payloadShape.values,
1554
+ fallbackPayloadPaths: payloadShape.fallbackPaths ?? [],
1555
+ fallbackPayloadValues: payloadShape.fallbackValues ?? {},
1556
+ };
1557
+ });
1558
+ }
1559
+ function executionSignature(testCase) {
1560
+ const operation = (testCase.body.match(/\b(?:GET|POST|PUT|PATCH|DELETE)\s+\/[^\s`|)]+/i)?.[0] ?? "operation-unavailable").toUpperCase();
1561
+ const inputPartition = [sectionBody(testCase.body, ["测试数据", "Test Data"]), testCase.scenarioTypes.join(" "), testCase.testPoints.join(" ")].join(" ");
1562
+ const precondition = sectionBody(testCase.body, ["前置条件", "Preconditions", "Precondition"]);
1563
+ const expectedOutcome = sectionBody(testCase.body, ["预期结果", "Expected Results", "Expected Result"]);
1564
+ const normalized = [operation, inputPartition, precondition, expectedOutcome]
1565
+ .map((value) => value.replaceAll(/\s+/g, " ").trim().toLowerCase())
1566
+ .join("\n");
1567
+ return createHash("sha256").update(normalized).digest("hex");
1568
+ }
1569
+ function assessPayloadContract(testCase, refs) {
1570
+ const requiredPaths = testCase.payloadContract.requiredPaths;
1571
+ const directObservedPaths = orderedUnique(refs.flatMap((item) => item.payloadPaths));
1572
+ const useFallback = requiredPaths.length > 0 && directObservedPaths.length === 0;
1573
+ const analyzedObservedPaths = useFallback
1574
+ ? orderedUnique(refs.flatMap((item) => item.fallbackPayloadPaths))
1575
+ : directObservedPaths;
1576
+ // A no-body target contract describes the target GET/DELETE request, not
1577
+ // setup POST/PUT calls in the same lifecycle test function.
1578
+ const observedPaths = testCase.payloadContract.noPayload ? [] : analyzedObservedPaths;
1579
+ const allowedPaths = testCase.payloadContract.allowedPaths;
1580
+ if (!testCase.payloadContract.declared) {
1581
+ return { status: "UNAVAILABLE", requiredPaths, allowedPaths, observedPaths, missingPaths: [], unexpectedPaths: [], enumMismatches: [] };
1582
+ }
1583
+ const missingPaths = requiredPaths.filter((item) => !observedPaths.includes(item));
1584
+ const unexpectedPaths = testCase.payloadContract.noPayload
1585
+ ? observedPaths
1586
+ : allowedPaths.length > 0
1587
+ ? observedPaths.filter((item) => !allowedPaths.includes(item) && !allowedPaths.some((allowed) => item.startsWith(`${allowed}.`)))
1588
+ : [];
1589
+ const enumMismatches = [];
1590
+ const delegatesInvalidEnumToScenarioGate = testCase.testPointBindings
1591
+ .filter((binding) => binding.mode === "variant")
1592
+ .some((binding) => /(?:ENUM|CATEGORY).*(?:INVALID|UNKNOWN|CASE|WHITESPACE|EMPTY|WRONG-TYPE)|(?:INVALID|UNKNOWN|CASE|WHITESPACE|EMPTY|WRONG-TYPE).*(?:ENUM|CATEGORY)/.test(binding.testPoint));
1593
+ for (const [field, allowed] of Object.entries(testCase.payloadContract.enumValues)) {
1594
+ const observed = orderedUnique(refs.flatMap((item) => (useFallback ? item.fallbackPayloadValues[field] : item.payloadValues[field]) ?? []).map((value) => value.replace(/^['"]|['"]$/g, "")));
1595
+ for (const value of observed) {
1596
+ if (!allowed.includes(value) && !delegatesInvalidEnumToScenarioGate)
1597
+ enumMismatches.push(`${field}=${value}; allowed=${allowed.join("|")}`);
1598
+ }
1599
+ }
1600
+ return {
1601
+ status: missingPaths.length > 0 || unexpectedPaths.length > 0 || enumMismatches.length > 0 ? "UNSAFE" : "SAFE",
1602
+ requiredPaths,
1603
+ allowedPaths,
1604
+ observedPaths,
1605
+ missingPaths,
1606
+ unexpectedPaths,
1607
+ enumMismatches,
1608
+ };
1609
+ }
1610
+ export async function analyzeBackendTestMarkdownPytestCorrespondence(input) {
1611
+ const layout = input.layout ?? resolveBackendTestLayout(undefined);
1612
+ const cases = await parseMarkdownCases(input.workspaceRoot, layout);
1613
+ const modulePaths = orderedUnique(cases.map((item) => item.markdownPath));
1614
+ const scripts = orderedUnique(cases.flatMap((item) => [...item.declaredScripts, expectedScript(item.markdownPath, layout)]));
1615
+ const allSymbols = [];
1616
+ const scriptFiles = [];
1617
+ for (const script of scripts) {
1618
+ const absolute = path.resolve(input.workspaceRoot, script);
1619
+ if (!(await exists(absolute)))
1620
+ continue;
1621
+ scriptFiles.push(absolute);
1622
+ allSymbols.push(...pytestSymbols(script, await readFile(absolute, "utf8")));
1623
+ }
1624
+ const findings = [];
1625
+ const entries = [];
1626
+ for (const testCase of cases) {
1627
+ const expected = expectedScript(testCase.markdownPath, layout);
1628
+ const declared = testCase.declaredScripts[0];
1629
+ const refs = allSymbols.filter((item) => item.caseIds.includes(testCase.caseId));
1630
+ const actualScripts = orderedUnique(refs.map((item) => item.script));
1631
+ const symbols = orderedUnique(refs.map((item) => item.symbol));
1632
+ const parameterIds = orderedUnique(refs.flatMap((item) => item.parameterIds));
1633
+ const assertionBindings = orderedUnique(refs.flatMap((item) => item.assertionTestPoints));
1634
+ const crossCuttingBindings = orderedUnique(refs.flatMap((item) => item.crossCuttingTestPoints));
1635
+ const variantTestPoints = testCase.testPointBindings.filter((item) => item.mode === "variant").map((item) => item.testPoint);
1636
+ const assertionTestPoints = testCase.testPointBindings.filter((item) => item.mode === "assertion").map((item) => item.testPoint);
1637
+ const crossCuttingTestPoints = testCase.testPointBindings.filter((item) => item.mode === "cross-cutting").map((item) => item.testPoint);
1638
+ const mappedTestPoints = orderedUnique([
1639
+ ...variantTestPoints.filter((point) => parameterIds.includes(point)),
1640
+ ...assertionTestPoints.filter((point) => assertionBindings.includes(point)),
1641
+ ...crossCuttingTestPoints.filter((point) => crossCuttingBindings.includes(point)),
1642
+ ]);
1643
+ const declaredPrimarySymbol = testCase.declaredPrimarySymbols[0];
1644
+ const evidenceOnly = isEvidenceOnlyMarkdownCase(testCase);
1645
+ const cardinality = refs.length === 0 ? "1:0" : refs.length === 1 ? "1:1" : "1:N";
1646
+ let status = evidenceOnly && refs.length === 0
1647
+ ? "EXACT_1_TO_1"
1648
+ : refs.length === 0
1649
+ ? "MISSING_PYTEST"
1650
+ : refs.length > 1
1651
+ ? "MULTIPLE_PYTEST"
1652
+ : "EXACT_1_TO_1";
1653
+ const entryFindings = [];
1654
+ const payloadAssessment = assessPayloadContract(testCase, refs);
1655
+ const signature = executionSignature(testCase);
1656
+ if (evidenceOnly) {
1657
+ if (refs.length > 0) {
1658
+ status = "SCRIPT_MISMATCH";
1659
+ entryFindings.push(`${testCase.caseId} evidence-only Markdown Case must not materialize a business pytest symbol; actualSymbols=${symbols.join(", ") || "none"}`);
1660
+ }
1661
+ }
1662
+ else {
1663
+ if (refs.length === 0)
1664
+ entryFindings.push(`${testCase.caseId} has no pytest symbol`);
1665
+ if (refs.length > 1)
1666
+ entryFindings.push(`${testCase.caseId} maps to multiple pytest symbols: ${symbols.join(", ")}`);
1667
+ if (!declared || declared !== expected || actualScripts.some((script) => script !== expected) || !declaredPrimarySymbol || symbols.some((symbol) => symbol !== declaredPrimarySymbol)) {
1668
+ if (refs.length > 0 && status === "EXACT_1_TO_1")
1669
+ status = "SCRIPT_MISMATCH";
1670
+ entryFindings.push(`${testCase.caseId} script/primary-symbol mapping mismatch: declaredScript=${declared ?? "none"}, expectedScript=${expected}, actual=${actualScripts.join(", ") || "none"}, declaredPrimarySymbol=${declaredPrimarySymbol ?? "none"}, actualSymbols=${symbols.join(", ") || "none"}`);
1671
+ }
1672
+ }
1673
+ const missingVariant = evidenceOnly
1674
+ ? []
1675
+ : variantTestPoints.filter((point) => !parameterIds.includes(point));
1676
+ const missingAssertion = evidenceOnly
1677
+ ? []
1678
+ : assertionTestPoints.filter((point) => !assertionBindings.includes(point));
1679
+ const missingCrossCutting = evidenceOnly
1680
+ ? []
1681
+ : crossCuttingTestPoints.filter((point) => !crossCuttingBindings.includes(point));
1682
+ const extraPoints = orderedUnique([...parameterIds, ...assertionBindings, ...crossCuttingBindings]).filter((point) => !testCase.testPoints.includes(point));
1683
+ if (testCase.unclassifiedTestPoints.length > 0 || testCase.duplicateBindingTestPoints.length > 0) {
1684
+ if (refs.length > 0 && status === "EXACT_1_TO_1")
1685
+ status = "TEST_POINT_BINDING_DUPLICATE";
1686
+ entryFindings.push(`${testCase.caseId} invalid Markdown Test Point bindings: unclassified=${testCase.unclassifiedTestPoints.join(", ") || "none"}; duplicate=${testCase.duplicateBindingTestPoints.join(", ") || "none"}`);
1687
+ }
1688
+ if (missingVariant.length > 0) {
1689
+ if (refs.length > 0 && status === "EXACT_1_TO_1")
1690
+ status = "VARIANT_PARAMETER_MISSING";
1691
+ entryFindings.push(`${testCase.caseId} missing variant pytest parameter IDs: ${missingVariant.join(", ")}`);
1692
+ }
1693
+ if (missingAssertion.length > 0) {
1694
+ if (refs.length > 0 && status === "EXACT_1_TO_1")
1695
+ status = "ASSERTION_BINDING_MISSING";
1696
+ entryFindings.push(`${testCase.caseId} missing assertion docstring bindings: ${missingAssertion.join(", ")}`);
1697
+ }
1698
+ if (missingCrossCutting.length > 0) {
1699
+ if (refs.length > 0 && status === "EXACT_1_TO_1")
1700
+ status = "CROSS_CUTTING_EVIDENCE_MISSING";
1701
+ entryFindings.push(`${testCase.caseId} missing cross-cutting evidence bindings: ${missingCrossCutting.join(", ")}`);
1702
+ }
1703
+ if (extraPoints.length > 0) {
1704
+ if (refs.length > 0 && status === "EXACT_1_TO_1")
1705
+ status = "TEST_POINT_BINDING_EXTRA";
1706
+ entryFindings.push(`${testCase.caseId} has extra pytest Test Point bindings: ${extraPoints.join(", ")}`);
1707
+ }
1708
+ if (payloadAssessment.status === "UNAVAILABLE") {
1709
+ if (refs.length > 0 && status === "EXACT_1_TO_1")
1710
+ status = "PAYLOAD_CONTRACT_UNAVAILABLE";
1711
+ entryFindings.push(`${testCase.caseId} has no deterministic Payload Contract declaration`);
1712
+ }
1713
+ else if (payloadAssessment.status === "UNSAFE") {
1714
+ if (refs.length > 0 && status === "EXACT_1_TO_1")
1715
+ status = "PAYLOAD_SHAPE_MISMATCH";
1716
+ entryFindings.push(`${testCase.caseId} payload contract mismatch: missing=${payloadAssessment.missingPaths.join(", ") || "none"}; unexpected=${payloadAssessment.unexpectedPaths.join(", ") || "none"}; enum=${payloadAssessment.enumMismatches.join(", ") || "none"}`);
1717
+ }
1718
+ findings.push(...entryFindings);
1719
+ entries.push({
1720
+ markdownModule: testCase.markdownPath,
1721
+ caseId: testCase.caseId,
1722
+ testPoints: testCase.testPoints,
1723
+ variantTestPoints,
1724
+ assertionTestPoints,
1725
+ crossCuttingTestPoints,
1726
+ mappedTestPoints,
1727
+ ...(declared ? { declaredScript: declared } : {}),
1728
+ ...(declaredPrimarySymbol ? { declaredPrimarySymbol } : {}),
1729
+ expectedScript: expected,
1730
+ actualScripts,
1731
+ pytestSymbols: symbols,
1732
+ parameterIds,
1733
+ cardinality,
1734
+ status,
1735
+ payloadAssessment,
1736
+ executionSignature: signature,
1737
+ findings: entryFindings,
1738
+ });
1739
+ }
1740
+ for (const symbol of allSymbols) {
1741
+ const boundKnown = symbol.caseIds.some((id) => cases.some((testCase) => testCase.caseId === id));
1742
+ if (boundKnown)
1743
+ continue;
1744
+ const unknownId = symbol.caseIds[0];
1745
+ const message = unknownId
1746
+ ? `${symbol.script}#${symbol.symbol} references unknown Markdown Case ${unknownId}`
1747
+ : `${symbol.script}#${symbol.symbol} has no Markdown Case ID`;
1748
+ findings.push(message);
1749
+ entries.push({
1750
+ markdownModule: "—",
1751
+ ...(unknownId ? { caseId: unknownId } : {}),
1752
+ testPoints: symbol.testPoints,
1753
+ variantTestPoints: [],
1754
+ assertionTestPoints: [],
1755
+ crossCuttingTestPoints: [],
1756
+ mappedTestPoints: [],
1757
+ expectedScript: symbol.script,
1758
+ actualScripts: [symbol.script],
1759
+ pytestSymbols: [symbol.symbol],
1760
+ parameterIds: symbol.parameterIds,
1761
+ cardinality: "0:1",
1762
+ status: unknownId ? "EXTRA_PYTEST" : "SYMBOL_MISSING_CASE_ID",
1763
+ payloadAssessment: {
1764
+ status: "UNAVAILABLE",
1765
+ requiredPaths: [],
1766
+ allowedPaths: [],
1767
+ observedPaths: symbol.payloadPaths,
1768
+ missingPaths: [],
1769
+ unexpectedPaths: [],
1770
+ enumMismatches: [],
1771
+ },
1772
+ findings: [message],
1773
+ });
1774
+ }
1775
+ const duplicateSignatures = new Map();
1776
+ for (const entry of entries.filter((item) => item.caseId && item.executionSignature)) {
1777
+ const owners = duplicateSignatures.get(entry.executionSignature) ?? [];
1778
+ owners.push(entry.caseId);
1779
+ duplicateSignatures.set(entry.executionSignature, owners);
1780
+ }
1781
+ const duplicateSignatureGroups = [...duplicateSignatures.entries()].filter(([, owners]) => orderedUnique(owners).length > 1);
1782
+ for (const [signature, owners] of duplicateSignatureGroups)
1783
+ findings.push(`duplicate execution signature ${signature}: ${orderedUnique(owners).join(", ")}`);
1784
+ const exactModuleCount = modulePaths.filter((modulePath) => {
1785
+ const expected = expectedScript(modulePath, layout);
1786
+ const moduleCases = cases.filter((item) => item.markdownPath === modulePath);
1787
+ return moduleCases.every((item) => item.declaredScripts.length === 1 && item.declaredScripts[0] === expected) && scriptFiles.some((file) => path.relative(input.workspaceRoot, file).replaceAll(path.sep, "/") === expected);
1788
+ }).length;
1789
+ const caseEntries = entries.filter((entry) => entry.cardinality !== "0:1");
1790
+ const facts = backendTestMarkdownPytestCorrespondenceFactsSchema.parse({
1791
+ schemaId: "backend-test-markdown-pytest-correspondence-facts-v3",
1792
+ schemaVersion: 3,
1793
+ taskId: input.taskId,
1794
+ status: findings.length === 0 ? "PASS" : "FAIL",
1795
+ inputFiles: await inputFileFacts(input.workspaceRoot, [
1796
+ path.join(input.workspaceRoot, ...layout.readmePath.split("/")),
1797
+ ...(await markdownModuleFiles(input.workspaceRoot)),
1798
+ ...scriptFiles,
1799
+ ].filter((file) => file && path.isAbsolute(file))),
1800
+ summary: {
1801
+ markdownModuleCount: modulePaths.length,
1802
+ exactModuleCount,
1803
+ markdownCaseCount: cases.length,
1804
+ exactCorrespondenceCount: caseEntries.filter((entry) => entry.status === "EXACT_1_TO_1").length,
1805
+ primarySymbolCount: orderedUnique(caseEntries.flatMap((entry) => entry.pytestSymbols)).length,
1806
+ missingPytestCount: caseEntries.filter((entry) => entry.status === "MISSING_PYTEST").length,
1807
+ multiplePytestCount: caseEntries.filter((entry) => entry.status === "MULTIPLE_PYTEST").length,
1808
+ extraPytestCount: entries.filter((entry) => entry.cardinality === "0:1").length,
1809
+ scriptMismatchCount: caseEntries.filter((entry) => entry.status === "SCRIPT_MISMATCH").length,
1810
+ testPointCount: cases.reduce((sum, item) => sum + item.testPoints.length, 0),
1811
+ mappedTestPointCount: caseEntries.reduce((sum, entry) => sum + entry.mappedTestPoints.length, 0),
1812
+ variantTestPointCount: cases.reduce((sum, item) => sum + item.testPointBindings.filter((binding) => binding.mode === "variant").length, 0),
1813
+ assertionTestPointCount: cases.reduce((sum, item) => sum + item.testPointBindings.filter((binding) => binding.mode === "assertion").length, 0),
1814
+ crossCuttingTestPointCount: cases.reduce((sum, item) => sum + item.testPointBindings.filter((binding) => binding.mode === "cross-cutting").length, 0),
1815
+ unclassifiedTestPointCount: cases.reduce((sum, item) => sum + item.unclassifiedTestPoints.length, 0),
1816
+ duplicateBindingTestPointCount: cases.reduce((sum, item) => sum + item.duplicateBindingTestPoints.length, 0),
1817
+ payloadSafeCount: caseEntries.filter((entry) => entry.payloadAssessment.status === "SAFE").length,
1818
+ payloadUnsafeCount: caseEntries.filter((entry) => entry.payloadAssessment.status === "UNSAFE").length,
1819
+ payloadUnavailableCount: caseEntries.filter((entry) => entry.payloadAssessment.status === "UNAVAILABLE").length,
1820
+ duplicateExecutionSignatureCount: duplicateSignatureGroups.length,
1821
+ },
1822
+ entries,
1823
+ findings: orderedUnique(findings),
1824
+ });
1825
+ const markdown = [
1826
+ "# Backend Test Markdown → pytest Correspondence",
1827
+ "",
1828
+ "## Status",
1829
+ "",
1830
+ facts.status,
1831
+ "",
1832
+ "## Summary",
1833
+ "",
1834
+ `- Markdown modules: ${facts.summary.markdownModuleCount}`,
1835
+ `- Exact module mappings: ${facts.summary.exactModuleCount}`,
1836
+ `- Markdown Cases: ${facts.summary.markdownCaseCount}`,
1837
+ `- Exact 1:1: ${facts.summary.exactCorrespondenceCount}`,
1838
+ `- Primary pytest symbols: ${facts.summary.primarySymbolCount}`,
1839
+ `- Missing pytest: ${facts.summary.missingPytestCount}`,
1840
+ `- Multiple pytest: ${facts.summary.multiplePytestCount}`,
1841
+ `- Extra pytest: ${facts.summary.extraPytestCount}`,
1842
+ `- Test Points: ${facts.summary.mappedTestPointCount}/${facts.summary.testPointCount}`,
1843
+ `- Variant Test Points: ${facts.summary.variantTestPointCount}`,
1844
+ `- Assertion Test Points: ${facts.summary.assertionTestPointCount}`,
1845
+ `- Cross-Cutting Test Points: ${facts.summary.crossCuttingTestPointCount}`,
1846
+ `- Unclassified Test Points: ${facts.summary.unclassifiedTestPointCount}`,
1847
+ `- Duplicate Test Point Bindings: ${facts.summary.duplicateBindingTestPointCount}`,
1848
+ `- Payload SAFE: ${facts.summary.payloadSafeCount}`,
1849
+ `- Payload UNSAFE: ${facts.summary.payloadUnsafeCount}`,
1850
+ `- Payload UNAVAILABLE: ${facts.summary.payloadUnavailableCount}`,
1851
+ `- Duplicate Execution Signatures: ${facts.summary.duplicateExecutionSignatureCount}`,
1852
+ "",
1853
+ "## Correspondence Matrix",
1854
+ "",
1855
+ "| Markdown Module | Case ID | Variant | Assertion | Cross-Cutting | Mapped | Declared Script | Primary Symbol | Actual Script | Pytest Symbol | Parameter IDs | Payload | Missing Paths | Unexpected Paths | Enum Mismatch | Cardinality | Status |",
1856
+ "|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|",
1857
+ ...facts.entries.map((entry) => `| ${entry.markdownModule} | ${entry.caseId ?? "—"} | ${entry.variantTestPoints.join(", ") || "—"} | ${entry.assertionTestPoints.join(", ") || "—"} | ${entry.crossCuttingTestPoints.join(", ") || "—"} | ${entry.mappedTestPoints.join(", ") || "—"} | ${entry.declaredScript ?? "—"} | ${entry.declaredPrimarySymbol ?? "—"} | ${entry.actualScripts.join(", ") || "—"} | ${entry.pytestSymbols.join(", ") || "—"} | ${entry.parameterIds.join(", ") || "—"} | ${entry.payloadAssessment.status} | ${entry.payloadAssessment.missingPaths.join(", ") || "—"} | ${entry.payloadAssessment.unexpectedPaths.join(", ") || "—"} | ${entry.payloadAssessment.enumMismatches.join(", ") || "—"} | ${entry.cardinality} | ${entry.status} |`),
1858
+ "",
1859
+ "## Findings",
1860
+ "",
1861
+ ...(facts.findings.length ? facts.findings.map((item) => `- ${item}`) : ["- None"]),
1862
+ "",
1863
+ ].join("\n");
1864
+ return { facts, markdown };
1865
+ }
1866
+ async function readFacts(filePath, schema) {
1867
+ try {
1868
+ const parsed = schema.safeParse(JSON.parse(await readFile(filePath, "utf8")));
1869
+ if (!parsed.success)
1870
+ return { issue: `${path.basename(filePath)} schema invalid: ${parsed.error.issues[0]?.message ?? "unknown"}` };
1871
+ return { value: parsed.data };
1872
+ }
1873
+ catch (error) {
1874
+ return { issue: `${path.basename(filePath)} unavailable: ${error instanceof Error ? error.message : String(error)}` };
1875
+ }
1876
+ }
1877
+ async function freshnessIssues(workspaceRoot, inputFiles) {
1878
+ if (!workspaceRoot)
1879
+ return [];
1880
+ const issues = [];
1881
+ for (const item of inputFiles) {
1882
+ const absolute = path.resolve(workspaceRoot, item.path);
1883
+ if (!(await exists(absolute)))
1884
+ issues.push(`input file missing since facts generation: ${item.path}`);
1885
+ else if ((await fileHash(absolute)) !== item.sha256)
1886
+ issues.push(`input file hash changed since facts generation: ${item.path}`);
1887
+ }
1888
+ return issues;
1889
+ }
1890
+ export async function materializeBackendTestCaseManifestFromFacts(input) {
1891
+ const contractsDir = path.join(input.runDir, "contracts");
1892
+ await mkdir(contractsDir, { recursive: true });
1893
+ const coveragePath = path.join(contractsDir, "backend-test-case-coverage-facts.json");
1894
+ const correspondencePath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-facts.json");
1895
+ const coverageResult = await readFacts(coveragePath, backendTestCaseCoverageFactsSchema);
1896
+ const correspondenceResult = await readFacts(correspondencePath, backendTestMarkdownPytestCorrespondenceFactsSchema);
1897
+ const coverageCases = new Map((coverageResult.value?.cases ?? []).map((item) => [item.caseId, item]));
1898
+ const correspondenceCases = new Map((correspondenceResult.value?.entries ?? []).filter((item) => item.caseId && item.cardinality !== "0:1").map((item) => [item.caseId, item]));
1899
+ const keyIssues = coverageResult.value && correspondenceResult.value ? [
1900
+ ...[...coverageCases.keys()].filter((caseId) => !correspondenceCases.has(caseId)).map((caseId) => `coverage Case missing from correspondence facts: ${caseId}`),
1901
+ ...[...correspondenceCases.keys()].filter((caseId) => !coverageCases.has(caseId)).map((caseId) => `correspondence Case missing from coverage facts: ${caseId}`),
1902
+ ...[...coverageCases.entries()].flatMap(([caseId, testCase]) => {
1903
+ const mapping = correspondenceCases.get(caseId);
1904
+ return mapping && JSON.stringify([...testCase.testPoints].sort()) !== JSON.stringify([...mapping.testPoints].sort())
1905
+ ? [`Case Test Point keys disagree between facts: ${caseId}`]
1906
+ : [];
1907
+ }),
1908
+ ] : [];
1909
+ const sourceFactsIssues = orderedUnique([
1910
+ ...(coverageResult.issue ? [coverageResult.issue] : []),
1911
+ ...(correspondenceResult.issue ? [correspondenceResult.issue] : []),
1912
+ ...(coverageResult.value?.taskId !== undefined && coverageResult.value.taskId !== input.sourceBinding.taskId ? ["coverage facts taskId does not match source binding"] : []),
1913
+ ...(coverageResult.value && JSON.stringify(coverageResult.value.sourceBinding) !== JSON.stringify(input.sourceBinding) ? ["coverage facts source binding does not match manifest source binding"] : []),
1914
+ ...(correspondenceResult.value?.taskId !== undefined && correspondenceResult.value.taskId !== input.sourceBinding.taskId ? ["correspondence facts taskId does not match source binding"] : []),
1915
+ ...keyIssues,
1916
+ ...(coverageResult.value ? await freshnessIssues(input.workspaceRoot, coverageResult.value.inputFiles) : []),
1917
+ ...(correspondenceResult.value ? await freshnessIssues(input.workspaceRoot, correspondenceResult.value.inputFiles) : []),
1918
+ ]);
1919
+ const materializationStatus = !coverageResult.value && !correspondenceResult.value
1920
+ ? "unavailable"
1921
+ : sourceFactsIssues.length > 0 || !coverageResult.value || !correspondenceResult.value
1922
+ ? "partial"
1923
+ : "available";
1924
+ const correspondenceByCase = new Map((correspondenceResult.value?.entries ?? [])
1925
+ .filter((entry) => entry.caseId && entry.cardinality !== "0:1")
1926
+ .map((entry) => [entry.caseId, entry]));
1927
+ const manifestCases = (coverageResult.value?.cases ?? []).map((testCase) => {
1928
+ const mapping = correspondenceByCase.get(testCase.caseId);
1929
+ const generated = Boolean(mapping && mapping.pytestSymbols.length > 0 && mapping.actualScripts.length > 0);
1930
+ return {
1931
+ caseId: testCase.caseId,
1932
+ acIds: testCase.acIds,
1933
+ title: testCase.title,
1934
+ category: inferCategory(testCase.scenarioTypes),
1935
+ automationStatus: generated ? "generated" : "planned",
1936
+ ruleRefs: testCase.ruleKeys,
1937
+ ...(generated ? { file: mapping.actualScripts[0], symbol: mapping.pytestSymbols[0] } : { gapReason: "pytest correspondence is missing or unavailable" }),
1938
+ };
1939
+ });
1940
+ const base = {
1941
+ schemaVersion: 1,
1942
+ sourceBinding: input.sourceBinding,
1943
+ cases: manifestCases,
1944
+ evidenceGaps: [],
1945
+ };
1946
+ const coverageSummary = materializationStatus === "available" ? computeCaseManifestCoverageSummary(base) : undefined;
1947
+ const manifest = backendTestCaseManifestSchema.parse({
1948
+ ...base,
1949
+ materializationStatus,
1950
+ sourceFactsIssues,
1951
+ coverageScope: coverageResult.value?.coverageScope,
1952
+ ruleCoverageSummary: coverageResult.value?.summary,
1953
+ correspondenceSummary: correspondenceResult.value?.summary,
1954
+ artifactRefs: {
1955
+ caseCoverageFacts: await artifactRef(input.runDir, coveragePath),
1956
+ correspondenceFacts: await artifactRef(input.runDir, correspondencePath),
1957
+ },
1958
+ ...(coverageSummary ? { coverageSummary } : {}),
1959
+ });
1960
+ await writeFile(path.join(contractsDir, "backend-test-case-manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
1961
+ return manifest;
1962
+ }
1963
+ function inferCategory(types) {
1964
+ const text = types.join(" ").toLowerCase();
1965
+ if (/negative|invalid|error/.test(text))
1966
+ return "negative";
1967
+ if (/boundary|format|enum/.test(text))
1968
+ return "boundary";
1969
+ if (/state|business|lifecycle|uniqueness/.test(text))
1970
+ return "state-transition";
1971
+ if (/auth|permission/.test(text))
1972
+ return "auth";
1973
+ if (/timeout/.test(text))
1974
+ return "timeout";
1975
+ if (/concurr/.test(text))
1976
+ return "concurrency";
1977
+ if (/positive|requirement/.test(text))
1978
+ return "positive";
1979
+ return "other";
1980
+ }
1981
+ async function artifactRef(runDir, absolutePath) {
1982
+ if (!(await exists(absolutePath)))
1983
+ return { path: path.relative(runDir, absolutePath).replaceAll(path.sep, "/") };
1984
+ return { path: path.relative(runDir, absolutePath).replaceAll(path.sep, "/"), sha256: await fileHash(absolutePath) };
1985
+ }