@tea-agent/loop-agent 0.39.0-next.9 → 0.39.1

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 (288) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +269 -69
  3. package/README.md +11 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/dag/generate-task-dag.js +9 -0
  6. package/dist/application/task-lifecycle/advance.js +16 -0
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +16 -5
  9. package/dist/cli/program.js +16 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli/update/runtime-activity.js +1 -29
  12. package/dist/cli.js +2 -1
  13. package/dist/commands/dag-rerun.js +55 -1
  14. package/dist/commands/init-model-catalog.js +464 -0
  15. package/dist/commands/init-upgrade.js +265 -97
  16. package/dist/commands/init.js +428 -27
  17. package/dist/commands/inspect-next.js +8 -0
  18. package/dist/commands/task-advance.js +31 -0
  19. package/dist/executors/dag-pi-executor.js +124 -18
  20. package/dist/executors/pi-event-serializer.js +42 -1
  21. package/dist/executors/pi-executor.js +51 -0
  22. package/dist/executors/pi-extension-resolver.js +233 -0
  23. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  24. package/dist/executors/pi-sdk-executor.js +187 -59
  25. package/dist/executors/shell-executor.js +555 -34
  26. package/dist/executors/shell-write-guard.js +7 -0
  27. package/dist/shared/dag-failure-category.js +138 -0
  28. package/dist/shared/dag-prompt-override.js +27 -0
  29. package/dist/shared/operator/capabilities.js +181 -27
  30. package/dist/shared/playwright-cli-command-policy.js +15 -0
  31. package/dist/shared/update/console-notifier.js +100 -0
  32. package/dist/{cli → shared}/update/npm-client.js +70 -15
  33. package/dist/{cli → shared}/update/state.js +41 -13
  34. package/dist/task/config-types.js +28 -0
  35. package/dist/task/contract/apply.js +11 -1
  36. package/dist/task/contract/constants.js +3 -0
  37. package/dist/task/contract/diff.js +26 -0
  38. package/dist/task/contract/hash.js +3 -0
  39. package/dist/task/contract/observe.js +25 -4
  40. package/dist/task/contract/paths.js +2 -1
  41. package/dist/task/contract/project.js +20 -1
  42. package/dist/task/contract/schema.js +8 -1
  43. package/dist/task/contract/transaction.js +12 -0
  44. package/dist/task/source-prepare/build-draft.js +15 -0
  45. package/dist/task/source-prepare/completeness.js +188 -0
  46. package/dist/task/source-prepare/fragment-inventory.js +474 -0
  47. package/dist/task/source-prepare/index.js +3 -0
  48. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  49. package/dist/task/source-prepare/ledger-review.js +214 -0
  50. package/dist/task/source-prepare/ledger.js +545 -0
  51. package/dist/task/source-prepare/parse-intent.js +24 -4
  52. package/dist/task/source-prepare/prepare.js +262 -1
  53. package/dist/task/source-prepare/semantic-intake.js +19 -3
  54. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  55. package/dist/task/source-prepare/types.js +22 -0
  56. package/dist/worker/cli.js +18 -2
  57. package/dist/worker/console/chat/deferred-turn.js +91 -0
  58. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  59. package/dist/worker/console/chat/operation-card.js +8 -1
  60. package/dist/worker/console/chat/pi-console-config.js +81 -10
  61. package/dist/worker/console/chat/pi-runtime.js +165 -83
  62. package/dist/worker/console/chat/repo-walk.js +10 -1
  63. package/dist/worker/console/chat/routes.js +480 -55
  64. package/dist/worker/console/chat/semantic-activity.js +20 -8
  65. package/dist/worker/console/chat/session-store.js +146 -2
  66. package/dist/worker/console/chat/turn-process.js +10 -10
  67. package/dist/worker/console/console-handoff.js +82 -0
  68. package/dist/worker/console/console-update-and-init.js +115 -0
  69. package/dist/worker/console/console-update-runtime.js +84 -0
  70. package/dist/worker/console/dag-execution-receipt.js +33 -0
  71. package/dist/worker/console/draft-store.js +49 -0
  72. package/dist/worker/console/inspect-split.js +18 -0
  73. package/dist/worker/console/operation-run-facts.js +19 -3
  74. package/dist/worker/console/operation-store.js +1 -1
  75. package/dist/worker/console/operator-actions.js +320 -4
  76. package/dist/worker/console/operator-surface-health.js +1 -0
  77. package/dist/worker/console/operator-user-error.js +2 -2
  78. package/dist/worker/console/pi-readiness.js +5 -4
  79. package/dist/worker/console/prd-intake-bridge.js +13 -0
  80. package/dist/worker/console/recovery-cta.js +4 -4
  81. package/dist/worker/console/recovery-error-copy.js +4 -4
  82. package/dist/worker/console/routes.js +46 -2
  83. package/dist/worker/console/security.js +63 -9
  84. package/dist/worker/console/server.js +111 -7
  85. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BLXFe4qI.js → abnfDiagram-N423BO3Z-CTx1TrNa.js} +1 -1
  86. package/dist/worker/console/static/assets/{arc-7iOJeqho.js → arc-B4i9cVxX.js} +1 -1
  87. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CYoyj5K7.js → architectureDiagram-T3A2C74G-BAXE8_Rl.js} +1 -1
  88. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-fK01JNlR.js → blockDiagram-VBNYF7ZC-C5Xh59at.js} +1 -1
  89. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-bzbw5D_Z.js → c4Diagram-5PPSVZJV-B_asOzAu.js} +1 -1
  90. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +1 -0
  91. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Cf6hJFFG.js → chunk-2GRJ4B5K-wRO_iRNc.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DSFXR-et.js → chunk-2Q5K7J3B-ByOaSh_G.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BagJechl.js → chunk-5RXB4S5H-DjLzudan.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-E7QNMFaW.js → chunk-5VM5RSS4-JCNGkXYT.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BG_eOBrC.js → chunk-6Q2QTUOP-BhrPXXMZ.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bo42dqol.js → chunk-GF5L2VYU-TJ5JuEBT.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-dibM-87v.js → chunk-JWPE2WC7-VVthn_mD.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bytrp3Oe.js → chunk-KBJHAD2P-D60-Mny4.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DBABILrr.js → chunk-RYQCIY6F-zXv7aVye.js} +1 -1
  100. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DCUk5Pf9.js → chunk-XXDRQBXY-By9VQoi1.js} +1 -1
  101. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +1 -0
  102. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +1 -0
  103. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BlGE9v6E.js → cose-bilkent-JH36ORCC-C4zQLe-M.js} +1 -1
  104. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DGl5ewph.js → cynefin-VYW2F7L2-wh5N2ReN.js} +1 -1
  105. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DsJ_SVH_.js → cynefinDiagram-MW4NZA55-Dlu5wIqs.js} +1 -1
  106. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BIhjFdzv.js → dagre-VZM6K2ZE-CrDgthc6.js} +1 -1
  107. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dmy1ctTb.js → diagram-7IWD3JNH-zN1clg27.js} +1 -1
  108. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DYCtWUF3.js → diagram-B4RE2ZJO-BN-o7J7-.js} +1 -1
  109. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BMZFHVd2.js → diagram-LBJQPF4R-C8SuPt0d.js} +1 -1
  110. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-b_U8gfaO.js → diagram-Q27KOJAE-BoizuHbZ.js} +1 -1
  111. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dzy_bn-I.js → diagram-UB23O5K3-BzUzXixG.js} +1 -1
  112. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DJCiMNEI.js → ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js} +1 -1
  113. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-28lD016B.js → erDiagram-JOGREHBK-GALUzRbs.js} +1 -1
  114. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DGyHJmIw.js → flowDiagram-UKHOOZJN-CZJN65Sb.js} +1 -1
  115. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CzdwUjrO.js → ganttDiagram-PKOTCBZU-JLB9OESl.js} +1 -1
  116. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D9bGwuo8.js → gitGraphDiagram-DS77QQ5N-CcvCr4GD.js} +1 -1
  117. package/dist/worker/console/static/assets/index-CW88Fjbw.js +406 -0
  118. package/dist/worker/console/static/assets/index-DP0ZJl39.css +1 -0
  119. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-LJgJUQFC.js → infoDiagram-6WML65LV-HG5z7aVg.js} +1 -1
  120. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-98LaB8zb.js → ishikawaDiagram-WSZJBQD7-BHEu9chR.js} +1 -1
  121. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-ISzwlQ.js → journeyDiagram-NVQOT4AX-Cp_Up8-a.js} +1 -1
  122. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-gfYOI-EL.js → kanban-definition-27J2QSJJ-DvVeY-em.js} +1 -1
  123. package/dist/worker/console/static/assets/{linear-CyntZgKV.js → linear-aGmcjgTn.js} +1 -1
  124. package/dist/worker/console/static/assets/{mermaid.core-BBOR7dg0.js → mermaid.core-Biyo-tqA.js} +5 -5
  125. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CQAgwPMk.js → mindmap-definition-FAOFIHXS-DyxY2pIb.js} +1 -1
  126. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DkMiZFuV.js → pegDiagram-VL7TDLO6-CkdGuc50.js} +1 -1
  127. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DLNXa_0J.js → pieDiagram-7S7Q4E2Y-NNQI9xVR.js} +1 -1
  128. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Dc7bdzjm.js → quadrantDiagram-CIZ2JOQS-PAJkuz_G.js} +1 -1
  129. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BxMB0QqW.js → railroadDiagram-AXF67PYL-ZwrQod0O.js} +1 -1
  130. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-FjumoKn4.js → requirementDiagram-LRYGKXZP-D3ZfrzM2.js} +1 -1
  131. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CkhKc_E3.js → sankeyDiagram-W5VNT64P-DDGFmWk5.js} +1 -1
  132. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DoBWqTTH.js → sequenceDiagram-SI44F4Z6-DO6mmXTA.js} +1 -1
  133. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CQeh_YeE.js → sizeCapture-X5ZJPWSS-DMbCQiJt.js} +1 -1
  134. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-LuHmFW-I.js → stateDiagram-OKZ733FA-Di_YUEfC.js} +1 -1
  135. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +1 -0
  136. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CHwbDctY.js → swimlanes-SLNWSIFB-Bn2PphNJ.js} +2 -2
  137. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +8 -0
  138. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CoMngk0X.js → timeline-definition-Z64GVDOM-DPh8pRsp.js} +1 -1
  139. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CURYbtbO.js → vennDiagram-T6HMQDX7-GMfl26pW.js} +1 -1
  140. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BdqSECjU.js → wardleyDiagram-T6FBY63Y-DTlmVrmF.js} +1 -1
  141. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DbMUYuAg.js → xychartDiagram-ELKLHX3M-YGBjKAS5.js} +1 -1
  142. package/dist/worker/console/static/index.html +2 -2
  143. package/dist/worker/console/static-src/app/console-types.js +40 -1
  144. package/dist/worker/console/static-src/app/useOperatorActions.js +6 -1
  145. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  146. package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
  147. package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
  148. package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
  149. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  150. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +74 -26
  151. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  152. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  153. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  154. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  155. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  156. package/dist/worker/console/static-src/operator-chat/turn-submission.js +54 -0
  157. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
  158. package/dist/worker/console/static-src/operator-chat/useChatStream.js +237 -11
  159. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  160. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  161. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  162. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  163. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  164. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  165. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  166. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  167. package/dist/worker/observability/init-runtime-activity.js +26 -0
  168. package/dist/worker/observability/read-model.js +22 -0
  169. package/dist/worker/observe/node-input.js +160 -3
  170. package/dist/worker/observe/node-process.js +377 -0
  171. package/dist/worker/observe/routes.js +224 -3
  172. package/dist/worker/observe/shell-handler-keys.js +34 -0
  173. package/dist/worker/observe/static/api.js +67 -1
  174. package/dist/worker/observe/static/constants.js +8 -1
  175. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  176. package/dist/worker/observe/static/format.js +60 -0
  177. package/dist/worker/observe/static/index.html +5 -0
  178. package/dist/worker/observe/static/markdown-render.js +20 -1
  179. package/dist/worker/observe/static/operator-chrome.js +4 -0
  180. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  181. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  182. package/dist/worker/observe/static/state.js +13 -2
  183. package/dist/worker/observe/static/styles.css +85 -0
  184. package/dist/worker/observe/static/views/dag-inspector.js +912 -8
  185. package/dist/worker/observe/static/views/dag.js +8 -1
  186. package/dist/worker/observe/static/views/failures.js +5 -2
  187. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  188. package/dist/worker/observe/static/views/task.js +55 -0
  189. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +856 -54
  190. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  191. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  192. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  193. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  194. package/dist/workflows/dag/backend-test-layout.js +133 -0
  195. package/dist/workflows/dag/backend-test-markdown-workflow.js +343 -33
  196. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  197. package/dist/workflows/dag/backend-test-pytest-collection.js +454 -36
  198. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  199. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  200. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  201. package/dist/workflows/dag/backend-test-writer-completeness.js +110 -61
  202. package/dist/workflows/dag/contract-output-registry.js +15 -0
  203. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  204. package/dist/workflows/dag/convergence/controller.js +141 -0
  205. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  206. package/dist/workflows/dag/dynamic-runtime/map.js +25 -9
  207. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  208. package/dist/workflows/dag/failure-category.js +7 -116
  209. package/dist/workflows/dag/frontend-implementation-contract.js +465 -6
  210. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  211. package/dist/workflows/dag/frontend-prewrite-gate.js +293 -84
  212. package/dist/workflows/dag/frontend-repair.js +14 -0
  213. package/dist/workflows/dag/frontend-review-context.js +92 -10
  214. package/dist/workflows/dag/frontend-risk.js +15 -2
  215. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  216. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  217. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  218. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  219. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  220. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  221. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  222. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  223. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  224. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  225. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  226. package/dist/workflows/dag/init-hybrid.js +1019 -209
  227. package/dist/workflows/dag/node-execution.js +228 -15
  228. package/dist/workflows/dag/prompt.js +60 -1
  229. package/dist/workflows/dag/rerun-feedback.js +212 -0
  230. package/dist/workflows/dag/rerun-plan.js +224 -27
  231. package/dist/workflows/dag/rerun-run.js +61 -6
  232. package/dist/workflows/dag/rerun-task.js +61 -2
  233. package/dist/workflows/dag/retry-policy.js +109 -0
  234. package/dist/workflows/dag/structured-output-repair.js +712 -0
  235. package/dist/workflows/dag/types.js +185 -12
  236. package/dist/workflows/dag/validate.js +64 -20
  237. package/docs/README.md +1 -0
  238. package/docs/architecture/evolution.md +45 -0
  239. package/docs/init-surface.manifest.json +6 -0
  240. package/docs/operations/README.md +1 -1
  241. package/docs/operations/local-development-environment.md +1 -5
  242. package/docs/skills/vetted-skill-registry.md +14 -0
  243. package/docs/templates/README.md +2 -2
  244. package/docs/templates/agent-dag.schema.json +44 -0
  245. package/docs/templates/backend-test-dag.json +60 -35
  246. package/docs/templates/frontend-implementation-contract.schema.json +681 -24
  247. package/docs/templates/frontend-test-case-checklist.md +2 -2
  248. package/docs/templates/frontend-test-dag.json +8 -10
  249. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  250. package/docs/templates/init-managed-agents.md +13 -3
  251. package/harness.json +6 -4
  252. package/package.json +12 -4
  253. package/scripts/next-info.mjs +356 -0
  254. package/scripts/next-publish-gate.mjs +238 -0
  255. package/scripts/release-source-binding.mjs +251 -0
  256. package/skills/analyze-product-requirements/SKILL.md +8 -5
  257. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  258. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  259. package/skills/analyze-product-requirements/references/example.md +50 -0
  260. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  261. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  262. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  263. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  264. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  265. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  266. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  267. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  268. package/skills/codebase-scout/SKILL.md +1 -1
  269. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  270. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  271. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  272. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  273. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  274. package/skills/improve-codebase-architecture/deepening.md +37 -0
  275. package/skills/improve-codebase-architecture/html-report.md +123 -0
  276. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  277. package/skills/improve-codebase-architecture/language.md +53 -0
  278. package/skills/loop-agent/references/command-reference.md +11 -4
  279. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  280. package/skills/playwright-cli/SKILL.md +23 -1
  281. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  282. package/dist/worker/console/static/assets/channel-BBOTJHvD.js +0 -1
  283. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cm8Yf8tD.js +0 -1
  284. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cm8Yf8tD.js +0 -1
  285. package/dist/worker/console/static/assets/index-B-qWGThd.js +0 -325
  286. package/dist/worker/console/static/assets/index-C-xRgY_0.css +0 -1
  287. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DZihfdKc.js +0 -1
  288. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CKnXCjHX.js +0 -8
