loopmath 0.2.0__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.0 → loopmath-0.2.2}/PKG-INFO +1 -1
  2. {loopmath-0.2.0 → loopmath-0.2.2}/pyproject.toml +1 -1
  3. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/__init__.py +1 -1
  4. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/belief/design.py +45 -10
  5. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/belief/fit.py +59 -1
  6. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/belief/forest.py +1 -0
  7. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/belief/lookahead.py +3 -3
  8. loopmath-0.2.2/src/loopmath/belief/pricing.py +142 -0
  9. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/belief/state.py +28 -9
  10. loopmath-0.2.2/src/loopmath/builder/__init__.py +1 -0
  11. loopmath-0.2.2/src/loopmath/builder/cli.py +34 -0
  12. loopmath-0.2.2/src/loopmath/builder/context.py +353 -0
  13. loopmath-0.2.2/src/loopmath/builder/predict.py +306 -0
  14. loopmath-0.2.2/src/loopmath/builder/server.py +171 -0
  15. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/cli_registry.py +8 -3
  16. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/benchmarks.toml +465 -3
  17. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/build.py +25 -1
  18. loopmath-0.2.2/src/loopmath/priors/bundle/README.md +3 -0
  19. loopmath-0.2.2/src/loopmath/priors/bundle/lanes.jsonl.gz +0 -0
  20. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/bundle/manifest.json +65 -1
  21. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/commands.py +3 -3
  22. loopmath-0.2.2/src/loopmath/priors/lanes.py +267 -0
  23. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/reduce.py +4 -2
  24. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/registry.py +8 -5
  25. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/recommend/commands.py +72 -22
  26. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/recommend/curve.py +86 -5
  27. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/recommend/engine.py +216 -38
  28. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/recommend/message.py +101 -14
  29. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/recommend/search.py +10 -6
  30. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath/SKILL.md +1 -1
  31. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath-import-runs/SKILL.md +5 -4
  32. loopmath-0.2.2/src/loopmath/skill/skills/loopmath-plan-task/SKILL.md +119 -0
  33. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath-update-fit/SKILL.md +6 -3
  34. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/skill/skills/reference.md +20 -4
  35. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/store/commands.py +38 -3
  36. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/store/config.py +21 -4
  37. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/assets/base.css +7 -0
  38. loopmath-0.2.2/src/loopmath/views/assets/builder.css +303 -0
  39. loopmath-0.2.2/src/loopmath/views/assets/builder.html +70 -0
  40. loopmath-0.2.2/src/loopmath/views/assets/builder.js +1314 -0
  41. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/assets/common.js +85 -1
  42. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/assets/estimates.js +11 -6
  43. loopmath-0.2.2/src/loopmath/views/assets/plans.css +39 -0
  44. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/assets/plans.js +249 -52
  45. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/assets/results.js +44 -16
  46. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/assets/runs.js +1 -0
  47. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/assets/viz.css +14 -0
  48. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/assets/viz.js +46 -21
  49. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/plans.py +3 -1
  50. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/posterior.py +219 -20
  51. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath.egg-info/PKG-INFO +1 -1
  52. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath.egg-info/SOURCES.txt +12 -0
  53. loopmath-0.2.2/tests/test_cli_help_lists.py +40 -0
  54. loopmath-0.2.0/src/loopmath/priors/bundle/README.md +0 -1
  55. loopmath-0.2.0/src/loopmath/skill/skills/loopmath-plan-task/SKILL.md +0 -95
  56. loopmath-0.2.0/src/loopmath/views/assets/plans.css +0 -9
  57. {loopmath-0.2.0 → loopmath-0.2.2}/LICENSE +0 -0
  58. {loopmath-0.2.0 → loopmath-0.2.2}/README.md +0 -0
  59. {loopmath-0.2.0 → loopmath-0.2.2}/setup.cfg +0 -0
  60. {loopmath-0.2.0 → loopmath-0.2.2}/spec/ocp-v0.2.schema.json +0 -0
  61. {loopmath-0.2.0 → loopmath-0.2.2}/spec/ocp-v0.schema.json +0 -0
  62. {loopmath-0.2.0 → loopmath-0.2.2}/spec/ocp_conformance.py +0 -0
  63. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/__main__.py +0 -0
  64. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapter_host.py +0 -0
  65. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/__init__.py +0 -0
  66. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/atrium.py +0 -0
  67. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/base.py +0 -0
  68. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/bb.py +0 -0
  69. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/bb_support.py +0 -0
  70. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/omp.py +0 -0
  71. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/omp_facts.py +0 -0
  72. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/omp_links.py +0 -0
  73. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/omp_parse.py +0 -0
  74. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/omp_types.py +0 -0
  75. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/opencode.py +0 -0
  76. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/opencode_store.py +0 -0
  77. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/orca.py +0 -0
  78. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai.py +0 -0
  79. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai_graph.py +0 -0
  80. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai_parse.py +0 -0
  81. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai_types.py +0 -0
  82. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai_usage.py +0 -0
  83. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/paseo.py +0 -0
  84. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/pi.py +0 -0
  85. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/pi_support.py +0 -0
  86. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/adapters/registry.py +0 -0
  87. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/belief/__init__.py +0 -0
  88. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/belief/block.py +0 -0
  89. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/belief/check_pymc.py +0 -0
  90. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/belief/commands.py +0 -0
  91. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/belief/compose.py +0 -0
  92. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/belief/features.py +0 -0
  93. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/belief/gaussian.py +0 -0
  94. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/belief/outcome.py +0 -0
  95. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/belief/priors.py +0 -0
  96. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/cli.py +0 -0
  97. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/cli_graph.py +0 -0
  98. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/cli_support.py +0 -0
  99. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/e0/__init__.py +0 -0
  100. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/e0/arms.py +0 -0
  101. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/e0/cli_e0.py +0 -0
  102. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/e0/estimate.py +0 -0
  103. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/e0/estimate_rework.py +0 -0
  104. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/e0/figures.py +0 -0
  105. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/e0/io.py +0 -0
  106. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/e0/verb.py +0 -0
  107. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/explain.py +0 -0
  108. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/fit.py +0 -0
  109. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/fit_assembly.py +0 -0
  110. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/fit_bayes.py +0 -0
  111. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/fit_masks.py +0 -0
  112. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/fit_predict.py +0 -0
  113. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/fit_pricing.py +0 -0
  114. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/fit_transfer.py +0 -0
  115. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/gaps.py +0 -0
  116. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/gitwalk.py +0 -0
  117. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/grade.py +0 -0
  118. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/grade_support.py +0 -0
  119. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/__init__.py +0 -0
  120. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/artifact_git.py +0 -0
  121. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/artifact_join.py +0 -0
  122. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/artifact_kinds.py +0 -0
  123. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/artifacts.py +0 -0
  124. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/bashparse.py +0 -0
  125. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/bashread_ops.py +0 -0
  126. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/bashwrite_ops.py +0 -0
  127. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/bashwrites.py +0 -0
  128. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/cache.py +0 -0
  129. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/codexio.py +0 -0
  130. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/codexio_parse.py +0 -0
  131. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/codexio_scan.py +0 -0
  132. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/dataset.py +0 -0
  133. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/dataset_build.py +0 -0
  134. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/dataset_core.py +0 -0
  135. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/dataset_edges.py +0 -0
  136. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/dataset_nodes.py +0 -0
  137. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/extract.py +0 -0
  138. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/html_common.py +0 -0
  139. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/html_costcurve.py +0 -0
  140. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/html_css.py +0 -0
  141. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/html_data.py +0 -0
  142. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/html_force.py +0 -0
  143. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/html_render.py +0 -0
  144. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/html_swimlanes.py +0 -0
  145. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler.py +0 -0
  146. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_cli.py +0 -0
  147. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_common.py +0 -0
  148. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_parse.py +0 -0
  149. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_pricing.py +0 -0
  150. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt.py +0 -0
  151. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_build.py +0 -0
  152. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_dispatch.py +0 -0
  153. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_evidence.py +0 -0
  154. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_node.py +0 -0
  155. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_records.py +0 -0
  156. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_result.py +0 -0
  157. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_session.py +0 -0
  158. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_tools.py +0 -0
  159. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_validation.py +0 -0
  160. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_workflow.py +0 -0
  161. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_report.py +0 -0
  162. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labeler_score.py +0 -0
  163. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/labels.py +0 -0
  164. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/launch.py +0 -0
  165. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/launch_origin.py +0 -0
  166. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/ocp.py +0 -0
  167. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/ocp_emit.py +0 -0
  168. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/ocp_support.py +0 -0
  169. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/render.py +0 -0
  170. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/runfile.py +0 -0
  171. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/runfile_export.py +0 -0
  172. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/runfile_finish.py +0 -0
  173. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/scan.py +0 -0
  174. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/schema.py +0 -0
  175. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/graph/token_completeness.py +0 -0
  176. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ingest/__init__.py +0 -0
  177. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ingest/base.py +0 -0
  178. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ingest/claude_code.py +0 -0
  179. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ingest/claude_code_support.py +0 -0
  180. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ingest/codex.py +0 -0
  181. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ingest/codex_support.py +0 -0
  182. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ingest/ocp.py +0 -0
  183. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ingest/ocp_analysis.py +0 -0
  184. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ingest/ocp_artifact.py +0 -0
  185. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ingest/ocp_common.py +0 -0
  186. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ingest/ocp_convert.py +0 -0
  187. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ingest/ocp_projection.py +0 -0
  188. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/logmatch/__init__.py +0 -0
  189. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/logmatch/artifacts.py +0 -0
  190. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/logmatch/clip.py +0 -0
  191. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/logmatch/costs.py +0 -0
  192. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/logmatch/match.py +0 -0
  193. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/logmatch/settle.py +0 -0
  194. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/logmatch/tariff.py +0 -0
  195. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ocp/__init__.py +0 -0
  196. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ocp/canonical.py +0 -0
  197. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ocp/commands.py +0 -0
  198. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ocp/conformance.py +0 -0
  199. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ocp/contractv3.py +0 -0
  200. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ocp/emit.py +0 -0
  201. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ocp/migrate.py +0 -0
  202. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ocp/schema/ocp-v0.2.schema.json +0 -0
  203. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ocp/schema/ocp-v0.3.schema.json +0 -0
  204. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ocp/schema/ocp-v0.schema.json +0 -0
  205. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/ocp/version.py +0 -0
  206. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/onboard/__init__.py +0 -0
  207. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/onboard/commands.py +0 -0
  208. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/onboard/history.py +0 -0
  209. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/onboard/label.py +0 -0
  210. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/onboard/usual.py +0 -0
  211. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/output.py +0 -0
  212. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/pool.py +0 -0
  213. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/price.py +0 -0
  214. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/price_validation.py +0 -0
  215. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/prices.toml +0 -0
  216. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/__init__.py +0 -0
  217. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/benchmarks.py +0 -0
  218. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/bundle/e0.jsonl.gz +0 -0
  219. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/bundle/rq1.jsonl.gz +0 -0
  220. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/bundle/sweep.jsonl.gz +0 -0
  221. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/e0.py +0 -0
  222. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/history.py +0 -0
  223. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/ocpdoc.py +0 -0
  224. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/rq1.py +0 -0
  225. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/show.py +0 -0
  226. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/sweep.py +0 -0
  227. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/priors/validate.py +0 -0
  228. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/receipts.py +0 -0
  229. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/recommend/__init__.py +0 -0
  230. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/recommend/backlog.py +0 -0
  231. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/recommend/gain.py +0 -0
  232. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/recommend/receipts.py +0 -0
  233. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/recommend/stats.py +0 -0
  234. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/recommend/storeread.py +0 -0
  235. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/report/__init__.py +0 -0
  236. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/report/html.py +0 -0
  237. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/report/terminal.py +0 -0
  238. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/report/terminal_exclusions.py +0 -0
  239. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/report/terminal_format.py +0 -0
  240. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/report/terminal_read.py +0 -0
  241. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/research_defaults.py +0 -0
  242. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/research_paths.py +0 -0
  243. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/scoring.py +0 -0
  244. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/scoring_comparisons.py +0 -0
  245. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/scoring_predictive.py +0 -0
  246. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/share/__init__.py +0 -0
  247. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/share/commands.py +0 -0
  248. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/share/export.py +0 -0
  249. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/share/import_.py +0 -0
  250. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/skill/__init__.py +0 -0
  251. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/skill/commands.py +0 -0
  252. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/skill/doctor.py +0 -0
  253. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/skill/install.py +0 -0
  254. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath-onboard/SKILL.md +0 -0
  255. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath-record-run/SKILL.md +0 -0
  256. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/store/__init__.py +0 -0
  257. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/store/budget.py +0 -0
  258. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/store/finish.py +0 -0
  259. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/store/fitjob.py +0 -0
  260. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/store/home.py +0 -0
  261. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/store/ids.py +0 -0
  262. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/store/lock.py +0 -0
  263. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/store/record.py +0 -0
  264. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/store/report.py +0 -0
  265. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/store/runs.py +0 -0
  266. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/store/status.py +0 -0
  267. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/surface.py +0 -0
  268. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/surface_bootstrap.py +0 -0
  269. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/surface_estimation.py +0 -0
  270. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/surface_frame.py +0 -0
  271. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/surface_presentation.py +0 -0
  272. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/surface_support.py +0 -0
  273. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/taskmodel.py +0 -0
  274. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/types.py +0 -0
  275. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/__init__.py +0 -0
  276. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/assets/README.md +0 -0
  277. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/assets/estimates.css +0 -0
  278. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/assets/graph.css +0 -0
  279. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/assets/graph.js +0 -0
  280. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/assets/results.css +0 -0
  281. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/common.py +0 -0
  282. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/views/runs.py +0 -0
  283. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/__init__.py +0 -0
  284. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/candidates.py +0 -0
  285. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/catalog/best_of_n.toml +0 -0
  286. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/catalog/implement_review.toml +0 -0
  287. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/catalog/plan_implement.toml +0 -0
  288. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/catalog/plan_implement_review.toml +0 -0
  289. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/catalog/solo.toml +0 -0
  290. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/catalog/swarm.toml +0 -0
  291. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/commands.py +0 -0
  292. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/diff.py +0 -0
  293. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/format.py +0 -0
  294. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/graphview.py +0 -0
  295. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/ids.py +0 -0
  296. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/infer.py +0 -0
  297. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/models.py +0 -0
  298. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/ocp.py +0 -0
  299. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath/workflows/shapes.py +0 -0
  300. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath.egg-info/dependency_links.txt +0 -0
  301. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath.egg-info/entry_points.txt +0 -0
  302. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath.egg-info/requires.txt +0 -0
  303. {loopmath-0.2.0 → loopmath-0.2.2}/src/loopmath.egg-info/top_level.txt +0 -0
  304. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_adapter_atrium.py +0 -0
  305. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_adapter_bb.py +0 -0
  306. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_adapter_omp.py +0 -0
  307. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_adapter_opencode.py +0 -0
  308. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_adapter_opencode_output_tokens.py +0 -0
  309. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_adapter_orca.py +0 -0
  310. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_adapter_otel_genai.py +0 -0
  311. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_adapter_paseo.py +0 -0
  312. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_adapter_services.py +0 -0
  313. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_adapters.py +0 -0
  314. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_adapters_pi.py +0 -0
  315. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_arms.py +0 -0
  316. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_cli_graph.py +0 -0
  317. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_cli_labeler.py +0 -0
  318. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_cli_main_module.py +0 -0
  319. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_cli_plan_hidden.py +0 -0
  320. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_cli_prices.py +0 -0
  321. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_cli_startup.py +0 -0
  322. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_color_proof.py +0 -0
  323. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_duplicate_guards.py +0 -0
  324. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_estimate.py +0 -0
  325. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_explain.py +0 -0
  326. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_fit.py +0 -0
  327. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_gaps.py +0 -0
  328. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_gitwalk.py +0 -0
  329. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_grade.py +0 -0
  330. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_artifact_recording.py +0 -0
  331. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_artifacts.py +0 -0
  332. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_bashwrites.py +0 -0
  333. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_cache.py +0 -0
  334. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_codexio.py +0 -0
  335. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_dataset.py +0 -0
  336. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_extract.py +0 -0
  337. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_glue.py +0 -0
  338. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_html.py +0 -0
  339. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_labeler.py +0 -0
  340. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_labeler_evidence_matrix.py +0 -0
  341. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_launch.py +0 -0
  342. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_ocp.py +0 -0
  343. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_render.py +0 -0
  344. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_runfile.py +0 -0
  345. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_graph_schema.py +0 -0
  346. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_home_guard.py +0 -0
  347. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_html.py +0 -0
  348. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_ingest.py +0 -0
  349. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_ingest_claude_code.py +0 -0
  350. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_ingest_codex.py +0 -0
  351. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_ingest_ocp.py +0 -0
  352. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_io.py +0 -0
  353. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_not_run_summary.py +0 -0
  354. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_ocp_conformance.py +0 -0
  355. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_ocp_golden.py +0 -0
  356. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_ocp_open_enums.py +0 -0
  357. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_ocp_versioning.py +0 -0
  358. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_output_html_path.py +0 -0
  359. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_packaging_metadata.py +0 -0
  360. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_packaging_smoke.py +0 -0
  361. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_pool.py +0 -0
  362. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_preview_script.py +0 -0
  363. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_price.py +0 -0
  364. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_receipts.py +0 -0
  365. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_release_paths.py +0 -0
  366. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_runfile_privacy_n1b.py +0 -0
  367. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_scoring.py +0 -0
  368. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_split_rebindings.py +0 -0
  369. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_surface.py +0 -0
  370. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_taskmodel_features.py +0 -0
  371. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_terminal.py +0 -0
  372. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_types_control.py +0 -0
  373. {loopmath-0.2.0 → loopmath-0.2.2}/tests/test_types_piece_prediction.py +0 -0
  374. {loopmath-0.2.0 → 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.0
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.0"
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.0"
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
 
@@ -35,6 +36,11 @@ HEURISTIC_COST_WEIGHT = 0.7 # spec 04 section 2 and D27: allocated (heuristic)
35
36
  USER_SOURCES = ("live", "backlog", "history", "user", "orchestrator", "onboard", "habit", "")
36
37
  HORIZON_NODES = ("horizon:timebox", "horizon:log2h") # fixed terms of a timeboxed run (spec 04 section 1)
37
38
  RQ1_EXT = "dev.loopmath.rq1" # loopmath-exp's ext key; its horizon_s is the fallback for older RQ1 runs
39
+ # 0.2.1 (F5, spec 04 section 1): in the cost and tokens rows of a timeboxed task the terms a timebox caps (effort
40
+ # 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
41
+ # (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.
42
+ TIMEBOX_EFFORT_COST = 0.3
43
+ TIMEBOX_LEVELS = ("effort:", "family_effort:", "topology:", "position:")
38
44
 
39
45
 
40
46
  def other_design(meta: dict) -> str | None:
@@ -304,22 +310,45 @@ def structure(config: Configuration) -> Structure:
304
310
 
305
311
  # ---------------------------------------------------------------- rows
306
312
 
313
+ def effort_weight(task: Task, timebox_effort: float | None = None) -> float:
314
+ """The value of the timebox-capped terms (TIMEBOX_LEVELS) in a cost or tokens row of `task`: `timebox_effort`
315
+ (default TIMEBOX_EFFORT_COST) when the task has a horizon, else 1 (spec 04 section 1, effort and shape under
316
+ a timebox)."""
317
+ if task_horizon(task) is None:
318
+ return 1.0
319
+ return float(TIMEBOX_EFFORT_COST if timebox_effort is None else timebox_effort)
320
+
321
+
322
+ @lru_cache(maxsize=65536)
323
+ def _effort_scaled(terms: tuple[Term, ...], weight: float) -> tuple[Term, ...]:
324
+ return tuple((n, p, v * weight) if n.startswith(TIMEBOX_LEVELS) else (n, p, v) for n, p, v in terms)
325
+
326
+
307
327
  def cost_rest(st: Structure, piece: str, k: int, setting: Setting | None = None, *,
308
- source: str | None = None) -> tuple[Term, ...]:
328
+ source: str | None = None, effort: float = 1.0,
329
+ prices: Mapping[str, float] | None = None) -> tuple[Term, ...]:
309
330
  """Cost and tokens rows without the task part: setting, role, topology, position, round, control,
310
331
  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)."""
