loopmath 0.2.1__tar.gz → 0.2.2__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 (374) hide show
  1. {loopmath-0.2.1 → loopmath-0.2.2}/PKG-INFO +1 -1
  2. {loopmath-0.2.1 → loopmath-0.2.2}/pyproject.toml +1 -1
  3. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/__init__.py +1 -1
  4. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/design.py +17 -7
  5. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/fit.py +35 -0
  6. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/forest.py +1 -0
  7. loopmath-0.2.2/src/loopmath/belief/pricing.py +142 -0
  8. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/state.py +5 -2
  9. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/builder/context.py +132 -19
  10. loopmath-0.2.2/src/loopmath/builder/predict.py +306 -0
  11. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/builder/server.py +15 -11
  12. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/cli_registry.py +1 -0
  13. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/benchmarks.toml +465 -3
  14. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/build.py +25 -1
  15. loopmath-0.2.2/src/loopmath/priors/bundle/README.md +3 -0
  16. loopmath-0.2.2/src/loopmath/priors/bundle/lanes.jsonl.gz +0 -0
  17. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/bundle/manifest.json +65 -1
  18. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/commands.py +3 -3
  19. loopmath-0.2.2/src/loopmath/priors/lanes.py +267 -0
  20. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/reduce.py +4 -2
  21. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/registry.py +8 -5
  22. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/commands.py +25 -11
  23. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/engine.py +41 -13
  24. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/search.py +9 -6
  25. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/config.py +2 -1
  26. loopmath-0.2.2/src/loopmath/views/assets/builder.css +303 -0
  27. loopmath-0.2.2/src/loopmath/views/assets/builder.html +70 -0
  28. loopmath-0.2.2/src/loopmath/views/assets/builder.js +1314 -0
  29. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/plans.js +19 -3
  30. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath.egg-info/PKG-INFO +1 -1
  31. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath.egg-info/SOURCES.txt +5 -0
  32. loopmath-0.2.1/src/loopmath/builder/predict.py +0 -180
  33. loopmath-0.2.1/src/loopmath/priors/bundle/README.md +0 -1
  34. loopmath-0.2.1/src/loopmath/views/assets/builder.html +0 -109
  35. {loopmath-0.2.1 → loopmath-0.2.2}/LICENSE +0 -0
  36. {loopmath-0.2.1 → loopmath-0.2.2}/README.md +0 -0
  37. {loopmath-0.2.1 → loopmath-0.2.2}/setup.cfg +0 -0
  38. {loopmath-0.2.1 → loopmath-0.2.2}/spec/ocp-v0.2.schema.json +0 -0
  39. {loopmath-0.2.1 → loopmath-0.2.2}/spec/ocp-v0.schema.json +0 -0
  40. {loopmath-0.2.1 → loopmath-0.2.2}/spec/ocp_conformance.py +0 -0
  41. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/__main__.py +0 -0
  42. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapter_host.py +0 -0
  43. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/__init__.py +0 -0
  44. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/atrium.py +0 -0
  45. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/base.py +0 -0
  46. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/bb.py +0 -0
  47. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/bb_support.py +0 -0
  48. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/omp.py +0 -0
  49. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/omp_facts.py +0 -0
  50. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/omp_links.py +0 -0
  51. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/omp_parse.py +0 -0
  52. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/omp_types.py +0 -0
  53. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/opencode.py +0 -0
  54. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/opencode_store.py +0 -0
  55. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/orca.py +0 -0
  56. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai.py +0 -0
  57. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai_graph.py +0 -0
  58. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai_parse.py +0 -0
  59. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai_types.py +0 -0
  60. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai_usage.py +0 -0
  61. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/paseo.py +0 -0
  62. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/pi.py +0 -0
  63. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/pi_support.py +0 -0
  64. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/registry.py +0 -0
  65. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/__init__.py +0 -0
  66. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/block.py +0 -0
  67. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/check_pymc.py +0 -0
  68. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/commands.py +0 -0
  69. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/compose.py +0 -0
  70. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/features.py +0 -0
  71. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/gaussian.py +0 -0
  72. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/lookahead.py +0 -0
  73. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/outcome.py +0 -0
  74. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/priors.py +0 -0
  75. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/builder/__init__.py +0 -0
  76. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/builder/cli.py +0 -0
  77. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/cli.py +0 -0
  78. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/cli_graph.py +0 -0
  79. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/cli_support.py +0 -0
  80. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/__init__.py +0 -0
  81. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/arms.py +0 -0
  82. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/cli_e0.py +0 -0
  83. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/estimate.py +0 -0
  84. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/estimate_rework.py +0 -0
  85. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/figures.py +0 -0
  86. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/io.py +0 -0
  87. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/verb.py +0 -0
  88. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/explain.py +0 -0
  89. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/fit.py +0 -0
  90. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/fit_assembly.py +0 -0
  91. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/fit_bayes.py +0 -0
  92. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/fit_masks.py +0 -0
  93. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/fit_predict.py +0 -0
  94. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/fit_pricing.py +0 -0
  95. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/fit_transfer.py +0 -0
  96. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/gaps.py +0 -0
  97. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/gitwalk.py +0 -0
  98. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/grade.py +0 -0
  99. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/grade_support.py +0 -0
  100. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/__init__.py +0 -0
  101. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/artifact_git.py +0 -0
  102. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/artifact_join.py +0 -0
  103. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/artifact_kinds.py +0 -0
  104. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/artifacts.py +0 -0
  105. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/bashparse.py +0 -0
  106. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/bashread_ops.py +0 -0
  107. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/bashwrite_ops.py +0 -0
  108. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/bashwrites.py +0 -0
  109. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/cache.py +0 -0
  110. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/codexio.py +0 -0
  111. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/codexio_parse.py +0 -0
  112. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/codexio_scan.py +0 -0
  113. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/dataset.py +0 -0
  114. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/dataset_build.py +0 -0
  115. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/dataset_core.py +0 -0
  116. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/dataset_edges.py +0 -0
  117. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/dataset_nodes.py +0 -0
  118. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/extract.py +0 -0
  119. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/html_common.py +0 -0
  120. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/html_costcurve.py +0 -0
  121. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/html_css.py +0 -0
  122. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/html_data.py +0 -0
  123. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/html_force.py +0 -0
  124. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/html_render.py +0 -0
  125. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/html_swimlanes.py +0 -0
  126. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler.py +0 -0
  127. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_cli.py +0 -0
  128. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_common.py +0 -0
  129. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_parse.py +0 -0
  130. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_pricing.py +0 -0
  131. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt.py +0 -0
  132. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_build.py +0 -0
  133. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_dispatch.py +0 -0
  134. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_evidence.py +0 -0
  135. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_node.py +0 -0
  136. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_records.py +0 -0
  137. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_result.py +0 -0
  138. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_session.py +0 -0
  139. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_tools.py +0 -0
  140. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_validation.py +0 -0
  141. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_workflow.py +0 -0
  142. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_report.py +0 -0
  143. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_score.py +0 -0
  144. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labels.py +0 -0
  145. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/launch.py +0 -0
  146. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/launch_origin.py +0 -0
  147. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/ocp.py +0 -0
  148. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/ocp_emit.py +0 -0
  149. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/ocp_support.py +0 -0
  150. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/render.py +0 -0
  151. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/runfile.py +0 -0
  152. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/runfile_export.py +0 -0
  153. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/runfile_finish.py +0 -0
  154. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/scan.py +0 -0
  155. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/schema.py +0 -0
  156. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/token_completeness.py +0 -0
  157. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/__init__.py +0 -0
  158. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/base.py +0 -0
  159. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/claude_code.py +0 -0
  160. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/claude_code_support.py +0 -0
  161. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/codex.py +0 -0
  162. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/codex_support.py +0 -0
  163. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/ocp.py +0 -0
  164. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/ocp_analysis.py +0 -0
  165. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/ocp_artifact.py +0 -0
  166. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/ocp_common.py +0 -0
  167. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/ocp_convert.py +0 -0
  168. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/ocp_projection.py +0 -0
  169. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/logmatch/__init__.py +0 -0
  170. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/logmatch/artifacts.py +0 -0
  171. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/logmatch/clip.py +0 -0
  172. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/logmatch/costs.py +0 -0
  173. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/logmatch/match.py +0 -0
  174. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/logmatch/settle.py +0 -0
  175. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/logmatch/tariff.py +0 -0
  176. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/__init__.py +0 -0
  177. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/canonical.py +0 -0
  178. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/commands.py +0 -0
  179. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/conformance.py +0 -0
  180. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/contractv3.py +0 -0
  181. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/emit.py +0 -0
  182. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/migrate.py +0 -0
  183. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/schema/ocp-v0.2.schema.json +0 -0
  184. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/schema/ocp-v0.3.schema.json +0 -0
  185. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/schema/ocp-v0.schema.json +0 -0
  186. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/version.py +0 -0
  187. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/onboard/__init__.py +0 -0
  188. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/onboard/commands.py +0 -0
  189. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/onboard/history.py +0 -0
  190. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/onboard/label.py +0 -0
  191. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/onboard/usual.py +0 -0
  192. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/output.py +0 -0
  193. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/pool.py +0 -0
  194. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/price.py +0 -0
  195. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/price_validation.py +0 -0
  196. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/prices.toml +0 -0
  197. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/__init__.py +0 -0
  198. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/benchmarks.py +0 -0
  199. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/bundle/e0.jsonl.gz +0 -0
  200. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/bundle/rq1.jsonl.gz +0 -0
  201. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/bundle/sweep.jsonl.gz +0 -0
  202. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/e0.py +0 -0
  203. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/history.py +0 -0
  204. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/ocpdoc.py +0 -0
  205. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/rq1.py +0 -0
  206. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/show.py +0 -0
  207. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/sweep.py +0 -0
  208. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/validate.py +0 -0
  209. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/receipts.py +0 -0
  210. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/__init__.py +0 -0
  211. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/backlog.py +0 -0
  212. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/curve.py +0 -0
  213. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/gain.py +0 -0
  214. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/message.py +0 -0
  215. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/receipts.py +0 -0
  216. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/stats.py +0 -0
  217. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/storeread.py +0 -0
  218. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/report/__init__.py +0 -0
  219. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/report/html.py +0 -0
  220. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/report/terminal.py +0 -0
  221. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/report/terminal_exclusions.py +0 -0
  222. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/report/terminal_format.py +0 -0
  223. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/report/terminal_read.py +0 -0
  224. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/research_defaults.py +0 -0
  225. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/research_paths.py +0 -0
  226. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/scoring.py +0 -0
  227. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/scoring_comparisons.py +0 -0
  228. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/scoring_predictive.py +0 -0
  229. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/share/__init__.py +0 -0
  230. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/share/commands.py +0 -0
  231. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/share/export.py +0 -0
  232. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/share/import_.py +0 -0
  233. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/__init__.py +0 -0
  234. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/commands.py +0 -0
  235. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/doctor.py +0 -0
  236. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/install.py +0 -0
  237. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath/SKILL.md +0 -0
  238. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath-import-runs/SKILL.md +0 -0
  239. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath-onboard/SKILL.md +0 -0
  240. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath-plan-task/SKILL.md +0 -0
  241. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath-record-run/SKILL.md +0 -0
  242. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath-update-fit/SKILL.md +0 -0
  243. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/skills/reference.md +0 -0
  244. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/__init__.py +0 -0
  245. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/budget.py +0 -0
  246. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/commands.py +0 -0
  247. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/finish.py +0 -0
  248. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/fitjob.py +0 -0
  249. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/home.py +0 -0
  250. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/ids.py +0 -0
  251. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/lock.py +0 -0
  252. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/record.py +0 -0
  253. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/report.py +0 -0
  254. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/runs.py +0 -0
  255. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/status.py +0 -0
  256. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/surface.py +0 -0
  257. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/surface_bootstrap.py +0 -0
  258. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/surface_estimation.py +0 -0
  259. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/surface_frame.py +0 -0
  260. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/surface_presentation.py +0 -0
  261. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/surface_support.py +0 -0
  262. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/taskmodel.py +0 -0
  263. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/types.py +0 -0
  264. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/__init__.py +0 -0
  265. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/README.md +0 -0
  266. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/base.css +0 -0
  267. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/common.js +0 -0
  268. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/estimates.css +0 -0
  269. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/estimates.js +0 -0
  270. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/graph.css +0 -0
  271. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/graph.js +0 -0
  272. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/plans.css +0 -0
  273. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/results.css +0 -0
  274. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/results.js +0 -0
  275. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/runs.js +0 -0
  276. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/viz.css +0 -0
  277. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/viz.js +0 -0
  278. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/common.py +0 -0
  279. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/plans.py +0 -0
  280. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/posterior.py +0 -0
  281. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/runs.py +0 -0
  282. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/__init__.py +0 -0
  283. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/candidates.py +0 -0
  284. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/catalog/best_of_n.toml +0 -0
  285. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/catalog/implement_review.toml +0 -0
  286. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/catalog/plan_implement.toml +0 -0
  287. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/catalog/plan_implement_review.toml +0 -0
  288. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/catalog/solo.toml +0 -0
  289. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/catalog/swarm.toml +0 -0
  290. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/commands.py +0 -0
  291. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/diff.py +0 -0
  292. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/format.py +0 -0
  293. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/graphview.py +0 -0
  294. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/ids.py +0 -0
  295. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/infer.py +0 -0
  296. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/models.py +0 -0
  297. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/ocp.py +0 -0
  298. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/shapes.py +0 -0
  299. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath.egg-info/dependency_links.txt +0 -0
  300. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath.egg-info/entry_points.txt +0 -0
  301. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath.egg-info/requires.txt +0 -0
  302. {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath.egg-info/top_level.txt +0 -0
  303. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_atrium.py +0 -0
  304. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_bb.py +0 -0
  305. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_omp.py +0 -0
  306. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_opencode.py +0 -0
  307. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_opencode_output_tokens.py +0 -0
  308. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_orca.py +0 -0
  309. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_otel_genai.py +0 -0
  310. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_paseo.py +0 -0
  311. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_services.py +0 -0
  312. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapters.py +0 -0
  313. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapters_pi.py +0 -0
  314. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_arms.py +0 -0
  315. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_cli_graph.py +0 -0
  316. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_cli_help_lists.py +0 -0
  317. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_cli_labeler.py +0 -0
  318. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_cli_main_module.py +0 -0
  319. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_cli_plan_hidden.py +0 -0
  320. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_cli_prices.py +0 -0
  321. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_cli_startup.py +0 -0
  322. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_color_proof.py +0 -0
  323. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_duplicate_guards.py +0 -0
  324. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_estimate.py +0 -0
  325. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_explain.py +0 -0
  326. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_fit.py +0 -0
  327. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_gaps.py +0 -0
  328. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_gitwalk.py +0 -0
  329. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_grade.py +0 -0
  330. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_artifact_recording.py +0 -0
  331. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_artifacts.py +0 -0
  332. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_bashwrites.py +0 -0
  333. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_cache.py +0 -0
  334. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_codexio.py +0 -0
  335. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_dataset.py +0 -0
  336. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_extract.py +0 -0
  337. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_glue.py +0 -0
  338. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_html.py +0 -0
  339. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_labeler.py +0 -0
  340. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_labeler_evidence_matrix.py +0 -0
  341. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_launch.py +0 -0
  342. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_ocp.py +0 -0
  343. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_render.py +0 -0
  344. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_runfile.py +0 -0
  345. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_schema.py +0 -0
  346. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_home_guard.py +0 -0
  347. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_html.py +0 -0
  348. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ingest.py +0 -0
  349. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ingest_claude_code.py +0 -0
  350. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ingest_codex.py +0 -0
  351. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ingest_ocp.py +0 -0
  352. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_io.py +0 -0
  353. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_not_run_summary.py +0 -0
  354. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ocp_conformance.py +0 -0
  355. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ocp_golden.py +0 -0
  356. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ocp_open_enums.py +0 -0
  357. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ocp_versioning.py +0 -0
  358. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_output_html_path.py +0 -0
  359. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_packaging_metadata.py +0 -0
  360. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_packaging_smoke.py +0 -0
  361. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_pool.py +0 -0
  362. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_preview_script.py +0 -0
  363. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_price.py +0 -0
  364. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_receipts.py +0 -0
  365. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_release_paths.py +0 -0
  366. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_runfile_privacy_n1b.py +0 -0
  367. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_scoring.py +0 -0
  368. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_split_rebindings.py +0 -0
  369. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_surface.py +0 -0
  370. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_taskmodel_features.py +0 -0
  371. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_terminal.py +0 -0
  372. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_types_control.py +0 -0
  373. {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_types_piece_prediction.py +0 -0
  374. {loopmath-0.2.1 → loopmath-0.2.2}/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.1
3
+ Version: 0.2.2
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.1"
7
+ version = "0.2.2"
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.1"
8
+ __version__ = "0.2.2"
@@ -13,7 +13,7 @@ A term is `(node_id, parent_id, value)`. Rows:
13
13
  from __future__ import annotations
14
14
 
15
15
  import math
16
- from collections.abc import Iterable
16
+ from collections.abc import Iterable, Mapping
17
17
  from dataclasses import dataclass, field
18
18
  from datetime import datetime
19
19
  from functools import lru_cache
@@ -25,6 +25,7 @@ from ..types import (
25
25
  Workflow,
26
26
  )
27
27
  from .forest import canonical_model_id, canonical_role, model_path
28
+ from .pricing import price_term
28
29
 
29
30
  Term = tuple[str, "str | None", float]
30
31
 
@@ -324,12 +325,15 @@ def _effort_scaled(terms: tuple[Term, ...], weight: float) -> tuple[Term, ...]:
324
325
 
325
326
 
326
327
  def cost_rest(st: Structure, piece: str, k: int, setting: Setting | None = None, *,
327
- source: str | None = None, effort: float = 1.0) -> tuple[Term, ...]:
328
+ source: str | None = None, effort: float = 1.0,
329
+ prices: Mapping[str, float] | None = None) -> tuple[Term, ...]:
328
330
  """Cost and tokens rows without the task part: setting, role, topology, position, round, control,
329
331
  and with a `source` the position x source node `psrc:<shape>#<pos>|<source>` and the family x source
330
332
  node `fsrc:<family>|<source>` (spec 04 section 1). `effort` is the value of the terms a timebox caps, the
331
333
  effort and shape (topology, position) terms (`effort_weight`: below 1 under a timebox); the role, psrc and
332
- fsrc terms stay at 1."""
334
+ fsrc terms stay at 1. `prices` is a fit's price offsets (`FitState.price_offsets`, spec 04 section 2): the
335
+ model's `price:offset` term goes last. The cost head has that node and the tokens head does not, so a
336
+ prediction's shared row prices only its cost."""
333
337
  s = setting or st.settings[piece]
334
338
  h, m, e = _s(s)
335
339
  shape = st.shape or st.workflow_id
@@ -343,7 +347,8 @@ def cost_rest(st: Structure, piece: str, k: int, setting: Setting | None = None,
343
347
  fam = model_path(m)[1]
344
348
  terms += ((f"psrc:{position}|{source}", f"position:{position}", 1.0),
345
349
  (f"fsrc:{fam}|{source}", f"family:{fam}", 1.0))
346
- return terms + tuple(round_terms(k)) + tuple(_control_terms(st.k_max, st.widths[piece]))
350
+ return (terms + tuple(round_terms(k)) + tuple(_control_terms(st.k_max, st.widths[piece]))
351
+ + price_term(prices, m))
347
352
 
348
353
 
349
354
  def gate_rest(st: Structure, gate: GateInfo, k: int) -> tuple[Term, ...]:
@@ -421,6 +426,7 @@ class AttemptObs:
421
426
  weight: float
422
427
  setting: Setting
423
428
  repriced: bool = True # False: the recorded dollars, not the current tariff
429
+ streams: tuple[float, ...] = () # input, cache read, cache write, output tokens (pricing.STREAMS order)
424
430
 
425
431
 
426
432
  @dataclass
@@ -458,8 +464,10 @@ def source_label(doc: dict) -> str:
458
464
  ext = run.get("ext") or {}
459
465
  share = ext.get("dev.loopmath.share")
460
466
  if isinstance(share, dict):
461
- org = share.get("org") or share.get("org_hash") or (run.get("task") or {}).get("org") or "shared"
462
- return f"shared:{org}"
467
+ # the importer writes `source` and `task.org` as `shared:<org_hash>` already: never prefix twice
468
+ org = (share.get("source") or share.get("org") or share.get("org_hash") or (run.get("task") or {}).get("org")
469
+ or "shared")
470
+ return "shared:" + str(org).removeprefix("shared:")
463
471
  src = (run.get("task") or {}).get("source")
464
472
  kind = src.get("kind") if isinstance(src, dict) else src
465
473
  kind = str(kind or "").strip().lower()
@@ -761,7 +769,9 @@ def parse_run(doc: dict, *, now: datetime | None = None, rule: AcceptanceRule |
761
769
  if weight <= 0 or ((usd is None or usd <= 0) and not tokens):
762
770
  dropped["attempt without usable cost"] = dropped.get("attempt without usable cost", 0) + 1
763
771
  continue
764
- attempts.append(AttemptObs(piece, k, usd if usd and usd > 0 else None, tokens, weight, setting, repriced))
772
+ streams = tuple(_ocp_fields(cost)[name] for name in _OCP_TOKEN_FIELDS)
773
+ attempts.append(AttemptObs(piece, k, usd if usd and usd > 0 else None, tokens, weight, setting, repriced,
774
+ streams))
765
775
 
766
776
  gates = _gate_observations(doc, st, by_piece_round)
767
777
  use_rule = rule or rule_from_doc(doc)
@@ -36,6 +36,7 @@ from .design import (DESIGN_VERSION, Term, Unusable, cost_row, gate_row, parse_r
36
36
  from .features import FeatureTally, add_horizon, drop_features, horizon_nodes, horizon_specs
37
37
  from .block import leaf_split
38
38
  from .forest import Forest, default_scale, fixed_sd, scale_group
39
+ from .pricing import PRICE_NODE, PRICE_PRIOR_SD, STREAMS, fit_offsets, price_term
39
40
  from .gaussian import N_DRAWS, Factors, GaussianHead, HeadFit, LogisticHead, Prior
40
41
  from .state import SCORE_CLAMP, inverse_transform, transform # noqa: F401 (re-exported)
41
42
 
@@ -163,6 +164,7 @@ class HeadRows:
163
164
  sources: list[str] = field(default_factory=list)
164
165
  types: list[str] = field(default_factory=list)
165
166
  not_repriced: int = 0 # cost rows on recorded dollars, not the current tariff
167
+ prices: dict = field(default_factory=dict) # cost head: the fit's price offsets (pricing.fit_offsets)
166
168
 
167
169
  def add(self, terms, y, w, run, source, ttype):
168
170
  self.rows.append(terms)
@@ -213,6 +215,8 @@ def collect_rows(docs: Iterable[dict | tuple[str | None, dict]], *, without: tup
213
215
  stored: set[str] = set() # run ids of the user's store, whose copy wins
214
216
  overlap: Counter = Counter()
215
217
  labels: Counter = Counter()
218
+ model_runs: dict[str, set[str]] = defaultdict(set) # the price offset's inputs (spec 04 section 2)
219
+ model_streams: dict[str, list[float]] = defaultdict(lambda: [0.0] * len(STREAMS))
216
220
  for item in docs:
217
221
  origin, doc = item if isinstance(item, tuple) else (None, item)
218
222
  if origin is not None and _excluded(origin, without):
@@ -258,6 +262,10 @@ def collect_rows(docs: Iterable[dict | tuple[str | None, dict]], *, without: tup
258
262
  if a.usd:
259
263
  heads["cost"].add(terms, math.log(a.usd), a.weight, pr.run_id, pr.source, ttype)
260
264
  heads["cost"].not_repriced += not a.repriced
265
+ model_runs[a.setting.model].add(pr.run_id)
266
+ acc = model_streams[a.setting.model]
267
+ for i, v in enumerate(a.streams):
268
+ acc[i] += v
261
269
  if a.tokens:
262
270
  heads["tokens"].add(terms, math.log(a.tokens), a.weight, pr.run_id, pr.source, ttype)
263
271
  for gi, k, passed in pr.gates:
@@ -289,10 +297,34 @@ def collect_rows(docs: Iterable[dict | tuple[str | None, dict]], *, without: tup
289
297
  coding = tally.horizon_coding() # the horizon terms need every run's horizon first (spec 04 section 1)
290
298
  for hr in heads.values():
291
299
  hr.rows = add_horizon(drop_features(hr.rows, tally.nodes), hr.runs, tally.run_horizon, coding)
300
+ add_prices(heads["cost"], {m: len(r) for m, r in model_runs.items()}, model_streams)
292
301
  return (heads, score_info, dict(dropped), dict(runs_by_source), {c: dict(n) for c, n in config_support.items()},
293
302
  dict(checks), dict(overlap), dict(labels), tally)
294
303
 
295
304
 
305
+ def add_prices(hr: HeadRows, runs: dict[str, int], streams: dict[str, list[float]]) -> None:
306
+ """The price offset (spec 04 section 2): the fit's offsets from its runs and token mix under the current price
307
+ table, and each cost row with its model's `price:offset` term (`pricing.price_term`, as at prediction)."""
308
+ hr.prices = fit_offsets(runs, streams, design_rows._price_table())
309
+ offsets = hr.prices["models"]
310
+ if not offsets:
311
+ return
312
+ rows = []
313
+ for row in hr.rows:
314
+ model = next((n[len("model:"):] for n, _, _ in row if n.startswith("model:")), None)
315
+ term = price_term(offsets, model) if model else ()
316
+ rows.append(row + list(term) if term else row)
317
+ hr.rows = rows
318
+
319
+
320
+ def price_spec(hr: HeadRows) -> list:
321
+ """The prior that holds `price:offset` at 1, on the cost head of a fit with offsets, --no-prior included."""
322
+ if not hr.prices.get("models"):
323
+ return []
324
+ return [prior_data.FactorSpec("cost", [(PRICE_NODE, None, 1.0)], 1.0, PRICE_PRIOR_SD ** 2,
325
+ f"{PRICE_NODE} held at 1, prior N(1, {PRICE_PRIOR_SD}^2)")]
326
+
327
+
296
328
  # ---------------------------------------------------------------- matrices and priors
297
329
 
298
330
  def build_matrix(rows: list[list[Term]], forest: Forest, extra_nodes: Iterable[tuple[str, str | None]] = ()):
@@ -532,6 +564,8 @@ def fit(home: Path, *, no_prior: bool = False, without: tuple[str, ...] = (), fu
532
564
  nodes = horizon_nodes(hr.rows)
533
565
  if nodes: # the horizon priors, --no-prior included (spec 04 section 1)
534
566
  head_specs += horizon_specs(name, hr.kind, nodes)
567
+ if name == "cost": # the price offset (spec 04 section 2)
568
+ head_specs += price_spec(hr)
535
569
  fitted[name] = fit_head(hr, forest, head_specs, eb=eb)
536
570
  fit_id = new_fit_id(fits, now)
537
571
  partial = fits / f"{fit_id}.partial"
@@ -592,6 +626,7 @@ def fit(home: Path, *, no_prior: bool = False, without: tuple[str, ...] = (), fu
592
626
  "design_version": DESIGN_VERSION,
593
627
  "features": {**tally.report, **({"error": features_error} if features_error else {})},
594
628
  "horizons": tally.horizon_report(),
629
+ "price_offsets": heads_rows["cost"].prices,
595
630
  }
596
631
  if full:
597
632
  meta["full"] = _full_check(fitted)
@@ -55,6 +55,7 @@ FIXED_SD: dict[str, tuple[float, float, float]] = {
55
55
  "round": (2.0, 2.0, 2.0),
56
56
  "control": (1.0, 1.0, 1.0),
57
57
  "horizon": (10.0, 10.0, 10.0), # wide: the informative prior is a FactorSpec in fit() (spec 04 section 1)
58
+ "price": (10.0, 10.0, 10.0), # wide: fit() holds `price:offset` at 1 by a FactorSpec (spec 04 section 2)
58
59
  }
59
60
 
60
61
  HYPER_SD = 0.7 # sd of the log-normal hyperprior on each phi, around the defaults above
@@ -0,0 +1,142 @@
1
+ """The price offset on the cost head (spec 04 section 2, 0.2.2, lane 22C).
2
+
3
+ A model's run cost moves with its list price. Every cost row of a model version `m` (not the tokens, gate,
4
+ success or score rows) carries the fixed term `price:offset` with the value
5
+
6
+ o_m = s_m * log(P_m / R_m), s_m = n0 / (n0 + n_m),
7
+
8
+ and the node's coefficient is held at 1 by a tight prior, so the term is a fixed offset on the log cost scale,
9
+ the same at fit and at prediction:
10
+
11
+ - `P_m` is m's blended list price ($/Mtok, the current price table) under the token mix (the shares of input,
12
+ cache read, cache write and output tokens) of the cost rows of m's family in the fit.
13
+ - `R_m` is the run-weighted geometric mean of the blended prices, under the same mix, of the family's models
14
+ with cost rows. A family with no cost rows anchors on its provider the same way (the provider's mix and
15
+ models). No anchor, or no price for m, gives no term.
16
+ - `n_m` is m's runs with a cost row in the fit; `PRICE_OFFSET_RUNS` is n0: the price counts like n0 of the
17
+ model's own runs, and its share of the prediction shrinks as they accumulate.
18
+
19
+ `fit_offsets` computes the offset of every model with cost rows and of every model in the price table once per
20
+ fit; `meta.json` `price_offsets` stores them, and prediction and the workflow search read them from there.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import math
26
+ from collections.abc import Iterable, Mapping
27
+
28
+ from .forest import canonical_model_id, model_path
29
+
30
+ PRICE_NODE = "price:offset"
31
+ PRICE_LEVEL = "price"
32
+ # n0: the list price counts like this many of the model's own runs (as one benchmark result counts like
33
+ # BENCHMARK_PRIOR_WEIGHT = 5 runs). A heuristic, to tune when stores have new versions with a few runs.
34
+ PRICE_OFFSET_RUNS = 5.0
35
+ PRICE_PRIOR_SD = 1e-3 # the coefficient's prior N(1, sd^2): a fixed offset, not a learned elasticity
36
+ STREAMS = ("input", "cache_read", "cache_write", "output") # price table rate keys, in the order of `streams`
37
+
38
+
39
+ def mix_of(totals: Iterable[float]) -> tuple[float, ...] | None:
40
+ """Token shares per stream (STREAMS order), or None without tokens."""
41
+ t = [max(0.0, float(x)) for x in totals]
42
+ s = sum(t)
43
+ return tuple(x / s for x in t) if s > 0 else None
44
+
45
+
46
+ def blended(rate: Mapping[str, float] | None, mix: tuple[float, ...] | None) -> float | None:
47
+ """$/Mtok under `mix`, or None when the model has no rate, no mix, or a blended price of 0 (free)."""
48
+ if rate is None or mix is None:
49
+ return None
50
+ p = sum(share * float(rate.get(stream) or 0.0) for share, stream in zip(mix, STREAMS))
51
+ return p if p > 0 else None
52
+
53
+
54
+ def shrink(runs: float, n0: float = PRICE_OFFSET_RUNS) -> float:
55
+ return n0 / (n0 + max(0.0, float(runs)))
56
+
57
+
58
+ def _add(acc: dict[str, list[float]], key: str, values: Iterable[float]) -> None:
59
+ cur = acc.setdefault(key, [0.0] * len(STREAMS))
60
+ for i, v in enumerate(values):
61
+ cur[i] += float(v)
62
+
63
+
64
+ def fit_offsets(runs: Mapping[str, int], streams: Mapping[str, Iterable[float]], table, *,
65
+ n0: float = PRICE_OFFSET_RUNS) -> dict:
66
+ """The fit's `price_offsets`: {n0, models: {model: offset}, detail, anchors}.
67
+
68
+ `runs`: model (canonical id) to its runs with a cost row; `streams`: model to its token totals over those
69
+ rows (STREAMS order); `table`: a price.PriceTable, or None (no offsets). Offsets of 0 are left out of
70
+ `models`; `detail` says for each priced model which anchor it used, or why it has none."""
71
+ out: dict = {"n0": n0, "models": {}, "detail": {}, "anchors": {}}
72
+ if table is None:
73
+ return out
74
+ fam_tok: dict[str, list[float]] = {}
75
+ prov_tok: dict[str, list[float]] = {}
76
+ fam_models: dict[str, dict[str, int]] = {}
77
+ prov_models: dict[str, dict[str, int]] = {}
78
+ for model, n in sorted(runs.items()):
79
+ if n <= 0:
80
+ continue
81
+ prov, fam, _ = model_path(model)
82
+ fam_models.setdefault(fam, {})[model] = int(n)
83
+ prov_models.setdefault(prov, {})[model] = int(n)
84
+ values = list(streams.get(model) or [0.0] * len(STREAMS))
85
+ _add(fam_tok, fam, values)
86
+ _add(prov_tok, prov, values)
87
+ candidates = set(runs) | {canonical_model_id(k) for k in table.rates}
88
+ anchors: dict[str, dict | None] = {}
89
+
90
+ def anchor(key: str, members: dict[str, int], tokens: list[float] | None) -> dict | None:
91
+ if key in anchors:
92
+ return anchors[key]
93
+ mix = mix_of(tokens or [])
94
+ priced = {m: (n, blended(table.rate(m), mix)) for m, n in members.items()}
95
+ priced = {m: (n, p) for m, (n, p) in priced.items() if p is not None}
96
+ found = None
97
+ if mix is not None and priced:
98
+ total = sum(n for n, _ in priced.values())
99
+ log_ref = sum(n * math.log(p) for n, p in priced.values()) / total
100
+ found = {"mix": mix, "log_ref": log_ref,
101
+ "reference_usd_per_mtok": round(math.exp(log_ref), 6),
102
+ "models": {m: n for m, (n, _) in sorted(priced.items())},
103
+ "todo": sorted(m for m in priced if table.is_todo(m))}
104
+ anchors[key] = found
105
+ return found
106
+
107
+ for model in sorted(candidates):
108
+ prov, fam, _ = model_path(model)
109
+ a, key = None, None
110
+ if fam in fam_models:
111
+ key = f"family:{fam}"
112
+ a = anchor(key, fam_models[fam], fam_tok.get(fam))
113
+ if a is None and prov in prov_models and fam not in fam_models:
114
+ key = f"provider:{prov}"
115
+ a = anchor(key, prov_models[prov], prov_tok.get(prov))
116
+ if a is None:
117
+ out["detail"][model] = {"runs": int(runs.get(model, 0)), "note": "no anchor with runs and prices"}
118
+ continue
119
+ p = blended(table.rate(model), a["mix"])
120
+ if p is None:
121
+ out["detail"][model] = {"runs": int(runs.get(model, 0)), "anchor": key, "note": "no price"}
122
+ continue
123
+ n = int(runs.get(model, 0))
124
+ s = shrink(n, n0)
125
+ offset = s * (math.log(p) - a["log_ref"])
126
+ out["detail"][model] = {"runs": n, "anchor": key, "usd_per_mtok": round(p, 6), "shrink": round(s, 6),
127
+ "log_ratio": round(math.log(p) - a["log_ref"], 6), "offset": round(offset, 6),
128
+ **({"todo": True} if table.is_todo(model) else {})}
129
+ if offset != 0.0:
130
+ out["models"][model] = offset
131
+ out["anchors"] = {k: {**{kk: vv for kk, vv in v.items() if kk != "log_ref"}, "mix": [round(x, 6) for x in v["mix"]]}
132
+ for k, v in sorted(anchors.items()) if v}
133
+ return out
134
+
135
+
136
+ def price_term(offsets: Mapping[str, float] | None, model: str) -> tuple[tuple[str, None, float], ...]:
137
+ """The cost row's price term for `model` (a canonical id) under a fit's offsets: none for an offset of 0 or
138
+ a fit without offsets."""
139
+ if not offsets:
140
+ return ()
141
+ o = offsets.get(model)
142
+ return ((PRICE_NODE, None, float(o)),) if o else ()
@@ -67,7 +67,7 @@ MIN_SCORE_SWITCH = 5 # spec 04 section 2: g = p_reach once the score head has 5
67
67
  BASE_HEADS = {"cost": ("gaussian", "cost"), "tokens": ("gaussian", "cost"), "gate": ("logistic", "logit"),
68
68
  "success": ("logistic", "logit")}
69
69
  TASK_CHAIN = ("task", "subtype", "repo", "type")
70
- FIXED_LEVELS = ("fixed", "round", "control")
70
+ FIXED_LEVELS = ("fixed", "round", "control", "price")
71
71
  SCORE_CLAMP = (0.005, 0.995)
72
72
  _CACHE_LIMIT = 200_000
73
73
  MEAN_NODES = 12 # Gauss-Hermite nodes over an unseen normal effect
@@ -339,6 +339,8 @@ class FitState:
339
339
  self.sim_seed = _seed(self.seed_key, "simulate")
340
340
  # spec 04 section 1: the value of the timebox-capped terms (effort, shape) in a timeboxed task's cost rows
341
341
  self.timebox_effort = float(self.meta.get("timebox_effort", 1.0))
342
+ # spec 04 section 2: the fit's price offset per model (log scale); none in fits before 0.2.2
343
+ self.price_offsets: dict[str, float] = dict((self.meta.get("price_offsets") or {}).get("models") or {})
342
344
  if heads is None:
343
345
  heads = {}
344
346
  for name, hmeta in (self.meta.get("heads") or {}).items():
@@ -418,7 +420,8 @@ class FitState:
418
420
  for piece in st.pieces:
419
421
  rounds = st.k_max if st.piece_loop.get(piece) is not None else 1
420
422
  for k in range(1, rounds + 1):
421
- cost[(piece, k)] = cost_rest(st, piece, k, source=PREDICT_SOURCE, effort=effort)
423
+ cost[(piece, k)] = cost_rest(st, piece, k, source=PREDICT_SOURCE, effort=effort,
424
+ prices=self.price_offsets)
422
425
  gate = {}
423
426
  for gi, g in enumerate(st.gates):
424
427
  for k in range(1, (st.k_max if st.gate_loop.get(gi) is not None else 1) + 1):
@@ -21,7 +21,8 @@ from typing import Any
21
21
  from ..output import EXIT_NOT_FOUND, EXIT_OK, fail, home as home_dir
22
22
  from ..recommend import commands as C
23
23
  from ..recommend import engine, storeread
24
- from ..recommend.engine import Recommendation
24
+ from ..recommend.curve import accepted_within
25
+ from ..recommend.engine import Recommendation, r6
25
26
  from ..recommend.storeread import Conf
26
27
  from ..types import AcceptanceRule, Configuration, Task
27
28
 
@@ -45,6 +46,8 @@ class Session:
45
46
  cache: OrderedDict = field(default_factory=OrderedDict) # config id -> predict answer
46
47
  context: dict[str, Any] | None = None
47
48
  models: dict[str, int] | None = None # `known_models`, read once
49
+ home: Any = None # the store, for the runs behind each model
50
+ offered: list[str] = field(default_factory=list) # `commands.offered_models`: the models the page offers
48
51
 
49
52
 
50
53
  # ---------------------------------------------------------------- start
@@ -70,6 +73,13 @@ def asked_task(task: Task, id_given: bool, belief: Any, rule: AcceptanceRule, co
70
73
  return C.question_task(task, key_of(belief) if callable(key_of) else belief.fit_id, rule, configs)
71
74
 
72
75
 
76
+ def offered_kw(models: list[str]) -> dict[str, Any]:
77
+ """`offered=` for `engine.recommend` when it takes it (0.2.2), so the builder asks what `recommend` asks."""
78
+ import inspect
79
+
80
+ return {"offered": models} if "offered" in inspect.signature(engine.recommend).parameters else {}
81
+
82
+
73
83
  def build_session(args: argparse.Namespace) -> tuple[Session | None, int]:
74
84
  """The session, or None and the exit code after the message `recommend` would print."""
75
85
  home = home_dir(getattr(args, "home", None))
@@ -98,6 +108,7 @@ def build_session(args: argparse.Namespace) -> tuple[Session | None, int]:
98
108
  spend, cap = C.budget(home, conf)
99
109
  settings = C.settings_from(conf, args, spend, cap)
100
110
  models = C.model_list(getattr(args, "models", None))
111
+ offered = C.offered_models(conf, models)
101
112
  except C.NotFound as exc:
102
113
  return None, fail(str(exc), EXIT_NOT_FOUND)
103
114
  except ValueError as exc:
@@ -125,12 +136,12 @@ def build_session(args: argparse.Namespace) -> tuple[Session | None, int]:
125
136
  asked = asked_task(task, id_given, belief, rule, [usual, *(c for c, _ in configs)])
126
137
  rec = engine.recommend(belief, asked, rule, usual=usual, usual_from=usual_from, configs=configs,
127
138
  settings=settings, diff=C.diff_fn(), keep=[*(u.id for u in user),
128
- *(r.id for r in recorded)])
139
+ *(r.id for r in recorded)],
140
+ **offered_kw(offered))
129
141
  rec.task = task
130
142
  start = None
131
143
  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)
144
+ start = rec_config(rec, args.start) or storeread.find_config(home, args.start)
134
145
  if start is None:
135
146
  raise C.NotFound(f"configuration {args.start} not found in this recommendation, earlier "
136
147
  f"recommendations or stored runs")
@@ -139,7 +150,20 @@ def build_session(args: argparse.Namespace) -> tuple[Session | None, int]:
139
150
  except ValueError as exc:
140
151
  return None, fail(str(exc))
141
152
  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
153
+ return Session(belief, asked, rule, rec, fit, rec_id=getattr(args, "rec", None), start=start,
154
+ home=home, offered=offered), EXIT_OK
155
+
156
+
157
+ def rec_config(rec: Recommendation, cfg_id: str) -> Configuration | None:
158
+ """A configuration of the recommendation by id: a candidate (every choice is one), the reference or the
159
+ rescue workflow."""
160
+ found = rec.by_id(cfg_id)
161
+ if found is not None:
162
+ return found.config
163
+ for cfg in (rec.usual.config, rec.rescue_config):
164
+ if cfg is not None and cfg.id == cfg_id:
165
+ return cfg
166
+ return None
143
167
 
144
168
 
145
169
  # ---------------------------------------------------------------- catalog
@@ -161,32 +185,93 @@ def rec_configs(rec: Recommendation) -> list[Configuration]:
161
185
 
162
186
 
163
187
  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)."""
188
+ """Model id -> runs the fit has behind it, for the offered models (0.2.2: `recommend`'s list, so a retired
189
+ model is not offered), in offered order; without a list, the fit's models and any a candidate uses."""
165
190
  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)
191
+ fit = fit_models(session.belief)
192
+ if session.offered:
193
+ out = {m: fit.get(m, 0) for m in session.offered}
194
+ else:
195
+ out = fit
196
+ for cfg in rec_configs(session.rec):
197
+ for s in cfg.settings.values():
198
+ out.setdefault(s.model, 0)
170
199
  session.models = out
171
200
  return session.models
172
201
 
173
202
 
203
+ def retired_ids(rec: Recommendation) -> dict[str, list[str]]:
204
+ return dict(getattr(rec, "retired", None) or {})
205
+
206
+
207
+ def recorded_runs(home: Any) -> list[tuple[Configuration, int]]:
208
+ """Every configuration the store's runs used, with its number of runs (one run document read per
209
+ configuration, as `storeread.recorded_configs` reads them)."""
210
+ from collections import Counter
211
+
212
+ if home is None:
213
+ return []
214
+ counts: Counter = Counter()
215
+ run_of: dict[str, str] = {}
216
+ for row in storeread.run_rows(home):
217
+ cfg = row.get("config")
218
+ if cfg:
219
+ counts[str(cfg)] += 1
220
+ if row.get("run"):
221
+ run_of[str(cfg)] = str(row["run"])
222
+ out = []
223
+ for cfg_id, n in counts.items():
224
+ doc = storeread.read_json(home / "runs" / f"{run_of[cfg_id]}.ocp.json") if cfg_id in run_of else None
225
+ conf = doc.get("run", {}).get("configuration") if isinstance(doc, dict) else None
226
+ cfg = storeread.config_from_any(conf) if conf else None
227
+ if cfg is not None:
228
+ out.append((cfg, n))
229
+ return out
230
+
231
+
232
+ def runs_behind(home: Any) -> tuple[dict[str, dict[str, Any]], dict[str, int]]:
233
+ """(by model: `{total, by_role, by_effort}`, by role: runs) from the user's recorded runs. A run counts
234
+ once per model, once per (model, role), once per (model, role, effort) and once per role."""
235
+ models: dict[str, dict[str, Any]] = {}
236
+ roles: dict[str, int] = {}
237
+ for cfg, n in recorded_runs(home):
238
+ used: set[tuple[str, str, str]] = set()
239
+ for p in cfg.workflow.pieces:
240
+ s = cfg.settings.get(p.id)
241
+ if s is not None and s.model:
242
+ used.add((s.model, p.role, s.effort or ""))
243
+ for role in {p.role for p in cfg.workflow.pieces}:
244
+ roles[role] = roles.get(role, 0) + n
245
+ for model in {m for m, _, _ in used}:
246
+ m = models.setdefault(model, {"total": 0, "by_role": {}, "by_effort": {}})
247
+ m["total"] += n
248
+ for role in {r for mm, r, _ in used if mm == model}:
249
+ m["by_role"][role] = m["by_role"].get(role, 0) + n
250
+ for _, role, effort in (u for u in used if u[0] == model):
251
+ e = m["by_effort"].setdefault(role, {})
252
+ e[effort] = e.get(effort, 0) + n
253
+ return models, roles
254
+
255
+
174
256
  def catalog(session: Session) -> dict[str, Any]:
175
257
  from ..workflows.format import KNOWN_ROLES, catalog as shapes
176
258
  from ..workflows.models import DEFAULT_EFFORTS, efforts_for, family_of, harness_for, sort_efforts
177
259
 
178
260
  seen = [s for cfg in rec_configs(session.rec) for s in cfg.settings.values()]
179
261
  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]))
262
+ behind, role_runs = runs_behind(session.home)
263
+ empty = {"total": 0, "by_role": {}, "by_effort": {}}
264
+ order = {m: i for i, m in enumerate(known_models(session))}
265
+ models = sorted(order, key=lambda m: (-behind.get(m, empty)["total"], order[m]))
181
266
  by_harness = {h: list(efforts_for(h)) for h in harnesses}
182
267
  efforts = sort_efforts([e for v in by_harness.values() for e in v] + [s.effort for s in seen])
183
268
  return {
184
269
  "harnesses": harnesses,
185
- "models": [{"id": m, "family": family_of(m), "harness": harness_for(m), "runs_behind": n}
186
- for m, n in models],
270
+ "models": [{"id": m, "family": family_of(m), "harness": harness_for(m),
271
+ "runs_behind": behind.get(m, empty)} for m in models],
187
272
  "efforts": list(efforts),
188
273
  "efforts_by_harness": by_harness,
189
- "roles": list(KNOWN_ROLES),
274
+ "roles": [{"id": r, "runs": role_runs.get(r, 0)} for r in dict.fromkeys([*KNOWN_ROLES, *role_runs])],
190
275
  "shapes": [{"id": wf.id, "title": wf.title,
191
276
  "pieces": [{"id": p.id, "role": p.role, "width": p.width} for p in wf.pieces],
192
277
  "edges": [list(e) for e in wf.edges],
@@ -196,8 +281,27 @@ def catalog(session: Session) -> dict[str, Any]:
196
281
 
197
282
 
198
283
  # ---------------------------------------------------------------- the context
284
+ def within_bands(bands: dict[str, Any], rescue: Any) -> dict[str, Any]:
285
+ """`bands` with `p_accepted_within`, mapped end by end from the chance band (`g + (1 - g) p_fix` is
286
+ increasing in `g`, as `Recommendation.within` reads its interval)."""
287
+ chance = bands.get("chance") if isinstance(bands, dict) else None
288
+ if not isinstance(chance, dict):
289
+ return bands
290
+
291
+ def f(g: Any) -> float | None:
292
+ return None if g is None else r6(accepted_within(float(g), rescue))
293
+
294
+ return {**bands, "p_accepted_within": {lv: [f(v[0]), f(v[1])] for lv, v in chance.items()}}
295
+
296
+
297
+ def numbers_of(rec: Recommendation, pred: Any) -> dict[str, Any]:
298
+ """`Recommendation.numbers()` with the `p_accepted_within` band."""
299
+ n = rec.numbers(pred)
300
+ return {**n, "bands": within_bands(n["bands"], rec.rescue)}
301
+
302
+
199
303
  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),
304
+ return {"config": c.config.to_dict(), "label": rec.label(c.config), "numbers": numbers_of(rec, c.prediction),
201
305
  "origin": c.origin, **rec.search_fields(c.config.id)}
202
306
 
203
307
 
@@ -210,12 +314,20 @@ def context_payload(session: Session) -> dict[str, Any]:
210
314
  choices = []
211
315
  for ch in core["choices"]:
212
316
  c = rec.by_id(ch["config"])
213
- choices.append({**ch, "configuration": c.config.to_dict() if c is not None else None})
317
+ choices.append({**ch, "bands": within_bands(ch["bands"], rec.rescue),
318
+ "configuration": c.config.to_dict() if c is not None else None})
214
319
  reference = {k: v for k, v in core["reference"].items() if k != "prediction"}
215
- top = rec.candidates[:TOP_CANDIDATES]
320
+ reference["numbers"] = numbers_of(rec, rec.usual.prediction)
321
+ retired = retired_ids(rec) # 0.2.2: a workflow on a retired model is the reference at most, never a candidate
322
+ offer = [c for c in rec.candidates if c.config.id not in retired]
323
+ top = offer[:TOP_CANDIDATES]
216
324
  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:]
325
+ # every option's workflows (the pair's exploration one too) and the rescue workflow, whatever the cutoff (22R B3)
326
+ wanted = {rec.usual.config.id, *(i for ch in core["choices"]
327
+ for i in (ch["config"], *ch.get("members", ()), ch.get("explore_config")) if i)}
328
+ if rec.rescue_config is not None:
329
+ wanted.add(rec.rescue_config.id)
330
+ top += [c for c in offer[TOP_CANDIDATES:]
219
331
  if c.config.id not in ids and (c.origin in KEEP_ORIGINS or c.config.id in wanted)]
220
332
  session.context = {
221
333
  "schema": SCHEMA,
@@ -223,6 +335,7 @@ def context_payload(session: Session) -> dict[str, Any]:
223
335
  "rule": core["rule"],
224
336
  "fit": session.fit,
225
337
  "rec": session.rec_id,
338
+ "goal_config_id": rec.goal.config.id,
226
339
  "rescue": core["rescue"],
227
340
  "reference": reference,
228
341
  "choices": choices,