@@ -6,6 +6,7 @@ import { createWriteStream } from "node:fs";
6
6
  import path from "node:path";
7
7
  import { promisify } from "node:util";
8
8
  import { isPriorityOnlyBackendTestModuleStem } from "./backend-test-module-stem.js";
9
+ import { resolveBackendTestLayout, } from "./backend-test-layout.js";
9
10
  import { parseJacocoXml, } from "./backend-test-coverage-contract.js";
10
11
  import { backendTestCaseManifestSchema, computeCaseManifestCoverageSummary, } from "./backend-test-case-manifest.js";
11
12
  const execFileAsync = promisify(execFile);
@@ -14,10 +15,22 @@ const execFileAsync = promisify(execFile);
14
15
  // heading width cannot collapse the whole manifest/report to zero cases.
15
16
  const CASE_ID = /\bBE-[A-Z0-9_-]+-\d{2,3}\b/g;
16
17
  const CANONICAL_CASE_ID = /\bBE-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}\b/;
18
+ const SHARED_SETUP_REF_LINE = /^\s*引用前置\s*:\s*(\S+)\s*#\s*([A-Za-z0-9_.\-\u4e00-\u9fa5]+)\s*$/;
19
+ const sharedSetupAnchorRe = /^SS-[A-Z0-9][A-Z0-9._-]*$/i;
20
+ export function extractBackendSharedSetupRefs(markdown) {
21
+ const refs = [];
22
+ for (const line of markdown.split("\n")) {
23
+ const match = SHARED_SETUP_REF_LINE.exec(line);
24
+ if (match) {
25
+ refs.push({ raw: match[0].trim(), path: match[1], anchor: match[2] });
26
+ }
27
+ }
28
+ return refs;
29
+ }
17
30
  const AC_ID = /\bAC-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g;