332
+ node `fsrc:<family>|<source>` (spec 04 section 1). `effort` is the value of the terms a timebox caps, the
333
+ effort and shape (topology, position) terms (`effort_weight`: below 1 under a timebox); the role, psrc and
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."""
312
337
  s = setting or st.settings[piece]
313
338
  h, m, e = _s(s)
314
339
  shape = st.shape or st.workflow_id
315
340
  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)))
341
+ st_terms = setting_terms(h, m, e)
342
+ shape_terms = ((f"topology:{shape}", None, 1.0), (f"position:{position}", f"topology:{shape}", 1.0))
343
+ if effort != 1.0:
344
+ st_terms, shape_terms = _effort_scaled(st_terms, float(effort)), _effort_scaled(shape_terms, float(effort))
345
+ terms = st_terms + role_terms(st.roles[piece], m) + shape_terms
318
346
  if source is not None:
319
347
  fam = model_path(m)[1]
320
348
  terms += ((f"psrc:{position}|{source}", f"position:{position}", 1.0),
321
349
  (f"fsrc:{fam}|{source}", f"family:{fam}", 1.0))
322
- 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))
323
352
 
324
353
 
325
354
  def gate_rest(st: Structure, gate: GateInfo, k: int) -> tuple[Term, ...]:
@@ -345,7 +374,8 @@ def run_rest(st: Structure) -> tuple[Term, ...]:
345
374
 
346
375
  def cost_row(task: Task, source: str, st: Structure, piece: str, k: int,
347
376
  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))
377
+ return task_terms(task, source, features) + list(cost_rest(st, piece, k, setting, source=source,
378
+ effort=effort_weight(task)))
349
379
 
350
380
 
351
381
  def gate_row(task: Task, source: str, st: Structure, gate: GateInfo, k: int,
@@ -396,6 +426,7 @@ class AttemptObs:
396
426
  weight: float
397
427
  setting: Setting
398
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)
399
430
 
400
431
 
401
432
  @dataclass
@@ -433,8 +464,10 @@ def source_label(doc: dict) -> str:
433
464
  ext = run.get("ext") or {}
434
465
  share = ext.get("dev.loopmath.share")
435
466
  if isinstance(share, dict):
436
- org = share.get("org") or share.get("org_hash") or (run.get("task") or {}).get("org") or "shared"
437
- 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:")
438
471
  src = (run.get("task") or {}).get("source")
439
472
  kind = src.get("kind") if isinstance(src, dict) else src
440
473
  kind = str(kind or "").strip().lower()
@@ -736,7 +769,9 @@ def parse_run(doc: dict, *, now: datetime | None = None, rule: AcceptanceRule |
736
769
  if weight <= 0 or ((usd is None or usd <= 0) and not tokens):
737
770
  dropped["attempt without usable cost"] = dropped.get("attempt without usable cost", 0) + 1
738
771
  continue
739
- 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))
740
775
 
741
776
  gates = _gate_observations(doc, st, by_piece_round)
742
777
  use_rule = rule or rule_from_doc(doc)
@@ -29,12 +29,14 @@ 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)
35
36
  from .features import FeatureTally, add_horizon, drop_features, horizon_nodes, horizon_specs
36
37
  from .block import leaf_split
37
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
38
40
  from .gaussian import N_DRAWS, Factors, GaussianHead, HeadFit, LogisticHead, Prior
39
41
  from .state import SCORE_CLAMP, inverse_transform, transform # noqa: F401 (re-exported)
40
42
 
@@ -162,6 +164,7 @@ class HeadRows:
162
164
  sources: list[str] = field(default_factory=list)
163
165
  types: list[str] = field(default_factory=list)
164
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)
165
168
 
166
169
  def add(self, terms, y, w, run, source, ttype):
167
170
  self.rows.append(terms)
@@ -212,6 +215,8 @@ def collect_rows(docs: Iterable[dict | tuple[str | None, dict]], *, without: tup
212
215
  stored: set[str] = set() # run ids of the user's store, whose copy wins
213
216
  overlap: Counter = Counter()
214
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))
215
220
  for item in docs:
216
221
  origin, doc = item if isinstance(item, tuple) else (None, item)
217
222
  if origin is not None and _excluded(origin, without):
@@ -257,6 +262,10 @@ def collect_rows(docs: Iterable[dict | tuple[str | None, dict]], *, without: tup
257
262
  if a.usd:
258
263
  heads["cost"].add(terms, math.log(a.usd), a.weight, pr.run_id, pr.source, ttype)
259
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
260
269
  if a.tokens:
261
270
  heads["tokens"].add(terms, math.log(a.tokens), a.weight, pr.run_id, pr.source, ttype)
262
271
  for gi, k, passed in pr.gates:
@@ -288,10 +297,34 @@ def collect_rows(docs: Iterable[dict | tuple[str | None, dict]], *, without: tup
288
297
  coding = tally.horizon_coding() # the horizon terms need every run's horizon first (spec 04 section 1)
289
298
  for hr in heads.values():
290
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)
291
301
  return (heads, score_info, dict(dropped), dict(runs_by_source), {c: dict(n) for c, n in config_support.items()},
292
302
  dict(checks), dict(overlap), dict(labels), tally)
293
303
 
294
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
+
295
328
  # ---------------------------------------------------------------- matrices and priors
296
329
 
297
330
  def build_matrix(rows: list[list[Term]], forest: Forest, extra_nodes: Iterable[tuple[str, str | None]] = ()):
@@ -362,9 +395,27 @@ def _support(hr: HeadRows, X: sparse.csr_matrix, node_ids: list[str]) -> dict[st
362
395
 
363
396
 
364
397
  def seed_for(fit_id: str, head: str) -> int:
398
+ """A head's seed from the fit's `seed_key` (its fit id before 0.2.1)."""
365
399
  return int.from_bytes(hashlib.sha256(f"{fit_id}/{head}".encode()).digest()[:4], "little")
366
400
 
367
401
 
402
+ def input_key(heads_rows: dict[str, HeadRows], specs: list, settings: dict) -> str:
403
+ """The fit's `seed_key` (spec 04 section 3): the sha256 of the design version, the settings that change the
404
+ posterior, the prior factors and every head's rows (terms, response, weight, run, source), each head's rows
405
+ sorted, so the same data and settings seed the same draws whatever the store's reading order."""
406
+ h = hashlib.sha256()
407
+ h.update(repr((DESIGN_VERSION, design_rows.TIMEBOX_EFFORT_COST, design_rows.TIMEBOX_LEVELS,
408
+ sorted(settings.items()))).encode())
409
+ for spec in specs:
410
+ h.update(repr((spec.head, spec.terms, spec.mean, spec.var)).encode())
411
+ for name in sorted(heads_rows):
412
+ hr = heads_rows[name]
413
+ h.update(f"\n{name}:{len(hr.rows)}".encode())
414
+ 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)):
415
+ h.update(line.encode())
416
+ return h.hexdigest()
417
+
418
+
368
419
  def fit_head(hr: HeadRows, forest: Forest, factor_specs: list, *, eb: bool = True) -> dict:
