cmpnd 0.60.0__tar.gz → 0.61.0__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 (391) hide show
  1. {cmpnd-0.60.0 → cmpnd-0.61.0}/PKG-INFO +1 -1
  2. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/programs.py +214 -51
  3. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/valsets.py +29 -4
  4. {cmpnd-0.60.0 → cmpnd-0.61.0}/docs/cli.md +45 -10
  5. {cmpnd-0.60.0 → cmpnd-0.61.0}/pyproject.toml +1 -1
  6. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_programs.py +247 -38
  7. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_valsets.py +66 -3
  8. {cmpnd-0.60.0 → cmpnd-0.61.0}/uv.lock +1 -1
  9. {cmpnd-0.60.0 → cmpnd-0.61.0}/.gitignore +0 -0
  10. {cmpnd-0.60.0 → cmpnd-0.61.0}/AGENTS.md +0 -0
  11. {cmpnd-0.60.0 → cmpnd-0.61.0}/CLAUDE.md +0 -0
  12. {cmpnd-0.60.0 → cmpnd-0.61.0}/README.md +0 -0
  13. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/__init__.py +0 -0
  14. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/_assessments.py +0 -0
  15. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/_authored_import.py +0 -0
  16. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/_deployments.py +0 -0
  17. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/_exporter_pool.py +0 -0
  18. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/_gepa_patch.py +0 -0
  19. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/_http.py +0 -0
  20. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/_program_patch.py +0 -0
  21. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/_rlm_patch.py +0 -0
  22. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/_runtime_tools.py +0 -0
  23. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/_timespec.py +0 -0
  24. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/_traces.py +0 -0
  25. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/_valsets.py +0 -0
  26. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/authored.py +0 -0
  27. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/callback.py +0 -0
  28. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/__init__.py +0 -0
  29. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/api_client.py +0 -0
  30. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/__init__.py +0 -0
  31. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/admin.py +0 -0
  32. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/assessments.py +0 -0
  33. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/auth.py +0 -0
  34. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/datasets.py +0 -0
  35. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/deployments.py +0 -0
  36. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/evals.py +0 -0
  37. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/login.py +0 -0
  38. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/optimizations.py +0 -0
  39. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/org.py +0 -0
  40. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  41. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/signatures.py +0 -0
  42. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  43. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/top.py +0 -0
  44. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/commands/traces.py +0 -0
  45. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/credentials.py +0 -0
  46. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/deprecation.py +0 -0
  47. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/pagination.py +0 -0
  48. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/shell.py +0 -0
  49. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/trace_render.py +0 -0
  50. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/tree_help.py +0 -0
  51. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/cli/verbs.py +0 -0
  52. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/client.py +0 -0
  53. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/configuration.py +0 -0
  54. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/context.py +0 -0
  55. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/credentials.py +0 -0
  56. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/dataset_sync.py +0 -0
  57. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/datasets.py +0 -0
  58. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/decorators.py +0 -0
  59. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/deployment.py +0 -0
  60. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/deployment_metadata.py +0 -0
  61. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/deployment_runs.py +0 -0
  62. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/eval_handler.py +0 -0
  63. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/execution.py +0 -0
  64. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/exporter.py +0 -0
  65. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/exporter_http.py +0 -0
  66. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/helpers.py +0 -0
  67. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/identity.py +0 -0
  68. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/imaging.py +0 -0
  69. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/ir/__init__.py +0 -0
  70. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/ir/decode.py +0 -0
  71. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/ir/encode.py +0 -0
  72. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/ir/hash.py +0 -0
  73. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/ir/program.py +0 -0
  74. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/ir/reflection.py +0 -0
  75. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/ir/types.py +0 -0
  76. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/ir/xxh64.py +0 -0
  77. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/lm_configuration.py +0 -0
  78. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/mcp_servers.py +0 -0
  79. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/models.py +0 -0
  80. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/named_programs.py +0 -0
  81. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/optimization.py +0 -0
  82. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/optimizers/__init__.py +0 -0
  83. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/optimizers/gepa.py +0 -0
  84. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  85. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/optimizers/resume.py +0 -0
  86. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/optimizers/tracker.py +0 -0
  87. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/packaging.py +0 -0
  88. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/programs.py +0 -0
  89. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/references.py +0 -0
  90. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/retry.py +0 -0
  91. {cmpnd-0.60.0 → cmpnd-0.61.0}/cmpnd/traces.py +0 -0
  92. {cmpnd-0.60.0 → cmpnd-0.61.0}/docs/2026-04-02-code-review.md +0 -0
  93. {cmpnd-0.60.0 → cmpnd-0.61.0}/docs/client.md +0 -0
  94. {cmpnd-0.60.0 → cmpnd-0.61.0}/docs/custom-proposer.md +0 -0
  95. {cmpnd-0.60.0 → cmpnd-0.61.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  96. {cmpnd-0.60.0 → cmpnd-0.61.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  97. {cmpnd-0.60.0 → cmpnd-0.61.0}/docs/reading-traces.md +0 -0
  98. {cmpnd-0.60.0 → cmpnd-0.61.0}/docs/sdk-instrumentation.md +0 -0
  99. {cmpnd-0.60.0 → cmpnd-0.61.0}/examples/generalizing_proposer.py +0 -0
  100. {cmpnd-0.60.0 → cmpnd-0.61.0}/examples/local_ollama.py +0 -0
  101. {cmpnd-0.60.0 → cmpnd-0.61.0}/ingest/LINEAGE.md +0 -0
  102. {cmpnd-0.60.0 → cmpnd-0.61.0}/ingest/README.md +0 -0
  103. {cmpnd-0.60.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  104. {cmpnd-0.60.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  105. {cmpnd-0.60.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/client_floor.py +0 -0
  106. {cmpnd-0.60.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  107. {cmpnd-0.60.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/program_names.py +0 -0
  108. {cmpnd-0.60.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  109. {cmpnd-0.60.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/py.typed +0 -0
  110. {cmpnd-0.60.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  111. {cmpnd-0.60.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/retry.py +0 -0
  112. {cmpnd-0.60.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  113. {cmpnd-0.60.0 → cmpnd-0.61.0}/ingest/pyproject.toml +0 -0
  114. {cmpnd-0.60.0 → cmpnd-0.61.0}/scripts/audit-sdist.sh +0 -0
  115. {cmpnd-0.60.0 → cmpnd-0.61.0}/scripts/capture_review_run.py +0 -0
  116. {cmpnd-0.60.0 → cmpnd-0.61.0}/scripts/check-pip-install.sh +0 -0
  117. {cmpnd-0.60.0 → cmpnd-0.61.0}/scripts/check_release_artifacts.py +0 -0
  118. {cmpnd-0.60.0 → cmpnd-0.61.0}/scripts/fixtures/review_run.json.gz +0 -0
  119. {cmpnd-0.60.0 → cmpnd-0.61.0}/scripts/gepa_resume_bug.py +0 -0
  120. {cmpnd-0.60.0 → cmpnd-0.61.0}/scripts/install-test-env.sh +0 -0
  121. {cmpnd-0.60.0 → cmpnd-0.61.0}/scripts/release-stress.sh +0 -0
  122. {cmpnd-0.60.0 → cmpnd-0.61.0}/scripts/require_version_bump.py +0 -0
  123. {cmpnd-0.60.0 → cmpnd-0.61.0}/scripts/seed_review_run.py +0 -0
  124. {cmpnd-0.60.0 → cmpnd-0.61.0}/scripts/selfhost_compat.py +0 -0
  125. {cmpnd-0.60.0 → cmpnd-0.61.0}/scripts/test-in-linux.sh +0 -0
  126. {cmpnd-0.60.0 → cmpnd-0.61.0}/stubs/dspy/__init__.pyi +0 -0
  127. {cmpnd-0.60.0 → cmpnd-0.61.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  128. {cmpnd-0.60.0 → cmpnd-0.61.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  129. {cmpnd-0.60.0 → cmpnd-0.61.0}/stubs/dspy/utils/__init__.pyi +0 -0
  130. {cmpnd-0.60.0 → cmpnd-0.61.0}/stubs/dspy/utils/callback.pyi +0 -0
  131. {cmpnd-0.60.0 → cmpnd-0.61.0}/stubs/dspy/utils/dummies.pyi +0 -0
  132. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/__init__.py +0 -0
  133. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/_credential_route.py +0 -0
  134. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/_deployment_signature_helpers.py +0 -0
  135. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/_exporter_helpers.py +0 -0
  136. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/_fake_clock.py +0 -0
  137. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/_identity_helpers.py +0 -0
  138. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/_resolve_refs.py +0 -0
  139. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/_traced_run.py +0 -0
  140. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/conftest.py +0 -0
  141. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/__init__.py +0 -0
  142. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/committee_task.py +0 -0
  143. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/conftest.py +0 -0
  144. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  145. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  146. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/fake_lm.py +0 -0
  147. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/helpers.py +0 -0
  148. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/target_capabilities.py +0 -0
  149. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_assessments_cli.py +0 -0
  150. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_capture_prompts.py +0 -0
  151. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_composite_module.py +0 -0
  152. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  153. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_deploy_execute.py +0 -0
  154. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_deploy_pandas.py +0 -0
  155. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_deploy_programs.py +0 -0
  156. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_edge_cases.py +0 -0
  157. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_example_hash.py +0 -0
  158. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_gepa_optimization.py +0 -0
  159. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_interpreter_spans.py +0 -0
  160. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_lm_accuracy.py +0 -0
  161. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_module_tracing.py +0 -0
  162. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_optimization_lineage.py +0 -0
  163. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  164. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_optimize_failures.py +0 -0
  165. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_optimize_loop.py +0 -0
  166. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  167. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  168. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_parallel_export.py +0 -0
  169. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_project_attribution.py +0 -0
  170. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_react_tool_spans.py +0 -0
  171. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_save_load.py +0 -0
  172. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_trace_structure.py +0 -0
  173. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_traces_read.py +0 -0
  174. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_user_attribution.py +0 -0
  175. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/e2e/test_valsets_cli.py +0 -0
  176. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  177. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  178. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  179. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  180. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  181. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  182. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  183. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  184. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  185. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/10_react.json +0 -0
  186. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/11_refine.json +0 -0
  187. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/12_retrieve.json +0 -0
  188. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/13_knn.json +0 -0
  189. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/14_parallel.json +0 -0
  190. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/identity_vectors/README.md +0 -0
  191. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/ingest_conformance.py +0 -0
  192. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/integration/__init__.py +0 -0
  193. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/integration/authored_optimize/judge.py +0 -0
  194. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  195. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/integration/conftest.py +0 -0
  196. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  197. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/integration/hosted_optimize_probe.py +0 -0
  198. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/integration/optimization_run_probe.py +0 -0
  199. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  200. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/integration/test_logs_renders_traces.py +0 -0
  201. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/integration/test_optimization_run.py +0 -0
  202. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/integration/test_optimizing_progress.py +0 -0
  203. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/integration/test_unhappy_path.py +0 -0
  204. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/load/__init__.py +0 -0
  205. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/load/__main__.py +0 -0
  206. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/load/drain.py +0 -0
  207. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/load/ingest.py +0 -0
  208. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/load/latency_transport.py +0 -0
  209. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/load/rlm_shape.py +0 -0
  210. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/load/rlm_size_report.py +0 -0
  211. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/load/sweep.py +0 -0
  212. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/load/test_rlm_shape.py +0 -0
  213. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/load/test_rlm_size_report.py +0 -0
  214. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/parity/__init__.py +0 -0
  215. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/parity/conftest.py +0 -0
  216. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/parity/seed_parity.py +0 -0
  217. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/parity/test_auth.py +0 -0
  218. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/parity/test_datasets.py +0 -0
  219. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/parity/test_deployments.py +0 -0
  220. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/parity/test_evals.py +0 -0
  221. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/parity/test_health.py +0 -0
  222. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/parity/test_modules.py +0 -0
  223. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/parity/test_optimizations.py +0 -0
  224. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/parity/test_programs.py +0 -0
  225. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/parity/test_signatures.py +0 -0
  226. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/parity/test_trace_streaming.py +0 -0
  227. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/parity/test_traces.py +0 -0
  228. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/span_reference_fixture.py +0 -0
  229. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/__init__.py +0 -0
  230. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/_provider.py +0 -0
  231. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/_readiness.py +0 -0
  232. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/_reliability.py +0 -0
  233. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/_result.py +0 -0
  234. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/mint_bedrock_token.py +0 -0
  235. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/test_deploy_programs.py +0 -0
  236. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/test_multi_client_stress.py +0 -0
  237. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/test_optimize_client_committee.py +0 -0
  238. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/test_optimize_loop.py +0 -0
  239. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  240. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/test_optimize_stress.py +0 -0
  241. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/test_provider_roles.py +0 -0
  242. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/test_readiness.py +0 -0
  243. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/test_real_provider_twins.py +0 -0
  244. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/test_reliability.py +0 -0
  245. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/test_reliability_classify.py +0 -0
  246. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/test_rlm_wasm.py +0 -0
  247. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/stress/test_trace_search.py +0 -0
  248. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_adapter_reentrancy.py +0 -0
  249. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_assessments_api.py +0 -0
  250. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_authored.py +0 -0
  251. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_authored_binding_credentials.py +0 -0
  252. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_authored_bundle_collisions.py +0 -0
  253. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_authored_metric_contract.py +0 -0
  254. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_authored_named_models.py +0 -0
  255. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_authored_optimize.py +0 -0
  256. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_callback.py +0 -0
  257. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_candidate_lineage.py +0 -0
  258. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/__init__.py +0 -0
  259. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/conftest.py +0 -0
  260. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_admin.py +0 -0
  261. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_api_client.py +0 -0
  262. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_assessments.py +0 -0
  263. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_auth.py +0 -0
  264. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_credentials.py +0 -0
  265. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_datasets.py +0 -0
  266. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_deployment_runs.py +0 -0
  267. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_deployments.py +0 -0
  268. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_evals.py +0 -0
  269. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_login.py +0 -0
  270. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_main.py +0 -0
  271. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_optimizations.py +0 -0
  272. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_org.py +0 -0
  273. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_pagination.py +0 -0
  274. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_parity.py +0 -0
  275. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_provider_credentials.py +0 -0
  276. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_schema_sample.py +0 -0
  277. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_shell.py +0 -0
  278. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_signatures.py +0 -0
  279. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_tool_servers.py +0 -0
  280. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_top.py +0 -0
  281. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_traces.py +0 -0
  282. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  283. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cli/test_tree_help.py +0 -0
  284. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_client.py +0 -0
  285. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_client_floor.py +0 -0
  286. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_client_program_names.py +0 -0
  287. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_clustering/__init__.py +0 -0
  288. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_clustering/conftest.py +0 -0
  289. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_clustering/test_edge_cases.py +0 -0
  290. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  291. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_clustering/test_type_conversion.py +0 -0
  292. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_cmp551_grouping_identity.py +0 -0
  293. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_configuration.py +0 -0
  294. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_context.py +0 -0
  295. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_context_propagation.py +0 -0
  296. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_credential_kwargs.py +0 -0
  297. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_credential_selection.py +0 -0
  298. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_credential_values.py +0 -0
  299. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_dataset_polling.py +0 -0
  300. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_dataset_sync.py +0 -0
  301. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_datasets.py +0 -0
  302. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_decision_client_spans.py +0 -0
  303. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_decorators.py +0 -0
  304. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_deploy.py +0 -0
  305. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  306. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_deploy_program_id.py +0 -0
  307. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_deployment_metadata.py +0 -0
  308. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_deployment_runs.py +0 -0
  309. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_deployments_read.py +0 -0
  310. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_eval.py +0 -0
  311. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_eval_example_attribution.py +0 -0
  312. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_eval_start.py +0 -0
  313. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_eval_trace_grouping.py +0 -0
  314. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_exception_paths.py +0 -0
  315. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_execute.py +0 -0
  316. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_execute_run_identity.py +0 -0
  317. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_exporter.py +0 -0
  318. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_exporter_program_names.py +0 -0
  319. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_exporter_queue.py +0 -0
  320. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_exporter_real.py +0 -0
  321. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_fake_lm.py +0 -0
  322. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_gepa_callback.py +0 -0
  323. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_gepa_integration.py +0 -0
  324. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_gepa_output_normalization.py +0 -0
  325. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_gepa_resume.py +0 -0
  326. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_identity.py +0 -0
  327. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_imaging.py +0 -0
  328. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_import_weight.py +0 -0
  329. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_ingest_conformance.py +0 -0
  330. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_ingest_second_producer.py +0 -0
  331. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_integration.py +0 -0
  332. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_interpreter_callback.py +0 -0
  333. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_ir_encoder_properties.py +0 -0
  334. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_ir_golden_vectors.py +0 -0
  335. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_ir_roundtrip.py +0 -0
  336. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_lm15_native_calls.py +0 -0
  337. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_lm_configuration.py +0 -0
  338. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_lm_usage_shapes.py +0 -0
  339. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_mcp_tools.py +0 -0
  340. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_models.py +0 -0
  341. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_multi_client_stress_readiness.py +0 -0
  342. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_optimization_progress.py +0 -0
  343. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_optimization_start.py +0 -0
  344. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_optimize.py +0 -0
  345. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_optimize_budget.py +0 -0
  346. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_optimize_lift.py +0 -0
  347. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_optimize_lms.py +0 -0
  348. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_optimize_proposer.py +0 -0
  349. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_optimize_reflection_lm.py +0 -0
  350. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_optimize_terminal_failure.py +0 -0
  351. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_packaging.py +0 -0
  352. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_packaging_callable_source.py +0 -0
  353. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_packaging_mcp_tools.py +0 -0
  354. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_packaging_program_source.py +0 -0
  355. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_poll_until.py +0 -0
  356. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_program_id_deprecation.py +0 -0
  357. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_program_identity.py +0 -0
  358. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_program_lineage_persist.py +0 -0
  359. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_program_names_in_deploy.py +0 -0
  360. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_programs_create.py +0 -0
  361. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_project_payload.py +0 -0
  362. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_real_provider_twins_readiness.py +0 -0
  363. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_reflector_properties.py +0 -0
  364. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_reflector_smoke.py +0 -0
  365. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_release_stress_script.py +0 -0
  366. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_replaced_names.py +0 -0
  367. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_repro_reactv2_bugs.py +0 -0
  368. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_require_version_bump.py +0 -0
  369. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_retry.py +0 -0
  370. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_reversible_failures.py +0 -0
  371. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_rlm_patch.py +0 -0
  372. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_run_attribution_paths.py +0 -0
  373. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_runtime_tools.py +0 -0
  374. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_schemas.py +0 -0
  375. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_seal_lm_credential.py +0 -0
  376. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_seed_review_run.py +0 -0
  377. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_signature_inputs.py +0 -0
  378. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_span_references.py +0 -0
  379. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_span_sent_once.py +0 -0
  380. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_system_one.py +0 -0
  381. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_trace_context_concurrency.py +0 -0
  382. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_trace_fact_wire_contract.py +0 -0
  383. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_trace_facts.py +0 -0
  384. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_trace_render.py +0 -0
  385. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_traces_read.py +0 -0
  386. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_tracked_gepa.py +0 -0
  387. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_tracker_enrich.py +0 -0
  388. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_truncation.py +0 -0
  389. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_upload_auth_headers.py +0 -0
  390. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_valsets_api.py +0 -0
  391. {cmpnd-0.60.0 → cmpnd-0.61.0}/tests/test_xxh64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.60.0
3
+ Version: 0.61.0
4
4
  Summary: DSPy observability and deployment SDK for cmpnd
5
5
  Project-URL: Homepage, https://cmpnd.ai
6
6
  Author-email: cmpnd <hello@cmpnd.ai>
@@ -13,12 +13,12 @@ from .. import api_client, pagination
13
13
  from . import provider_credentials
14
14
 
15
15
 
16
- # The hash that show, traces, stats, usage and select take is the value `programs optimized list`
17
- # prints as `program_hash`, which the routes resolve as a *compiled* program hash — they look it up
18
- # against optimization runs and map it to a program signature hash internally.
19
- # The two hashes are different values, so a real signature hash 404s here. The
20
- # argument help says which one it wants rather than naming the concept the
21
- # route resolves it to, because the caller has only the former to paste.
16
+ # `show`, `traces`, `stats` and `usage` take a program name or a p_ id, as the
17
+ # Trash commands do. The `programs optimized` group and `select` take a compiled
18
+ # program hash, the value `programs optimized list` prints as `program_hash`.
19
+ # The routes resolve that hash against optimization runs, so a program
20
+ # signature hash 404s there. A compiled hash and a program name have the same
21
+ # shape, so no command takes both in one slot.
22
22
  def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
23
23
  parser = subparsers.add_parser("programs", help="Create and browse programs")
24
24
  sub = parser.add_subparsers(dest="action")
@@ -83,7 +83,8 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
83
83
  help="List optimized programs by compiled program hash",
84
84
  description=(
85
85
  "List the compiled programs your optimization runs produced, one row per compiled program hash, with "
86
- "their run counts and scores. `programs show`, `traces`, `stats` and `usage` take the hash from the "
86
+ "their run counts and scores. `programs optimized show`, `traces`, `stats` and `usage` take the hash "
87
+ "from the "
87
88
  "program_hash column."
88
89
  ),
89
90
  )
@@ -94,37 +95,54 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
94
95
  pagination.add_args(opt_list, with_page_aliases=True, default_limit=50)
95
96
  opt_list.set_defaults(func=handle_optimized_list)
96
97
 
97
- show = sub.add_parser("show", help="Show program detail")
98
- show.add_argument(
99
- "id", help="Compiled program hash, as `programs optimized list` prints it in the program_hash column"
98
+ opt_show = optimized_sub.add_parser("show", help="Show the optimization runs that produced a compiled program")
99
+ opt_show.add_argument("hash", help=_COMPILED_HASH_HELP)
100
+ opt_show.set_defaults(func=handle_optimized_show)
101
+
102
+ opt_traces = optimized_sub.add_parser("traces", help="List the traces of a compiled program's program")
103
+ opt_traces.add_argument("hash", help=_COMPILED_HASH_HELP)
104
+ pagination.add_args(opt_traces, with_page_aliases=False, default_limit=50)
105
+ opt_traces.set_defaults(func=handle_optimized_traces)
106
+
107
+ opt_stats = optimized_sub.add_parser("stats", help="Show the execution statistics of a compiled program's program")
108
+ opt_stats.add_argument("hash", help=_COMPILED_HASH_HELP)
109
+ opt_stats.set_defaults(func=handle_optimized_stats)
110
+
111
+ opt_usage = optimized_sub.add_parser("usage", help="Show the usage over time of a compiled program's program")
112
+ opt_usage.add_argument("hash", help=_COMPILED_HASH_HELP)
113
+ _add_usage_window(opt_usage)
114
+ opt_usage.set_defaults(func=handle_optimized_usage)
115
+
116
+ show = sub.add_parser(
117
+ "show",
118
+ help="Show a program",
119
+ description=(
120
+ "Show PROGRAM: its name, p_ id, whether it is in the Trash, its trace count and latest trace, and "
121
+ "the optimized programs its optimization runs produced."
122
+ ),
100
123
  )
124
+ show.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
101
125
  show.set_defaults(func=handle_show)
102
126
 
103
- tr = sub.add_parser("traces", help="List traces for a program")
104
- tr.add_argument(
105
- "id", help="Compiled program hash, as `programs optimized list` prints it in the program_hash column"
127
+ tr = sub.add_parser(
128
+ "traces",
129
+ help="List a program's traces, newest first, over the widest window trace search reads",
130
+ description=(
131
+ "List PROGRAM's traces, newest first, through trace search over its widest window, 365 days. "
132
+ "`cmpnd traces list --hash ID --since DATE` sets the window itself. --limit is at most 500."
133
+ ),
106
134
  )
135
+ tr.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
107
136
  pagination.add_args(tr, with_page_aliases=False, default_limit=50)
108
137
  tr.set_defaults(func=handle_traces)
109
138
 
110
- stats = sub.add_parser("stats", help="Show a program's execution statistics")
111
- stats.add_argument(
112
- "id", help="Compiled program hash, as `programs optimized list` prints it in the program_hash column"
113
- )
139
+ stats = sub.add_parser("stats", help="Show a program's execution statistics over all history")
140
+ stats.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
114
141
  stats.set_defaults(func=handle_stats)
115
142
 
116
143
  usage = sub.add_parser("usage", help="Show a program's trace volume, tokens and cost over time")
117
- usage.add_argument(
118
- "id", help="Compiled program hash, as `programs optimized list` prints it in the program_hash column"
119
- )
120
- usage.add_argument("--hours", type=int, default=24, help="Rolling window in hours (default %(default)s)")
121
- # Constrained on purpose: an unrecognised period buckets hourly server-side
122
- # rather than failing, so a typo would answer a different question silently.
123
- usage.add_argument(
124
- "--period",
125
- choices=["minute", "5m", "10m", "30m", "hour", "day"],
126
- help="Bucket width (default: hour)",
127
- )
144
+ usage.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
145
+ _add_usage_window(usage)
128
146
  usage.set_defaults(func=handle_usage)
129
147
 
130
148
  # The registry is the programs an SDK client registered, as distinct from
@@ -173,13 +191,32 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
173
191
  purge.set_defaults(func=handle_purge)
174
192
 
175
193
  select = sub.add_parser("select", help="Pin an optimization run as this program's chosen version")
176
- select.add_argument(
177
- "id", help="Compiled program hash, as `programs optimized list` prints it in the program_hash column"
178
- )
194
+ select.add_argument("id", help=_COMPILED_HASH_HELP)
179
195
  select.add_argument("--run", dest="run_id", required=True, help="Optimization run ID (op_...) to pin")
180
196
  select.set_defaults(func=handle_select)
181
197
 
182
198
 
199
+ _COMPILED_HASH_HELP = "Compiled program hash, as `programs optimized list` prints it in the program_hash column"
200
+
201
+
202
+ def _add_usage_window(parser: ArgumentParser) -> None:
203
+ parser.add_argument("--hours", type=int, default=24, help="Rolling window in hours (default %(default)s)")
204
+ # Constrained on purpose: an unrecognised period buckets hourly server-side
205
+ # rather than failing, so a typo would answer a different question silently.
206
+ parser.add_argument(
207
+ "--period",
208
+ choices=["minute", "5m", "10m", "30m", "hour", "day"],
209
+ help="Bucket width (default: hour)",
210
+ )
211
+
212
+
213
+ def _usage_params(args: Any) -> dict[str, str]:
214
+ params = {"hours": str(args.hours)}
215
+ if args.period:
216
+ params["period"] = args.period
217
+ return params
218
+
219
+
183
220
  def handle_create(client: api_client.ApiClient, args: Any) -> Any:
184
221
  """Create the program named ``args.name``, and refuse a name that already has one.
185
222
 
@@ -246,7 +283,7 @@ def handle_list(client: api_client.ApiClient, args: Any) -> Any:
246
283
  ]:
247
284
  if getattr(args, attr, default) != default:
248
285
  raise api_client.CliError(f"{flag} does not apply to --trashed, which lists the whole Trash")
249
- return client.get("/api/v1/programs/trash")
286
+ return _trash_listing(client)
250
287
  params = pagination.wire(args, "limit_offset", as_str=True)
251
288
  # Read as text: a server from before the program list, such as self-host release 2026-001, redirects this
252
289
  # path to its dispatcher, and the redirect's body is HTML.
@@ -310,24 +347,125 @@ def handle_optimized_list(client: api_client.ApiClient, args: Any) -> Any:
310
347
  return client.get("/api/v1/programs/v2", params=params)
311
348
 
312
349
 
350
+ # A server from before program names, p_ ids and the Trash API, such as self-host release 2026-001, reads every
351
+ # path under /api/v1/programs/ as a compiled program hash. A GET there answers 404 and a POST answers 405, and its
352
+ # trace search ignores program_id. A current server answers the Trash listing with 200, so that read tells them
353
+ # apart once a command has already failed.
354
+ _OLD_SERVER = "this cmpnd server predates program names, p_ ids and the Trash API, as self-host release 2026-001 does."
355
+
356
+
357
+ def _predates_program_ids(client: api_client.ApiClient) -> bool:
358
+ try:
359
+ client.get("/api/v1/programs/trash")
360
+ except api_client.CliError as exc:
361
+ return exc.status_code == 404
362
+ return False
363
+
364
+
365
+ def _old_server_refusal(remedy: str) -> api_client.CliError:
366
+ return api_client.CliError(f"{_OLD_SERVER} {remedy}")
367
+
368
+
369
+ def _refuse_if_old(client: api_client.ApiClient, exc: api_client.CliError, remedy: str) -> None:
370
+ """Raise the old-server refusal in place of ``exc`` when the server predates program ids."""
371
+ if exc.status_code in (404, 405) and _predates_program_ids(client):
372
+ raise _old_server_refusal(remedy) from exc
373
+
374
+
375
+ def _trash_remedy() -> str:
376
+ return "Manage its Trash in the web app, at /ui/account/trash, or upgrade the server."
377
+
378
+
379
+ def _read_remedy(verb: str) -> str:
380
+ from . import top
381
+
382
+ return (
383
+ f"`{top.hint_programs('optimized', verb, 'HASH')}` reads a compiled program hash there, and "
384
+ f"`{top.hint_programs('list')}` lists the programs it recorded traces for."
385
+ )
386
+
387
+
388
+ def _resolve_for_read(client: api_client.ApiClient, ref: str, verb: str, *, trashed_ok: bool = False) -> str:
389
+ """Return the p_ id of the program ``ref`` names, for a read of its activity.
390
+
391
+ A program in the Trash has no activity to read, so it is refused unless
392
+ ``trashed_ok``. An unknown name may be a compiled program hash, which the
393
+ ``programs optimized`` command of the same verb takes.
394
+ """
395
+ from . import top
396
+
397
+ program_id, trashed = resolve(
398
+ client,
399
+ ref,
400
+ unknown_hint=f"A compiled program hash goes to `{top.hint_programs('optimized', verb, ref)}`.",
401
+ old_server_remedy=_read_remedy(verb),
402
+ )
403
+ if trashed and not trashed_ok:
404
+ raise api_client.CliError(
405
+ f"{ref} is in the Trash, so it has no activity to read. "
406
+ f"Restore it with `{top.hint_programs('restore', ref)}`."
407
+ )
408
+ return program_id
409
+
410
+
411
+ def _read(client: api_client.ApiClient, verb: str, path: str, params: dict[str, str] | None = None) -> Any:
412
+ try:
413
+ return client.get(path) if params is None else client.get(path, params=params)
414
+ except api_client.CliError as exc:
415
+ _refuse_if_old(client, exc, _read_remedy(verb))
416
+ raise
417
+
418
+
313
419
  def handle_show(client: api_client.ApiClient, args: Any) -> Any:
314
- return client.get(f"/api/v1/programs/{args.id}")
420
+ program_id = _resolve_for_read(client, args.program, "show", trashed_ok=True)
421
+ return _read(client, "show", f"/api/v1/programs/{program_id}")
422
+
423
+
424
+ # Trace search reads a page_size above this as its default of 50, which would answer other rows than the ones asked
425
+ # for.
426
+ _MAX_TRACE_PAGE = 500
315
427
 
316
428
 
317
429
  def handle_traces(client: api_client.ApiClient, args: Any) -> Any:
318
- params = pagination.wire(args, "limit_offset", as_str=True)
319
- return client.get(f"/api/v1/programs/{args.id}/traces", params=params)
430
+ """List the program's traces through trace search, which takes a program id, over its widest window."""
431
+ if args.limit > _MAX_TRACE_PAGE:
432
+ raise api_client.CliError(
433
+ f"--limit is at most {_MAX_TRACE_PAGE}, the largest page trace search returns. Page with --offset."
434
+ )
435
+ program_id = _resolve_for_read(client, args.program, "traces")
436
+ body: dict[str, Any] = {"program_id": program_id, **pagination.wire(args, "page_size"), "max_window": True}
437
+ answer = client.post("/api/v1/traces/search", json=body)
438
+ # A server that ignores program_id answers every trace in the org, which must not pass for the program's.
439
+ if "program_id" in (answer.get("ignored_fields") or []):
440
+ raise _old_server_refusal(_read_remedy("traces"))
441
+ return answer
320
442
 
321
443
 
322
444
  def handle_stats(client: api_client.ApiClient, args: Any) -> Any:
323
- return client.get(f"/api/v1/programs/{args.id}/execution-stats")
445
+ program_id = _resolve_for_read(client, args.program, "stats")
446
+ return _read(client, "stats", f"/api/v1/programs/{program_id}/execution-stats")
324
447
 
325
448
 
326
449
  def handle_usage(client: api_client.ApiClient, args: Any) -> Any:
327
- params = {"hours": str(args.hours)}
328
- if args.period:
329
- params["period"] = args.period
330
- return client.get(f"/api/v1/programs/{args.id}/usage-history", params=params)
450
+ program_id = _resolve_for_read(client, args.program, "usage")
451
+ return _read(client, "usage", f"/api/v1/programs/{program_id}/usage-history", _usage_params(args))
452
+
453
+
454
+ def handle_optimized_show(client: api_client.ApiClient, args: Any) -> Any:
455
+ return client.get(f"/api/v1/programs/{args.hash}")
456
+
457
+
458
+ def handle_optimized_traces(client: api_client.ApiClient, args: Any) -> Any:
459
+ params = pagination.wire(args, "limit_offset", as_str=True)
460
+ return client.get(f"/api/v1/programs/{args.hash}/traces", params=params)
461
+
462
+
463
+ def handle_optimized_stats(client: api_client.ApiClient, args: Any) -> Any:
464
+ return client.get(f"/api/v1/programs/{args.hash}/execution-stats")
465
+
466
+
467
+ def handle_optimized_usage(client: api_client.ApiClient, args: Any) -> Any:
468
+ return client.get(f"/api/v1/programs/{args.hash}/usage-history", params=_usage_params(args))
331
469
 
332
470
 
333
471
  def handle_registry_list(client: api_client.ApiClient, args: Any) -> Any:
@@ -351,12 +489,17 @@ def handle_select(client: api_client.ApiClient, args: Any) -> Any:
351
489
  return client.post(f"/api/v1/programs/{args.id}/select", json={"optimization_run_id": args.run_id})
352
490
 
353
491
 
354
- def _resolve(client: api_client.ApiClient, ref: str) -> tuple[str, bool | None]:
492
+ def resolve(
493
+ client: api_client.ApiClient, ref: str, *, unknown_hint: str = "", old_server_remedy: str = ""
494
+ ) -> tuple[str, bool | None]:
355
495
  """Return the program id ``ref`` names, and whether it is in the Trash when known.
356
496
 
357
497
  A ``p_`` id is used as given, because no program name starts with ``p_``, and its
358
498
  Trash state is unknown until the route answers. A name is looked up, which never
359
- makes a program, and the answer says whether it is in the Trash.
499
+ makes a program, and the answer says whether it is in the Trash. ``unknown_hint``
500
+ follows the error for a name no program has. On a server from before program
501
+ names the lookup fails too, and the error is the old-server refusal with
502
+ ``old_server_remedy``.
360
503
  """
361
504
  from ... import programs
362
505
 
@@ -370,7 +513,9 @@ def _resolve(client: api_client.ApiClient, ref: str) -> tuple[str, bool | None]:
370
513
  answer = client.get("/api/v1/programs/by-name/" + urllib.parse.quote(ref, safe=""))
371
514
  except api_client.CliError as exc:
372
515
  if exc.status_code == 404:
373
- raise api_client.CliError(f"no program in your organization is named {ref!r}") from exc
516
+ _refuse_if_old(client, exc, old_server_remedy)
517
+ message = f"no program in your organization is named {ref!r}"
518
+ raise api_client.CliError(f"{message}. {unknown_hint}" if unknown_hint else message) from exc
374
519
  if exc.status_code == 410:
375
520
  raise api_client.CliError(f"{ref!r} belongs to a purged program, which nothing restores") from exc
376
521
  raise
@@ -381,21 +526,21 @@ def handle_trash(client: api_client.ApiClient, args: Any) -> Any:
381
526
  """Move a program to the Trash, which frees its place under the plan's program limit."""
382
527
  from . import top
383
528
 
384
- program_id, trashed = _resolve(client, args.program)
529
+ program_id, trashed = resolve(client, args.program, old_server_remedy=_trash_remedy())
385
530
  if trashed:
386
531
  print(
387
532
  f"{args.program} is already in the Trash. Restore it with `{top.hint_programs('restore', args.program)}`.",
388
533
  file=sys.stderr,
389
534
  )
390
535
  return {"program_id": program_id, "trashed": True}
391
- return client.post(f"/api/v1/programs/{program_id}/trash")
536
+ return _trash_write(client, f"/api/v1/programs/{program_id}/trash")
392
537
 
393
538
 
394
539
  def handle_restore(client: api_client.ApiClient, args: Any) -> Any:
395
540
  """Return a program from the Trash. The server refuses it when the plan's program limit is full."""
396
541
  from . import top
397
542
 
398
- program_id, trashed = _resolve(client, args.program)
543
+ program_id, trashed = resolve(client, args.program, old_server_remedy=_trash_remedy())
399
544
  if trashed is False:
400
545
  # A name another program took from a Trashed one reaches the new, live program. The Trashed one is
401
546
  # restored by its id.
@@ -403,7 +548,7 @@ def handle_restore(client: api_client.ApiClient, args: Any) -> Any:
403
548
  f"{args.program} is not in the Trash. If its name now belongs to another program, restore the one "
404
549
  f"in the Trash by the p_ id that `{top.hint_programs('list', '--trashed')}` prints."
405
550
  )
406
- return client.post(f"/api/v1/programs/{program_id}/restore")
551
+ return _trash_write(client, f"/api/v1/programs/{program_id}/restore")
407
552
 
408
553
 
409
554
  def handle_purge(client: api_client.ApiClient, args: Any) -> Any:
@@ -417,20 +562,38 @@ def handle_purge(client: api_client.ApiClient, args: Any) -> Any:
417
562
  # The count is for the person confirming. --force skips the listing, which needs traces:read, so a key
418
563
  # that may only write can still empty the Trash.
419
564
  if not args.force:
420
- listed = client.get("/api/v1/programs/trash")
565
+ listed = _trash_listing(client)
421
566
  count = len(listed.get("programs") or [])
422
567
  if count == 0:
423
568
  return {"purged": 0, "kept": 0}
424
569
  noun = "program" if count == 1 else "programs"
425
570
  provider_credentials.confirm(args, f"the {count} {noun} in the Trash for good", consequence)
426
- return client.post("/api/v1/programs/trash/empty")
571
+ return _trash_write(client, "/api/v1/programs/trash/empty")
427
572
  if args.program is None:
428
573
  raise api_client.CliError("name the PROGRAM to purge, or pass --all to empty the Trash")
429
- program_id, trashed = _resolve(client, args.program)
574
+ program_id, trashed = resolve(client, args.program, old_server_remedy=_trash_remedy())
430
575
  if trashed is False:
431
576
  raise api_client.CliError(
432
577
  f"{args.program} is live, and only a program in the Trash can be purged. "
433
578
  f"Move it there first with `{top.hint_programs('trash', args.program)}`."
434
579
  )
435
580
  provider_credentials.confirm(args, f"the program {args.program} for good", consequence)
436
- return client.post(f"/api/v1/programs/{program_id}/purge")
581
+ return _trash_write(client, f"/api/v1/programs/{program_id}/purge")
582
+
583
+
584
+ def _trash_listing(client: api_client.ApiClient) -> Any:
585
+ """GET the Trash. On a server from before the Trash API it answers 404, which is the old-server signal."""
586
+ try:
587
+ return client.get("/api/v1/programs/trash")
588
+ except api_client.CliError as exc:
589
+ if exc.status_code == 404:
590
+ raise _old_server_refusal(_trash_remedy()) from exc
591
+ raise
592
+
593
+
594
+ def _trash_write(client: api_client.ApiClient, path: str) -> Any:
595
+ try:
596
+ return client.post(path)
597
+ except api_client.CliError as exc:
598
+ _refuse_if_old(client, exc, _trash_remedy())
599
+ raise
@@ -8,6 +8,7 @@ inputs and expected outputs of one validation example.
8
8
  from __future__ import annotations
9
9
 
10
10
  import json
11
+ import re
11
12
  from argparse import ArgumentParser, _SubParsersAction
12
13
  from typing import Any
13
14
 
@@ -23,7 +24,7 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
23
24
  create.add_argument(
24
25
  "program_hash",
25
26
  metavar="program",
26
- help="Owning program: a program id (p_…), or a legacy signature hash "
27
+ help="Owning program: a program name, a program id (p_…), or a legacy signature hash "
27
28
  "(16-hex, as `cmpnd signatures list` shows)",
28
29
  )
29
30
  create.add_argument(
@@ -33,7 +34,7 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
33
34
  create.set_defaults(func=handle_create)
34
35
 
35
36
  ls = sub.add_parser("list", help="List a program's validation sets")
36
- ls.add_argument("program", help="Program id (p_…), or a legacy signature hash (16-hex)")
37
+ ls.add_argument("program", help="Program name, program id (p_…), or a legacy signature hash (16-hex)")
37
38
  pagination.add_args(ls, with_page_aliases=False, default_limit=50)
38
39
  ls.set_defaults(func=handle_list)
39
40
 
@@ -75,15 +76,39 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
75
76
  edelete.set_defaults(func=handle_entries_delete)
76
77
 
77
78
 
79
+ # A legacy program signature hash in its display form: 16 hex digits.
80
+ _LEGACY_HASH = re.compile(r"[0-9a-fA-F]{16}")
81
+
82
+
83
+ def _program(client: api_client.ApiClient, ref: str) -> str:
84
+ """The value the valsets routes take for ``ref``.
85
+
86
+ A p_ id and a legacy 16-hex hash pass unchanged. Anything else is a program
87
+ name, which resolves to its p_ id without making a program. A program whose
88
+ name is 16 hex digits is reached by its p_ id.
89
+ """
90
+ from . import programs
91
+
92
+ if _LEGACY_HASH.fullmatch(ref):
93
+ return ref
94
+ program_id, _ = programs.resolve(
95
+ client,
96
+ ref,
97
+ old_server_remedy="Name the program by its 16-hex signature hash, as `cmpnd signatures list` shows, "
98
+ "or upgrade the server.",
99
+ )
100
+ return program_id
101
+
102
+
78
103
  def handle_create(client: api_client.ApiClient, args: Any) -> Any:
79
104
  # One generator (cmpnd._valsets.default_name) for both the CLI and the
80
105
  # Client, so the collision-avoiding suffix can't drift between them.
81
106
  name = args.name or _valsets.default_name()
82
- return client.post("/api/v1/valsets", json={"program": args.program_hash, "name": name})
107
+ return client.post("/api/v1/valsets", json={"program": _program(client, args.program_hash), "name": name})
83
108
 
84
109
 
85
110
  def handle_list(client: api_client.ApiClient, args: Any) -> Any:
86
- params = {"program": args.program, **pagination.wire(args, "limit_offset", as_str=True)}
111
+ params = {"program": _program(client, args.program), **pagination.wire(args, "limit_offset", as_str=True)}
87
112
  return client.get("/api/v1/valsets", params=params)
88
113
 
89
114
 
@@ -62,6 +62,32 @@ runs produced, one row per compiled program hash, from
62
62
  `--order`. `cmpnd programs list` refuses those flags with an error that names
63
63
  `cmpnd programs optimized list`.
64
64
 
65
+ `cmpnd programs show`, `traces`, `stats` and `usage` take a program name or a
66
+ `p_` id. The CLI looks a name up with `GET /api/v1/programs/by-name/{name}`,
67
+ which never makes a program. An unknown name fails with an error that names
68
+ the `cmpnd programs optimized` command for a compiled program hash.
69
+
70
+ - `show` prints the program's name and id, whether it is in the Trash, its
71
+ trace count, when its latest trace started, and the optimized programs its
72
+ completed optimization runs produced, newest first. It holds at most 499 of
73
+ them, and `optimized_programs_complete` is false when the program has more.
74
+ - `traces` lists the program's traces, newest first, over the widest window
75
+ trace search reads, 365 days. It sends `POST /api/v1/traces/search` with
76
+ `program_id` and `max_window`. `--limit` is at most 500, the largest page the
77
+ search returns. `cmpnd traces list --hash ID --since DATE` reaches older
78
+ traces.
79
+ - `stats` covers all history, and `usage` covers `--hours`, 24 by default.
80
+ - A program in the Trash has no activity to read, so `traces`, `stats` and
81
+ `usage` refuse it. `show` reports it as Trashed.
82
+ - On a server from before program names, such as self-host release 2026-001,
83
+ each of them fails with an error that names the `cmpnd programs optimized`
84
+ command for a compiled hash. Its trace search ignores `program_id`, so
85
+ `traces` refuses rather than print every trace in the organization.
86
+
87
+ `cmpnd programs optimized show`, `traces`, `stats` and `usage` take a compiled
88
+ program hash, the value in the `program_hash` column of
89
+ `cmpnd programs optimized list`, as `cmpnd programs select` does.
90
+
65
91
  ## The Trash
66
92
 
67
93
  A program in the Trash leaves every view, and its traces with it, until it is
@@ -90,6 +116,13 @@ The purge job purges a program 7 days after it entered the Trash. Listing the
90
116
  Trash needs `traces:read`, and the other commands need `traces:write` or
91
117
  `optimizations:write`.
92
118
 
119
+ A server from before program names, `p_` ids and the Trash API, such as
120
+ self-host release 2026-001, reads every path under `/api/v1/programs/` as a
121
+ compiled program hash. Against one, each Trash command fails with an error that
122
+ says the server predates the Trash API and points at the web app's Trash page,
123
+ `/ui/account/trash`. The CLI recognizes such a server once a command fails, by
124
+ the `404` it answers to `GET /api/v1/programs/trash`.
125
+
93
126
  A plan may cap an organization's live programs, the ones outside the Trash
94
127
  that no optimization produced. At the cap, a new program name in
95
128
  `cmpnd programs create`, `cmpnd deploy` or `cmpnd optimize` and
@@ -354,12 +387,11 @@ The `signatures` and `programs` groups name programs differently.
354
387
  hash, which identifies a program's inputs and outputs and everything recorded
355
388
  under them: traces, examples, evals and revisions. The CLI passes an id
356
389
  through unchanged, and serve tells the two apart by the `p_` prefix.
357
- `cmpnd programs show`, `traces`, `stats`, `usage` and `select` take a compiled
358
- program hash, the value in the `program_hash` column of
359
- `cmpnd programs optimized list`. `cmpnd programs list` prints names and ids, and
360
- the Trash commands take a program name or id ("The Trash" above). Its routes resolve the hash
361
- through optimization runs, so they do not find a signature hash. Each group's
362
- `--help` names the column its IDs come from.
390
+ `cmpnd programs` commands take a program name or id ("Program names" and "The
391
+ Trash" above). `cmpnd programs optimized` commands and `cmpnd programs select`
392
+ take a compiled program hash, which their routes resolve through optimization
393
+ runs, so they do not find a signature hash. Each group's `--help` names the
394
+ column its IDs come from.
363
395
 
364
396
  `cmpnd deployments openapi <id>` prints the OpenAPI document a deployment
365
397
  serves, which an agent reads before it calls the deployment.
@@ -413,16 +445,19 @@ converted key stays in use, `org list` marks its org active, and
413
445
 
414
446
  `cmpnd valsets` manages validation sets that a program owns. A validation set
415
447
  is a named set of validation examples whose fields come from the owning
416
- program's signature. Name the program by its program id (`p_…`) or by the
417
- legacy 16-hex signature hash that `cmpnd signatures list` shows. Serve tells
418
- them apart by the `p_` prefix. Entries live in an `entries` subgroup, the same
448
+ program's signature. Name the program by its program name, its program id
449
+ (`p_…`) or the legacy 16-hex signature hash that `cmpnd signatures list` shows.
450
+ The CLI looks a name up with `GET /api/v1/programs/by-name/{name}`, which never
451
+ makes a program, and sends the id. A value of 16 hex digits is a hash, so a
452
+ program whose name is 16 hex digits is named by its id. On a server from before
453
+ program names, a name fails with an error that asks for the hash. Entries live in an `entries` subgroup, the same
419
454
  shape as `cmpnd datasets examples`.
420
455
 
421
456
  The group is in development. The CLI registers it only when
422
457
  `CMPND_CLI_DEV=1` is set. Serve still checks each action against the
423
458
  `valsets:*` scopes.
424
459
 
425
- - `cmpnd valsets create <program-hash> [--name <name>]` creates a validation
460
+ - `cmpnd valsets create <program> [--name <name>]` creates a validation
426
461
  set for a program. The valset ID is its identity, and the name is a label
427
462
  that need not be unique. Without `--name`, the CLI assigns a date label
428
463
  (`valset-<date>`).
@@ -5,7 +5,7 @@ name = "cmpnd"
5
5
  # it's new) — no git tags. Bump with `uv version --bump {minor,patch}` or by
6
6
  # hand. Runtime `cmpnd.__version__` reads installed metadata, so it tracks
7
7
  # this automatically. See docs/plans/2026-07-02-sdk-pypi-tag-free-publish.md.
8
- version = "0.60.0"
8
+ version = "0.61.0"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.11"