18
31
  const SECRET = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|authorization)\s*[:=]\s*\S+/i;
19
32
  const SECRET_ASSIGNMENT = /\b(password|passwd|secret|token|api[_-]?key|private[_-]?key|authorization|cookie|set-cookie|credential)\b\s*[:=]\s*([^\r\n,;]+)/gi;
20
- const SECRET_VALUE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:ghp|github_pat|xox[baprs]|AKIA|sk)[-_A-Za-z0-9]{12,}\b/i;
33
+ const SECRET_VALUE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b|\bsk[-_][-_A-Za-z0-9]{10,}\b/i;
21
34
  const PLACEHOLDER_SECRET_VALUE = /^(?:<[^>]+>|\$\{[^}]+\}|\*+|x+|redacted|masked|example(?:[-_].*)?|sample(?:[-_].*)?|dummy(?:[-_].*)?|test(?:[-_].*)?|your[-_].*)$/i;
22
35
  const PLACEHOLDER = /\b(?:TODO|TBD|FIXME)\b|后续补充|暂不实现|接口正常|结果正确|按实际情况处理/i;
23
36
  const CASE_SECTION_ALIASES = {
@@ -187,13 +200,170 @@ export async function inspectBackendTestEnvironment(input) {
187
200
  serverEntries,
188
201
  };
189
202
  }