369
420
  """Fit one head; returns arrays and metadata for the state files."""
370
421
  extra = [(n, p) for spec in factor_specs for n, p, _ in spec.terms]
@@ -502,6 +553,8 @@ def fit(home: Path, *, no_prior: bool = False, without: tuple[str, ...] = (), fu
502
553
  specs = []
503
554
  if not no_prior and "benchmark" not in without:
504
555
  specs = prior_data.benchmark_factors(benchmarks, weight=weight)
556
+ seed_key = input_key(heads_rows, specs, {"no_prior": no_prior, "without": sorted(without), "eb": eb,
557
+ "benchmark_prior_weight": weight})
505
558
  forest = Forest()
506
559
  fitted = {}
507
560
  for name, hr in heads_rows.items():
@@ -511,6 +564,8 @@ def fit(home: Path, *, no_prior: bool = False, without: tuple[str, ...] = (), fu
511
564
  nodes = horizon_nodes(hr.rows)
512
565
  if nodes: # the horizon priors, --no-prior included (spec 04 section 1)
513
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)
514
569
  fitted[name] = fit_head(hr, forest, head_specs, eb=eb)
515
570
  fit_id = new_fit_id(fits, now)
516
571
  partial = fits / f"{fit_id}.partial"
@@ -546,6 +601,8 @@ def fit(home: Path, *, no_prior: bool = False, without: tuple[str, ...] = (), fu
546
601
  _atomic_json(partial / "design.json", design)
547
602
  meta = {
548
603
  "fit": fit_id, "created_at": now.isoformat(timespec="seconds"), "code_version": _code_version(),
604
+ "seed_key": seed_key, "timebox_effort": design_rows.TIMEBOX_EFFORT_COST,
605
+ "timebox_terms": list(design_rows.TIMEBOX_LEVELS),
549
606
  "options": {"no_prior": no_prior, "without": list(without), "full": full, "eb": eb,
550
607
  "benchmark_prior_weight": weight},
551
608
  "runs_by_source": runs_by_source,
@@ -562,13 +619,14 @@ def fit(home: Path, *, no_prior: bool = False, without: tuple[str, ...] = (), fu
562
619
  "center": h["center"], "scale": h["scale"], "baseline": h["baseline"],
563
620
  "iterations": h["fit"].iterations, "optimizer": h["fit"].info,
564
621
  "log_evidence": _finite(h["fit"].log_evidence), "factors": h["factors"],
565
- "seed": seed_for(fit_id, name)}
622
+ "seed": seed_for(seed_key, name)}
566
623
  for name, h in fitted.items()},
567
624
  "scores": score_info,
568
625
  "draws": N_DRAWS,
569
626
  "design_version": DESIGN_VERSION,
570
627
  "features": {**tally.report, **({"error": features_error} if features_error else {})},
571
628
  "horizons": tally.horizon_report(),
629
+ "price_offsets": heads_rows["cost"].prices,
572
630
  }
