loopmath 0.2.0__tar.gz → 0.2.1__tar.gz

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 (368) hide show
  1. {loopmath-0.2.0 → loopmath-0.2.1}/PKG-INFO +1 -1
  2. {loopmath-0.2.0 → loopmath-0.2.1}/pyproject.toml +1 -1
  3. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/__init__.py +1 -1
  4. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/design.py +30 -5
  5. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/fit.py +24 -1
  6. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/lookahead.py +3 -3
  7. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/state.py +24 -8
  8. loopmath-0.2.1/src/loopmath/builder/__init__.py +1 -0
  9. loopmath-0.2.1/src/loopmath/builder/cli.py +34 -0
  10. loopmath-0.2.1/src/loopmath/builder/context.py +240 -0
  11. loopmath-0.2.1/src/loopmath/builder/predict.py +180 -0
  12. loopmath-0.2.1/src/loopmath/builder/server.py +167 -0
  13. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/cli_registry.py +7 -3
  14. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/commands.py +47 -11
  15. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/curve.py +86 -5
  16. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/engine.py +178 -28
  17. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/message.py +101 -14
  18. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/search.py +7 -6
  19. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/skills/loopmath/SKILL.md +1 -1
  20. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/skills/loopmath-import-runs/SKILL.md +5 -4
  21. loopmath-0.2.1/src/loopmath/skill/skills/loopmath-plan-task/SKILL.md +119 -0
  22. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/skills/loopmath-update-fit/SKILL.md +6 -3
  23. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/skills/reference.md +20 -4
  24. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/commands.py +38 -3
  25. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/config.py +19 -3
  26. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/base.css +7 -0
  27. loopmath-0.2.1/src/loopmath/views/assets/builder.html +109 -0
  28. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/common.js +85 -1
  29. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/estimates.js +11 -6
  30. loopmath-0.2.1/src/loopmath/views/assets/plans.css +39 -0
  31. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/plans.js +233 -52
  32. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/results.js +44 -16
  33. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/runs.js +1 -0
  34. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/viz.css +14 -0
  35. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/viz.js +46 -21
  36. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/plans.py +3 -1
  37. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/posterior.py +219 -20
  38. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath.egg-info/PKG-INFO +1 -1
  39. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath.egg-info/SOURCES.txt +7 -0
  40. loopmath-0.2.1/tests/test_cli_help_lists.py +40 -0
  41. loopmath-0.2.0/src/loopmath/skill/skills/loopmath-plan-task/SKILL.md +0 -95
  42. loopmath-0.2.0/src/loopmath/views/assets/plans.css +0 -9
  43. {loopmath-0.2.0 → loopmath-0.2.1}/LICENSE +0 -0
  44. {loopmath-0.2.0 → loopmath-0.2.1}/README.md +0 -0
  45. {loopmath-0.2.0 → loopmath-0.2.1}/setup.cfg +0 -0
  46. {loopmath-0.2.0 → loopmath-0.2.1}/spec/ocp-v0.2.schema.json +0 -0
  47. {loopmath-0.2.0 → loopmath-0.2.1}/spec/ocp-v0.schema.json +0 -0
  48. {loopmath-0.2.0 → loopmath-0.2.1}/spec/ocp_conformance.py +0 -0
  49. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/__main__.py +0 -0
  50. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapter_host.py +0 -0
  51. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/__init__.py +0 -0
  52. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/atrium.py +0 -0
  53. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/base.py +0 -0
  54. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/bb.py +0 -0
  55. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/bb_support.py +0 -0
  56. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/omp.py +0 -0
  57. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/omp_facts.py +0 -0
  58. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/omp_links.py +0 -0
  59. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/omp_parse.py +0 -0
  60. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/omp_types.py +0 -0
  61. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/opencode.py +0 -0
  62. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/opencode_store.py +0 -0
  63. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/orca.py +0 -0
  64. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/otel_genai.py +0 -0
  65. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/otel_genai_graph.py +0 -0
  66. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/otel_genai_parse.py +0 -0
  67. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/otel_genai_types.py +0 -0
  68. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/otel_genai_usage.py +0 -0
  69. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/paseo.py +0 -0
  70. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/pi.py +0 -0
  71. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/pi_support.py +0 -0
  72. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/registry.py +0 -0
  73. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/__init__.py +0 -0
  74. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/block.py +0 -0
  75. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/check_pymc.py +0 -0
  76. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/commands.py +0 -0
  77. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/compose.py +0 -0
  78. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/features.py +0 -0
  79. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/forest.py +0 -0
  80. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/gaussian.py +0 -0
  81. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/outcome.py +0 -0
  82. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/priors.py +0 -0
  83. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/cli.py +0 -0
  84. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/cli_graph.py +0 -0
  85. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/cli_support.py +0 -0
  86. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/__init__.py +0 -0
  87. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/arms.py +0 -0
  88. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/cli_e0.py +0 -0
  89. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/estimate.py +0 -0
  90. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/estimate_rework.py +0 -0
  91. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/figures.py +0 -0
  92. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/io.py +0 -0
  93. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/verb.py +0 -0
  94. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/explain.py +0 -0
  95. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/fit.py +0 -0
  96. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/fit_assembly.py +0 -0
  97. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/fit_bayes.py +0 -0
  98. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/fit_masks.py +0 -0
  99. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/fit_predict.py +0 -0
  100. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/fit_pricing.py +0 -0
  101. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/fit_transfer.py +0 -0
  102. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/gaps.py +0 -0
  103. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/gitwalk.py +0 -0
  104. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/grade.py +0 -0
  105. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/grade_support.py +0 -0
  106. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/__init__.py +0 -0
  107. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/artifact_git.py +0 -0
  108. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/artifact_join.py +0 -0
  109. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/artifact_kinds.py +0 -0
  110. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/artifacts.py +0 -0
  111. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/bashparse.py +0 -0
  112. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/bashread_ops.py +0 -0
  113. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/bashwrite_ops.py +0 -0
  114. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/bashwrites.py +0 -0
  115. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/cache.py +0 -0
  116. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/codexio.py +0 -0
  117. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/codexio_parse.py +0 -0
  118. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/codexio_scan.py +0 -0
  119. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/dataset.py +0 -0
  120. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/dataset_build.py +0 -0
  121. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/dataset_core.py +0 -0
  122. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/dataset_edges.py +0 -0
  123. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/dataset_nodes.py +0 -0
  124. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/extract.py +0 -0
  125. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/html_common.py +0 -0
  126. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/html_costcurve.py +0 -0
  127. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/html_css.py +0 -0
  128. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/html_data.py +0 -0
  129. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/html_force.py +0 -0
  130. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/html_render.py +0 -0
  131. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/html_swimlanes.py +0 -0
  132. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler.py +0 -0
  133. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_cli.py +0 -0
  134. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_common.py +0 -0
  135. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_parse.py +0 -0
  136. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_pricing.py +0 -0
  137. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt.py +0 -0
  138. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_build.py +0 -0
  139. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_dispatch.py +0 -0
  140. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_evidence.py +0 -0
  141. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_node.py +0 -0
  142. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_records.py +0 -0
  143. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_result.py +0 -0
  144. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_session.py +0 -0
  145. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_tools.py +0 -0
  146. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_validation.py +0 -0
  147. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_workflow.py +0 -0
  148. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_report.py +0 -0
  149. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_score.py +0 -0
  150. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labels.py +0 -0
  151. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/launch.py +0 -0
  152. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/launch_origin.py +0 -0
  153. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/ocp.py +0 -0
  154. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/ocp_emit.py +0 -0
  155. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/ocp_support.py +0 -0
  156. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/render.py +0 -0
  157. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/runfile.py +0 -0
  158. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/runfile_export.py +0 -0
  159. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/runfile_finish.py +0 -0
  160. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/scan.py +0 -0
  161. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/schema.py +0 -0
  162. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/token_completeness.py +0 -0
  163. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/__init__.py +0 -0
  164. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/base.py +0 -0
  165. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/claude_code.py +0 -0
  166. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/claude_code_support.py +0 -0
  167. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/codex.py +0 -0
  168. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/codex_support.py +0 -0
  169. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/ocp.py +0 -0
  170. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/ocp_analysis.py +0 -0
  171. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/ocp_artifact.py +0 -0
  172. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/ocp_common.py +0 -0
  173. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/ocp_convert.py +0 -0
  174. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/ocp_projection.py +0 -0
  175. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/logmatch/__init__.py +0 -0
  176. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/logmatch/artifacts.py +0 -0
  177. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/logmatch/clip.py +0 -0
  178. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/logmatch/costs.py +0 -0
  179. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/logmatch/match.py +0 -0
  180. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/logmatch/settle.py +0 -0
  181. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/logmatch/tariff.py +0 -0
  182. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/__init__.py +0 -0
  183. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/canonical.py +0 -0
  184. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/commands.py +0 -0
  185. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/conformance.py +0 -0
  186. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/contractv3.py +0 -0
  187. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/emit.py +0 -0
  188. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/migrate.py +0 -0
  189. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/schema/ocp-v0.2.schema.json +0 -0
  190. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/schema/ocp-v0.3.schema.json +0 -0
  191. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/schema/ocp-v0.schema.json +0 -0
  192. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/version.py +0 -0
  193. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/onboard/__init__.py +0 -0
  194. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/onboard/commands.py +0 -0
  195. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/onboard/history.py +0 -0
  196. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/onboard/label.py +0 -0
  197. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/onboard/usual.py +0 -0
  198. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/output.py +0 -0
  199. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/pool.py +0 -0
  200. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/price.py +0 -0
  201. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/price_validation.py +0 -0
  202. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/prices.toml +0 -0
  203. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/__init__.py +0 -0
  204. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/benchmarks.py +0 -0
  205. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/benchmarks.toml +0 -0
  206. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/build.py +0 -0
  207. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/bundle/README.md +0 -0
  208. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/bundle/e0.jsonl.gz +0 -0
  209. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/bundle/manifest.json +0 -0
  210. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/bundle/rq1.jsonl.gz +0 -0
  211. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/bundle/sweep.jsonl.gz +0 -0
  212. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/commands.py +0 -0
  213. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/e0.py +0 -0
  214. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/history.py +0 -0
  215. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/ocpdoc.py +0 -0
  216. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/reduce.py +0 -0
  217. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/registry.py +0 -0
  218. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/rq1.py +0 -0
  219. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/show.py +0 -0
  220. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/sweep.py +0 -0
  221. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/validate.py +0 -0
  222. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/receipts.py +0 -0
  223. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/__init__.py +0 -0
  224. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/backlog.py +0 -0
  225. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/gain.py +0 -0
  226. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/receipts.py +0 -0
  227. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/stats.py +0 -0
  228. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/storeread.py +0 -0
  229. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/report/__init__.py +0 -0
  230. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/report/html.py +0 -0
  231. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/report/terminal.py +0 -0
  232. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/report/terminal_exclusions.py +0 -0
  233. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/report/terminal_format.py +0 -0
  234. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/report/terminal_read.py +0 -0
  235. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/research_defaults.py +0 -0
  236. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/research_paths.py +0 -0
  237. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/scoring.py +0 -0
  238. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/scoring_comparisons.py +0 -0
  239. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/scoring_predictive.py +0 -0
  240. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/share/__init__.py +0 -0
  241. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/share/commands.py +0 -0
  242. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/share/export.py +0 -0
  243. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/share/import_.py +0 -0
  244. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/__init__.py +0 -0
  245. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/commands.py +0 -0
  246. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/doctor.py +0 -0
  247. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/install.py +0 -0
  248. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/skills/loopmath-onboard/SKILL.md +0 -0
  249. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/skills/loopmath-record-run/SKILL.md +0 -0
  250. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/__init__.py +0 -0
  251. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/budget.py +0 -0
  252. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/finish.py +0 -0
  253. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/fitjob.py +0 -0
  254. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/home.py +0 -0
  255. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/ids.py +0 -0
  256. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/lock.py +0 -0
  257. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/record.py +0 -0
  258. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/report.py +0 -0
  259. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/runs.py +0 -0
  260. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/status.py +0 -0
  261. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/surface.py +0 -0
  262. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/surface_bootstrap.py +0 -0
  263. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/surface_estimation.py +0 -0
  264. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/surface_frame.py +0 -0
  265. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/surface_presentation.py +0 -0
  266. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/surface_support.py +0 -0
  267. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/taskmodel.py +0 -0
  268. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/types.py +0 -0
  269. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/__init__.py +0 -0
  270. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/README.md +0 -0
  271. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/estimates.css +0 -0
  272. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/graph.css +0 -0
  273. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/graph.js +0 -0
  274. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/results.css +0 -0
  275. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/common.py +0 -0
  276. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/runs.py +0 -0
  277. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/__init__.py +0 -0
  278. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/candidates.py +0 -0
  279. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/catalog/best_of_n.toml +0 -0
  280. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/catalog/implement_review.toml +0 -0
  281. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/catalog/plan_implement.toml +0 -0
  282. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/catalog/plan_implement_review.toml +0 -0
  283. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/catalog/solo.toml +0 -0
  284. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/catalog/swarm.toml +0 -0
  285. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/commands.py +0 -0
  286. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/diff.py +0 -0
  287. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/format.py +0 -0
  288. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/graphview.py +0 -0
  289. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/ids.py +0 -0
  290. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/infer.py +0 -0
  291. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/models.py +0 -0
  292. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/ocp.py +0 -0
  293. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/shapes.py +0 -0
  294. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath.egg-info/dependency_links.txt +0 -0
  295. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath.egg-info/entry_points.txt +0 -0
  296. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath.egg-info/requires.txt +0 -0
  297. {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath.egg-info/top_level.txt +0 -0
  298. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_atrium.py +0 -0
  299. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_bb.py +0 -0
  300. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_omp.py +0 -0
  301. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_opencode.py +0 -0
  302. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_opencode_output_tokens.py +0 -0
  303. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_orca.py +0 -0
  304. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_otel_genai.py +0 -0
  305. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_paseo.py +0 -0
  306. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_services.py +0 -0
  307. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapters.py +0 -0
  308. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapters_pi.py +0 -0
  309. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_arms.py +0 -0
  310. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_cli_graph.py +0 -0
  311. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_cli_labeler.py +0 -0
  312. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_cli_main_module.py +0 -0
  313. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_cli_plan_hidden.py +0 -0
  314. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_cli_prices.py +0 -0
  315. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_cli_startup.py +0 -0
  316. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_color_proof.py +0 -0
  317. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_duplicate_guards.py +0 -0
  318. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_estimate.py +0 -0
  319. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_explain.py +0 -0
  320. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_fit.py +0 -0
  321. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_gaps.py +0 -0
  322. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_gitwalk.py +0 -0
  323. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_grade.py +0 -0
  324. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_artifact_recording.py +0 -0
  325. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_artifacts.py +0 -0
  326. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_bashwrites.py +0 -0
  327. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_cache.py +0 -0
  328. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_codexio.py +0 -0
  329. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_dataset.py +0 -0
  330. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_extract.py +0 -0
  331. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_glue.py +0 -0
  332. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_html.py +0 -0
  333. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_labeler.py +0 -0
  334. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_labeler_evidence_matrix.py +0 -0
  335. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_launch.py +0 -0
  336. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_ocp.py +0 -0
  337. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_render.py +0 -0
  338. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_runfile.py +0 -0
  339. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_schema.py +0 -0
  340. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_home_guard.py +0 -0
  341. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_html.py +0 -0
  342. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ingest.py +0 -0
  343. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ingest_claude_code.py +0 -0
  344. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ingest_codex.py +0 -0
  345. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ingest_ocp.py +0 -0
  346. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_io.py +0 -0
  347. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_not_run_summary.py +0 -0
  348. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ocp_conformance.py +0 -0
  349. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ocp_golden.py +0 -0
  350. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ocp_open_enums.py +0 -0
  351. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ocp_versioning.py +0 -0
  352. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_output_html_path.py +0 -0
  353. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_packaging_metadata.py +0 -0
  354. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_packaging_smoke.py +0 -0
  355. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_pool.py +0 -0
  356. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_preview_script.py +0 -0
  357. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_price.py +0 -0
  358. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_receipts.py +0 -0
  359. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_release_paths.py +0 -0
  360. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_runfile_privacy_n1b.py +0 -0
  361. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_scoring.py +0 -0
  362. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_split_rebindings.py +0 -0
  363. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_surface.py +0 -0
  364. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_taskmodel_features.py +0 -0
  365. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_terminal.py +0 -0
  366. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_types_control.py +0 -0
  367. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_types_piece_prediction.py +0 -0
  368. {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_v01_scaffold.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loopmath
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Plan agent workflows, record how they went, and learn from your own Claude Code and Codex logs
5
5
  Author: QKV Labs
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "loopmath"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Plan agent workflows, record how they went, and learn from your own Claude Code and Codex logs"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5,4 +5,4 @@ The command line (`loopmath`, or its alias `loop`) is the interface; see the
5
5
  README and `loopmath --help`.
6
6
  """
7
7
 
8
- __version__ = "0.2.0"
8
+ __version__ = "0.2.1"
@@ -35,6 +35,11 @@ HEURISTIC_COST_WEIGHT = 0.7 # spec 04 section 2 and D27: allocated (heuristic)
35
35
  USER_SOURCES = ("live", "backlog", "history", "user", "orchestrator", "onboard", "habit", "")
36
36
  HORIZON_NODES = ("horizon:timebox", "horizon:log2h") # fixed terms of a timeboxed run (spec 04 section 1)
37
37
  RQ1_EXT = "dev.loopmath.rq1" # loopmath-exp's ext key; its horizon_s is the fallback for older RQ1 runs
38
+ # 0.2.1 (F5, spec 04 section 1): in the cost and tokens rows of a timeboxed task the terms a timebox caps (effort
39
+ # and shape) enter at this value, not 1. Set from the RQ1 2 h runs: open-ended effort terms gave max over low x2.64
40
+ # (solo luna) and x2.23 (solo sol) against x1.34 and x1.27 observed; ln 1.34 / ln 2.64 = ln 1.27 / ln 2.23 = 0.30.
41
+ TIMEBOX_EFFORT_COST = 0.3
42
+ TIMEBOX_LEVELS = ("effort:", "family_effort:", "topology:", "position:")
38
43
 
39
44
 
40
45
  def other_design(meta: dict) -> str | None:
@@ -304,17 +309,36 @@ def structure(config: Configuration) -> Structure:
304
309
 
305
310
  # ---------------------------------------------------------------- rows
306
311
 
312
+ def effort_weight(task: Task, timebox_effort: float | None = None) -> float:
313
+ """The value of the timebox-capped terms (TIMEBOX_LEVELS) in a cost or tokens row of `task`: `timebox_effort`
314
+ (default TIMEBOX_EFFORT_COST) when the task has a horizon, else 1 (spec 04 section 1, effort and shape under
315
+ a timebox)."""
316
+ if task_horizon(task) is None:
317
+ return 1.0
318
+ return float(TIMEBOX_EFFORT_COST if timebox_effort is None else timebox_effort)
319
+
320
+
321
+ @lru_cache(maxsize=65536)
322
+ def _effort_scaled(terms: tuple[Term, ...], weight: float) -> tuple[Term, ...]:
323
+ return tuple((n, p, v * weight) if n.startswith(TIMEBOX_LEVELS) else (n, p, v) for n, p, v in terms)
324
+
325
+
307
326
  def cost_rest(st: Structure, piece: str, k: int, setting: Setting | None = None, *,
308
- source: str | None = None) -> tuple[Term, ...]:
327
+ source: str | None = None, effort: float = 1.0) -> tuple[Term, ...]:
309
328
  """Cost and tokens rows without the task part: setting, role, topology, position, round, control,
310
329
  and with a `source` the position x source node `psrc:<shape>#<pos>|<source>` and the family x source
311
- node `fsrc:<family>|<source>` (spec 04 section 1)."""
330
+ node `fsrc:<family>|<source>` (spec 04 section 1). `effort` is the value of the terms a timebox caps, the
331
+ effort and shape (topology, position) terms (`effort_weight`: below 1 under a timebox); the role, psrc and
332
+ fsrc terms stay at 1."""
312
333
  s = setting or st.settings[piece]
313
334
  h, m, e = _s(s)
314
335
  shape = st.shape or st.workflow_id
315
336
  position = f"{shape}#{st.pieces.index(piece)}"
316
- terms = (setting_terms(h, m, e) + role_terms(st.roles[piece], m)
317
- + ((f"topology:{shape}", None, 1.0), (f"position:{position}", f"topology:{shape}", 1.0)))
337
+ st_terms = setting_terms(h, m, e)
338
+ shape_terms = ((f"topology:{shape}", None, 1.0), (f"position:{position}", f"topology:{shape}", 1.0))
339
+ if effort != 1.0:
340
+ st_terms, shape_terms = _effort_scaled(st_terms, float(effort)), _effort_scaled(shape_terms, float(effort))
341
+ terms = st_terms + role_terms(st.roles[piece], m) + shape_terms
318
342
  if source is not None:
319
343
  fam = model_path(m)[1]
320
344
  terms += ((f"psrc:{position}|{source}", f"position:{position}", 1.0),
@@ -345,7 +369,8 @@ def run_rest(st: Structure) -> tuple[Term, ...]:
345
369
 
346
370
  def cost_row(task: Task, source: str, st: Structure, piece: str, k: int,
347
371
  setting: Setting | None = None, features: FeatureSet | None = None) -> list[Term]:
348
- return task_terms(task, source, features) + list(cost_rest(st, piece, k, setting, source=source))
372
+ return task_terms(task, source, features) + list(cost_rest(st, piece, k, setting, source=source,
373
+ effort=effort_weight(task)))
349
374
 
350
375
 
351
376
  def gate_row(task: Task, source: str, st: Structure, gate: GateInfo, k: int,
@@ -29,6 +29,7 @@ import numpy as np
29
29
  from scipy import sparse
30
30
 
31
31
  from ..taskmodel import FeatureConfigError, FeatureSet
32
+ from . import design as design_rows
32
33
  from . import priors as prior_data
33
34
  from .design import (DESIGN_VERSION, Term, Unusable, cost_row, gate_row, parse_run, run_row, structure,
34
35
  task_features, task_terms)
@@ -362,9 +363,27 @@ def _support(hr: HeadRows, X: sparse.csr_matrix, node_ids: list[str]) -> dict[st
362
363
 
363
364
 
364
365
  def seed_for(fit_id: str, head: str) -> int:
366
+ """A head's seed from the fit's `seed_key` (its fit id before 0.2.1)."""
365
367
  return int.from_bytes(hashlib.sha256(f"{fit_id}/{head}".encode()).digest()[:4], "little")
366
368
 
367
369
 
370
+ def input_key(heads_rows: dict[str, HeadRows], specs: list, settings: dict) -> str:
371
+ """The fit's `seed_key` (spec 04 section 3): the sha256 of the design version, the settings that change the
372
+ posterior, the prior factors and every head's rows (terms, response, weight, run, source), each head's rows
373
+ sorted, so the same data and settings seed the same draws whatever the store's reading order."""
374
+ h = hashlib.sha256()
375
+ h.update(repr((DESIGN_VERSION, design_rows.TIMEBOX_EFFORT_COST, design_rows.TIMEBOX_LEVELS,
376
+ sorted(settings.items()))).encode())
377
+ for spec in specs:
378
+ h.update(repr((spec.head, spec.terms, spec.mean, spec.var)).encode())
379
+ for name in sorted(heads_rows):
380
+ hr = heads_rows[name]
381
+ h.update(f"\n{name}:{len(hr.rows)}".encode())
382
+ for line in sorted(repr((t, y, w, r, src)) for t, y, w, r, src in zip(hr.rows, hr.y, hr.w, hr.runs, hr.sources)):
383
+ h.update(line.encode())
384
+ return h.hexdigest()
385
+
386
+
368
387
  def fit_head(hr: HeadRows, forest: Forest, factor_specs: list, *, eb: bool = True) -> dict:
369
388
  """Fit one head; returns arrays and metadata for the state files."""
370
389
  extra = [(n, p) for spec in factor_specs for n, p, _ in spec.terms]
@@ -502,6 +521,8 @@ def fit(home: Path, *, no_prior: bool = False, without: tuple[str, ...] = (), fu
502
521
  specs = []
503
522
  if not no_prior and "benchmark" not in without:
504
523
  specs = prior_data.benchmark_factors(benchmarks, weight=weight)
524
+ seed_key = input_key(heads_rows, specs, {"no_prior": no_prior, "without": sorted(without), "eb": eb,
525
+ "benchmark_prior_weight": weight})
505
526
  forest = Forest()
506
527
  fitted = {}
507
528
  for name, hr in heads_rows.items():
@@ -546,6 +567,8 @@ def fit(home: Path, *, no_prior: bool = False, without: tuple[str, ...] = (), fu
546
567
  _atomic_json(partial / "design.json", design)
547
568
  meta = {
548
569
  "fit": fit_id, "created_at": now.isoformat(timespec="seconds"), "code_version": _code_version(),
570
+ "seed_key": seed_key, "timebox_effort": design_rows.TIMEBOX_EFFORT_COST,
571
+ "timebox_terms": list(design_rows.TIMEBOX_LEVELS),
549
572
  "options": {"no_prior": no_prior, "without": list(without), "full": full, "eb": eb,
550
573
  "benchmark_prior_weight": weight},
551
574
  "runs_by_source": runs_by_source,
@@ -562,7 +585,7 @@ def fit(home: Path, *, no_prior: bool = False, without: tuple[str, ...] = (), fu
562
585
  "center": h["center"], "scale": h["scale"], "baseline": h["baseline"],
563
586
  "iterations": h["fit"].iterations, "optimizer": h["fit"].info,
564
587
  "log_evidence": _finite(h["fit"].log_evidence), "factors": h["factors"],
565
- "seed": seed_for(fit_id, name)}
588
+ "seed": seed_for(seed_key, name)}
566
589
  for name, h in fitted.items()},
567
590
  "scores": score_info,
568
591
  "draws": N_DRAWS,
@@ -165,7 +165,7 @@ def _prepare(state: FitState, task: Task, configs: list[Configuration], rule: Ac
165
165
  tt = state.task_terms(task)
166
166
  cost_rows, coef, owner_r, owner_c, run_rows, plans = [], [], [], [], [], []
167
167
  for ci, (cfg, (_, d)) in enumerate(zip(configs, out)):
168
- plan = state._plan(cfg)
168
+ plan = state._plan(cfg, state.effort_for(task))
169
169
  plans.append(plan)
170
170
  st = plan["st"]
171
171
  reach = d["reach"] # E[r(k)] with the unseen task effect on the gates integrated out
@@ -193,7 +193,7 @@ def _prepare(state: FitState, task: Task, configs: list[Configuration], rule: Ac
193
193
  def _explore_cost_obs(state: FitState, task: Task, config: Configuration) -> tuple[list[tuple], int]:
194
194
  """Composite observation: the mean of the round-1 cost rows of the configuration's pieces,
195
195
  and the number of pieces (the observation's noise variance is sigma^2 / n)."""
196
- plan = state._plan(config)
196
+ plan = state._plan(config, state.effort_for(task))
197
197
  st = plan["st"]
198
198
  tt = state.task_terms(task)
199
199
  acc: dict[str, list] = {}
@@ -385,7 +385,7 @@ def _condition_head(head: HeadState, terms: Sequence[tuple], tau2: float) -> Hea
385
385
  draws = mean[:, None] + U @ Z
386
386
  meta = dict(head.meta) or {"engine": head.engine, "kind": head.kind, "sigma": head.sigma}
387
387
  out = HeadState(head.name, meta, head.nodes + new_nodes, None, head.fit_id, mean=mean, U=U, draws=draws,
388
- unit=Z)
388
+ unit=Z, seed_key=head.seed_key)
389
389
  out.fitted = head.fitted
390
390
  out.phi = dict(head.phi)
391
391
  return out
@@ -152,7 +152,7 @@ class HeadState:
152
152
  `loader(part)` returns a stored array or None when the fit did not store it."""
153
153
 
154
154
  def __init__(self, name: str, meta: dict, nodes: Sequence[str], loader: Callable[[str], np.ndarray] | None,
155
- fit_id: str, *, mean=None, U=None, draws=None, unit=None):
155
+ fit_id: str, *, mean=None, U=None, draws=None, unit=None, seed_key: str | None = None):
156
156
  base = BASE_HEADS.get(name, ("gaussian", "score"))
157
157
  self.name = name
158
158
  self.engine = meta.get("engine") or base[0]
@@ -165,6 +165,7 @@ class HeadState:
165
165
  self.seed = int(meta.get("seed", 0))
166
166
  self.meta = meta
167
167
  self.fit_id = fit_id
168
+ self.seed_key = seed_key or fit_id # spec 04 section 3: the fit's input hash; the fit id before 0.2.1
168
169
  self.nodes = list(nodes)
169
170
  self.index = {n: j for j, n in enumerate(self.nodes)}
170
171
  self._loader = loader
@@ -252,7 +253,7 @@ class HeadState:
252
253
  """Standard normal draws of an unseen node, fixed by the fit, the head and the node id."""
253
254
  z = self._virtual.get(node)
254
255
  if z is None:
255
- z = np.random.default_rng(_seed(self.fit_id, self.name, node)).standard_normal(N_DRAWS)
256
+ z = np.random.default_rng(_seed(self.seed_key, self.name, node)).standard_normal(N_DRAWS)
256
257
  self._virtual[node] = z
257
258
  return z
258
259
 
@@ -332,16 +333,21 @@ class FitState:
332
333
  self._design = design
333
334
  self._npz = None
334
335
  self._cache: dict = {}
335
- self.sim_seed = _seed(self.fit_id, "simulate")
336
+ # spec 04 section 3: every draw is seeded from the fit's input hash, so identical fits give identical
337
+ # output; fits before 0.2.1 have no `seed_key` and keep their fit id
338
+ self.seed_key: str = self.meta.get("seed_key") or self.fit_id
339
+ self.sim_seed = _seed(self.seed_key, "simulate")
340
+ # spec 04 section 1: the value of the timebox-capped terms (effort, shape) in a timeboxed task's cost rows
341
+ self.timebox_effort = float(self.meta.get("timebox_effort", 1.0))
336
342
  if heads is None:
337
343
  heads = {}
338
344
  for name, hmeta in (self.meta.get("heads") or {}).items():
339
345
  key = name.replace(":", "@")
340
346
  ids = [str(x) for x in self._array(f"{key}__ids")]
341
- heads[name] = HeadState(name, hmeta, ids, self._loader(key), self.fit_id)
347
+ heads[name] = HeadState(name, hmeta, ids, self._loader(key), self.fit_id, seed_key=self.seed_key)
342
348
  for name in BASE_HEADS:
343
349
  if name not in heads:
344
- heads[name] = HeadState(name, {}, [], None, self.fit_id)
350
+ heads[name] = HeadState(name, {}, [], None, self.fit_id, seed_key=self.seed_key)
345
351
  self.heads = heads
346
352
  # the fit's feature declarations (the built-ins for fits before 0.2), so predictions read
347
353
  # features the way the fit did
@@ -397,13 +403,22 @@ class FitState:
397
403
  def _row(self, head: HeadState, rest: tuple) -> Row:
398
404
  return self._cached(head, [rest])[0]
399
405
 
400
- def _plan(self, config: Configuration) -> dict:
406
+ def effort_for(self, task: Task) -> float:
407
+ """The value of the timebox-capped terms (effort and shape, `design.TIMEBOX_LEVELS`) in the task's cost
408
+ rows: the fit's `timebox_effort` when the task's resolved horizon (given, else recorded) is a timebox the
409
+ fit uses (it has timeboxed runs), else 1 (spec 04 section 1)."""
410
+ if self.timebox_effort == 1.0:
411
+ return 1.0
412
+ return self.timebox_effort if self.resolve_task(task)[1]["horizon"]["used"] else 1.0
413
+
414
+ def _plan(self, config: Configuration, effort: float = 1.0) -> dict:
415
+ """The configuration's rows without the task part; `effort` from `effort_for(task)`."""
401
416
  st = structure(config)
402
417
  cost = {}
403
418
  for piece in st.pieces:
404
419
  rounds = st.k_max if st.piece_loop.get(piece) is not None else 1
405
420
  for k in range(1, rounds + 1):
406
- cost[(piece, k)] = cost_rest(st, piece, k, source=PREDICT_SOURCE)
421
+ cost[(piece, k)] = cost_rest(st, piece, k, source=PREDICT_SOURCE, effort=effort)
407
422
  gate = {}
408
423
  for gi, g in enumerate(st.gates):
409
424
  for k in range(1, (st.k_max if st.gate_loop.get(gi) is not None else 1) + 1):
@@ -790,7 +805,8 @@ class FitState:
790
805
 
791
806
  def _predict_many(self, task: Task, configs: Sequence[Configuration], rule: AcceptanceRule | None,
792
807
  rescue_usd: float | None) -> list[tuple[Prediction, dict]]:
793
- plans = [self._plan(c) for c in configs]
808
+ effort = self.effort_for(task)
809
+ plans = [self._plan(c, effort) for c in configs]
794
810
  tparts = self._task_parts(task)
795
811
  self._warm(plans)
796
812
  return [self._predict(task, p, tparts, rule, rescue_usd) for p in plans]
@@ -0,0 +1 @@
1
+ """`loopmath builder`: the workflow builder (P8), a local page over the fitted model (spec 02, `builder`)."""
@@ -0,0 +1,34 @@
1
+ """The `loopmath builder` parser, added by `cli_registry.register` (spec 02, `builder`).
2
+
3
+ It takes the task and rule flags of `recommend`, from the registry's own helpers, and imports nothing
4
+ heavy: the handler is `builder.server:serve`, loaded only when the command runs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ def add(sub) -> None:
11
+ from ..cli_registry import FIT_HELP, _common, _lazy, _task_args
12
+
13
+ p = sub.add_parser("builder", help="build your own workflow on a local page and see its estimates change",
14
+ description="Serves the workflow builder on 127.0.0.1: pick a shape, change models, efforts, "
15
+ "widths and pieces, and see the estimated numbers for the task, the same ones "
16
+ "recommend gives. Ctrl+C stops it.")
17
+ _task_args(p)
18
+ rule = p.add_mutually_exclusive_group()
19
+ rule.add_argument("--rule", default=None, metavar="RULE", help="a named acceptance rule from config")
20
+ rule.add_argument("--target", default=None, metavar="NAME>=X", help="score-target rule, quoted: e.g. 'heldout_perf>=2400'")
21
+ p.add_argument("--goal", default=None, choices=["default", "p50", "p70", "p80", "p90", "p95", "p99"],
22
+ help="the pick to aim for, as for recommend (default: config goal)")
23
+ p.add_argument("--usual", default=None, metavar="CFG", help="a configuration id to treat as the usual one, as for recommend")
24
+ p.add_argument("--workflow", action="append", default=[], metavar="FILE.toml", help="also consider this workflow (repeatable)")
25
+ p.add_argument("--models", default=None, metavar="M,M,...", help="restrict candidate models")
26
+ p.add_argument("--fit", default=None, metavar="ID", help=FIT_HELP)
27
+ p.add_argument("--rec", default=None, metavar="REC", help="start from a saved recommendation: its task, rule and fit "
28
+ "(task flags given as well override its task)")
29
+ p.add_argument("--start", default=None, metavar="CFG", help="open this configuration first: a cfg_ id from the "
30
+ "recommendation, an earlier one or your runs")
31
+ p.add_argument("--port", type=int, default=0, metavar="N", help="port on 127.0.0.1 (default 0: a free port)")
32
+ p.add_argument("--no-open", action="store_true", help="print the URL without opening the browser")
33
+ _common(p, json_flag=False)
34
+ p.set_defaults(func=_lazy("loopmath.builder.server:serve"))
@@ -0,0 +1,240 @@
1
+ """The builder's session: the fit and the recommendation it answers from, and `GET /api/context`.
2
+
3
+ At start `build_session` reads the task and rule as `recommend` does, loads the fit once and runs the
4
+ recommend engine once, with the same helpers in the same order as `recommend.commands.recommend`, so every
5
+ number the page shows is the one `recommend` gives. The session keeps the belief, the task as the belief
6
+ sees it, the rule and the recommendation in memory; `predict.py` answers edited configurations from them.
7
+ Nothing is written to the store.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import dataclasses
14
+ import re
15
+ import sys
16
+ import threading
17
+ from collections import OrderedDict
18
+ from dataclasses import dataclass, field
19
+ from typing import Any
20
+
21
+ from ..output import EXIT_NOT_FOUND, EXIT_OK, fail, home as home_dir
22
+ from ..recommend import commands as C
23
+ from ..recommend import engine, storeread
24
+ from ..recommend.engine import Recommendation
25
+ from ..recommend.storeread import Conf
26
+ from ..types import AcceptanceRule, Configuration, Task
27
+
28
+ SCHEMA = "loopmath.builder.context/1"
29
+ TOP_CANDIDATES = 60 # the page's candidates: the first 60 by cost per accepted result, plus recorded ones
30
+ KEEP_ORIGINS = ("usual", "user", "recorded")
31
+ MADE_UP_TASK_ID = re.compile(r"^tsk_[0-9A-HJKMNP-TV-Z]{26}$") # `storeread.ulid()`: an id no user gave
32
+ TASK_KEYS = ("type", "repo", "title", "subtype", "org", "features", "base_commit")
33
+
34
+
35
+ @dataclass
36
+ class Session:
37
+ belief: Any
38
+ task: Task # the task as the belief sees it (recommend's `asked`); predictions use this one
39
+ rule: AcceptanceRule
40
+ rec: Recommendation # `rec.task` is the task as shown
41
+ fit: dict[str, Any]
42
+ rec_id: str | None = None # `--rec`
43
+ start: Configuration | None = None # `--start`
44
+ lock: threading.Lock = field(default_factory=threading.Lock) # the belief caches rows; one predict at a time
45
+ cache: OrderedDict = field(default_factory=OrderedDict) # config id -> predict answer
46
+ context: dict[str, Any] | None = None
47
+ models: dict[str, int] | None = None # `known_models`, read once
48
+
49
+
50
+ # ---------------------------------------------------------------- start
51
+ def rec_task(stored: dict[str, Any], args: argparse.Namespace) -> tuple[Task, bool]:
52
+ """A stored recommendation's task, with the task flags given on top. Its id counts as the user's only
53
+ when it is not one `recommend` made up, so the belief sees the same question."""
54
+ t = stored.get("task") if isinstance(stored.get("task"), dict) else {}
55
+ d = {k: t[k] for k in TASK_KEYS if t.get(k) is not None}
56
+ tid = str(t.get("id") or "")
57
+ given = bool(tid) and not MADE_UP_TASK_ID.match(tid)
58
+ if given:
59
+ d["id"] = tid
60
+ return C.task_from_dict(d, overrides=args), given
61
+
62
+
63
+ def asked_task(task: Task, id_given: bool, belief: Any, rule: AcceptanceRule, configs: list) -> Task:
64
+ """The task as `recommend` asks the belief: the user's when they named it, else `commands.question_task`
65
+ keyed as `recommend` keys it, by `commands.question_key(belief)` once `recommend` has it (0.2.1 lane
66
+ 21F: the fit's input seed key, so identical fits agree), else by the fit id as in 0.2.0."""
67
+ if id_given:
68
+ return task
69
+ key_of = getattr(C, "question_key", None)
70
+ return C.question_task(task, key_of(belief) if callable(key_of) else belief.fit_id, rule, configs)
71
+
72
+
73
+ def build_session(args: argparse.Namespace) -> tuple[Session | None, int]:
74
+ """The session, or None and the exit code after the message `recommend` would print."""
75
+ home = home_dir(getattr(args, "home", None))
76
+ now = storeread.now_local()
77
+ stored = None
78
+ try:
79
+ if getattr(args, "rec", None):
80
+ stored = storeread.load_rec(home, args.rec)
81
+ if stored is None:
82
+ raise C.NotFound(f"recommendation {args.rec} not found in {home / 'recs'}")
83
+ task, id_given = rec_task(stored, args)
84
+ ref = stored.get("reference") or {}
85
+ if not getattr(args, "usual", None) and ref.get("from") == "flag":
86
+ args.usual = (ref.get("config") or {}).get("id")
87
+ goal = (stored.get("goal") or {}).get("choice")
88
+ if not getattr(args, "goal", None) and isinstance(goal, str) and goal.startswith("p"):
89
+ args.goal = goal
90
+ else:
91
+ task, id_given = C.task_from_args(args)
92
+ conf = Conf.load(home)
93
+ if stored is not None and not (getattr(args, "rule", None) or getattr(args, "target", None)) \
94
+ and isinstance(stored.get("rule"), dict):
95
+ rule = AcceptanceRule.from_dict(stored["rule"])
96
+ else:
97
+ rule = C.rule_from_args(args, conf)
98
+ spend, cap = C.budget(home, conf)
99
+ settings = C.settings_from(conf, args, spend, cap)
100
+ models = C.model_list(getattr(args, "models", None))
101
+ except C.NotFound as exc:
102
+ return None, fail(str(exc), EXIT_NOT_FOUND)
103
+ except ValueError as exc:
104
+ return None, fail(str(exc))
105
+ fit_id = getattr(args, "fit", None)
106
+ if stored is not None and not fit_id: # the recommendation's own fit while it is kept, else the latest
107
+ fit_id = (stored.get("fit") or {}).get("id")
108
+ if fit_id and not (home / "fits" / str(fit_id)).is_dir():
109
+ print(f"warning: {args.rec} was made with fit {fit_id}, which is no longer kept; using the latest fit",
110
+ file=sys.stderr)
111
+ fit_id = None
112
+ belief, code = C.load_belief(home, fit_id)
113
+ if belief is None:
114
+ return None, code
115
+ try: # `recommend.commands.recommend`, step for step
116
+ usual, usual_from = C.resolve_usual(home, conf, task, getattr(args, "usual", None), models)
117
+ recorded = [c for c, _ in storeread.recorded_configs(home, task.type, task.repo)[0]]
118
+ if usual_from == "default" and recorded:
119
+ asked = asked_task(task, id_given, belief, rule, recorded)
120
+ best = C.reference_for(belief, asked, rule, recorded, models)
121
+ if best is not None:
122
+ usual, usual_from = best, "recorded"
123
+ user = [C.workflow_file_config(p, usual) for p in getattr(args, "workflow", None) or []]
124
+ configs = C.candidate_configs(task, usual, conf, models, user + C.store_user_configs(home, usual), recorded)
125
+ asked = asked_task(task, id_given, belief, rule, [usual, *(c for c, _ in configs)])
126
+ rec = engine.recommend(belief, asked, rule, usual=usual, usual_from=usual_from, configs=configs,
127
+ settings=settings, diff=C.diff_fn(), keep=[*(u.id for u in user),
128
+ *(r.id for r in recorded)])
129
+ rec.task = task
130
+ start = None
131
+ if getattr(args, "start", None):
132
+ found = rec.by_id(args.start)
133
+ start = found.config if found is not None else storeread.find_config(home, args.start)
134
+ if start is None:
135
+ raise C.NotFound(f"configuration {args.start} not found in this recommendation, earlier "
136
+ f"recommendations or stored runs")
137
+ except C.NotFound as exc:
138
+ return None, fail(str(exc), EXIT_NOT_FOUND)
139
+ except ValueError as exc:
140
+ return None, fail(str(exc))
141
+ fit = {**C.fit_info(belief, now), "runs": int(getattr(belief, "n_runs", 0) or 0)}
142
+ return Session(belief, asked, rule, rec, fit, rec_id=getattr(args, "rec", None), start=start), EXIT_OK
143
+
144
+
145
+ # ---------------------------------------------------------------- catalog
146
+ def fit_models(belief: Any) -> dict[str, int]:
147
+ """Models the fit has a node for, with the most runs any head has behind that node."""
148
+ design = getattr(belief, "design", None)
149
+ support = (design.get("support") if isinstance(design, dict) else None) or {}
150
+ out: dict[str, int] = {}
151
+ for head in support.values():
152
+ for node, val in (head or {}).items():
153
+ if isinstance(node, str) and node.startswith("model:"):
154
+ n = int(val[0]) if isinstance(val, (list, tuple)) and val else 0
155
+ out[node[len("model:"):]] = max(out.get(node[len("model:"):], 0), n)
156
+ return out
157
+
158
+
159
+ def rec_configs(rec: Recommendation) -> list[Configuration]:
160
+ return [rec.usual.config, *(c.config for c in rec.candidates)]
161
+
162
+
163
+ def known_models(session: Session) -> dict[str, int]:
164
+ """Model id -> runs behind it: the fit's models and any a candidate uses (0 runs when the fit has no node)."""
165
+ if session.models is None:
166
+ out = fit_models(session.belief)
167
+ for cfg in rec_configs(session.rec):
168
+ for s in cfg.settings.values():
169
+ out.setdefault(s.model, 0)
170
+ session.models = out
171
+ return session.models
172
+
173
+
174
+ def catalog(session: Session) -> dict[str, Any]:
175
+ from ..workflows.format import KNOWN_ROLES, catalog as shapes
176
+ from ..workflows.models import DEFAULT_EFFORTS, efforts_for, family_of, harness_for, sort_efforts
177
+
178
+ seen = [s for cfg in rec_configs(session.rec) for s in cfg.settings.values()]
179
+ harnesses = list(dict.fromkeys([*DEFAULT_EFFORTS, *(s.harness for s in seen)]))
180
+ models = sorted(known_models(session).items(), key=lambda kv: (-kv[1], kv[0]))
181
+ by_harness = {h: list(efforts_for(h)) for h in harnesses}
182
+ efforts = sort_efforts([e for v in by_harness.values() for e in v] + [s.effort for s in seen])
183
+ return {
184
+ "harnesses": harnesses,
185
+ "models": [{"id": m, "family": family_of(m), "harness": harness_for(m), "runs_behind": n}
186
+ for m, n in models],
187
+ "efforts": list(efforts),
188
+ "efforts_by_harness": by_harness,
189
+ "roles": list(KNOWN_ROLES),
190
+ "shapes": [{"id": wf.id, "title": wf.title,
191
+ "pieces": [{"id": p.id, "role": p.role, "width": p.width} for p in wf.pieces],
192
+ "edges": [list(e) for e in wf.edges],
193
+ "gates": [g.to_dict() for g in wf.control.gates],
194
+ "workflow": wf.to_dict()} for wf in shapes().values()],
195
+ }
196
+
197
+
198
+ # ---------------------------------------------------------------- the context
199
+ def candidate_entry(rec: Recommendation, c: Any) -> dict[str, Any]:
200
+ return {"config": c.config.to_dict(), "label": rec.label(c.config), "numbers": rec.numbers(c.prediction),
201
+ "origin": c.origin, **rec.search_fields(c.config.id)}
202
+
203
+
204
+ def context_payload(session: Session) -> dict[str, Any]:
205
+ """`GET /api/context` (spec 02, `builder`), built once per session."""
206
+ if session.context is not None:
207
+ return session.context
208
+ rec = session.rec
209
+ core = rec.payload()
210
+ choices = []
211
+ for ch in core["choices"]:
212
+ c = rec.by_id(ch["config"])
213
+ choices.append({**ch, "configuration": c.config.to_dict() if c is not None else None})
214
+ reference = {k: v for k, v in core["reference"].items() if k != "prediction"}
215
+ top = rec.candidates[:TOP_CANDIDATES]
216
+ ids = {c.config.id for c in top}
217
+ wanted = {rec.usual.config.id, *(ch["config"] for ch in core["choices"])}
218
+ top += [c for c in rec.candidates[TOP_CANDIDATES:]
219
+ if c.config.id not in ids and (c.origin in KEEP_ORIGINS or c.config.id in wanted)]
220
+ session.context = {
221
+ "schema": SCHEMA,
222
+ "task": C.task_block(rec.task, session.belief),
223
+ "rule": core["rule"],
224
+ "fit": session.fit,
225
+ "rec": session.rec_id,
226
+ "rescue": core["rescue"],
227
+ "reference": reference,
228
+ "choices": choices,
229
+ "candidates": [candidate_entry(rec, c) for c in top],
230
+ "catalog": catalog(session),
231
+ "start": session.start.to_dict() if session.start is not None else None,
232
+ }
233
+ return session.context
234
+
235
+
236
+ def with_numbers(rec: Recommendation, **by_id: dict[str, Any]) -> Recommendation:
237
+ """The recommendation with more medians (and bands) by config id, so `numbers()` formats a new prediction
238
+ exactly as it formats a candidate's."""
239
+ changes = {k: {**getattr(rec, k), **v} for k, v in by_id.items() if v and hasattr(rec, k)}
240
+ return dataclasses.replace(rec, **changes) if changes else rec