190
- async function markdownFiles(root) {
191
- const directory = path.join(root, "testcase", "md");
203
+ /**
204
+ * Filenames declared by the canonical `## Module Index` section of the backend
205
+ * README. Only a unique exact `## Module Index` heading is authoritative; a
206
+ * missing/duplicate section yields an empty declaration set (callers then fall
207
+ * back to the whole directory so legacy workspaces without a manifest keep
208
+ * their prior behaviour). Reads `[x](./x.md)`, `` `testcase/md/<x>.md` `` and
209
+ * inline `x.md` tokens inside the section; README.md itself is never a module.
210
+ */
211
+ export function extractDeclaredModuleIndexFilenames(readme) {
212
+ const lines = readme
213
+ .replaceAll("\r\n", "\n")
214
+ .replaceAll("\r", "\n")
215
+ .split("\n");
216
+ const headings = lines.flatMap((line, index) => line.trim() === "## Module Index" ? [index] : []);
217
+ if (headings.length !== 1)
218
+ return [];
219
+ const start = headings[0] + 1;
220
+ const relativeEnd = lines
221
+ .slice(start)
222
+ .findIndex((line) => /^##\s+\S/.test(line.trim()));
223
+ const end = relativeEnd < 0 ? lines.length : start + relativeEnd;
224
+ const section = lines.slice(start, end).join("\n");
225
+ return unique([...section.matchAll(/`?[A-Za-z0-9_.-]+\.md`?/g)]
226
+ .map((match) => match[0].replaceAll("`", ""))
227
+ .filter((name) => name.toLowerCase() !== "readme.md"));
228
+ }
229
+ /**
230
+ * Count exact `## Module Index` headings in the backend README.
231
+ */
232
+ function moduleIndexHeadingCountIn(readme) {
233
+ return readme
234
+ .replaceAll("\r\n", "\n")
235
+ .replaceAll("\r", "\n")
236
+ .split("\n")
237
+ .filter((line) => line.trim() === "## Module Index").length;
238
+ }
239
+ /**
240
+ * Machine-readable fail-closed finding for an invalid backend-test Module Index.
241
+ * These are distinct states (missing/unreadable/duplicate/empty-unparsable) so a
242
+ * stripped Module Index can never silently fall back to a whole-directory scan.
243
+ */
244
+ export function backendManifestStatusFinding(manifestStatus, moduleIndexHeadingCount = 0) {
245
+ switch (manifestStatus) {
246
+ case "missing":
247
+ return "README is missing; backend-test current round fails closed without a declarative Module Index";
248
+ case "unreadable":
249
+ return "README is unreadable; backend-test current round fails closed without a usable Module Index";
250
+ case "duplicate":
251
+ return moduleIndexHeadingCount === 0
252
+ ? "README Module Index heading is missing; backend-test current round fails closed without a declarative Module Index"
253
+ : `duplicate ## Module Index heading (${moduleIndexHeadingCount} occurrences); backend-test current round fails closed without a unique Module Index`;
254
+ case "empty-unparsable":
255
+ return "README Module Index is empty/unparsable; backend-test current round fails closed with no declared module";
256
+ case "valid":
257
+ return "";
258
+ }
259
+ }
260
+ /**
261
+ * Resolve the current-round Markdown module manifest. The README `## Module
262
+ * Index` is the authoritative source for which module files belong to this
263
+ * round. Fail closed on every non-`valid` state (missing/unreadable/duplicate/
264
+ * empty-unparsable): the backend-test current round must NEVER re-scan the whole
265
+ * directory, or historical `.md` files could leak back into Case/coverage/
266
+ * partition facts.
267
+ *
268
+ * - `manifestStatus==="valid"` — unique heading with >= 1 declared `.md` file.
269
+ * - `missing` — README file does not exist.
270
+ * - `unreadable` — README exists but could not be read.
271
+ * - `duplicate` — README has 0 or duplicate `## Module Index`.
272
+ * - `empty-unparsable` — unique heading but no `.md` filename parsed.
273
+ * - `declaredMdFiles` — declared filenames that exist on disk (relative paths).
274
+ * - `declaredMissing` — declared filenames with no file on disk (fail-closed
275
+ * evidence, never silently dropped).
276
+ * - `unmanifested` — existing `.md` files that the Module Index does not
277
+ * declare (non-silent advisory; they must not enter Case/coverage/partition).
278
+ */
279
+ export async function resolveBackendTestManifestModules(workspaceRoot, layout) {
280
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
281
+ const readmePath = path.join(workspaceRoot, ...resolved.readmePath.split("/"));
282
+ const invalid = (manifestStatus, moduleIndexHeadingCount) => ({
283
+ hasManifest: false,
284
+ manifestStatus,
285
+ moduleIndexHeadingCount,
286
+ declaredStems: [],
287
+ declaredMdFiles: [],
288
+ declaredMissing: [],
289
+ unmanifested: [],
290
+ });
291
+ let readme = "";
292
+ if (await exists(readmePath)) {
293
+ try {
294
+ readme = await readFile(readmePath, "utf8");
295
+ }
296
+ catch {
297
+ // Unreadable README is fail-closed (not silently treated as no manifest).
298
+ return invalid("unreadable", 0);
299
+ }
300
+ }
301
+ else {
302
+ return invalid("missing", 0);
303
+ }
304
+ const moduleIndexHeadingCount = moduleIndexHeadingCountIn(readme);
305
+ if (moduleIndexHeadingCount !== 1) {
306
+ return invalid("duplicate", moduleIndexHeadingCount);
307
+ }
308
+ const declaredStems = extractDeclaredModuleIndexFilenames(readme);
309
+ if (declaredStems.length === 0) {
310
+ return invalid("empty-unparsable", moduleIndexHeadingCount);
311
+ }
312
+ const hasManifest = true;
313
+ const directory = path.join(workspaceRoot, ...resolved.markdownDir.split("/"));
314
+ let existingNames = [];
315
+ try {
316
+ existingNames = (await readdir(directory, { withFileTypes: true }))
317
+ .filter((entry) => entry.isFile() &&
318
+ entry.name.toLowerCase().endsWith(".md") &&
319
+ entry.name.toLowerCase() !== "readme.md")
320
+ .map((entry) => entry.name);
321
+ }
322
+ catch {
323
+ // missing markdown directory -> no existing modules
324
+ }
325
+ const declaredByLower = new Map();
326
+ for (const name of declaredStems) {
327
+ const base = path.posix.basename(name);
328
+ declaredByLower.set(base.toLowerCase(), base);
329
+ }
330
+ const declaredMdFiles = [];
331
+ const declaredMissing = [];
332
+ for (const base of declaredByLower.values()) {
333
+ const disk = existingNames.find((name) => name.toLowerCase() === base.toLowerCase());
334
+ if (disk)
335
+ declaredMdFiles.push(`${resolved.markdownDir}/${disk}`);
336
+ else
337
+ declaredMissing.push(`${resolved.markdownDir}/${base}`);
338
+ }
339
+ const unmanifested = existingNames
340
+ .filter((name) => !declaredByLower.has(name.toLowerCase()))
341
+ .map((name) => `${resolved.markdownDir}/${name}`);
342
+ return {
343
+ hasManifest,
344
+ manifestStatus: "valid",
345
+ moduleIndexHeadingCount,
346
+ declaredStems,
347
+ declaredMdFiles: declaredMdFiles.sort(),
348
+ declaredMissing: declaredMissing.sort(),
349
+ unmanifested: unmanifested.sort(),
350
+ };
351
+ }
352
+ export async function markdownFiles(root, layout) {
353
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
354
+ const directory = path.join(root, ...resolved.markdownDir.split("/"));
192
355
  const entries = await readdir(directory, { withFileTypes: true });
193
- return entries
356
+ const all = entries
194
357
  .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
195
358
  .map((entry) => path.join(directory, entry.name))
196
359
  .sort();
360
+ const manifest = await resolveBackendTestManifestModules(root, resolved);
361
+ if (manifest.manifestStatus !== "valid")
362
+ return [];
363
+ const declaredAbs = new Set(manifest.declaredMdFiles.map((rel) => path.join(root, ...rel.split("/")).toLowerCase()));
364
+ return all
365
+ .filter((file) => declaredAbs.has(file.toLowerCase()))
366
+ .sort();
197
367
  }
198
368
  function canonicalBackendCaseId(value) {
199
369
  const upper = value.toUpperCase();
@@ -303,6 +473,8 @@ function isCleanupStatement(value) {
303
473
  return false;
304
474
  if (/\b(?:assert|pytest\.fail|raise)\b/.test(trimmed))
305
475
  return false;
476
+ if (/\b[A-Za-z_][A-Za-z0-9_.]*request\s*\(\s*["']DELETE["']/i.test(trimmed))
477
+ return true;
306
478
  return /(?:^|[._])(?:cleanup|clean_up|teardown|delete|remove|close|release|rollback|disconnect|dispose|unlink)(?:[_(.]|$)/i.test(trimmed);
307
479
  }
308
480
  function findUnsafeSwallowedException(source) {
@@ -326,7 +498,18 @@ function findUnsafeSwallowedException(source) {
326
498
  continue;
327
499
  }
328
500
  const exceptionType = exceptMatch[2]?.trim() || "bare except";
329
- if (exceptionType !== "requests.RequestException") {
501
+ const narrowDecodeTypes = exceptionType
502
+ .replace(/[()]/g, "")
503
+ .split(",")
504
+ .map((value) => value.trim())
505
+ .filter(Boolean);
506
+ const precedingDecodeRegion = lines.slice(Math.max(0, index - 8), index).join("\n");
507
+ if (narrowDecodeTypes.length > 0 &&
508
+ narrowDecodeTypes.every((value) => value === "UnicodeDecodeError" || value === "json.JSONDecodeError") &&
509
+ /\.json\s*\(\s*\)/.test(precedingDecodeRegion)) {
510
+ continue;
511
+ }
512
+ if (!new Set(["requests.RequestException", "urllib.error.URLError", "httpx.RequestError"]).has(exceptionType)) {
330
513
  return exceptionType;
331
514
  }
332
515
  let tryIndex = index - 1;
@@ -366,9 +549,18 @@ function httpLoggingFindings(relativeScript, source) {
366
549
  if (!hasHttpRequest)
367
550
  return [];
368
551
  const findings = [];
369
- const hasRequestLog = /(?:print|log(?:ger)?\.(?:debug|info|warning|error))\s*\([^\n]*(?:request|请求)[^\n]*(?:method|url|path|params|query|json|body|payload|参数)/i.test(source);
370
- const hasResponseLog = /(?:print|log(?:ger)?\.(?:debug|info|warning|error))\s*\([^\n]*(?:response|响应)[^\n]*(?:status|status_code|body|json|text|result|结果)/i.test(source);
371
- const hasSanitizer = /\b(?:sanitize|sanitise|redact|mask)[A-Za-z0-9_]*\s*\(|\b[A-Za-z0-9_]*(?:sanitize|sanitise|redact|mask)[A-Za-z0-9_]*\s*\(/i.test(source);
552
+ const hasRequestLog = /(?:print|log(?:ger)?\.(?:debug|info|warning|error))\s*\([^\n]*(?:request|请求)[^\n]*(?:method|url|path|params|query|json|body|payload|参数)/i.test(source)
553
+ || /["']record["']\s*:\s*["']request["'][\s\S]{0,600}["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source)
554
+ || /\b[A-Za-z_][A-Za-z0-9_]*log[A-Za-z0-9_]*\s*\(\s*["'](?:HTTP|API)_REQUEST["'][\s\S]{0,600}["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source)
555
+ || /print\s*\(\s*f?["'](?:HTTP|API)(?:_|\s+)REQUEST\b/i.test(source) && /["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source)
556
+ || /\b[A-Za-z_][A-Za-z0-9_]*log\s*\(\s*["']REQUEST["'][\s\S]{0,600}["'](?:method|url|path|params|query|json|body|payload)["']\s*:/i.test(source);
557
+ const hasResponseLog = /(?:print|log(?:ger)?\.(?:debug|info|warning|error))\s*\([^\n]*(?:response|响应)[^\n]*(?:status|status_code|body|json|text|result|结果)/i.test(source)
558
+ || /["']record["']\s*:\s*["']response["'][\s\S]{0,600}["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source)
559
+ || /\b[A-Za-z_][A-Za-z0-9_]*log[A-Za-z0-9_]*\s*\(\s*["'](?:HTTP|API)_RESPONSE["'][\s\S]{0,600}["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source)
560
+ || /print\s*\(\s*f?["'](?:HTTP|API)(?:_|\s+)RESPONSE\b/i.test(source) && /["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source)
561
+ || /\b[A-Za-z_][A-Za-z0-9_]*log\s*\(\s*["']RESPONSE["'][\s\S]{0,600}["'](?:status|status_code|body|json|text|result)["']\s*:/i.test(source);
562
+ const hasSanitizer = /\b(?:sanitize|sanitise|redact|mask)[A-Za-z0-9_]*\s*\(|\b[A-Za-z0-9_]*(?:sanitize|sanitise|redact|mask)[A-Za-z0-9_]*\s*\(/i.test(source)
563
+ || /(?:SENSITIVE_KEYS|sensitive_keys)[\s\S]{0,1600}["']<redacted>["']/i.test(source);
372
564
  const hasBoundedOutput = /(?:MAX_[A-Z_]*(?:LOG|BODY|RESPONSE)|max_(?:log|body|response)|truncate|truncat|\[:\s*\d{3,}\s*\])/i.test(source);
373
565
  if (!hasRequestLog)
374
566
  findings.push(`request logging evidence is missing in mapped backend pytest: ${relativeScript}`);
@@ -403,22 +595,34 @@ export function hasBlockingBackendMarkdownModuleStemFindings(report) {
403
595
  return /^## Module Stem Status\s*\r?\n\s*BLOCKED\s*$/m.test(report);
404
596
  }
405
597
  export async function validateBackendMarkdownCases(input) {
598
+ const layout = input.layout ?? resolveBackendTestLayout(undefined);
406
599
  const findings = [];
407
600
  const safetyFindings = [];
408
601
  const moduleStemFindings = [];
409
602
  let files = [];
410
603
  try {
411
- files = await markdownFiles(input.workspaceRoot);
604
+ files = await markdownFiles(input.workspaceRoot, layout);
412
605
  }
413
606
  catch {
414
- findings.push("missing testcase/md directory");
607
+ findings.push(`missing ${layout.markdownDir} directory`);
415
608
  }
416
- const readme = path.join(input.workspaceRoot, "testcase", "md", "README.md");
609
+ const readme = path.join(input.workspaceRoot, ...layout.readmePath.split("/"));
417
610
  if (!(await exists(readme)))
418
- findings.push("missing testcase/md/README.md");
611
+ findings.push(`missing ${layout.readmePath}`);
612
+ const manifest = await resolveBackendTestManifestModules(input.workspaceRoot, layout);
613
+ // Fail closed on README Module Index entries that were declared but never
614
+ // materialized — never let a declared module silently vanish from the round.
615
+ if (manifest.declaredMissing.length > 0) {
616
+ findings.push(`declared Markdown module is missing (Module Index): ${manifest.declaredMissing.join(", ")}`);
617
+ }
618
+ // Fail closed on every non-valid Module Index state: without a valid unique
619
+ // Module Index the current round must not re-scan historical `.md` files.
620
+ if (manifest.manifestStatus !== "valid") {
621
+ findings.push(backendManifestStatusFinding(manifest.manifestStatus, manifest.moduleIndexHeadingCount));
622
+ }
419
623
  const moduleFiles = files.filter((file) => path.basename(file).toLowerCase() !== "readme.md");
420
624
  if (moduleFiles.length === 0)
421
- findings.push("no testcase/md module case file found");
625
+ findings.push(`no ${layout.markdownDir} module case file found`);
422
626
  const seen = new Map();
423
627
  const coveredAc = new Set();
424
628
  let caseCount = 0;
@@ -436,6 +640,18 @@ export async function validateBackendMarkdownCases(input) {
436
640
  if (PLACEHOLDER.test(markdown)) {
437
641
  findings.push(`placeholder or non-assertable wording in ${relativeFile}`);
438
642
  }
643
+ const sharedSetupRefs = extractBackendSharedSetupRefs(markdown);
644
+ for (const ref of sharedSetupRefs) {
645
+ if (!input.sharedSetup) {
646
+ findings.push(`shared setup reference without a bound document: ${ref.raw} in ${relativeFile}`);
647
+ }
648
+ else if (ref.path !== input.sharedSetup.path) {
649
+ findings.push(`shared setup reference cites unbound path ${ref.path} (bound: ${input.sharedSetup.path}) in ${relativeFile}`);
650
+ }
651
+ else if (ref.anchor !== "SS-DEFAULT" && !sharedSetupAnchorRe.test(ref.anchor)) {
652
+ findings.push(`shared setup reference anchor is not a stable SS ID and not SS-DEFAULT: ${ref.raw} in ${relativeFile}`);
653
+ }
654
+ }
439
655
  const cases = splitCases(markdown);
440
656
  if (cases.length === 0) {
441
657
  findings.push(`no BE-<MODULE>-<NNN> case heading in ${relativeFile}`);
@@ -459,6 +675,15 @@ export async function validateBackendMarkdownCases(input) {
459
675
  findings.push(`${testCase.id} missing section: ${sectionAliases.join(" or ")}`);
460
676
  }
461
677
  }
678
+ const bindingFinding = markdownTestPointBindingFinding(testCase.id, testCase.body);
679
+ if (bindingFinding)
680
+ findings.push(bindingFinding);
681
+ const payloadLabelFinding = markdownPayloadLabelFinding(testCase.id, testCase.body);
682
+ if (payloadLabelFinding)
683
+ findings.push(payloadLabelFinding);
684
+ const scenarioIntentFinding = markdownScenarioIntentFinding(testCase.id, testCase.body);
685
+ if (scenarioIntentFinding)
686
+ findings.push(scenarioIntentFinding);
462
687
  const acIds = unique(testCase.body.match(AC_ID) ?? []);
463
688
  acIds.forEach((id) => coveredAc.add(id));
464
689
  const steps = sectionBody(testCase.body, CASE_SECTION_ALIASES.steps);
@@ -469,9 +694,12 @@ export async function validateBackendMarkdownCases(input) {
469
694
  if (!hasAssertableExpectedResult(expected)) {
470
695
  findings.push(`${testCase.id} has no structured expected result`);
471
696
  }
472
- const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile);
697
+ const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile, layout);
473
698
  const mappedScripts = extractMappedPytestScripts(testCase.body);
474
- if (mappedScripts.length === 0) {
699
+ if (isEvidenceOnlyMarkdownCaseBody(testCase.body)) {
700
+ // Evidence-only meta cases intentionally declare 脚本/primary symbol as 无 and keep only assertion/cross-cutting TPs.
701
+ }
702
+ else if (mappedScripts.length === 0) {
475
703
  findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射 (expected ${expectedScript} from module ${path.basename(relativeFile)})`);
476
704
  }
477
705
  else {
@@ -501,6 +729,7 @@ export async function validateBackendMarkdownCases(input) {
501
729
  `- Cases: ${caseCount}`,
502
730
  `- Covered AC IDs: ${[...coveredAc].sort().join(", ") || "none"}`,
503
731
  `- Environment evidence present: ${input.environmentMarkdown.trim() ? "yes" : "no"}`,
732
+ `- Manifest scope: ${manifest.manifestStatus === "valid" ? "declared-modules" : `fails-closed:${manifest.manifestStatus}`} (declared-missing=${manifest.declaredMissing.length}, unmanifested-advisory=${manifest.unmanifested.length}, module-index-heading-count=${manifest.moduleIndexHeadingCount})`,
504
733
  `- Findings: ${findings.length}`,
505
734
  "- Required sections: Preconditions/前置条件, Steps/操作步骤, Expected Results/预期结果",
506
735
  "- Source-reference validity check: not applied",
@@ -522,6 +751,13 @@ export async function validateBackendMarkdownCases(input) {
522
751
  ? safetyFindings.map((finding) => `- ${finding}`)
523
752
  : ["- No secret-shaped values detected."]),
524
753
  "",
754
+ "## Unmanifested Markdown Modules",
755
+ "",
756
+ "- Advisory: existing .md files not declared by the README Module Index are excluded from this round's Case/coverage/partition scope.",
757
+ ...(manifest.unmanifested.length > 0
758
+ ? manifest.unmanifested.map((item) => `- ${item}`)
759
+ : ["- None"]),
760
+ "",
525
761
  "## Findings",
526
762
  "",
527
763
  ...(findings.length > 0 ? findings.map((finding) => `- ${finding}`) : ["- None"]),
@@ -549,8 +785,9 @@ export function normalizeBackendTestModuleStem(moduleFileName) {
549
785
  return stem;
550
786
  }
551
787
  /** Expected pytest script path for a Markdown module file (basename or relative path). */
552
- export function expectedBackendTestPytestScriptForMarkdownModule(moduleFileName) {
553
- return `testcase/test_${normalizeBackendTestModuleStem(moduleFileName)}.py`;
788
+ export function expectedBackendTestPytestScriptForMarkdownModule(moduleFileName, layout) {
789
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
790
+ return `${resolved.scriptDir}/test_${normalizeBackendTestModuleStem(moduleFileName)}.py`;
554
791
  }
555
792
  function extractMappedPytestScripts(testCaseBody) {
556
793
  const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
@@ -560,12 +797,79 @@ function extractMappedPytestScripts(testCaseBody) {
560
797
  .map((match) => match[1].replaceAll("\\", "/"))
561
798
  .filter((value) => !value.includes("..")));
562
799
  }
563
- function isSafeBackendPytestScript(script, workspaceRoot) {
800
+ function markdownScenarioIntentFinding(testCaseId, testCaseBody) {
801
+ const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
802
+ if (!automation.trim())
803
+ return null;
804
+ const variantLine = /^(?:\s*[-*+]\s*)?(?:变体测试点|Variant Test Points)\s*[::]\s*(.*)$/im.exec(automation)?.[1] ?? "";
805
+ const variants = unique(variantLine.match(/\bTP-[A-Z0-9-]+\b/g) ?? []);
806
+ if (variants.length === 0)
807
+ return null;
808
+ const localIntentIds = unique([...testCaseBody.matchAll(/(?:场景意图|Scenario Intent)\s*[::]\s*(TP-[A-Z0-9-]+)/gi)].map((match) => match[1].toUpperCase()));
809
+ const missing = variants.filter((id) => !localIntentIds.includes(id.toUpperCase()));
810
+ return missing.length > 0
811
+ ? `${testCaseId} variant Test Points require a local Scenario Intent line inside the same Case: missing=${missing.join(", ")}`
812
+ : null;
813
+ }
814
+ function markdownPayloadLabelFinding(testCaseId, testCaseBody) {
815
+ const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
816
+ if (!automation.trim())
817
+ return null;
818
+ const payloadLabel = /Payload (?:Contract|Required Paths|Allowed Paths|Enum)\s*[::]/gi;
819
+ for (const line of automation.split(/\r?\n/)) {
820
+ const matches = line.match(payloadLabel) ?? [];
821
+ if (matches.length === 0)
822
+ continue;
823
+ if (matches.length !== 1 || !/^\s*[-*+]\s*Payload (?:Contract|Required Paths|Allowed Paths|Enum)\s*[::]/i.test(line)) {
824
+ return `${testCaseId} payload contract labels must each occupy one independent machine-readable list line`;
825
+ }
826
+ }
827
+ return null;
828
+ }
829
+ function markdownTestPointBindingFinding(testCaseId, testCaseBody) {
830
+ const declaredSection = sectionBody(testCaseBody, ["测试点", "Test Points"]);
831
+ const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
832
+ if (!declaredSection.trim() || !automation.trim())
833
+ return null;
834
+ const declared = unique(declaredSection.match(/\bTP-[A-Z0-9-]+\b/g) ?? []);
835
+ const bindingLines = [
836
+ /^(?:\s*[-*+]\s*)?(?:变体测试点|Variant Test Points)\s*[::]\s*(.*)$/gim,
837
+ /^(?:\s*[-*+]\s*)?(?:场景断言测试点|Assertion Test Points)\s*[::]\s*(.*)$/gim,
838
+ /^(?:\s*[-*+]\s*)?(?:横切证据测试点|Cross-Cutting Test Points)\s*[::]\s*(.*)$/gim,
839
+ ];
840
+ const bound = unique(bindingLines.flatMap((pattern) => [...automation.matchAll(pattern)].flatMap((match) => match[1]?.match(/\bTP-[A-Z0-9-]+\b/g) ?? [])));
841
+ const missingBindings = declared.filter((id) => !bound.includes(id));
842
+ const undeclaredBindings = bound.filter((id) => !declared.includes(id));
843
+ if (missingBindings.length === 0 && undeclaredBindings.length === 0)
844
+ return null;
845
+ return `${testCaseId} Test Point set must exactly equal variant/assertion/cross-cutting binding union: unclassified=${missingBindings.join(", ") || "none"}; undeclared-bindings=${undeclaredBindings.join(", ") || "none"}`;
846
+ }
847
+ function isEvidenceOnlyMarkdownCaseBody(testCaseBody) {
848
+ const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
849
+ if (!automation.trim())
850
+ return false;
851
+ const scriptLine = /^\s*[-*+]\s*(?:脚本|script)\s*[::]\s*(.*)$/im.exec(automation)?.[1]?.replaceAll("`", "").trim() ??
852
+ "";
853
+ const primaryLine = /^\s*[-*+]\s*(?:primary symbol|Primary Symbol|主测试符号)\s*[::]\s*(.*)$/im.exec(automation)?.[1]
854
+ ?.replaceAll("`", "")
855
+ .trim() ?? "";
856
+ const noneLike = /^(?:none|无|n\/a|not-applicable)(?:\s*[((].*[))])?$/i;
857
+ const scriptIsNone = noneLike.test(scriptLine) || scriptLine.length === 0;
858
+ const primaryIsNone = noneLike.test(primaryLine) || primaryLine.length === 0;
859
+ const hasMappedScript = extractMappedPytestScripts(testCaseBody).length > 0;
860
+ const hasPrimary = /\btest_[A-Za-z0-9_]+\b/.test(primaryLine);
861
+ const hasVariant = /(?:变体测试点|Variant Test Points)\s*[::]\s*(?!\s*(?:无|none)\b)[^\n]*\bTP-[A-Z0-9-]+/i.test(automation);
862
+ const hasAssertionOrCross = /(?:场景断言测试点|Assertion Test Points|横切证据测试点|Cross-Cutting Test Points)\s*[::]\s*(?!\s*(?:无|none)\b)[^\n]*\bTP-[A-Z0-9-]+/i.test(automation);
863
+ return scriptIsNone && primaryIsNone && !hasMappedScript && !hasPrimary && !hasVariant && hasAssertionOrCross;
864
+ }
865
+ function isSafeBackendPytestScript(script, workspaceRoot, layout) {
866
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
564
867
  const absolute = path.resolve(workspaceRoot, script);
565
868
  const relative = path.relative(workspaceRoot, absolute).replaceAll(path.sep, "/");
869
+ const allowedRoots = [`${resolved.testRoot}/`];
566
870
  return (!relative.startsWith("..") &&
567
871
  !path.isAbsolute(relative) &&
568
- script.startsWith("testcase/") &&
872
+ allowedRoots.some((prefix) => script.startsWith(prefix)) &&
569
873
  !script.includes("..") &&
570
874
  /^test_.*\.py$/i.test(path.basename(script)));
571
875
  }
@@ -609,8 +913,8 @@ function symbolCaseId(symbol) {
609
913
  ? canonicalBackendCaseId(match[1].replaceAll("_", "-"))
610
914
  : undefined;
611
915
  }
612
- export async function validateBackendMarkdownTraceability(workspaceRoot) {
613
- const files = await markdownFiles(workspaceRoot);
916
+ export async function validateBackendMarkdownTraceability(workspaceRoot, layout) {
917
+ const files = await markdownFiles(workspaceRoot, layout);
614
918
  const markdownIds = new Set();
615
919
  const expectedIdsByScript = new Map();
616
920
  const findings = [];
@@ -619,8 +923,11 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
619
923
  continue;
620
924
  const content = await readFile(file, "utf8");
621
925
  for (const testCase of splitCases(content)) {
622
- markdownIds.add(testCase.id);
623
926
  const scripts = extractMappedPytestScripts(testCase.body);
927
+ if (isEvidenceOnlyMarkdownCaseBody(testCase.body)) {
928
+ continue;
929
+ }
930
+ markdownIds.add(testCase.id);
624
931
  if (scripts.length === 0) {
625
932
  findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射`);
626
933
  continue;
@@ -632,6 +939,7 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
632
939
  }
633
940
  }
634
941
  }
942
+ const resolvedLayout = layout ?? resolveBackendTestLayout(undefined);
635
943
  const mappings = new Map();
636
944
  const automationIds = new Set();
637
945
  let requestResponseLoggingFindingCount = 0;
@@ -640,7 +948,7 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
640
948
  const relative = path.relative(workspaceRoot, absolute);
641
949
  if (relative.startsWith("..") ||
642
950
  path.isAbsolute(relative) ||
643
- !relativeScript.startsWith("testcase/") ||
951
+ !relativeScript.startsWith(`${resolvedLayout.testRoot}/`) ||
644
952
  !/^test_.*\.py$/i.test(path.basename(relativeScript))) {
645
953
  findings.push(`unsafe mapped pytest script: ${relativeScript}`);
646
954
  continue;
@@ -713,8 +1021,9 @@ export async function validateBackendMarkdownTraceability(workspaceRoot) {
713
1021
  ...(findings.length > 0 ? findings.map((finding) => `- ${finding}`) : ["- None"]),
714
1022
  ].join("\n") + "\n");
715
1023
  }
716
- export async function resolveBackendTestMappedPytestScripts(workspaceRoot) {
717
- const files = await markdownFiles(workspaceRoot);
1024
+ export async function resolveBackendTestMappedPytestScripts(workspaceRoot, layout) {
1025
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
1026
+ const files = await markdownFiles(workspaceRoot, resolved);
718
1027
  const expectedScripts = new Set();
719
1028
  const existingScripts = new Set();
720
1029
  const missingScripts = new Set();
@@ -735,7 +1044,7 @@ export async function resolveBackendTestMappedPytestScripts(workspaceRoot) {
735
1044
  if (path.basename(file).toLowerCase() === "readme.md")
736
1045
  continue;
737
1046
  const relativeFile = path.relative(workspaceRoot, file).replaceAll(path.sep, "/");
738
- const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile);
1047
+ const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile, resolved);
739
1048
  const markdown = await readFile(file, "utf8");
740
1049
  const mappedFromModule = new Set();
741
1050
  for (const testCase of splitCases(markdown)) {
@@ -755,14 +1064,14 @@ export async function resolveBackendTestMappedPytestScripts(workspaceRoot) {
755
1064
  continue;
756
1065
  }
757
1066
  for (const script of mappedFromModule) {
758
- if (!isSafeBackendPytestScript(script, workspaceRoot)) {
1067
+ if (!isSafeBackendPytestScript(script, workspaceRoot, resolved)) {
759
1068
  throw new Error(`unsafe mapped pytest script: ${script}`);
760
1069
  }
761
1070
  if (await mappedScriptExists(path.resolve(workspaceRoot, script))) {
762
1071
  recordScript(script, relativeFile, true);
763
1072
  continue;
764
1073
  }
765
- if (!isSafeBackendPytestScript(expectedScript, workspaceRoot)) {
1074
+ if (!isSafeBackendPytestScript(expectedScript, workspaceRoot, resolved)) {
766
1075
  throw new Error(`unsafe mapped pytest script: ${expectedScript}`);
767
1076
  }
768
1077
  recordScript(expectedScript, relativeFile, await mappedScriptExists(path.resolve(workspaceRoot, expectedScript)));
@@ -777,8 +1086,8 @@ export async function resolveBackendTestMappedPytestScripts(workspaceRoot) {
777
1086
  missingScripts: unique(missingScripts),
778
1087
  };
779
1088
  }
780
- export async function collectBackendTestMappedPytestScripts(workspaceRoot) {
781
- const resolution = await resolveBackendTestMappedPytestScripts(workspaceRoot);
1089
+ export async function collectBackendTestMappedPytestScripts(workspaceRoot, layout) {
1090
+ const resolution = await resolveBackendTestMappedPytestScripts(workspaceRoot, layout);
782
1091
  if (resolution.missingScripts.length > 0) {
783
1092
  throw new Error(`mapped pytest script is missing: ${resolution.missingScripts[0]}`);
784
1093
  }
@@ -791,8 +1100,8 @@ function cleanCaseTitle(rawHeading, id) {
791
1100
  .replace(/^BE-[A-Z0-9_-]+-\d{2,3}\s*(?:[||—–-]\s*)?/i, "")
792
1101
  .trim() || id;
793
1102
  }
794
- export async function collectBackendTestHumanCaseCatalog(workspaceRoot) {
795
- const files = await markdownFiles(workspaceRoot);
1103
+ export async function collectBackendTestHumanCaseCatalog(workspaceRoot, layout) {
1104
+ const files = await markdownFiles(workspaceRoot, layout);
796
1105
  const catalog = new Map();
797
1106
  for (const file of files) {
798
1107
  if (path.basename(file).toLowerCase() === "readme.md")
@@ -814,7 +1123,8 @@ export async function collectBackendTestHumanCaseCatalog(workspaceRoot) {
814
1123
  });
815
1124
  }
816
1125
  }
817
- const testcaseRoot = path.join(workspaceRoot, "testcase");
1126
+ const resolvedLayout = layout ?? resolveBackendTestLayout(undefined);
1127
+ const testcaseRoot = path.join(workspaceRoot, ...resolvedLayout.testRoot.split("/"));
818
1128
  const pythonFiles = [];
819
1129
  async function walk(directory) {
820
1130
  for (const entry of await readdir(directory, { withFileTypes: true })) {
@@ -1,5 +1,7 @@
1
1
  import path from "node:path";
2
2
  const PRIORITY_ONLY_MODULE_STEMS = new Set(["p0", "p1", "p2"]);
3
+ const OPAQUE_HASH_MODULE_STEM = /^[a-f][a-f0-9]{6,63}$/;
4
+ export const BACKEND_TEST_MAX_MODULE_COUNT = 8;
3
5
  export function normalizeBackendTestModuleStemCandidate(raw) {
4
6
  return path.basename(raw)
5
7
  .replace(/\.(?:md|py)$/i, "")
@@ -11,6 +13,9 @@ export function normalizeBackendTestModuleStemCandidate(raw) {
11
13
  export function isPriorityOnlyBackendTestModuleStem(raw) {
12
14
  return PRIORITY_ONLY_MODULE_STEMS.has(normalizeBackendTestModuleStemCandidate(raw));
13
15
  }
16
+ export function isOpaqueHashBackendTestModuleStem(raw) {
17
+ return OPAQUE_HASH_MODULE_STEM.test(normalizeBackendTestModuleStemCandidate(raw));
18
+ }
14
19
  export function isPriorityOnlyBackendPytestScript(script) {
15
20
  const basename = path.posix.basename(script.replaceAll("\\", "/"));
16
21
  const match = /^test_(.+)\.py$/i.exec(basename);