573
631
  if full:
574
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
@@ -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
@@ -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
@@ -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,23 @@ 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))
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 {})
336
344
  if heads is None:
337
345
  heads = {}
338
346
  for name, hmeta in (self.meta.get("heads") or {}).items():
339
347
  key = name.replace(":", "@")
340
348
  ids = [str(x) for x in self._array(f"{key}__ids")]
341
- heads[name] = HeadState(name, hmeta, ids, self._loader(key), self.fit_id)
349
+ heads[name] = HeadState(name, hmeta, ids, self._loader(key), self.fit_id, seed_key=self.seed_key)
342
350
  for name in BASE_HEADS:
343
351
  if name not in heads:
344
- heads[name] = HeadState(name, {}, [], None, self.fit_id)
352
+ heads[name] = HeadState(name, {}, [], None, self.fit_id, seed_key=self.seed_key)
345
353
  self.heads = heads
346
354
  # the fit's feature declarations (the built-ins for fits before 0.2), so predictions read
347
355
  # features the way the fit did
@@ -397,13 +405,23 @@ class FitState:
397
405
  def _row(self, head: HeadState, rest: tuple) -> Row:
398
406
  return self._cached(head, [rest])[0]
399
407
 
400
- def _plan(self, config: Configuration) -> dict:
408
+ def effort_for(self, task: Task) -> float:
409
+ """The value of the timebox-capped terms (effort and shape, `design.TIMEBOX_LEVELS`) in the task's cost
410
+ rows: the fit's `timebox_effort` when the task's resolved horizon (given, else recorded) is a timebox the
411
+ fit uses (it has timeboxed runs), else 1 (spec 04 section 1)."""
412
+ if self.timebox_effort == 1.0:
413
+ return 1.0
414
+ return self.timebox_effort if self.resolve_task(task)[1]["horizon"]["used"] else 1.0
415
+
416
+ def _plan(self, config: Configuration, effort: float = 1.0) -> dict:
417
+ """The configuration's rows without the task part; `effort` from `effort_for(task)`."""
401
418
  st = structure(config)
402
419
  cost = {}
403
420
  for piece in st.pieces:
404
421
  rounds = st.k_max if st.piece_loop.get(piece) is not None else 1
405
422
  for k in range(1, rounds + 1):
406
- cost[(piece, k)] = cost_rest(st, piece, k, source=PREDICT_SOURCE)
423
+ cost[(piece, k)] = cost_rest(st, piece, k, source=PREDICT_SOURCE, effort=effort,
424
+ prices=self.price_offsets)
407
425
  gate = {}
408
426
  for gi, g in enumerate(st.gates):
409
427
  for k in range(1, (st.k_max if st.gate_loop.get(gi) is not None else 1) + 1):
@@ -790,7 +808,8 @@ class FitState:
790
808
 
791
809
  def _predict_many(self, task: Task, configs: Sequence[Configuration], rule: AcceptanceRule | None,
792
810
  rescue_usd: float | None) -> list[tuple[Prediction, dict]]:
793
- plans = [self._plan(c) for c in configs]
811
+ effort = self.effort_for(task)
812
+ plans = [self._plan(c, effort) for c in configs]
794
813
  tparts = self._task_parts(task)
795
814
  self._warm(plans)
796
815
  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"))