@tea-agent/loop-agent 0.42.0-next.1 → 0.42.0-next.10

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 (238) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +319 -1
  3. package/dist/application/dag/generate-task-dag.js +75 -19
  4. package/dist/application/dag/run-dag.js +41 -0
  5. package/dist/application/task-lifecycle/advance.js +24 -5
  6. package/dist/application/task-lifecycle/observe.js +171 -17
  7. package/dist/application/task-lifecycle/plan-transitions.js +42 -7
  8. package/dist/application/task-lifecycle/recommendations.js +13 -2
  9. package/dist/build-stamp.json +3 -3
  10. package/dist/cli/command-definitions.js +7 -0
  11. package/dist/cli/program.js +6 -1
  12. package/dist/commands/client-recovery.js +3 -0
  13. package/dist/commands/dag-follow-up.js +138 -0
  14. package/dist/commands/dag-rerun-task.js +2 -0
  15. package/dist/commands/init.js +27 -1
  16. package/dist/commands/task-advance.js +19 -0
  17. package/dist/executors/dag-pi-executor.js +3976 -76
  18. package/dist/executors/pi-executor.js +15 -4
  19. package/dist/executors/pi-read-budget-policy.js +239 -0
  20. package/dist/executors/shell-executor.js +978 -188
  21. package/dist/executors/shell-write-guard.js +7 -0
  22. package/dist/infrastructure/console/operation-store.js +226 -6
  23. package/dist/shared/dag-failure-category.js +12 -0
  24. package/dist/shared/openspec-spec.js +70 -4
  25. package/dist/shared/operator/capabilities.js +7 -0
  26. package/dist/task/config-types.js +105 -5
  27. package/dist/task/contract/adopt.js +4 -0
  28. package/dist/task/contract/import-revision.js +4 -0
  29. package/dist/task/contract/project.js +3 -0
  30. package/dist/task/contract/schema.js +2 -1
  31. package/dist/task/frontend-project-capability.js +203 -20
  32. package/dist/task/runtime.js +5 -2
  33. package/dist/task/source-prepare/build-draft.js +3 -3
  34. package/dist/task/source-prepare/fragment-inventory.js +64 -25
  35. package/dist/task/source-prepare/prepare.js +126 -21
  36. package/dist/task/source-prepare/semantic-intake.js +6 -2
  37. package/dist/task/source-references.js +22 -1
  38. package/dist/worker/console/chat/chat-ui-policy.js +4 -2
  39. package/dist/worker/console/chat/model-resolver.js +7 -4
  40. package/dist/worker/console/chat/pi-runtime.js +258 -11
  41. package/dist/worker/console/chat/repo-browser.js +10 -3
  42. package/dist/worker/console/chat/routes.js +165 -71
  43. package/dist/worker/console/chat/sdd-data-alignment.js +222 -0
  44. package/dist/worker/console/chat/session-catalog.js +2 -0
  45. package/dist/worker/console/chat/session-store.js +159 -70
  46. package/dist/worker/console/chat/shortcuts.js +19 -3
  47. package/dist/worker/console/chat/turn-process.js +111 -56
  48. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  49. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  50. package/dist/worker/console/index.js +3 -0
  51. package/dist/worker/console/operator-actions.js +169 -7
  52. package/dist/worker/console/pi-readiness.js +3 -2
  53. package/dist/worker/console/server.js +23 -1
  54. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CcS17TBr.js} +1 -1
  55. package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-COptKq2S.js} +1 -1
  56. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-h4LKMHjP.js} +1 -1
  57. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-COA1MOH0.js} +1 -1
  58. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-DJUf0QPm.js} +1 -1
  59. package/dist/worker/console/static/assets/channel-DdBCaOJ6.js +1 -0
  60. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-mtWfKrUX.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-B8pXsxDQ.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-ipKzByl1.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-DYi3Ald_.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DQtGYoty.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-BU0qS2YV.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-DVH9dIGE.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-Dt60SmgM.js} +1 -1
  68. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-BoaUGuEY.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-_WHFDTp2.js} +1 -1
  70. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DZFra1GO.js +1 -0
  71. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DZFra1GO.js +1 -0
  72. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-BxkbTIRd.js} +1 -1
  73. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-Bf2UVnoG.js} +1 -1
  74. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-Bo23q1J_.js} +1 -1
  75. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-Dd_UU49i.js} +1 -1
  76. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-ebUa1a9y.js} +1 -1
  77. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-BucthU8r.js} +1 -1
  78. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-BgqNpAkm.js} +1 -1
  79. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-DC4q5NGa.js} +1 -1
  80. package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-CBqCaMeb.js} +1 -1
  81. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-CGtnbQ3-.js} +1 -1
  82. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-CG3LUao5.js} +1 -1
  83. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-D2ZVgoFS.js} +1 -1
  84. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DoYFZiKt.js} +1 -1
  85. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-CKoP1s6j.js} +1 -1
  86. package/dist/worker/console/static/assets/index-CAZ2fC_X.css +1 -0
  87. package/dist/worker/console/static/assets/index-vbTcFnFs.js +449 -0
  88. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-Duofv8p2.js} +1 -1
  89. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-D2nlCkA1.js} +1 -1
  90. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-Dd4IHdus.js} +1 -1
  91. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-Bi20AOdt.js} +1 -1
  92. package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-D59YJ9kB.js} +1 -1
  93. package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-Bl13LpNa.js} +5 -5
  94. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-C6DLP5eY.js} +1 -1
  95. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-BjO4cy64.js} +1 -1
  96. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-D7i2qZTG.js} +1 -1
  97. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-Dx8o_h0y.js} +1 -1
  98. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-B55orI_t.js} +1 -1
  99. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-zWaehp6f.js} +1 -1
  100. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-CcA-pjvD.js} +1 -1
  101. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-BOFbzNHI.js} +1 -1
  102. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-BjAejah1.js} +1 -1
  103. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-DXYwxJwZ.js} +1 -1
  104. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Clg3V9t1.js +1 -0
  105. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-OYiOch8n.js} +2 -2
  106. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DX1dxAAW.js +8 -0
  107. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-cZfH3nmU.js} +1 -1
  108. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-BDE7b3E1.js} +1 -1
  109. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-kyyGy9WJ.js} +1 -1
  110. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CJj6VTog.js} +1 -1
  111. package/dist/worker/console/static/index.html +2 -2
  112. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  113. package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -5
  114. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
  115. package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
  116. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
  117. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +10 -0
  118. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +258 -23
  119. package/dist/worker/console/static-src/operator-chat/useChatThread.js +112 -4
  120. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -4
  121. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +29 -6
  122. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +8 -2
  123. package/dist/worker/console/static-src/shell/console-update-reload.js +36 -9
  124. package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
  125. package/dist/worker/console/workspace-context.js +115 -1
  126. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  127. package/dist/worker/observe/routes.js +4 -0
  128. package/dist/worker/observe/static/dag-helpers.js +4 -2
  129. package/dist/worker/observe/static/dag-node-purpose.js +5 -0
  130. package/dist/worker/observe/static/inspect-workspace.js +23 -0
  131. package/dist/worker/observe/static/kpi.js +2 -2
  132. package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
  133. package/dist/worker/observe/static/operator-chrome.js +37 -23
  134. package/dist/worker/observe/static/relations.js +2 -2
  135. package/dist/worker/observe/static/router.d.ts +12 -1
  136. package/dist/worker/observe/static/router.js +48 -6
  137. package/dist/worker/observe/static/run-processing.js +4 -2
  138. package/dist/worker/observe/static/shell-chrome.js +2 -2
  139. package/dist/worker/observe/static/state.js +5 -0
  140. package/dist/worker/observe/static/styles.css +95 -0
  141. package/dist/worker/observe/static/views/dag-inspector.js +14 -7
  142. package/dist/worker/observe/static/views/dag.js +10 -2
  143. package/dist/worker/observe/static/views/dags.js +3 -1
  144. package/dist/worker/observe/static/views/dashboard.js +14 -6
  145. package/dist/worker/observe/static/views/pool.js +7 -2
  146. package/dist/worker/observe/static/views/run.js +12 -3
  147. package/dist/worker/observe/static/views/session-timeline.js +69 -10
  148. package/dist/worker/observe/static/views/task.js +7 -2
  149. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +215 -28
  150. package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
  151. package/dist/workflows/dag/backend-test-plan-protocol.js +106 -0
  152. package/dist/workflows/dag/backend-test-scenario-param.js +172 -25
  153. package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
  154. package/dist/workflows/dag/backend-test-writer-completeness.js +104 -12
  155. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  156. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  157. package/dist/workflows/dag/frontend-closeout.js +221 -0
  158. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  159. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  160. package/dist/workflows/dag/frontend-implementation-contract.js +1237 -192
  161. package/dist/workflows/dag/frontend-plan-render.js +2 -1
  162. package/dist/workflows/dag/frontend-prewrite-gate.js +256 -350
  163. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  164. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  165. package/dist/workflows/dag/frontend-recovery-controller.js +226 -0
  166. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  167. package/dist/workflows/dag/frontend-recovery-plan.js +21 -10
  168. package/dist/workflows/dag/frontend-recovery-run.js +166 -34
  169. package/dist/workflows/dag/frontend-repair.js +1 -432
  170. package/dist/workflows/dag/frontend-review-context.js +261 -15
  171. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  172. package/dist/workflows/dag/frontend-shadow-dual-write.js +941 -0
  173. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  174. package/dist/workflows/dag/frontend-shape-facts.js +419 -0
  175. package/dist/workflows/dag/frontend-shape.js +435 -0
  176. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  177. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  178. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  179. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  180. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  181. package/dist/workflows/dag/frontend-verification-trace.js +252 -25
  182. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  183. package/dist/workflows/dag/frontend-writer-admission.js +319 -0
  184. package/dist/workflows/dag/frontend-writer-status.js +256 -0
  185. package/dist/workflows/dag/init-hybrid.js +1028 -562
  186. package/dist/workflows/dag/interrupt-request.js +7 -0
  187. package/dist/workflows/dag/node-execution.js +828 -4
  188. package/dist/workflows/dag/prompt.js +70 -3
  189. package/dist/workflows/dag/recovery-lease.js +170 -0
  190. package/dist/workflows/dag/report.js +37 -1
  191. package/dist/workflows/dag/rerun-feedback.js +315 -1
  192. package/dist/workflows/dag/rerun-plan.js +17 -6
  193. package/dist/workflows/dag/rerun-run.js +44 -6
  194. package/dist/workflows/dag/rerun-task.js +251 -13
  195. package/dist/workflows/dag/retry-policy.js +219 -104
  196. package/dist/workflows/dag/runner.js +580 -124
  197. package/dist/workflows/dag/scheduler.js +133 -20
  198. package/dist/workflows/dag/types.js +244 -16
  199. package/dist/workflows/dag/validate.js +28 -12
  200. package/docs/examples/README.md +5 -0
  201. package/docs/init-surface.manifest.json +30 -12
  202. package/docs/skills/vetted-skill-registry.md +4 -2
  203. package/docs/templates/README.md +2 -0
  204. package/docs/templates/agent-dag-report.schema.json +8 -2
  205. package/docs/templates/agent-dag.schema.json +1 -1
  206. package/docs/templates/backend-test-dag.json +23 -20
  207. package/docs/templates/frontend-implementation-contract.schema.json +4 -1
  208. package/docs/templates/frontend-implementation-dag.json +89 -0
  209. package/docs/templates/spec-registry.schema.json +45 -0
  210. package/harness.json +1 -1
  211. package/package.json +4 -3
  212. package/skills/frontend-bounded-implement/SKILL.md +15 -14
  213. package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
  214. package/skills/frontend-contract/SKILL.md +23 -0
  215. package/skills/frontend-contract/references/contract-protocol.md +34 -0
  216. package/skills/frontend-design-review/SKILL.md +22 -41
  217. package/skills/frontend-plan/SKILL.md +26 -0
  218. package/skills/frontend-plan/references/decision-contract.md +37 -0
  219. package/skills/frontend-plan/references/design-decisions.md +17 -0
  220. package/skills/frontend-review/SKILL.md +20 -15
  221. package/skills/frontend-review/references/review-findings.md +6 -7
  222. package/skills/frontend-scout/SKILL.md +25 -0
  223. package/skills/frontend-scout/references/design-evidence.md +16 -0
  224. package/skills/frontend-scout/references/scout-evidence.md +23 -0
  225. package/skills/frontend-verification/SKILL.md +1 -1
  226. package/skills/loop-agent/references/command-reference.md +1 -0
  227. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  228. package/dist/worker/console/static/assets/channel-BU5gOilw.js +0 -1
  229. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +0 -1
  230. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +0 -1
  231. package/dist/worker/console/static/assets/index-H9rFJiGL.css +0 -1
  232. package/dist/worker/console/static/assets/index-xwu9GxEc.js +0 -451
  233. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +0 -1
  234. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +0 -8
  235. package/skills/frontend-implementation/SKILL.md +0 -52
  236. package/skills/frontend-implementation/references/code-standards.md +0 -33
  237. package/skills/frontend-implementation/references/design-spec.md +0 -56
  238. package/skills/frontend-implementation/references/node-contracts.md +0 -31
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Deterministic backend-test plan protocol helpers.
3
+ *
4
+ * English canonical `##` headings remain the parse authority. Known Chinese
5
+ * aliases are rewritten only as an exact whole-line `##` heading, never as
6
+ * prose. Partition slot coverage follows Scheme A: Case count may be smaller
7
+ * than the enum count, but every exact `TP-SP-...` slot must appear as an
8
+ * independent variant Test Point.
9
+ */
10
+ export const BACKEND_TEST_PLAN_HEADING_ALIASES = {
11
+ "模块索引": "Module Index",
12
+ "覆盖范围": "Coverage Scope",
13
+ "覆盖矩阵": "Coverage Matrix",
14
+ "场景分区": "Scenario Partitions",
15
+ };
16
+ export const BACKEND_TEST_SCENARIO_PARTITION_SLOTS_SCHEMA_ID = "backend-test-scenario-partition-slots-v1";
17
+ const CANONICAL_HEADING_LINE = /^##\s+(Module Index|Coverage Scope|Coverage Matrix|Scenario Partitions)\s*$/;
18
+ const ALIAS_HEADING_LINE = /^##\s+(模块索引|覆盖范围|覆盖矩阵|场景分区)\s*$/;
19
+ const CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-\d{2,3})\b/;
20
+ const SECTION_HEADING = /^###\s+/;
21
+ const TEST_POINT_RE = /\bTP-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g;
22
+ const VARIANT_LABEL = /变体测试点|Variant Test Points/i;
23
+ const FORBIDDEN_SLOT_ALIASES = new Set(["SINGLE", "MULTIPLE"]);
24
+ export function canonicalizeBackendTestPlanProtocolHeadings(markdown) {
25
+ const lines = markdown.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
26
+ const rewrittenHeadings = [];
27
+ const next = lines.map((line) => {
28
+ const trimmed = line.trim();
29
+ const alias = ALIAS_HEADING_LINE.exec(trimmed);
30
+ if (!alias)
31
+ return line;
32
+ const canonical = BACKEND_TEST_PLAN_HEADING_ALIASES[alias[1]];
33
+ rewrittenHeadings.push(canonical);
34
+ return `## ${canonical}`;
35
+ });
36
+ return {
37
+ markdown: next.join("\n"),
38
+ changed: rewrittenHeadings.length > 0,
39
+ rewrittenHeadings,
40
+ };
41
+ }
42
+ export function countCanonicalPlanHeading(markdown, heading) {
43
+ const canonical = canonicalizeBackendTestPlanProtocolHeadings(markdown).markdown;
44
+ return canonical
45
+ .split("\n")
46
+ .filter((line) => line.trim() === `## ${heading}`).length;
47
+ }
48
+ function orderedUnique(values) {
49
+ return [...new Set(values.filter(Boolean))];
50
+ }
51
+ function collectSectionTokens(body, headingNeedle) {
52
+ const lines = body.split("\n");
53
+ const start = lines.findIndex((line) => line.trim() === `### ${headingNeedle}`);
54
+ if (start < 0)
55
+ return [];
56
+ const tokens = [];
57
+ for (const line of lines.slice(start + 1)) {
58
+ if (SECTION_HEADING.test(line) || CASE_HEADING.test(line.trim()))
59
+ break;
60
+ tokens.push(...(line.match(TEST_POINT_RE) ?? []));
61
+ }
62
+ return tokens;
63
+ }
64
+ function collectLabeledTokens(body, label) {
65
+ return body
66
+ .split("\n")
67
+ .filter((line) => label.test(line))
68
+ .flatMap((line) => line.match(TEST_POINT_RE) ?? []);
69
+ }
70
+ export function collectMarkdownPartitionSlotCoverage(markdown, requiredVariantSlots) {
71
+ const declaredTestPoints = [];
72
+ const variantTestPoints = [];
73
+ const lines = markdown.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
74
+ const headingIndexes = lines.flatMap((line, index) => CASE_HEADING.test(line.trim()) ? [index] : []);
75
+ for (let i = 0; i < headingIndexes.length; i += 1) {
76
+ const start = headingIndexes[i];
77
+ const end = headingIndexes[i + 1] ?? lines.length;
78
+ const body = lines.slice(start, end).join("\n");
79
+ declaredTestPoints.push(...collectSectionTokens(body, "测试点"));
80
+ variantTestPoints.push(...collectLabeledTokens(body, VARIANT_LABEL));
81
+ }
82
+ const declaredSet = new Set(orderedUnique(declaredTestPoints));
83
+ const variantSet = new Set(orderedUnique(variantTestPoints));
84
+ const required = requiredVariantSlots.filter((slotId) => !FORBIDDEN_SLOT_ALIASES.has(slotId));
85
+ return {
86
+ declaredTestPoints: orderedUnique(declaredTestPoints),
87
+ variantTestPoints: orderedUnique(variantTestPoints),
88
+ missingFromDeclared: required.filter((slotId) => !declaredSet.has(slotId)),
89
+ missingFromVariant: required.filter((slotId) => !variantSet.has(slotId)),
90
+ };
91
+ }
92
+ export function isForbiddenPartitionSlotAlias(token) {
93
+ return FORBIDDEN_SLOT_ALIASES.has(token.trim().toUpperCase());
94
+ }
95
+ export function lookupRequiredVariantSlotsForMarkdownPath(inventory, markdownPath) {
96
+ if (!inventory)
97
+ return [];
98
+ const stem = markdownPath
99
+ .replace(/\\/g, "/")
100
+ .replace(/^.*\//, "")
101
+ .replace(/\.md$/i, "");
102
+ return inventory.modules.find((item) => item.stem === stem)?.requiredVariantSlots ?? [];
103
+ }
104
+ export function isCanonicalPlanHeadingLine(line) {
105
+ return CANONICAL_HEADING_LINE.test(line.trim());
106
+ }
@@ -59,6 +59,7 @@ const CLOSED_SET = new Set([
59
59
  "null",
60
60
  "min-1",
61
61
  "min",
62
+ "max-1",
62
63
  "max",
63
64
  "max+1",
64
65
  "pattern-invalid",
@@ -155,14 +156,19 @@ function parseScenarioBoundToken(raw) {
155
156
  function parseScenarioIntentLine(line) {
156
157
  // Target may contain commas (body.title,body.status). Intent may contain `|` / spaces.
157
158
  // Stop field values only at known trailing keys or end-of-line.
158
- const precise = /场景意图\s*[::]\s*(TP-[A-Z0-9-]+)\s*[|;]\s*operation\s*=\s*([^;\n]+?)\s*[|;]\s*target\s*=\s*([^;\n]+?)\s*[|;]\s*intent\s*=\s*([^;\n]+?)(?:\s*[|;]\s*bound\s*=\s*([^\s;|\n]+))?(?:\s*[|;]\s*example\s*=\s*([^;\n]+))?(?:\s*[|;]\s*expectedCode\s*=\s*[^;\n]+)?\s*$/i.exec(line.trim());
159
+ const unfenced = line
160
+ .trim()
161
+ .replace(/^[-*+]\s+/, "")
162
+ .replace(/^`(.+)`$/, "$1")
163
+ .trim();
164
+ const precise = /场景意图\s*[::]\s*(TP-[A-Z0-9-]+)\s*[|;]\s*operation\s*=\s*([^;\n]+?)\s*[|;]\s*target\s*=\s*([^;\n]+?)\s*[|;]\s*intent\s*=\s*([^;\n]+?)(?:\s*[|;]\s*bound\s*=\s*([^\s;`|\n]+))?(?:\s*[|;]\s*example\s*=\s*([^;\n]*))?(?:\s*[|;]\s*expectedCode\s*=\s*[^;\n]+)?\s*$/i.exec(unfenced);
159
165
  if (precise) {
160
166
  return {
161
167
  tpId: precise[1],
162
168
  rawIntent: (precise[4] ?? "unknown").trim(),
163
169
  target: precise[3]?.trim(),
164
170
  bound: parseScenarioBoundToken(precise[5]),
165
- example: precise[6]?.trim(),
171
+ example: precise[6] === undefined ? undefined : precise[6].trim(),
166
172
  legacy: false,
167
173
  };
168
174
  }
@@ -192,6 +198,53 @@ function resolveScenarioIntentField(target, field) {
192
198
  target.split(/\s*,\s*/)[0];
193
199
  return preferred?.split(".").at(-1);
194
200
  }
201
+ function normalizeLengthIntentAlias(rawIntent) {
202
+ const lower = rawIntent.trim().toLowerCase();
203
+ if (lower === "max-1" || lower === "maxminus1" || lower === "max_minus_1") {
204
+ return "max-1";
205
+ }
206
+ if (lower === "min-1" || lower === "minminus1" || lower === "min_minus_1") {
207
+ return "min-1";
208
+ }
209
+ return lower;
210
+ }
211
+ function proseLengthBound(haystack, field) {
212
+ const fieldAlt = field ? field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : undefined;
213
+ const scoped = fieldAlt ? new RegExp(`${fieldAlt}[^\\n]{0,160}`, "ig") : undefined;
214
+ const windows = [];
215
+ if (scoped) {
216
+ for (const match of haystack.matchAll(scoped)) {
217
+ windows.push(match[0] ?? "");
218
+ }
219
+ }
220
+ windows.push(haystack);
221
+ let max;
222
+ let min;
223
+ const proseMaxRe = /(\d{1,4})\s*个字符的最大/i;
224
+ for (const window of windows) {
225
+ const proseMax = proseMaxRe.exec(window);
226
+ if (proseMax && max === undefined)
227
+ max = Number(proseMax[1]);
228
+ const nums = [...window.matchAll(/(?<![+\-])\d{1,4}/g)]
229
+ .map((item) => Number(item[0]))
230
+ .filter((item) => Number.isFinite(item));
231
+ for (let i = 0; i + 2 < nums.length; i += 1) {
232
+ const a = nums[i];
233
+ const b = nums[i + 1];
234
+ const c = nums[i + 2];
235
+ // Prefer n-1,n,n+1 around a documented max (100/2000), not 0,1,99 or 99,100,101's first two plus leftover.
236
+ if (b === a + 1 && c === b + 1 && b >= 10) {
237
+ max ??= b;
238
+ break;
239
+ }
240
+ }
241
+ if (nums.includes(0) && nums.includes(1))
242
+ min ??= 1;
243
+ if (max !== undefined && min !== undefined)
244
+ break;
245
+ }
246
+ return { max, min };
247
+ }
195
248
  function resolveCustomLiteralIntent(rawIntent, field) {
196
249
  const lower = rawIntent.toLowerCase();
197
250
  if (!lower.startsWith("custom-literal:")) {
@@ -250,7 +303,7 @@ export function inferScenarioParamIntent(input) {
250
303
  const field = resolveScenarioIntentField(machine.target, machine.field);
251
304
  const rawIntent = machine.rawIntent.trim();
252
305
  const example = machine.example;
253
- const lower = rawIntent.toLowerCase();
306
+ const lower = normalizeLengthIntentAlias(rawIntent);
254
307
  let normalizedIntent = lower === "nominal-operation" ? "nominal" : lower;
255
308
  // Writers sometimes mark a short "typical" length sample as min/max with bound=LEN<nominal>.
256
309
  // Treat that as nominal so a short helper/string is not forced to equal maxLength.
@@ -273,16 +326,16 @@ export function inferScenarioParamIntent(input) {
273
326
  new RegExp(`(?:minLength|min(?:imum)?|下限|最小长度)\\s*[=::]?\\s*(\\d{1,4})[^\\n]{0,80}?${fieldEsc}`, "i").exec(haystack) ??
274
327
  /(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(haystack)
275
328
  : /(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(haystack);
329
+ const proseBound = proseLengthBound(haystack, field);
276
330
  const prefersMinBound = normalizedIntent === "min" ||
277
331
  normalizedIntent === "min-1" ||
278
332
  /(?:MIN|LEN1|LENGTH-?1|最短|最小)/i.test(tpId);
279
- const boundFromBody = prefersMinBound
280
- ? minBoundFromBody ?? maxBoundFromBody
281
- : maxBoundFromBody ?? minBoundFromBody;
333
+ const boundFromBody = prefersMinBound ? minBoundFromBody : maxBoundFromBody ?? minBoundFromBody;
282
334
  const boundFromTp = /(?:LEN(?:GTH)?|长度)[_-]?(\d{1,5})(?:[_-]|$)/i.exec(tpId) ??
283
- /(?:MIN|MAX)[_-]?(\d{1,5})(?:[_-]|$)/i.exec(tpId);
335
+ /(?:MIN|MAX)[_-](\d{2,5})(?:[_-]|$)/i.exec(tpId);
284
336
  const lengthIntent = normalizedIntent === "min" ||
285
337
  normalizedIntent === "min-1" ||
338
+ normalizedIntent === "max-1" ||
286
339
  normalizedIntent === "max" ||
287
340
  normalizedIntent === "max+1";
288
341
  // Length intents: explicit machine bound >0, then TP-encoded LEN1/LEN100,
@@ -294,13 +347,35 @@ export function inferScenarioParamIntent(input) {
294
347
  : normalizedIntent === "max+1"
295
348
  ? Math.max(0, boundFromTpNumber - 1)
296
349
  : boundFromTpNumber;
297
- const bound = machine.bound && machine.bound > 0
298
- ? machine.bound
299
- : lengthIntent && boundFromTpNormalized !== undefined
300
- ? boundFromTpNormalized
301
- : boundFromBody
302
- ? Number(boundFromBody[1])
303
- : machine.bound;
350
+ const boundFromProse = prefersMinBound ? proseBound.min : proseBound.max;
351
+ const offsetIntent = normalizedIntent === "max-1" ||
352
+ normalizedIntent === "max+1" ||
353
+ normalizedIntent === "min-1";
354
+ const contractBound = boundFromBody
355
+ ? Number(boundFromBody[1])
356
+ : boundFromProse !== undefined && boundFromProse >= 0
357
+ ? boundFromProse
358
+ : undefined;
359
+ const machineBoundPositive = typeof machine.bound === "number" && Number.isFinite(machine.bound) && machine.bound > 0;
360
+ const minLengthFromBody = prefersMinBound && minBoundFromBody ? Number(minBoundFromBody[1]) : undefined;
361
+ const machineBoundZeroMin = prefersMinBound &&
362
+ machine.bound === 0 &&
363
+ minLengthFromBody === undefined &&
364
+ boundFromTpNormalized === undefined;
365
+ const bound = offsetIntent && contractBound !== undefined && contractBound > 0
366
+ ? contractBound
367
+ : machineBoundPositive && !offsetIntent
368
+ ? machine.bound
369
+ : lengthIntent && boundFromTpNormalized !== undefined
370
+ ? boundFromTpNormalized
371
+ : minLengthFromBody !== undefined && minLengthFromBody >= 0
372
+ ? minLengthFromBody
373
+ : machineBoundZeroMin
374
+ ? 0
375
+ : contractBound !== undefined &&
376
+ (prefersMinBound ? contractBound >= 0 : contractBound > 0)
377
+ ? contractBound
378
+ : undefined;
304
379
  if (rawIntent.toLowerCase().startsWith("custom-literal:")) {
305
380
  const resolved = resolveCustomLiteralIntent(rawIntent, field);
306
381
  const rawLiteral = rawIntent.slice(rawIntent.indexOf(":") + 1).trim();
@@ -387,13 +462,15 @@ export function inferScenarioParamIntent(input) {
387
462
  const boundMatch = fieldScopedBound ??
388
463
  /(?:maxLength|max(?:imum)?|上限|最大长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`) ??
389
464
  /(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`);
390
- const bound = boundMatch ? Number(boundMatch[1]) : undefined;
465
+ const proseBound = proseLengthBound(`${caseBody}
466
+ ${dataSection}`, field);
467
+ const bound = boundMatch ? Number(boundMatch[1]) : proseBound.max ?? proseBound.min;
391
468
  const exampleMatch = /(?:反例|invalid example|example)\s*[=::]\s*[`"]?([^`"\n]+)[`"]?/i.exec(caseBody);
392
469
  const example = exampleMatch?.[1]?.trim();
393
470
  if (/EMPTY|空白|空串|空字符串/.test(upper) || /空字符串|空串|empty string/i.test(caseBody)) {
394
471
  return { intent: "empty", field, bound, example, intentSource: "tp-fallback" };
395
472
  }
396
- if (/MISSING|缺省|缺失|省略/.test(upper) || /缺少字段|缺失字段|missing field/i.test(caseBody)) {
473
+ if (/MISSING|OMITTED|\bOMIT\b|缺省|缺失|省略/.test(upper) || /缺少字段|缺失字段|missing field/i.test(caseBody)) {
397
474
  return { intent: "missing", field, bound, example, intentSource: "tp-fallback" };
398
475
  }
399
476
  if (/\bNULL\b|空值/.test(upper) || /\bnull\b/i.test(caseBody)) {
@@ -402,6 +479,9 @@ export function inferScenarioParamIntent(input) {
402
479
  if (/MAX[-_]?PLUS[-_]?1|MAX\+1|超长|越界|LENGTH[-_]?OVER|OVER[-_]?LENGTH|OVER/.test(upper)) {
403
480
  return { intent: "max+1", field, bound, example, intentSource: "tp-fallback" };
404
481
  }
482
+ if (/MAX[-_]1(?:$|[-_])|max-1/i.test(upper)) {
483
+ return { intent: "max-1", field, bound, example, intentSource: "tp-fallback" };
484
+ }
405
485
  if (/\bMAX\b|最大/.test(upper)) {
406
486
  return { intent: "max", field, bound, example, intentSource: "tp-fallback" };
407
487
  }
@@ -425,7 +505,7 @@ export function inferScenarioParamIntent(input) {
425
505
  }
426
506
  return { intent: "unknown", field, bound, example, intentSource: "tp-fallback" };
427
507
  }
428
- const OMIT_SENTINEL_RE = /^(?:_OMIT|OMIT|MISSING|Ellipsis|\.\.\.)\b/;
508
+ const OMIT_SENTINEL_RE = /^(?:_OMIT|_MISSING|OMIT|MISSING|Ellipsis|\.\.\.)\b/;
429
509
  const HTTP_EXPECTED_CODE_RE = /^(?:2xx|3xx|4xx|5xx|[1-5]\d{2}|HTTP_[1-5]\d{2})$/i;
430
510
  /** Semantic custom-literal tokens that describe a value class, not a raw string. */
431
511
  function isWhitespaceSemanticLiteral(token) {
@@ -774,6 +854,10 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
774
854
  (/["'][A-Za-z_][A-Za-z0-9_]*["']\s*:/.test(dictText) ||
775
855
  /(?:payload|body|data|json)\s*=/.test(block));
776
856
  if (dictLooksLikePayload && !field) {
857
+ const emptyField = /["']([A-Za-z_][A-Za-z0-9_.]*)["']\s*:\s*(?:""|'')/.exec(dictText);
858
+ if (emptyField?.[1]) {
859
+ return { kind: "empty-string", text: '""', length: 0, literal: "" };
860
+ }
777
861
  return { kind: "dict", text: dictText.replace(/\s+/g, " ").slice(0, 160) };
778
862
  }
779
863
  if (dictLooksLikePayload && field) {
@@ -803,7 +887,7 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
803
887
  };
804
888
  }
805
889
  // Bare omit sentinel before id=...
806
- if (/pytest\.param\s*\(\s*(?:_OMIT|OMIT|MISSING|\.\.\.|Ellipsis)\s*(?:,|\)|$)/.test(block)) {
890
+ if (/pytest\.param\s*\(\s*(?:_OMIT|_MISSING|OMIT|MISSING|\.\.\.|Ellipsis)\s*(?:,|\)|$)/.test(block)) {
807
891
  return {
808
892
  kind: "missing-key",
809
893
  text: field ? `missing:${field}` : "missing-key",
@@ -865,7 +949,7 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
865
949
  if (paramOpen) {
866
950
  const afterOpen = block.slice((paramOpen.index ?? 0) + paramOpen[0].length);
867
951
  const positionalTokens = [];
868
- const tokenRe = /\s*(_OMIT|OMIT|MISSING|Ellipsis|\.\.\.|None|True|False|-?\d+(?:\.\d+)?|"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|(?:["'][^"'\\]["']\s*\*\s*(?:\d{1,5}|[A-Za-z_][A-Za-z0-9_]*))|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[A-Za-z_][A-Za-z0-9_]*)\s*(?:,|\)|$)/g;
952
+ const tokenRe = /\s*(_OMIT|_MISSING|OMIT|MISSING|Ellipsis|\.\.\.|None|True|False|-?\d+(?:\.\d+)?|"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|(?:["'][^"'\\]["']\s*\*\s*(?:\d{1,5}|[A-Za-z_][A-Za-z0-9_]*))|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[A-Za-z_][A-Za-z0-9_]*)\s*(?:,|\)|$)/g;
869
953
  let match;
870
954
  while ((match = tokenRe.exec(afterOpen)) !== null) {
871
955
  const between = afterOpen.slice(0, match.index);
@@ -1008,12 +1092,25 @@ function compareIntent(intent, observed, bound, example, field) {
1008
1092
  return "MATCH";
1009
1093
  return len !== undefined ? "MISMATCH" : "UNDETERMINED";
1010
1094
  }
1095
+ case "max-1":
1096
+ if (bound === undefined)
1097
+ return "UNDETERMINED";
1098
+ {
1099
+ const expectedMax1 = Math.max(0, bound - 1);
1100
+ const len = observedLengthValue(observed);
1101
+ if (len === expectedMax1)
1102
+ return "MATCH";
1103
+ return len !== undefined ? "MISMATCH" : "UNDETERMINED";
1104
+ }
1011
1105
  case "min":
1012
1106
  if (bound === undefined)
1013
1107
  return "UNDETERMINED";
1014
1108
  {
1015
1109
  if (isMinLengthHelper(observed))
1016
1110
  return "MATCH";
1111
+ // Optional min=0 may omit the key instead of sending "".
1112
+ if (bound === 0 && observed.kind === "missing-key")
1113
+ return "MATCH";
1017
1114
  const len = observedLengthValue(observed);
1018
1115
  if (len === bound)
1019
1116
  return "MATCH";
@@ -1116,7 +1213,11 @@ function compareIntent(intent, observed, bound, example, field) {
1116
1213
  ? "UNDETERMINED"
1117
1214
  : "MISMATCH";
1118
1215
  }
1119
- return literal === expected
1216
+ const comparableObserved = observed.kind === "boolean"
1217
+ ? (observed.literal === "True" ? "true" : observed.literal === "False" ? "false" : literal)
1218
+ : literal;
1219
+ const comparableExpected = expected === "True" ? "true" : expected === "False" ? "false" : expected;
1220
+ return comparableObserved === comparableExpected
1120
1221
  ? "MATCH"
1121
1222
  : observed.kind === "unknown"
1122
1223
  ? "UNDETERMINED"
@@ -1163,6 +1264,10 @@ function suggestedFixFor(intent, field, bound, example) {
1163
1264
  return field && bound !== undefined
1164
1265
  ? `set ${field}="x"*${bound + 1}`
1165
1266
  : undefined;
1267
+ case "max-1":
1268
+ return field && bound !== undefined
1269
+ ? `set ${field}="x"*${Math.max(0, bound - 1)}`
1270
+ : undefined;
1166
1271
  case "min":
1167
1272
  return field && bound !== undefined
1168
1273
  ? `set ${field}="x"*${bound}`
@@ -1207,6 +1312,12 @@ function boundedScenarioText(value, logicalLength) {
1207
1312
  const suffix = `<truncated length=${logicalLength ?? value.length}>`;
1208
1313
  return `${value.slice(0, Math.max(0, MAX_PERSISTED_SCENARIO_TEXT - suffix.length - 3))}...${suffix}`;
1209
1314
  }
1315
+ /** Facts schema forbids negative lengths. Internal helper sentinels stay in-memory only. */
1316
+ function persistedObservedLength(length) {
1317
+ return typeof length === "number" && Number.isFinite(length) && length >= 0
1318
+ ? Math.trunc(length)
1319
+ : undefined;
1320
+ }
1210
1321
  function localPythonFunctionRegion(source, functionName) {
1211
1322
  const escaped = functionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1212
1323
  const startMatch = new RegExp(`^def\\s+${escaped}\\s*\\(`, "m").exec(source);
@@ -1365,6 +1476,32 @@ function hasSafeSameModulePayloadHelper(source, block, parameterNames) {
1365
1476
  return false;
1366
1477
  return Boolean(safeSameModuleDictHelper(source, call.name));
1367
1478
  }
1479
+ function observeBranchPayloadField(input) {
1480
+ const region = scenarioFunctionRegion(input.source, input.tpId);
1481
+ if (!region || !input.block)
1482
+ return undefined;
1483
+ const args = extractPytestParamTopLevelArgs(input.block);
1484
+ const escapedField = input.field.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
1485
+ for (let index = 0; index < input.parameterNames.length; index += 1) {
1486
+ const parameter = input.parameterNames[index];
1487
+ const raw = args[index]?.trim();
1488
+ if (!raw || !/^['\"](?:[^'\"\\\\]|\\\\.)*['\"]$/.test(raw))
1489
+ continue;
1490
+ const branchValue = raw.slice(1, -1).replace(/\\\\([\\\"'])/g, "$1");
1491
+ const escapedParameter = parameter.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
1492
+ const escapedBranchValue = branchValue.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
1493
+ const branch = new RegExp(`(?:^|\\n)([ \\t]*)if\\s+${escapedParameter}\\s*==\\s*['\"]${escapedBranchValue}['\"]\\s*:\\s*([\\s\\S]*?)(?=\\n\\1(?:elif\\b|else\\s*:)|\\n\\1(?:if\\b|def\\b)|$)`, "m").exec(region)?.[2];
1494
+ if (!branch)
1495
+ continue;
1496
+ const value = new RegExp(`['\"]${escapedField}['\"]\\s*:\\s*([^,}\\n]+)`).exec(branch)?.[1];
1497
+ if (!value)
1498
+ continue;
1499
+ const observed = observeLiteralToken(value.trim(), input.field, input.constants);
1500
+ if (observed)
1501
+ return observed;
1502
+ }
1503
+ return undefined;
1504
+ }
1368
1505
  function observeDeterministicFunctionFieldOverride(input) {
1369
1506
  const region = scenarioFunctionRegion(input.source, input.tpId);
1370
1507
  if (!region)
@@ -1449,11 +1586,12 @@ function scenarioParamDiagnosticValues(intent, observed) {
1449
1586
  const expectedRaw = intent.startsWith("custom-literal:")
1450
1587
  ? intent.slice("custom-literal:".length)
1451
1588
  : intent === "unknown" ? undefined : intent;
1589
+ const persistedLength = persistedObservedLength(observed.length);
1452
1590
  return {
1453
1591
  expectedRaw,
1454
- observedRaw: boundedScenarioText(observed.text, observed.length),
1592
+ observedRaw: boundedScenarioText(observed.text, persistedLength),
1455
1593
  expectedNormalized: expectedRaw === undefined ? undefined : normalizeCustomLiteralExpected(expectedRaw).slice(0, 512),
1456
- observedNormalized: observed.literal === undefined ? undefined : boundedScenarioText(normalizeScenarioLiteralText(observed.literal), observed.length),
1594
+ observedNormalized: observed.literal === undefined ? undefined : boundedScenarioText(normalizeScenarioLiteralText(observed.literal), persistedLength),
1457
1595
  };
1458
1596
  }
1459
1597
  export async function assessBackendScenarioParamConsistency(input) {
@@ -1496,7 +1634,10 @@ export async function assessBackendScenarioParamConsistency(input) {
1496
1634
  constants,
1497
1635
  })
1498
1636
  : undefined;
1499
- const observed = functionFieldOverride ?? helperFieldObserved ?? observeParamFeatures(block, inferred.field, constants, parameterNames);
1637
+ const branchFieldObserved = source && inferred.field
1638
+ ? observeBranchPayloadField({ source, tpId, block, parameterNames, field: inferred.field, constants })
1639
+ : undefined;
1640
+ const observed = functionFieldOverride ?? helperFieldObserved ?? branchFieldObserved ?? observeParamFeatures(block, inferred.field, constants, parameterNames);
1500
1641
  // Request-level / health checks often have no pytest.param payload row,
1501
1642
  // or only a TP-id label positional (no field value).
1502
1643
  const requestLevelNoParam = Boolean(source) &&
@@ -1539,8 +1680,8 @@ export async function assessBackendScenarioParamConsistency(input) {
1539
1680
  tpId,
1540
1681
  field: inferred.field,
1541
1682
  intent: inferred.intent,
1542
- observed: boundedScenarioText(observed.text, observed.length),
1543
- observedLength: observed.length,
1683
+ observed: boundedScenarioText(observed.text, persistedObservedLength(observed.length)),
1684
+ observedLength: persistedObservedLength(observed.length),
1544
1685
  status,
1545
1686
  repairability,
1546
1687
  reasonCode: status === "MATCH" ? "SCENARIO_PARAM_MATCH" : status === "MISMATCH" ? "SCENARIO_PARAM_MISMATCH" : "SCENARIO_PARAM_UNDETERMINED",
@@ -1709,6 +1850,12 @@ export function deterministicRewriteScenarioParam(input) {
1709
1850
  }
1710
1851
  valueLiteral = `"x" * ${bound + 1}`;
1711
1852
  break;
1853
+ case "max-1":
1854
+ if (bound === undefined) {
1855
+ return { ok: false, source: input.source, detail: "missing bound for max-1" };
1856
+ }
1857
+ valueLiteral = `"x" * ${Math.max(0, bound - 1)}`;
1858
+ break;
1712
1859
  case "min-1":
1713
1860
  if (bound === undefined) {
1714
1861
  return { ok: false, source: input.source, detail: "missing bound for min-1" };
@@ -43,6 +43,67 @@ function splitCells(raw) {
43
43
  .map((cell) => cell.trim())
44
44
  .filter((cell, index, cells) => !(cell === "" && (index === 0 || index === cells.length - 1)));
45
45
  }
46
+ function isExactSlotInventory(extras, partitionId) {
47
+ const tokens = extras
48
+ .join(";")
49
+ .split(/[;,,;]/)
50
+ .map((token) => token.trim().replace(/`/g, ""))
51
+ .filter((token) => token.length > 0);
52
+ if (tokens.length === 0)
53
+ return false;
54
+ const prefix = `TP-${partitionId.trim().toUpperCase()}-`;
55
+ return tokens.every((token) => /^TP-[A-Z0-9-]+$/i.test(token) &&
56
+ (token.toUpperCase().startsWith(prefix) || token.toUpperCase().startsWith("TP-SP-")));
57
+ }
58
+ function trimExactSlotInventoryCells(cells) {
59
+ if (cells.length <= 7)
60
+ return { cells, trimmed: false };
61
+ const partitionId = cells[0] ?? "";
62
+ if (!isExactSlotInventory(cells.slice(7), partitionId)) {
63
+ return { cells, trimmed: false };
64
+ }
65
+ return { cells: cells.slice(0, 7), trimmed: true };
66
+ }
67
+ export function canonicalizeScenarioPartitionTable(markdown) {
68
+ const lines = markdown.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
69
+ const headingIndex = lines.findIndex((line) => PARTITION_HEADING.test(line.trim()));
70
+ if (headingIndex === -1) {
71
+ return { markdown: lines.join("\n"), changed: false, trimmedRows: 0 };
72
+ }
73
+ let headerIndex = -1;
74
+ let sectionEnd = lines.length;
75
+ for (let i = headingIndex + 1; i < lines.length; i++) {
76
+ if (/^##\s+/.test(lines[i].trim()) && !PARTITION_HEADING.test(lines[i].trim())) {
77
+ sectionEnd = i;
78
+ break;
79
+ }
80
+ if (PARTITION_TABLE_HEADER.test(lines[i])) {
81
+ headerIndex = i;
82
+ }
83
+ }
84
+ if (headerIndex === -1) {
85
+ return { markdown: lines.join("\n"), changed: false, trimmedRows: 0 };
86
+ }
87
+ let trimmedRows = 0;
88
+ for (let i = headerIndex + 2; i < sectionEnd; i++) {
89
+ const line = lines[i];
90
+ if (!TABLE_ROW.test(line)) {
91
+ if (/^##\s+/.test(line) || /^#\s+/.test(line))
92
+ break;
93
+ continue;
94
+ }
95
+ const { cells, trimmed } = trimExactSlotInventoryCells(splitCells(line));
96
+ if (!trimmed)
97
+ continue;
98
+ lines[i] = `| ${cells.join(" | ")} |`;
99
+ trimmedRows += 1;
100
+ }
101
+ return {
102
+ markdown: lines.join("\n"),
103
+ changed: trimmedRows > 0,
104
+ trimmedRows,
105
+ };
106
+ }
46
107
  /**
47
108
  * Parse the `## Scenario Partitions` section of the README into declared
48
109
  * partitions. Returns { partitions, issues }; issues are fail-closed parse
@@ -77,7 +138,7 @@ export function parseScenarioPartitions(readmeMarkdown) {
77
138
  break;
78
139
  continue;
79
140
  }
80
- const cells = splitCells(line);
141
+ const { cells } = trimExactSlotInventoryCells(splitCells(line));
81
142
  if (cells.length !== 7) {
82
143
  issues.push(`MISSING_PARTITION: row has ${cells.length} cells, expected 7: ${line.trim()}`);
83
144
  continue;