@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
@@ -0,0 +1,321 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Plan C: deterministic query/filter axis scenario partitions.
4
+ *
5
+ * A README `## Scenario Partitions` table declares one partition per
6
+ * filterable enum/classification axis on an affected list/query operation.
7
+ * The deterministic expander turns each declared partition into machine
8
+ * slots — the model never invents slot IDs:
9
+ *
10
+ * each-value one slot per legal domain value (intent=nominal-filter)
11
+ * omitted only when the contract marks the parameter optional
12
+ * not-in-set exactly one complement slot (intent=enum-invalid)
13
+ *
14
+ * `not-in-set` expected semantics must come from the bound source; when the
15
+ * source does not document the complement expectation, the partition row
16
+ * keeps the slot but the expected column stays GAP so N6 reports it instead
17
+ * of guessing "empty list" or "400".
18
+ */
19
+ export const scenarioPartitionRowSchema = z.object({
20
+ partitionId: z.string().min(1),
21
+ operation: z.string().min(1),
22
+ axis: z.string().min(1),
23
+ /** Legal values copied verbatim from the bound OpenAPI enum / requirement sentence. */
24
+ domain: z.array(z.string().min(1)).min(1),
25
+ /** true when the parameter is optional and the omitted slot is required. */
26
+ optional: z.boolean(),
27
+ /**
28
+ * Complement expectation sourced from the contract. `unknown` keeps the
29
+ * not-in-set slot but marks expected as GAP (never guessed).
30
+ */
31
+ complementExpected: z.enum(["empty-result", "excluded-result", "unknown"]),
32
+ /** Which bind rule this partition enforces (advisory provenance). */
33
+ bindRule: z.string().min(1),
34
+ }).strict();
35
+ const PARTITION_HEADING = /^##\s+Scenario Partitions\s*$/;
36
+ const PARTITION_TABLE_HEADER = /^\|\s*Partition ID\s*\|\s*Operation\s*\|\s*Axis\s*\|\s*Domain\s*\|\s*Required Slots\s*\|\s*Expected by Slot\s*\|\s*Bind Rule\s*\|\s*$/;
37
+ const TABLE_ROW = /^\|(.+)\|$/;
38
+ const FINITE_DOMAIN_MEMBER = /^[A-Za-z0-9]+(?:[._\- ][A-Za-z0-9]+)*$/;
39
+ const RESERVED_SLOT_TOKENS = new Set(["OMITTED", "NOT-IN-SET"]);
40
+ function splitCells(raw) {
41
+ return raw
42
+ .split("|")
43
+ .map((cell) => cell.trim())
44
+ .filter((cell, index, cells) => !(cell === "" && (index === 0 || index === cells.length - 1)));
45
+ }
46
+ /**
47
+ * Parse the `## Scenario Partitions` section of the README into declared
48
+ * partitions. Returns { partitions, issues }; issues are fail-closed parse
49
+ * problems (malformed table), while semantic gaps surface in expansion.
50
+ */
51
+ export function parseScenarioPartitions(readmeMarkdown) {
52
+ const issues = [];
53
+ const partitions = [];
54
+ const lines = readmeMarkdown.split("\n");
55
+ const headingIndex = lines.findIndex((line) => PARTITION_HEADING.test(line));
56
+ if (headingIndex === -1)
57
+ return { partitions, issues };
58
+ // find table header within the section (before the next ## heading)
59
+ let headerIndex = -1;
60
+ for (let i = headingIndex + 1; i < lines.length; i++) {
61
+ if (/^##\s+/.test(lines[i]) && !PARTITION_HEADING.test(lines[i]))
62
+ break;
63
+ if (PARTITION_TABLE_HEADER.test(lines[i])) {
64
+ headerIndex = i;
65
+ break;
66
+ }
67
+ }
68
+ if (headerIndex === -1) {
69
+ issues.push("MISSING_PARTITION_TABLE: Scenario Partitions section has no canonical header row");
70
+ return { partitions, issues };
71
+ }
72
+ const seen = new Set();
73
+ for (let i = headerIndex + 2; i < lines.length; i++) {
74
+ const line = lines[i];
75
+ if (!TABLE_ROW.test(line)) {
76
+ if (/^##\s+/.test(line) || /^#\s+/.test(line))
77
+ break;
78
+ continue;
79
+ }
80
+ const cells = splitCells(line);
81
+ if (cells.length !== 7) {
82
+ issues.push(`MISSING_PARTITION: row has ${cells.length} cells, expected 7: ${line.trim()}`);
83
+ continue;
84
+ }
85
+ const [partitionId, operation, axis, domainRaw, requiredSlotsRaw, expectedBySlotRaw, bindRule,] = cells;
86
+ const domain = domainRaw
87
+ .split(/[;,,;]/)
88
+ .map((value) => value.trim())
89
+ .filter((value) => value.length > 0);
90
+ if (domain.length === 0) {
91
+ issues.push(`MISSING_PARTITION: empty domain: ${partitionId}`);
92
+ continue;
93
+ }
94
+ const optional = /omit/i.test(requiredSlotsRaw);
95
+ const complementExpected = /empty[- ]?result/i.test(expectedBySlotRaw)
96
+ ? "empty-result"
97
+ : /excluded[- ]?result/i.test(expectedBySlotRaw)
98
+ ? "excluded-result"
99
+ : "unknown";
100
+ if (seen.has(partitionId)) {
101
+ issues.push(`MISSING_PARTITION: duplicate partition id ${partitionId}`);
102
+ continue;
103
+ }
104
+ seen.add(partitionId);
105
+ const domainIssue = describeInvalidPartitionDomain(domain);
106
+ if (domainIssue) {
107
+ issues.push(`INVALID_PARTITION_DOMAIN: partition ${partitionId} ${domainIssue}`);
108
+ continue;
109
+ }
110
+ partitions.push({
111
+ partitionId,
112
+ operation,
113
+ axis,
114
+ domain,
115
+ optional,
116
+ complementExpected,
117
+ bindRule,
118
+ });
119
+ }
120
+ return { partitions, issues };
121
+ }
122
+ function slotToken(value) {
123
+ return value
124
+ .trim()
125
+ .toUpperCase()
126
+ .replace(/[^A-Z0-9]+/g, "-")
127
+ .replace(/^-+|-+$/g, "");
128
+ }
129
+ function describeInvalidPartitionDomain(domain) {
130
+ const tokens = [];
131
+ const seenTokens = new Set();
132
+ for (const value of domain) {
133
+ if (!FINITE_DOMAIN_MEMBER.test(value)) {
134
+ return `value is not a finite identifier member: ${value}`;
135
+ }
136
+ const token = slotToken(value);
137
+ if (!token) {
138
+ return `value cannot produce a stable non-empty slot token: ${value}`;
139
+ }
140
+ if (RESERVED_SLOT_TOKENS.has(token)) {
141
+ return `value maps to reserved slot token ${token}: ${value}`;
142
+ }
143
+ if (seenTokens.has(token)) {
144
+ return `values collapse to duplicate slot token ${token}`;
145
+ }
146
+ seenTokens.add(token);
147
+ tokens.push(token);
148
+ }
149
+ return tokens.length === 0 ? "domain produced no slot tokens" : undefined;
150
+ }
151
+ function normalizePartitionAxisName(value) {
152
+ return value.trim().replace(/^(?:query|path)\s*:\s*/i, "").toLowerCase();
153
+ }
154
+ function normalizePartitionOperation(value) {
155
+ return value.trim().replace(/\s+/g, " ").toUpperCase();
156
+ }
157
+ function sameFiniteSet(left, right) {
158
+ if (left.length !== right.length)
159
+ return false;
160
+ const expected = new Set(left);
161
+ return right.every((value) => expected.has(value));
162
+ }
163
+ const STABLE_PARTITION_ID = /^SP-[A-Z0-9][A-Z0-9._-]*$/i;
164
+ /**
165
+ * Slot IDs are `TP-<Partition ID>-<VALUE|OMITTED|NOT-IN-SET>`.
166
+ * The declared Partition ID is the single source of truth so model-written
167
+ * `TP-SP-GET-API-...` IDs match the expander (never drop the HTTP method).
168
+ */
169
+ function partitionToken(row) {
170
+ const id = row.partitionId.trim();
171
+ if (STABLE_PARTITION_ID.test(id))
172
+ return `TP-${id.toUpperCase()}`;
173
+ return `TP-SP-${slotToken(row.operation)}-${slotToken(row.axis)}`;
174
+ }
175
+ /**
176
+ * Deterministically expand declared partitions into slots. Pure function:
177
+ * same input rows always produce the same slot IDs.
178
+ */
179
+ export function expandScenarioPartitions(partitions) {
180
+ const slots = [];
181
+ for (const row of partitions) {
182
+ if (describeInvalidPartitionDomain(row.domain))
183
+ continue;
184
+ const prefix = partitionToken(row);
185
+ for (const value of row.domain) {
186
+ const token = slotToken(value);
187
+ slots.push({
188
+ partitionId: row.partitionId,
189
+ operation: row.operation,
190
+ axis: row.axis,
191
+ slotId: `${prefix}-${token}`,
192
+ kind: "each-value",
193
+ value,
194
+ intent: "nominal-filter",
195
+ expected: "domain-value",
196
+ });
197
+ }
198
+ if (row.optional) {
199
+ slots.push({
200
+ partitionId: row.partitionId,
201
+ operation: row.operation,
202
+ axis: row.axis,
203
+ slotId: `${prefix}-OMITTED`,
204
+ kind: "omitted",
205
+ value: "",
206
+ intent: "omitted",
207
+ expected: "default-behavior",
208
+ });
209
+ }
210
+ slots.push({
211
+ partitionId: row.partitionId,
212
+ operation: row.operation,
213
+ axis: row.axis,
214
+ slotId: `${prefix}-NOT-IN-SET`,
215
+ kind: "not-in-set",
216
+ value: "",
217
+ intent: "enum-invalid",
218
+ expected: row.complementExpected === "unknown" ? "GAP" : "complement",
219
+ });
220
+ }
221
+ return slots;
222
+ }
223
+ /**
224
+ * Bind parsed partitions to GET query/path OpenAPI axes.
225
+ * Unconfirmed axes keep identifier-valid Domain rows (requirement finite sets).
226
+ */
227
+ export function applyOpenApiPartitionDomainPolicy(partitions, axes) {
228
+ const issues = [];
229
+ const valid = [];
230
+ for (const row of partitions) {
231
+ const operation = normalizePartitionOperation(row.operation);
232
+ const axisName = normalizePartitionAxisName(row.axis);
233
+ const matched = axes.find((axis) => normalizePartitionOperation(axis.operation) === operation &&
234
+ normalizePartitionAxisName(axis.name) === axisName);
235
+ if (!matched) {
236
+ valid.push(row);
237
+ continue;
238
+ }
239
+ if (matched.enumValues === null) {
240
+ issues.push(`NO_FINITE_DOMAIN: partition ${row.partitionId} axis ${row.axis} exists on ${row.operation} without a documented enum`);
241
+ continue;
242
+ }
243
+ if (!sameFiniteSet(matched.enumValues, row.domain)) {
244
+ issues.push(`PARTITION_SOURCE_MISMATCH: partition ${row.partitionId} domain does not match OpenAPI enum for ${row.operation} ${row.axis}`);
245
+ continue;
246
+ }
247
+ valid.push(row);
248
+ }
249
+ return { partitions: valid, issues };
250
+ }
251
+ /** Facts projection consumed by N6 and plan D gap-fill. */
252
+ export const scenarioPartitionFactsSchema = z.object({
253
+ partitions: z.array(z.object({
254
+ partitionId: z.string(),
255
+ operation: z.string(),
256
+ axis: z.string(),
257
+ domain: z.array(z.string()),
258
+ optional: z.boolean(),
259
+ complementExpected: z.string(),
260
+ bindRule: z.string(),
261
+ slotIds: z.array(z.string()),
262
+ })),
263
+ slots: z.array(z.object({
264
+ slotId: z.string(),
265
+ partitionId: z.string(),
266
+ kind: z.string(),
267
+ intent: z.string(),
268
+ expected: z.string(),
269
+ })),
270
+ missingSlots: z.array(z.string()),
271
+ });
272
+ /**
273
+ * Compare expanded slots against the Case test-point universe. A slot is
274
+ * covered only when its exact slot ID is declared in some Case's 测试点 or
275
+ * binding lists. Missing each-value/omitted slots and the complement slot
276
+ * produce fail-closed finding codes; a GAP-expected complement that IS
277
+ * covered still reports a coverage gap (expected semantics undocumented).
278
+ */
279
+ export function assessScenarioPartitionCoverage(partitions, declaredTestPointIds) {
280
+ const slots = expandScenarioPartitions(partitions);
281
+ const findings = [];
282
+ const missingSlots = [];
283
+ for (const row of partitions) {
284
+ const rowSlots = slots.filter((slot) => slot.partitionId === row.partitionId);
285
+ for (const slot of rowSlots) {
286
+ if (!declaredTestPointIds.has(slot.slotId)) {
287
+ missingSlots.push(slot.slotId);
288
+ const code = slot.kind === "not-in-set" ? "MISSING_COMPLEMENT_SLOT" : "MISSING_PARTITION_SLOT";
289
+ findings.push(`${code}: partition ${row.partitionId} slot ${slot.slotId} (${slot.kind}${slot.value ? `=${slot.value}` : ""}) is not covered by any Case test point`);
290
+ }
291
+ else if (slot.kind === "not-in-set" && slot.expected === "GAP") {
292
+ findings.push(`PARTITION_COMPLEMENT_EXPECTED_GAP: partition ${row.partitionId} complement slot ${slot.slotId} has no documented expected semantics in the bound source; expected stays GAP instead of guessing`);
293
+ }
294
+ }
295
+ }
296
+ return {
297
+ facts: {
298
+ partitions: partitions.map((row) => ({
299
+ partitionId: row.partitionId,
300
+ operation: row.operation,
301
+ axis: row.axis,
302
+ domain: row.domain,
303
+ optional: row.optional,
304
+ complementExpected: row.complementExpected,
305
+ bindRule: row.bindRule,
306
+ slotIds: slots
307
+ .filter((slot) => slot.partitionId === row.partitionId)
308
+ .map((slot) => slot.slotId),
309
+ })),
310
+ slots: slots.map((slot) => ({
311
+ slotId: slot.slotId,
312
+ partitionId: slot.partitionId,
313
+ kind: slot.kind,
314
+ intent: slot.intent,
315
+ expected: slot.expected,
316
+ })),
317
+ missingSlots,
318
+ },
319
+ findings,
320
+ };
321
+ }
@@ -2,7 +2,8 @@ import { createHash } from "node:crypto";
2
2
  import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { z } from "zod";
5
- import { isPriorityOnlyBackendTestModuleStem } from "./backend-test-module-stem.js";
5
+ import { BACKEND_TEST_MAX_MODULE_COUNT, isOpaqueHashBackendTestModuleStem, isPriorityOnlyBackendTestModuleStem, } from "./backend-test-module-stem.js";
6
+ import { resolveBackendTestLayout, } from "./backend-test-layout.js";
6
7
  /**
7
8
  * Maps a backend-test generation writer task id to its writer-progress role.
8
9
  * Returns undefined for nodes that are not completeness-gated generators.
@@ -103,6 +104,8 @@ function moduleStemInvalidReason(raw) {
103
104
  const stem = normalizeBackendTestModuleStem(raw);
104
105
  if (isPriorityOnlyBackendTestModuleStem(stem))
105
106
  return "priority-only-module-stem";
107
+ if (isOpaqueHashBackendTestModuleStem(stem))
108
+ return "opaque-hash-module-stem";
106
109
  if (stem.toLowerCase() === "readme")
107
110
  return "reserved-module-stem";
108
111
  if (!/^[a-z][a-z0-9_]*$/.test(stem))
@@ -114,28 +117,40 @@ function moduleStemInvalidReason(raw) {
114
117
  function looksLikeValidModuleStem(raw) {
115
118
  return moduleStemInvalidReason(raw) === undefined;
116
119
  }
117
- /** Inspect only authoritative README module-index candidates. */
118
- export function inspectModuleStemsFromReadme(readme) {
119
- const candidates = [];
120
- const tableRowLines = readme
120
+ function extractCanonicalModuleIndexSection(readme) {
121
+ const lines = readme
121
122
  .replaceAll("\r\n", "\n")
122
123
  .replaceAll("\r", "\n")
123
- .split("\n")
124
- .filter((line) => line.includes("|"));
124
+ .split("\n");
125
+ const headings = lines.flatMap((line, index) => line.trim() === "## Module Index" ? [index] : []);
126
+ if (headings.length === 0) {
127
+ return { section: "", structureIssues: ["missing-module-index"] };
128
+ }
129
+ if (headings.length > 1) {
130
+ return { section: "", structureIssues: ["duplicate-module-index"] };
131
+ }
132
+ const start = headings[0] + 1;
133
+ const relativeEnd = lines.slice(start).findIndex((line) => /^##\s+\S/.test(line.trim()));
134
+ const end = relativeEnd < 0 ? lines.length : start + relativeEnd;
135
+ return { section: lines.slice(start, end).join("\n"), structureIssues: [] };
136
+ }
137
+ /** Inspect only candidates inside the unique exact `## Module Index` section. */
138
+ export function inspectModuleStemsFromReadme(readme) {
139
+ const extracted = extractCanonicalModuleIndexSection(readme);
140
+ const candidates = [];
141
+ const tableRowLines = extracted.section.split("\n").filter((line) => line.includes("|"));
125
142
  for (const line of tableRowLines) {
126
143
  for (const match of line.matchAll(/`?testcase\/md\/([A-Za-z0-9_.-]+)\.md`?/g)) {
127
144
  candidates.push(match[1]);
128
145
  }
129
146
  for (const match of line.matchAll(/\|\s*`?([A-Za-z0-9_.-]+)`?\s*\|\s*`?testcase\/test_/g)) {
130
- // This compatibility shape can also capture a numeric count column.
131
- // Keep valid stems and the priority-only defect we diagnose, while
132
- // ignoring non-stem incidental cells such as `1`.
133
- if (looksLikeValidModuleStem(match[1]) || isPriorityOnlyBackendTestModuleStem(match[1])) {
134
- candidates.push(match[1]);
147
+ const raw = match[1];
148
+ if (looksLikeValidModuleStem(raw) || moduleStemInvalidReason(raw) !== "invalid-syntax") {
149
+ candidates.push(raw);
135
150
  }
136
151
  }
137
152
  }
138
- for (const match of readme.matchAll(/\[[^\]]+\]\(\.\/([A-Za-z0-9_.-]+)\.md\)/g)) {
153
+ for (const match of extracted.section.matchAll(/\[[^\]]+\]\(\.\/([A-Za-z0-9_.-]+)\.md\)/g)) {
139
154
  candidates.push(match[1]);
140
155
  }
141
156
  const uniqueCandidates = orderedUnique(candidates);
@@ -145,12 +160,18 @@ export function inspectModuleStemsFromReadme(readme) {
145
160
  ? [{ raw, normalized: normalizeBackendTestModuleStem(raw), reasonCode }]
146
161
  : [];
147
162
  });
163
+ const validStems = orderedUnique(uniqueCandidates
164
+ .filter(looksLikeValidModuleStem)
165
+ .map((stem) => normalizeBackendTestModuleStem(stem)));
166
+ const structureIssues = [...extracted.structureIssues];
167
+ if (validStems.length > BACKEND_TEST_MAX_MODULE_COUNT) {
168
+ structureIssues.push(`excessive-module-count: ${validStems.length} > ${BACKEND_TEST_MAX_MODULE_COUNT}`);
169
+ }
148
170
  return {
149
171
  candidates: uniqueCandidates,
150
- validStems: orderedUnique(uniqueCandidates
151
- .filter(looksLikeValidModuleStem)
152
- .map((stem) => normalizeBackendTestModuleStem(stem))),
172
+ validStems,
153
173
  invalid,
174
+ structureIssues,
154
175
  };
155
176
  }
156
177
  /**
@@ -161,8 +182,9 @@ export function inspectModuleStemsFromReadme(readme) {
161
182
  export function extractModuleStemsFromReadme(readme) {
162
183
  return inspectModuleStemsFromReadme(readme).validStems;
163
184
  }
164
- async function listMarkdownModules(workspaceRoot) {
165
- const root = path.join(workspaceRoot, "testcase", "md");
185
+ async function listMarkdownModules(workspaceRoot, layout) {
186
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
187
+ const root = path.join(workspaceRoot, ...resolved.markdownDir.split("/"));
166
188
  if (!(await exists(root)))
167
189
  return [];
168
190
  const entries = await readdir(root, { withFileTypes: true });
@@ -170,7 +192,7 @@ async function listMarkdownModules(workspaceRoot) {
170
192
  .filter((entry) => entry.isFile() &&
171
193
  entry.name.toLowerCase().endsWith(".md") &&
172
194
  entry.name.toLowerCase() !== "readme.md")
173
- .map((entry) => path.posix.join("testcase/md", entry.name.replaceAll("\\", "/"))));
195
+ .map((entry) => path.posix.join(resolved.markdownDir, entry.name.replaceAll("\\", "/"))));
174
196
  }
175
197
  /**
176
198
  * Recursively list `.py` file paths relative to `dirRoot` (forward slashes).
@@ -363,12 +385,17 @@ export const FORBIDDEN_GENERATED_IMPORT_NAMESPACES = [
363
385
  "testcase.helpers",
364
386
  "testcase.factories",
365
387
  ];
366
- const FORBIDDEN_GENERATED_IMPORT_PATTERN = /^\s*(?:from\s+(testcase_helpers|testcase\.helpers|testcase\.factories)\b|import\s+(testcase_helpers|testcase\.helpers|testcase\.factories)\b)/gm;
388
+ const FORBIDDEN_GENERATED_IMPORT_PATTERN = /^\s*(?:from\s+(testcase_helpers|testcase\.helpers|testcase\.factories|__\d+[A-Za-z0-9_]*)\b|import\s+(testcase_helpers|testcase\.helpers|testcase\.factories|__\d+[A-Za-z0-9_]*)\b)/gm;
367
389
  /**
368
- * Scan Python source for imports from forbidden generated shared namespaces.
369
- * Anchored at line start with a word boundary so `testcase.helpers_other` or a
370
- * substring inside a docstring sentence is not falsely flagged. Returns the
371
- * list of offending namespace module names (deduplicated, order-preserving).
390
+ * Scan Python source for imports from forbidden generated shared namespaces
391
+ * or bare generated-local helper modules. Anchored at line start with a word
392
+ * boundary so `testcase.helpers_other` or a substring inside a docstring is not
393
+ * falsely flagged. In addition to the `testcase_helpers`/`testcase.helpers`/
394
+ * `testcase.factories` namespaces, bare `__<digits>...` helper modules (for
395
+ * example `from __551_manager_helpers import ...` / `import __551_manager_helpers`)
396
+ * are generated-local shared dependencies: a self-contained pytest child must
397
+ * inline or remove them rather than depend on a generated shared module.
398
+ * Returns the list of offending module names (deduplicated, order-preserving).
372
399
  */
373
400
  export function findForbiddenGeneratedImports(source) {
374
401
  const hits = [];
@@ -550,56 +577,58 @@ export async function assessBackendTestShardChildCompleteness(input) {
550
577
  issues,
551
578
  };
552
579
  }
553
- export async function assessBackendTestMdWriterCompleteness(workspaceRoot) {
580
+ export async function assessBackendTestMdWriterCompleteness(workspaceRoot, layout) {
581
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
582
+ const readmeRel = resolved.readmePath;
554
583
  const issues = [];
555
- const expectedPaths = ["testcase/md/README.md"];
584
+ const expectedPaths = [readmeRel];
556
585
  const actualPaths = [];
557
586
  const missingPaths = [];
558
587
  const brokenPaths = [];
559
- const readmePath = path.join(workspaceRoot, "testcase", "md", "README.md");
588
+ const readmePath = path.join(workspaceRoot, ...readmeRel.split("/"));
560
589
  let readme = "";
561
590
  if (!(await exists(readmePath))) {
562
- missingPaths.push("testcase/md/README.md");
591
+ missingPaths.push(readmeRel);
563
592
  issues.push({
564
593
  code: "T3",
565
- path: "testcase/md/README.md",
594
+ path: readmeRel,
566
595
  detail: "README.md is missing",
567
596
  recoverable: true,
568
597
  });
569
598
  }
570
599
  else {
571
- actualPaths.push("testcase/md/README.md");
600
+ actualPaths.push(readmeRel);
572
601
  readme = await readFile(readmePath, "utf8");
573
602
  if (!readme.includes("## Coverage Scope")) {
574
- brokenPaths.push("testcase/md/README.md");
603
+ brokenPaths.push(readmeRel);
575
604
  issues.push({
576
605
  code: "T5",
577
- path: "testcase/md/README.md",
606
+ path: readmeRel,
578
607
  detail: "README.md is missing ## Coverage Scope",
579
608
  recoverable: true,
580
609
  });
581
610
  }
582
611
  if (!readme.includes("## Coverage Matrix") ||
583
612
  !hasMarkdownTable(readme, "Rule Key")) {
584
- if (!brokenPaths.includes("testcase/md/README.md")) {
585
- brokenPaths.push("testcase/md/README.md");
613
+ if (!brokenPaths.includes(readmeRel)) {
614
+ brokenPaths.push(readmeRel);
586
615
  }
587
616
  issues.push({
588
617
  code: "T5",
589
- path: "testcase/md/README.md",
618
+ path: readmeRel,
590
619
  detail: "README.md Coverage Matrix table is missing or broken",
591
620
  recoverable: true,
592
621
  });
593
622
  }
594
623
  }
595
624
  const indexedStems = extractModuleStemsFromReadme(readme);
596
- const existingModules = await listMarkdownModules(workspaceRoot);
625
+ const existingModules = await listMarkdownModules(workspaceRoot, resolved);
597
626
  const existingByStem = new Map(existingModules.map((rel) => [
598
627
  normalizeBackendTestModuleStem(path.posix.basename(rel)),
599
628
  rel,
600
629
  ]));
601
630
  for (const stem of indexedStems) {
602
- const rel = `testcase/md/${stem}.md`;
631
+ const rel = `${resolved.markdownDir}/${stem}.md`;
603
632
  expectedPaths.push(rel);
604
633
  const existing = existingByStem.get(stem);
605
634
  if (!existing) {
@@ -668,15 +697,16 @@ export async function assessBackendTestMdWriterCompleteness(workspaceRoot) {
668
697
  issues,
669
698
  };
670
699
  }
671
- export async function assessBackendTestPytestWriterCompleteness(workspaceRoot) {
700
+ export async function assessBackendTestPytestWriterCompleteness(workspaceRoot, layout) {
701
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
672
702
  const issues = [];
673
703
  const expectedPaths = [];
674
704
  const actualPaths = [];
675
705
  const missingPaths = [];
676
706
  const brokenPaths = [];
677
- const modules = await listMarkdownModules(workspaceRoot);
707
+ const modules = await listMarkdownModules(workspaceRoot, resolved);
678
708
  for (const moduleRel of modules) {
679
- const expected = expectedBackendTestPytestScriptForMarkdownModule(path.posix.basename(moduleRel));
709
+ const expected = expectedBackendTestPytestScriptForMarkdownModule(path.posix.basename(moduleRel), resolved);
680
710
  expectedPaths.push(expected);
681
711
  const absolute = path.join(workspaceRoot, expected);
682
712
  if (!(await exists(absolute))) {
@@ -836,6 +866,11 @@ export function isBackendTestCompletenessRetryCandidate(task) {
836
866
  task.id === "generate-backend-pytest-pi" ||
837
867
  task.id === "generate-backend-md-plan-pi" ||
838
868
  task.id === "generate-backend-pytest-plan-pi" ||
869
+ // Plan D gap-fill patch writers share the same Completeness Gate retry
870
+ // contract as full-chain writers.
871
+ task.id === "patch-backend-readme-matrix-pi" ||
872
+ (task.id?.startsWith("patch-backend-md-case-") ?? false) ||
873
+ (task.id?.startsWith("patch-backend-pytest-case-") ?? false) ||
839
874
  (task.id?.startsWith("generate-backend-md-case-") ?? false) ||
840
875
  (task.id?.startsWith("generate-backend-pytest-case-") ?? false));
841
876
  }
@@ -872,42 +907,44 @@ export function isBackendTestPytestPlanTask(task) {
872
907
  * index), WITHOUT requiring the indexed module files to exist yet — those are
873
908
  * written by the downstream map_agent shard children.
874
909
  */
875
- export async function assessBackendTestMdPlanCompleteness(workspaceRoot) {
910
+ export async function assessBackendTestMdPlanCompleteness(workspaceRoot, layout) {
911
+ const resolvedPlan = layout ?? resolveBackendTestLayout(undefined);
912
+ const planReadmeRel = resolvedPlan.readmePath;
876
913
  const issues = [];
877
- const expectedPaths = ["testcase/md/README.md"];
914
+ const expectedPaths = [planReadmeRel];
878
915
  const actualPaths = [];
879
916
  const missingPaths = [];
880
917
  const brokenPaths = [];
881
- const readmePath = path.join(workspaceRoot, "testcase", "md", "README.md");
918
+ const readmePath = path.join(workspaceRoot, ...planReadmeRel.split("/"));
882
919
  if (!(await exists(readmePath))) {
883
- missingPaths.push("testcase/md/README.md");
920
+ missingPaths.push(planReadmeRel);
884
921
  issues.push({
885
922
  code: "T3",
886
- path: "testcase/md/README.md",
923
+ path: planReadmeRel,
887
924
  detail: "README.md is missing",
888
925
  recoverable: true,
889
926
  });
890
927
  }
891
928
  else {
892
- actualPaths.push("testcase/md/README.md");
929
+ actualPaths.push(planReadmeRel);
893
930
  const readme = await readFile(readmePath, "utf8");
894
931
  if (!readme.includes("## Coverage Scope")) {
895
- brokenPaths.push("testcase/md/README.md");
932
+ brokenPaths.push(planReadmeRel);
896
933
  issues.push({
897
934
  code: "T5",
898
- path: "testcase/md/README.md",
935
+ path: planReadmeRel,
899
936
  detail: "README.md is missing ## Coverage Scope",
900
937
  recoverable: true,
901
938
  });
902
939
  }
903
940
  if (!readme.includes("## Coverage Matrix") ||
904
941
  !hasMarkdownTable(readme, "Rule Key")) {
905
- if (!brokenPaths.includes("testcase/md/README.md")) {
906
- brokenPaths.push("testcase/md/README.md");
942
+ if (!brokenPaths.includes(planReadmeRel)) {
943
+ brokenPaths.push(planReadmeRel);
907
944
  }
908
945
  issues.push({
909
946
  code: "T5",
910
- path: "testcase/md/README.md",
947
+ path: planReadmeRel,
911
948
  detail: "README.md Coverage Matrix table is missing or broken",
912
949
  recoverable: true,
913
950
  });
@@ -917,24 +954,35 @@ export async function assessBackendTestMdPlanCompleteness(workspaceRoot) {
917
954
  // are produced by map_agent children and are NOT required here.
918
955
  const stemInspection = inspectModuleStemsFromReadme(readme);
919
956
  const indexedStems = stemInspection.validStems;
957
+ for (const structureIssue of stemInspection.structureIssues) {
958
+ if (!brokenPaths.includes(planReadmeRel)) {
959
+ brokenPaths.push(planReadmeRel);
960
+ }
961
+ issues.push({
962
+ code: "T5",
963
+ path: planReadmeRel,
964
+ detail: structureIssue,
965
+ recoverable: true,
966
+ });
967
+ }
920
968
  for (const invalid of stemInspection.invalid) {
921
- if (!brokenPaths.includes("testcase/md/README.md")) {
922
- brokenPaths.push("testcase/md/README.md");
969
+ if (!brokenPaths.includes(planReadmeRel)) {
970
+ brokenPaths.push(planReadmeRel);
923
971
  }
924
972
  issues.push({
925
973
  code: "T5",
926
- path: "testcase/md/README.md",
974
+ path: planReadmeRel,
927
975
  detail: `${invalid.reasonCode}: ${invalid.normalized}; use a stable lowercase business resource/domain stem`,
928
976
  recoverable: true,
929
977
  });
930
978
  }
931
979
  if (indexedStems.length === 0) {
932
- if (!brokenPaths.includes("testcase/md/README.md")) {
933
- brokenPaths.push("testcase/md/README.md");
980
+ if (!brokenPaths.includes(planReadmeRel)) {
981
+ brokenPaths.push(planReadmeRel);
934
982
  }
935
983
  issues.push({
936
984
  code: "T5",
937
- path: "testcase/md/README.md",
985
+ path: planReadmeRel,
938
986
  detail: "README.md module index is empty; downstream map_agent has no stems to shard",
939
987
  recoverable: true,
940
988
  });
@@ -960,16 +1008,17 @@ export async function assessBackendTestMdPlanCompleteness(workspaceRoot) {
960
1008
  * requiring per-module pytest scripts to exist yet — those are written by the
961
1009
  * downstream map_agent shard children.
962
1010
  */
963
- export async function assessBackendTestPytestPlanCompleteness(workspaceRoot) {
1011
+ export async function assessBackendTestPytestPlanCompleteness(workspaceRoot, layout) {
1012
+ const resolvedPlan = layout ?? resolveBackendTestLayout(undefined);
964
1013
  const issues = [];
965
1014
  const expectedPaths = [];
966
1015
  const actualPaths = [];
967
1016
  const missingPaths = [];
968
1017
  const brokenPaths = [];
969
- // Discover shared pytest resources under testcase/**/helpers/** and
970
- // testcase/**/factories/** (the plan node writeSet globs).
1018
+ // Discover shared pytest resources under <testRoot>/**/helpers/** and
1019
+ // <testRoot>/**/factories/** (the plan node writeSet globs).
971
1020
  const candidates = [];
972
- for (const dir of ["testcase/helpers", "testcase/factories"]) {
1021
+ for (const dir of [`${resolvedPlan.testRoot}/helpers`, `${resolvedPlan.testRoot}/factories`]) {
973
1022
  const absolute = path.join(workspaceRoot, dir);
974
1023
  if (await exists(absolute)) {
975
1024
  for (const file of await listPythonFiles(absolute)) {