cmpnd 0.19.0__tar.gz → 0.20.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 (305) hide show
  1. {cmpnd-0.19.0 → cmpnd-0.20.0}/PKG-INFO +1 -1
  2. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/__init__.py +11 -0
  3. cmpnd-0.20.0/cmpnd/cli/commands/assessments.py +67 -0
  4. cmpnd-0.20.0/cmpnd/cli/commands/valsets.py +101 -0
  5. {cmpnd-0.19.0 → cmpnd-0.20.0}/docs/cli.md +47 -0
  6. {cmpnd-0.19.0 → cmpnd-0.20.0}/pyproject.toml +1 -1
  7. cmpnd-0.20.0/tests/e2e/test_assessments_cli.py +68 -0
  8. cmpnd-0.20.0/tests/e2e/test_valsets_cli.py +102 -0
  9. cmpnd-0.20.0/tests/test_cli/test_assessments.py +84 -0
  10. cmpnd-0.20.0/tests/test_cli/test_valsets.py +122 -0
  11. {cmpnd-0.19.0 → cmpnd-0.20.0}/uv.lock +1 -1
  12. {cmpnd-0.19.0 → cmpnd-0.20.0}/.gitignore +0 -0
  13. {cmpnd-0.19.0 → cmpnd-0.20.0}/CLAUDE.md +0 -0
  14. {cmpnd-0.19.0 → cmpnd-0.20.0}/README.md +0 -0
  15. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/__init__.py +0 -0
  16. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/_gepa_patch.py +0 -0
  17. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/_http.py +0 -0
  18. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/_program_patch.py +0 -0
  19. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/_rlm_patch.py +0 -0
  20. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/_timespec.py +0 -0
  21. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/callback.py +0 -0
  22. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/api_client.py +0 -0
  23. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/__init__.py +0 -0
  24. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/admin.py +0 -0
  25. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/auth.py +0 -0
  26. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/datasets.py +0 -0
  27. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/deployments.py +0 -0
  28. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/evals.py +0 -0
  29. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/login.py +0 -0
  30. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/optimizations.py +0 -0
  31. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/org.py +0 -0
  32. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/programs.py +0 -0
  33. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  34. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/signatures.py +0 -0
  35. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  36. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/top.py +0 -0
  37. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/traces.py +0 -0
  38. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/credentials.py +0 -0
  39. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/deprecation.py +0 -0
  40. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/pagination.py +0 -0
  41. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/shell.py +0 -0
  42. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/trace_render.py +0 -0
  43. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/verbs.py +0 -0
  44. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/configuration.py +0 -0
  45. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/context.py +0 -0
  46. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/dataset_sync.py +0 -0
  47. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/datasets.py +0 -0
  48. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/decorators.py +0 -0
  49. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/deployment.py +0 -0
  50. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/eval_handler.py +0 -0
  51. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/execution.py +0 -0
  52. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/exporter.py +0 -0
  53. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/exporter_http.py +0 -0
  54. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/helpers.py +0 -0
  55. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/identity.py +0 -0
  56. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/imaging.py +0 -0
  57. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/__init__.py +0 -0
  58. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/decode.py +0 -0
  59. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/encode.py +0 -0
  60. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/hash.py +0 -0
  61. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/program.py +0 -0
  62. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/reflection.py +0 -0
  63. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/types.py +0 -0
  64. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/xxh64.py +0 -0
  65. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/mcp_servers.py +0 -0
  66. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/models.py +0 -0
  67. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/optimization.py +0 -0
  68. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/optimizers/__init__.py +0 -0
  69. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/optimizers/gepa.py +0 -0
  70. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  71. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/optimizers/resume.py +0 -0
  72. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/optimizers/tracker.py +0 -0
  73. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/packaging.py +0 -0
  74. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/retry.py +0 -0
  75. {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/traces.py +0 -0
  76. {cmpnd-0.19.0 → cmpnd-0.20.0}/docs/2026-04-02-code-review.md +0 -0
  77. {cmpnd-0.19.0 → cmpnd-0.20.0}/docs/custom-proposer.md +0 -0
  78. {cmpnd-0.19.0 → cmpnd-0.20.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  79. {cmpnd-0.19.0 → cmpnd-0.20.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  80. {cmpnd-0.19.0 → cmpnd-0.20.0}/docs/reading-traces.md +0 -0
  81. {cmpnd-0.19.0 → cmpnd-0.20.0}/docs/sdk-instrumentation.md +0 -0
  82. {cmpnd-0.19.0 → cmpnd-0.20.0}/examples/generalizing_proposer.py +0 -0
  83. {cmpnd-0.19.0 → cmpnd-0.20.0}/examples/local_ollama.py +0 -0
  84. {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/LINEAGE.md +0 -0
  85. {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/README.md +0 -0
  86. {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  87. {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  88. {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  89. {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  90. {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/cmpnd_ingest/py.typed +0 -0
  91. {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  92. {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/cmpnd_ingest/retry.py +0 -0
  93. {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/pyproject.toml +0 -0
  94. {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/audit-sdist.sh +0 -0
  95. {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/capture_review_run.py +0 -0
  96. {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/check_release_artifacts.py +0 -0
  97. {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/fixtures/review_run.json.gz +0 -0
  98. {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/gepa_resume_bug.py +0 -0
  99. {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/require_version_bump.py +0 -0
  100. {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/seed_review_run.py +0 -0
  101. {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/test-in-linux.sh +0 -0
  102. {cmpnd-0.19.0 → cmpnd-0.20.0}/stubs/dspy/__init__.pyi +0 -0
  103. {cmpnd-0.19.0 → cmpnd-0.20.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  104. {cmpnd-0.19.0 → cmpnd-0.20.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  105. {cmpnd-0.19.0 → cmpnd-0.20.0}/stubs/dspy/utils/__init__.pyi +0 -0
  106. {cmpnd-0.19.0 → cmpnd-0.20.0}/stubs/dspy/utils/callback.pyi +0 -0
  107. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/__init__.py +0 -0
  108. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/_exporter_helpers.py +0 -0
  109. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/_fake_clock.py +0 -0
  110. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/_identity_helpers.py +0 -0
  111. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/conftest.py +0 -0
  112. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/__init__.py +0 -0
  113. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/committee_task.py +0 -0
  114. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/conftest.py +0 -0
  115. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  116. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  117. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/fake_lm.py +0 -0
  118. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/helpers.py +0 -0
  119. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/target_capabilities.py +0 -0
  120. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_capture_prompts.py +0 -0
  121. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_composite_module.py +0 -0
  122. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  123. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_deploy_execute.py +0 -0
  124. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_deploy_programs.py +0 -0
  125. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_edge_cases.py +0 -0
  126. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_example_hash.py +0 -0
  127. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_gepa_optimization.py +0 -0
  128. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_interpreter_spans.py +0 -0
  129. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_lm_accuracy.py +0 -0
  130. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_module_tracing.py +0 -0
  131. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_optimization_lineage.py +0 -0
  132. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  133. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_optimize_failures.py +0 -0
  134. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_optimize_loop.py +0 -0
  135. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  136. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  137. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_parallel_export.py +0 -0
  138. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_project_attribution.py +0 -0
  139. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_react_tool_spans.py +0 -0
  140. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_save_load.py +0 -0
  141. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_trace_structure.py +0 -0
  142. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_traces_read.py +0 -0
  143. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_user_attribution.py +0 -0
  144. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  145. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  146. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  147. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  148. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  149. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  150. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  151. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  152. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  153. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/10_react.json +0 -0
  154. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/11_refine.json +0 -0
  155. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/12_retrieve.json +0 -0
  156. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/13_knn.json +0 -0
  157. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/14_parallel.json +0 -0
  158. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/README.md +0 -0
  159. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/ingest_conformance.py +0 -0
  160. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/__init__.py +0 -0
  161. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/conftest.py +0 -0
  162. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/hosted_optimize_probe.py +0 -0
  163. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/optimization_run_probe.py +0 -0
  164. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  165. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/test_logs_renders_traces.py +0 -0
  166. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/test_optimization_run.py +0 -0
  167. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/test_optimizing_progress.py +0 -0
  168. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/test_unhappy_path.py +0 -0
  169. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/__init__.py +0 -0
  170. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/__main__.py +0 -0
  171. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/drain.py +0 -0
  172. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/ingest.py +0 -0
  173. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/latency_transport.py +0 -0
  174. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/rlm_shape.py +0 -0
  175. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/sweep.py +0 -0
  176. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/test_rlm_shape.py +0 -0
  177. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/__init__.py +0 -0
  178. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/conftest.py +0 -0
  179. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/seed_parity.py +0 -0
  180. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_auth.py +0 -0
  181. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_datasets.py +0 -0
  182. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_deployments.py +0 -0
  183. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_evals.py +0 -0
  184. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_health.py +0 -0
  185. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_modules.py +0 -0
  186. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_optimizations.py +0 -0
  187. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_programs.py +0 -0
  188. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_signatures.py +0 -0
  189. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_trace_streaming.py +0 -0
  190. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_traces.py +0 -0
  191. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/__init__.py +0 -0
  192. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/_provider.py +0 -0
  193. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/_reliability.py +0 -0
  194. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/_result.py +0 -0
  195. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/mint_bedrock_token.py +0 -0
  196. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_deploy_programs.py +0 -0
  197. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_multi_client_stress.py +0 -0
  198. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_optimize_client_committee.py +0 -0
  199. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_optimize_loop.py +0 -0
  200. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  201. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_optimize_stress.py +0 -0
  202. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_provider_roles.py +0 -0
  203. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_real_provider_twins.py +0 -0
  204. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_reliability.py +0 -0
  205. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_reliability_classify.py +0 -0
  206. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_rlm_wasm.py +0 -0
  207. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_trace_search.py +0 -0
  208. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_callback.py +0 -0
  209. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_candidate_lineage.py +0 -0
  210. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/__init__.py +0 -0
  211. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/conftest.py +0 -0
  212. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_admin.py +0 -0
  213. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_api_client.py +0 -0
  214. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_auth.py +0 -0
  215. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_credentials.py +0 -0
  216. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_datasets.py +0 -0
  217. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_deployments.py +0 -0
  218. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_evals.py +0 -0
  219. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_login.py +0 -0
  220. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_main.py +0 -0
  221. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_optimizations.py +0 -0
  222. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_org.py +0 -0
  223. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_pagination.py +0 -0
  224. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_parity.py +0 -0
  225. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_programs.py +0 -0
  226. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_provider_credentials.py +0 -0
  227. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_schema_sample.py +0 -0
  228. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_shell.py +0 -0
  229. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_signatures.py +0 -0
  230. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_tool_servers.py +0 -0
  231. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_top.py +0 -0
  232. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_traces.py +0 -0
  233. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  234. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_clustering/__init__.py +0 -0
  235. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_clustering/conftest.py +0 -0
  236. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_clustering/test_edge_cases.py +0 -0
  237. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  238. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_clustering/test_type_conversion.py +0 -0
  239. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cmp551_grouping_identity.py +0 -0
  240. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_configuration.py +0 -0
  241. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_context.py +0 -0
  242. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_context_propagation.py +0 -0
  243. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_dataset_polling.py +0 -0
  244. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_dataset_sync.py +0 -0
  245. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_datasets.py +0 -0
  246. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_decorators.py +0 -0
  247. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_deploy.py +0 -0
  248. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  249. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_eval.py +0 -0
  250. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_eval_start.py +0 -0
  251. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_eval_trace_grouping.py +0 -0
  252. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_exception_paths.py +0 -0
  253. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_execute.py +0 -0
  254. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_exporter.py +0 -0
  255. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_exporter_queue.py +0 -0
  256. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_exporter_real.py +0 -0
  257. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_fake_lm.py +0 -0
  258. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_gepa_callback.py +0 -0
  259. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_gepa_integration.py +0 -0
  260. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_gepa_resume.py +0 -0
  261. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_identity.py +0 -0
  262. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_imaging.py +0 -0
  263. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_import_weight.py +0 -0
  264. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_ingest_conformance.py +0 -0
  265. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_ingest_second_producer.py +0 -0
  266. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_integration.py +0 -0
  267. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_interpreter_callback.py +0 -0
  268. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_ir_encoder_properties.py +0 -0
  269. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_ir_golden_vectors.py +0 -0
  270. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_ir_roundtrip.py +0 -0
  271. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_mcp_tools.py +0 -0
  272. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_models.py +0 -0
  273. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimization_progress.py +0 -0
  274. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimization_start.py +0 -0
  275. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimize.py +0 -0
  276. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimize_budget.py +0 -0
  277. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimize_lift.py +0 -0
  278. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimize_lms.py +0 -0
  279. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimize_proposer.py +0 -0
  280. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimize_reflection_lm.py +0 -0
  281. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimize_terminal_failure.py +0 -0
  282. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_packaging.py +0 -0
  283. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_packaging_callable_source.py +0 -0
  284. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_packaging_mcp_tools.py +0 -0
  285. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_packaging_program_source.py +0 -0
  286. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_poll_until.py +0 -0
  287. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_program_lineage_persist.py +0 -0
  288. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_project_payload.py +0 -0
  289. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_reflector_properties.py +0 -0
  290. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_reflector_smoke.py +0 -0
  291. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_repro_reactv2_bugs.py +0 -0
  292. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_require_version_bump.py +0 -0
  293. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_retry.py +0 -0
  294. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_reversible_failures.py +0 -0
  295. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_rlm_patch.py +0 -0
  296. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_schemas.py +0 -0
  297. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_seed_review_run.py +0 -0
  298. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_stats_export.py +0 -0
  299. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_trace_render.py +0 -0
  300. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_traces_read.py +0 -0
  301. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_tracked_gepa.py +0 -0
  302. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_tracker_enrich.py +0 -0
  303. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_truncation.py +0 -0
  304. {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_xxh64.py +0 -0
  305. {cmpnd-0.19.0 → cmpnd-0.20.0}/todo.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.19.0
3
+ Version: 0.20.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,6 +13,7 @@ import httpx
13
13
  from . import api_client, deprecation
14
14
  from .commands import (
15
15
  admin,
16
+ assessments,
16
17
  auth,
17
18
  datasets,
18
19
  deployments,
@@ -26,6 +27,7 @@ from .commands import (
26
27
  tool_servers,
27
28
  top,
28
29
  traces,
30
+ valsets,
29
31
  )
30
32
 
31
33
 
@@ -141,6 +143,15 @@ def main() -> None:
141
143
  datasets.register(subparsers)
142
144
  deployments.register(subparsers)
143
145
 
146
+ # Assessments and valsets CLIs are still in development, hidden behind
147
+ # CMPND_CLI_DEV=1 (mirroring the CMPND_CLI_ADMIN gate above). The env var
148
+ # only hides the commands from customers; the server still gates each action
149
+ # on the assessments:* / valsets:* scope, so this is discoverability, not
150
+ # enforcement.
151
+ if os.environ.get("CMPND_CLI_DEV") == "1":
152
+ assessments.register(subparsers)
153
+ valsets.register(subparsers)
154
+
144
155
  # Top-level Docker-style shortcuts (human-readable by default; --json opts in)
145
156
  top.register(subparsers)
146
157
 
@@ -0,0 +1,67 @@
1
+ """Assessments commands.
2
+
3
+ An assessment is a person's judgement of a trace: an up/down/neutral
4
+ conclusion, a prose body, and per-span annotations. See the assessments API.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from argparse import ArgumentParser, _SubParsersAction
10
+ from typing import Any
11
+
12
+ from .. import api_client
13
+
14
+
15
+ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
16
+ parser = subparsers.add_parser("assessments", help="Record and read judgements of traces")
17
+ sub = parser.add_subparsers(dest="action")
18
+
19
+ create = sub.add_parser("create", help="Record a judgement of a trace")
20
+ create.add_argument("trace_id", help="The trace to assess (its SDK trace id)")
21
+ conclusion = create.add_mutually_exclusive_group(required=True)
22
+ conclusion.add_argument(
23
+ "--up", dest="conclusion", action="store_const", const="up", help="Affirm the trace's output"
24
+ )
25
+ conclusion.add_argument(
26
+ "--down", dest="conclusion", action="store_const", const="down", help="Dispute the trace's output"
27
+ )
28
+ conclusion.add_argument(
29
+ "--neutral", dest="conclusion", action="store_const", const="neutral", help="Neither affirm nor dispute"
30
+ )
31
+ create.add_argument("-m", "--message", dest="body", help="The prose body of the judgement")
32
+ create.add_argument(
33
+ "--annotate",
34
+ dest="annotations",
35
+ action="append",
36
+ nargs=2,
37
+ metavar=("SPAN_ID", "NOTE"),
38
+ help="Attach a note to one span in the trace (repeat for multiple)",
39
+ )
40
+ create.set_defaults(func=handle_create)
41
+
42
+ ls = sub.add_parser("list", help="List a program's assessments")
43
+ ls.add_argument(
44
+ "--program", required=True, help="Program signature hash (16-hex, as `cmpnd signatures list` shows)"
45
+ )
46
+ ls.set_defaults(func=handle_list)
47
+
48
+ show = sub.add_parser("show", help="Show one assessment with its annotations")
49
+ show.add_argument("id", help="Assessment id")
50
+ show.set_defaults(func=handle_show)
51
+
52
+
53
+ def handle_create(client: api_client.ApiClient, args: Any) -> Any:
54
+ body: dict[str, Any] = {"trace_id": args.trace_id, "conclusion": args.conclusion}
55
+ if args.body:
56
+ body["body"] = args.body
57
+ if args.annotations:
58
+ body["annotations"] = [{"span_id": span, "note": note} for span, note in args.annotations]
59
+ return client.post("/api/v1/assessments", json=body)
60
+
61
+
62
+ def handle_list(client: api_client.ApiClient, args: Any) -> Any:
63
+ return client.get("/api/v1/assessments", params={"program": args.program})
64
+
65
+
66
+ def handle_show(client: api_client.ApiClient, args: Any) -> Any:
67
+ return client.get(f"/api/v1/assessments/{args.id}")
@@ -0,0 +1,101 @@
1
+ """Valsets commands.
2
+
3
+ A valset is a program-owned validation set: a name, the field shape derived
4
+ from the owning program's signature, and its entries. Entries carry the
5
+ inputs and expected outputs of one validation example.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from argparse import ArgumentParser, _SubParsersAction
12
+ from typing import Any
13
+
14
+ from .. import api_client
15
+
16
+
17
+ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
18
+ parser = subparsers.add_parser("valsets", help="Manage program-owned validation sets")
19
+ sub = parser.add_subparsers(dest="action")
20
+
21
+ create = sub.add_parser("create", help="Create a validation set owned by a program")
22
+ create.add_argument("program_hash", help="Owning program signature hash (16-hex, as `cmpnd signatures list` shows)")
23
+ create.add_argument("--name", required=True, help="Validation-set name (unique within the program)")
24
+ create.set_defaults(func=handle_create)
25
+
26
+ ls = sub.add_parser("list", help="List a program's validation sets")
27
+ ls.add_argument("--program", required=True, help="Program signature hash (16-hex)")
28
+ ls.set_defaults(func=handle_list)
29
+
30
+ show = sub.add_parser("show", help="Show a validation set")
31
+ show.add_argument("id", help="Valset id")
32
+ show.set_defaults(func=handle_show)
33
+
34
+ entries = sub.add_parser("entries", help="Manage a validation set's entries")
35
+ esub = entries.add_subparsers(dest="entries_action")
36
+
37
+ elist = esub.add_parser("list", help="List a valset's active entries")
38
+ elist.add_argument("valset_id", help="Valset id")
39
+ elist.set_defaults(func=handle_entries_list)
40
+
41
+ ecreate = esub.add_parser("create", help="Add an entry from JSON content")
42
+ ecreate.add_argument("valset_id", help="Valset id")
43
+ ecreate.add_argument(
44
+ "--inputs", required=True, metavar="JSON", help="Inputs object; its keys are the valset's input fields"
45
+ )
46
+ ecreate.add_argument(
47
+ "--expected-outputs",
48
+ dest="expected_outputs",
49
+ required=True,
50
+ metavar="JSON",
51
+ help="Expected-outputs object; its keys are the valset's output fields",
52
+ )
53
+ ecreate.add_argument(
54
+ "--from-assessment", dest="from_assessment", help="Source assessment id, recorded as the entry's provenance"
55
+ )
56
+ ecreate.set_defaults(func=handle_entries_create)
57
+
58
+ edelete = esub.add_parser("delete", help="Retire an entry (versioned, not a hard delete)")
59
+ edelete.add_argument("entry_id", help="Entry id")
60
+ edelete.set_defaults(func=handle_entries_delete)
61
+
62
+
63
+ def handle_create(client: api_client.ApiClient, args: Any) -> Any:
64
+ return client.post("/api/v1/valsets", json={"program": args.program_hash, "name": args.name})
65
+
66
+
67
+ def handle_list(client: api_client.ApiClient, args: Any) -> Any:
68
+ return client.get("/api/v1/valsets", params={"program": args.program})
69
+
70
+
71
+ def handle_show(client: api_client.ApiClient, args: Any) -> Any:
72
+ return client.get(f"/api/v1/valsets/{args.id}")
73
+
74
+
75
+ def handle_entries_list(client: api_client.ApiClient, args: Any) -> Any:
76
+ return client.get(f"/api/v1/valsets/{args.valset_id}/entries")
77
+
78
+
79
+ def handle_entries_create(client: api_client.ApiClient, args: Any) -> Any:
80
+ inputs = _parse_json_flag("--inputs", args.inputs)
81
+ expected = _parse_json_flag("--expected-outputs", args.expected_outputs)
82
+ body: dict[str, Any] = {"inputs": inputs, "expected_outputs": expected}
83
+ if args.from_assessment:
84
+ body["from_assessment"] = args.from_assessment
85
+ return client.post(f"/api/v1/valsets/{args.valset_id}/entries", json=body)
86
+
87
+
88
+ def handle_entries_delete(client: api_client.ApiClient, args: Any) -> Any:
89
+ client.delete(f"/api/v1/valsets/entries/{args.entry_id}")
90
+ # The route returns 204; echo a confirmation so the CLI does not print null.
91
+ return {"deleted": args.entry_id}
92
+
93
+
94
+ def _parse_json_flag(flag: str, value: str) -> dict[str, Any]:
95
+ try:
96
+ parsed = json.loads(value)
97
+ except json.JSONDecodeError as exc:
98
+ raise api_client.CliError(f"{flag} JSON is invalid: {exc}") from exc
99
+ if not isinstance(parsed, dict):
100
+ raise api_client.CliError(f"{flag} JSON must be an object")
101
+ return parsed
@@ -79,6 +79,27 @@ Every list command paginates with `--limit`/`--offset`. The older
79
79
  warn. On the page-number commands `--offset` must be a whole multiple of
80
80
  `--limit`; the error names the nearest valid offsets.
81
81
 
82
+ ## Assessments
83
+
84
+ `cmpnd assessments` records and reads a person's judgement of a trace: an
85
+ up/down/neutral conclusion, an optional prose body, and per-span annotations.
86
+
87
+ > **In development — behind `CMPND_CLI_DEV=1`.** The group is registered only
88
+ > when that env var is set (mirroring `CMPND_CLI_ADMIN`), so it stays hidden from
89
+ > `cmpnd --help` until the flow is finished. The env var only hides the commands;
90
+ > the server still gates each action on the `assessments:*` scope.
91
+
92
+ - `cmpnd assessments create <trace-id> --up|--down|--neutral [-m "prose"] [--annotate <span-id> "note" …]`
93
+ — record one judgement. Exactly one conclusion flag is required (`--up`
94
+ affirms the trace's output, `--down` disputes it, `--neutral` does neither);
95
+ they are mutually exclusive. `-m`/`--message` sets the prose body. `--annotate`
96
+ takes a span id and a note, and repeats to attach a note to more than one span
97
+ in the trace.
98
+ - `cmpnd assessments list --program <16-hex-hash>` — the assessments recorded
99
+ against a program, addressed by its program signature hash in the 16-char hex
100
+ form `cmpnd signatures list` prints.
101
+ - `cmpnd assessments show <assessment-id>` — one assessment with its annotations.
102
+
82
103
  ## Other query groups
83
104
 
84
105
  `cmpnd programs`, `cmpnd signatures`, `cmpnd evals`, `cmpnd datasets`,
@@ -99,6 +120,32 @@ found. Each group's `--help` names the column its ids come from.
99
120
  `cmpnd deployments openapi <id>` prints the OpenAPI document a deployment
100
121
  serves, which is what an agent needs before it can call one.
101
122
 
123
+ ## Validation sets
124
+
125
+ `cmpnd valsets` manages a program-owned validation set: a named set of
126
+ validation examples whose field shape comes from the owning program's signature.
127
+ The program hash is the 16-hex signature hash throughout (what `cmpnd signatures
128
+ list` shows). Entries live in a nested `entries` subgroup, the same shape as
129
+ `cmpnd datasets examples`.
130
+
131
+ > **In development — behind `CMPND_CLI_DEV=1`.** Registered only when that env
132
+ > var is set (mirroring `CMPND_CLI_ADMIN`), so it stays hidden from
133
+ > `cmpnd --help` until the flow is finished. The env var only hides the commands;
134
+ > the server still gates each action on the `valsets:*` scope.
135
+
136
+ - `cmpnd valsets create <program-hash> --name <name>` — create a validation set
137
+ owned by a program; the name is unique within that program.
138
+ - `cmpnd valsets list --program <16-hex-hash>` — list a program's validation sets.
139
+ - `cmpnd valsets show <valset-id>` — show one validation set.
140
+ - `cmpnd valsets entries list <valset-id>` — list a valset's active entries.
141
+ - `cmpnd valsets entries create <valset-id> --inputs '{…}' --expected-outputs
142
+ '{…}' [--from-assessment <id>]` — add an entry. `--inputs` and
143
+ `--expected-outputs` are JSON objects keyed by the valset's declared input and
144
+ output fields. `--from-assessment` records a source assessment id as the
145
+ entry's provenance.
146
+ - `cmpnd valsets entries delete <entry-id>` — retire an entry (versioned, not a
147
+ hard delete).
148
+
102
149
  ## Credentials
103
150
 
104
151
  `cmpnd credentials` covers two kinds of key, because both answer the same
@@ -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.19.0"
8
+ version = "0.20.0"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -0,0 +1,68 @@
1
+ """End-to-end tests for the `cmpnd assessments` CLI against a live server.
2
+
3
+ The CLI is an API consumer, so these drive the real command handlers through a
4
+ real ApiClient against a live backend — the level that catches a contract
5
+ mismatch a mocked client cannot (e.g. the program-hash wire format). Skipped
6
+ unless CMPND_E2E_API_KEY is set (see conftest).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+
13
+ import dspy
14
+ import pytest
15
+
16
+ from cmpnd.cli import api_client
17
+ from cmpnd.cli.commands import assessments as assessments_cmd
18
+ from tests.e2e.conftest import CMPND_E2E_API_KEY, E2E_ENDPOINT, flush_and_wait
19
+
20
+ pytestmark = pytest.mark.e2e
21
+
22
+
23
+ def _ns(**kw: object) -> argparse.Namespace:
24
+ return argparse.Namespace(**kw)
25
+
26
+
27
+ class TestAssessmentsCliE2E:
28
+ def test_create_list_show_against_live_api(self, server_client, trace_capture):
29
+ # Produce a real trace to assess, then read back its program hash + a span.
30
+ dspy.Predict("question -> answer")(question="Who represents District 4?")
31
+ flush_and_wait()
32
+ trace_id = trace_capture.last_trace_id
33
+ assert trace_id is not None, "no trace captured"
34
+ data = server_client.get_trace(trace_id)
35
+ # The trace API returns the hash as a signed int64 (decimal); the
36
+ # assessments and modules APIs speak the 16-hex display form. Normalize
37
+ # so the create/list calls and the assertion compare like for like.
38
+ program_hash = format(int(data["trace"]["program_signature_hash"]) & 0xFFFFFFFFFFFFFFFF, "016x")
39
+ assert data["spans"], "trace has no spans to annotate"
40
+ span_id = data["spans"][0]["span_id"]
41
+
42
+ client = api_client.ApiClient(CMPND_E2E_API_KEY, E2E_ENDPOINT)
43
+
44
+ # create — a down judgement with one span annotation.
45
+ created = assessments_cmd.handle_create(
46
+ client,
47
+ _ns(
48
+ trace_id=trace_id,
49
+ conclusion="down",
50
+ body="the answer names the office, not the representative",
51
+ annotations=[[span_id, "this predict returned the wrong field"]],
52
+ ),
53
+ )
54
+ assert created["id"].startswith("as_")
55
+ assert created["conclusion"] == "down"
56
+ # The program hash the server copied off the trace round-trips as the same
57
+ # 16-hex string the trace read returned — the CLI passes it through verbatim.
58
+ assert created["program_signature_hash"] == program_hash
59
+ assert created["annotations"][0]["span_id"] == span_id
60
+
61
+ # list by program (the 16-hex form) finds the assessment we just created.
62
+ listed = assessments_cmd.handle_list(client, _ns(program=program_hash))
63
+ assert created["id"] in [a["id"] for a in listed["assessments"]]
64
+
65
+ # show round-trips the body and the annotation note.
66
+ shown = assessments_cmd.handle_show(client, _ns(id=created["id"]))
67
+ assert shown["id"] == created["id"]
68
+ assert shown["annotations"][0]["note"] == "this predict returned the wrong field"
@@ -0,0 +1,102 @@
1
+ """End-to-end tests for the `cmpnd valsets` CLI against a live server.
2
+
3
+ Drives the real command handlers through a real ApiClient against a live
4
+ backend — the level that catches a CLI↔API contract mismatch a mocked client
5
+ cannot. Skipped unless CMPND_E2E_API_KEY is set (see conftest).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import uuid
12
+
13
+ import dspy
14
+ import pytest
15
+
16
+ from cmpnd.cli import api_client
17
+ from cmpnd.cli.commands import assessments as assessments_cmd
18
+ from cmpnd.cli.commands import valsets as valsets_cmd
19
+ from tests.e2e.conftest import CMPND_E2E_API_KEY, E2E_ENDPOINT, flush_and_wait
20
+
21
+ pytestmark = pytest.mark.e2e
22
+
23
+
24
+ def _ns(**kw: object) -> argparse.Namespace:
25
+ return argparse.Namespace(**kw)
26
+
27
+
28
+ def _program_and_trace(server_client, trace_capture) -> tuple[str, str, str]:
29
+ """Run a program, return (program_hash, trace_id, first span id)."""
30
+ dspy.Predict("question -> answer")(question="Who represents District 4?")
31
+ flush_and_wait()
32
+ trace_id = trace_capture.last_trace_id
33
+ assert trace_id is not None, "no trace captured"
34
+ data = server_client.get_trace(trace_id)
35
+ # The trace API returns the hash as a signed int64 (decimal); the valsets
36
+ # and modules APIs speak the 16-hex display form, so normalize before the
37
+ # create/list calls that carry it.
38
+ program_hash = format(int(data["trace"]["program_signature_hash"]) & 0xFFFFFFFFFFFFFFFF, "016x")
39
+ return program_hash, trace_id, data["spans"][0]["span_id"]
40
+
41
+
42
+ class TestValsetsCliE2E:
43
+ def test_valset_and_entry_lifecycle(self, server_client, trace_capture):
44
+ program_hash, _trace_id, _span = _program_and_trace(server_client, trace_capture)
45
+ client = api_client.ApiClient(CMPND_E2E_API_KEY, E2E_ENDPOINT)
46
+
47
+ # create derives the field shape from the owning program.
48
+ vs = valsets_cmd.handle_create(client, _ns(program_hash=program_hash, name=f"e2e gold {uuid.uuid4().hex[:8]}"))
49
+ assert vs["id"].startswith("vs_")
50
+ assert vs["program_signature_hash"] == program_hash
51
+ assert vs["input_fields"] == ["question"]
52
+
53
+ # list by program finds it; show returns the header.
54
+ listed = valsets_cmd.handle_list(client, _ns(program=program_hash))
55
+ assert vs["id"] in [v["id"] for v in listed["valsets"]]
56
+ assert valsets_cmd.handle_show(client, _ns(id=vs["id"]))["id"] == vs["id"]
57
+
58
+ # entries create — a manual entry matching the field shape.
59
+ entry = valsets_cmd.handle_entries_create(
60
+ client,
61
+ _ns(
62
+ valset_id=vs["id"],
63
+ inputs='{"question": "Who represents District 4?"}',
64
+ expected_outputs='{"answer": "Rep. Alice Nguyen"}',
65
+ from_assessment=None,
66
+ ),
67
+ )
68
+ assert entry["id"].startswith("ve_")
69
+ assert entry["origin"] == "manual"
70
+
71
+ # entries list shows it; entries delete retires it (versioned).
72
+ assert entry["id"] in [
73
+ e["id"] for e in valsets_cmd.handle_entries_list(client, _ns(valset_id=vs["id"]))["entries"]
74
+ ]
75
+ assert valsets_cmd.handle_entries_delete(client, _ns(entry_id=entry["id"])) == {"deleted": entry["id"]}
76
+ assert entry["id"] not in [
77
+ e["id"] for e in valsets_cmd.handle_entries_list(client, _ns(valset_id=vs["id"]))["entries"]
78
+ ]
79
+
80
+ def test_entry_from_assessment(self, server_client, trace_capture):
81
+ program_hash, trace_id, span_id = _program_and_trace(server_client, trace_capture)
82
+ client = api_client.ApiClient(CMPND_E2E_API_KEY, E2E_ENDPOINT)
83
+
84
+ # An assessment of the trace, then an entry created from it.
85
+ assessment = assessments_cmd.handle_create(
86
+ client,
87
+ _ns(trace_id=trace_id, conclusion="down", body="corrected", annotations=[[span_id, "wrong field"]]),
88
+ )
89
+ vs = valsets_cmd.handle_create(
90
+ client, _ns(program_hash=program_hash, name=f"e2e from-assessment {uuid.uuid4().hex[:8]}")
91
+ )
92
+ entry = valsets_cmd.handle_entries_create(
93
+ client,
94
+ _ns(
95
+ valset_id=vs["id"],
96
+ inputs='{"question": "Who represents District 4?"}',
97
+ expected_outputs='{"answer": "Rep. Alice Nguyen"}',
98
+ from_assessment=assessment["id"],
99
+ ),
100
+ )
101
+ assert entry["origin"] == "assessment"
102
+ assert entry["source_assessment_id"] == assessment["id"]
@@ -0,0 +1,84 @@
1
+ """Tests for cmpnd.cli.commands.assessments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from cmpnd.cli.commands import assessments as assessments_cmd
8
+ from cmpnd.cli.commands.assessments import handle_create, handle_list, handle_show
9
+
10
+
11
+ class TestAssessmentsCreate:
12
+ def test_minimal_up_judgement(self, mock_client):
13
+ mock_client.post.return_value = {"id": "as_1"}
14
+ args = argparse.Namespace(trace_id="t-1", conclusion="up", body=None, annotations=None)
15
+ handle_create(mock_client, args)
16
+ mock_client.post.assert_called_once_with("/api/v1/assessments", json={"trace_id": "t-1", "conclusion": "up"})
17
+
18
+ def test_body_and_repeatable_annotations_assemble_the_array(self, mock_client):
19
+ mock_client.post.return_value = {"id": "as_1"}
20
+ args = argparse.Namespace(
21
+ trace_id="t-1",
22
+ conclusion="down",
23
+ body="the office is wrong",
24
+ annotations=[["span-a", "dropped the district"], ["span-b", "mislabeled it"]],
25
+ )
26
+ handle_create(mock_client, args)
27
+ mock_client.post.assert_called_once_with(
28
+ "/api/v1/assessments",
29
+ json={
30
+ "trace_id": "t-1",
31
+ "conclusion": "down",
32
+ "body": "the office is wrong",
33
+ "annotations": [
34
+ {"span_id": "span-a", "note": "dropped the district"},
35
+ {"span_id": "span-b", "note": "mislabeled it"},
36
+ ],
37
+ },
38
+ )
39
+
40
+
41
+ class TestAssessmentsList:
42
+ def test_passes_program_through(self, mock_client):
43
+ mock_client.get.return_value = {"assessments": [], "total": 0}
44
+ args = argparse.Namespace(program="08729442d1b3a3bf")
45
+ handle_list(mock_client, args)
46
+ mock_client.get.assert_called_once_with("/api/v1/assessments", params={"program": "08729442d1b3a3bf"})
47
+
48
+
49
+ class TestAssessmentsShow:
50
+ def test_show_by_id(self, mock_client):
51
+ mock_client.get.return_value = {"id": "as_9"}
52
+ args = argparse.Namespace(id="as_9")
53
+ handle_show(mock_client, args)
54
+ mock_client.get.assert_called_once_with("/api/v1/assessments/as_9")
55
+
56
+
57
+ class TestAssessmentsRegistration:
58
+ def test_subcommands_register(self):
59
+ import argparse as ap
60
+
61
+ parser = ap.ArgumentParser()
62
+ sub = parser.add_subparsers(dest="resource")
63
+ assessments_cmd.register(sub)
64
+ group = sub.choices["assessments"]
65
+ actions = group._subparsers._group_actions[0].choices # type: ignore[union-attr]
66
+ assert set(actions) == {"create", "list", "show"}
67
+
68
+ def test_conclusion_is_required(self):
69
+ import argparse as ap
70
+
71
+ parser = ap.ArgumentParser()
72
+ sub = parser.add_subparsers(dest="resource")
73
+ assessments_cmd.register(sub)
74
+ # A create with no conclusion flag is rejected by the mutually-exclusive group.
75
+ import contextlib
76
+ import io
77
+
78
+ with contextlib.redirect_stderr(io.StringIO()):
79
+ try:
80
+ parser.parse_args(["assessments", "create", "t-1"])
81
+ except SystemExit as exc:
82
+ assert exc.code == 2
83
+ else:
84
+ raise AssertionError("expected create without a conclusion to exit 2")
@@ -0,0 +1,122 @@
1
+ """Tests for cmpnd.cli.commands.valsets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ import pytest
8
+
9
+ from cmpnd.cli.api_client import CliError
10
+ from cmpnd.cli.commands import valsets as valsets_cmd
11
+ from cmpnd.cli.commands.valsets import (
12
+ handle_create,
13
+ handle_entries_create,
14
+ handle_entries_delete,
15
+ handle_entries_list,
16
+ handle_list,
17
+ handle_show,
18
+ )
19
+
20
+
21
+ class TestValsetsCreate:
22
+ def test_sends_program_and_name(self, mock_client):
23
+ mock_client.post.return_value = {"id": "vs_1"}
24
+ args = argparse.Namespace(program_hash="08729442d1b3a3bf", name="gold")
25
+ handle_create(mock_client, args)
26
+ mock_client.post.assert_called_once_with(
27
+ "/api/v1/valsets", json={"program": "08729442d1b3a3bf", "name": "gold"}
28
+ )
29
+
30
+
31
+ class TestValsetsList:
32
+ def test_passes_program_through(self, mock_client):
33
+ mock_client.get.return_value = {"valsets": [], "total": 0}
34
+ handle_list(mock_client, argparse.Namespace(program="08729442d1b3a3bf"))
35
+ mock_client.get.assert_called_once_with("/api/v1/valsets", params={"program": "08729442d1b3a3bf"})
36
+
37
+
38
+ class TestValsetsShow:
39
+ def test_show_by_id(self, mock_client):
40
+ mock_client.get.return_value = {"id": "vs_9"}
41
+ handle_show(mock_client, argparse.Namespace(id="vs_9"))
42
+ mock_client.get.assert_called_once_with("/api/v1/valsets/vs_9")
43
+
44
+
45
+ class TestValsetsEntries:
46
+ def test_entries_list(self, mock_client):
47
+ mock_client.get.return_value = {"entries": [], "total": 0}
48
+ handle_entries_list(mock_client, argparse.Namespace(valset_id="vs_1"))
49
+ mock_client.get.assert_called_once_with("/api/v1/valsets/vs_1/entries")
50
+
51
+ def test_entries_create_parses_json_and_omits_absent_source(self, mock_client):
52
+ mock_client.post.return_value = {"id": "ve_1"}
53
+ args = argparse.Namespace(
54
+ valset_id="vs_1",
55
+ inputs='{"question": "who?"}',
56
+ expected_outputs='{"answer": "them"}',
57
+ from_assessment=None,
58
+ )
59
+ handle_entries_create(mock_client, args)
60
+ mock_client.post.assert_called_once_with(
61
+ "/api/v1/valsets/vs_1/entries",
62
+ json={"inputs": {"question": "who?"}, "expected_outputs": {"answer": "them"}},
63
+ )
64
+
65
+ def test_entries_create_includes_source_assessment(self, mock_client):
66
+ mock_client.post.return_value = {"id": "ve_1"}
67
+ args = argparse.Namespace(
68
+ valset_id="vs_1",
69
+ inputs='{"question": "who?"}',
70
+ expected_outputs='{"answer": "them"}',
71
+ from_assessment="as_9",
72
+ )
73
+ handle_entries_create(mock_client, args)
74
+ mock_client.post.assert_called_once_with(
75
+ "/api/v1/valsets/vs_1/entries",
76
+ json={
77
+ "inputs": {"question": "who?"},
78
+ "expected_outputs": {"answer": "them"},
79
+ "from_assessment": "as_9",
80
+ },
81
+ )
82
+
83
+ def test_entries_create_rejects_bad_json(self, mock_client):
84
+ args = argparse.Namespace(valset_id="vs_1", inputs="{not json", expected_outputs="{}", from_assessment=None)
85
+ with pytest.raises(CliError, match="--inputs JSON is invalid"):
86
+ handle_entries_create(mock_client, args)
87
+ mock_client.post.assert_not_called()
88
+
89
+ @pytest.mark.parametrize("value", ["[]", "42", "null", '"text"'])
90
+ def test_entries_create_rejects_non_object_json(self, mock_client, value):
91
+ args = argparse.Namespace(
92
+ valset_id="vs_1", inputs=value, expected_outputs='{"answer": "them"}', from_assessment=None
93
+ )
94
+ with pytest.raises(CliError, match="--inputs JSON must be an object"):
95
+ handle_entries_create(mock_client, args)
96
+ mock_client.post.assert_not_called()
97
+
98
+ def test_entries_create_rejects_non_object_expected_outputs(self, mock_client):
99
+ args = argparse.Namespace(
100
+ valset_id="vs_1", inputs='{"question": "who?"}', expected_outputs="[]", from_assessment=None
101
+ )
102
+ with pytest.raises(CliError, match="--expected-outputs JSON must be an object"):
103
+ handle_entries_create(mock_client, args)
104
+ mock_client.post.assert_not_called()
105
+
106
+ def test_entries_delete_confirms(self, mock_client):
107
+ mock_client.delete.return_value = None
108
+ result = handle_entries_delete(mock_client, argparse.Namespace(entry_id="ve_1"))
109
+ mock_client.delete.assert_called_once_with("/api/v1/valsets/entries/ve_1")
110
+ assert result == {"deleted": "ve_1"}
111
+
112
+
113
+ class TestValsetsRegistration:
114
+ def test_subcommands_register(self):
115
+ parser = argparse.ArgumentParser()
116
+ sub = parser.add_subparsers(dest="resource")
117
+ valsets_cmd.register(sub)
118
+ group = sub.choices["valsets"]
119
+ actions = group._subparsers._group_actions[0].choices # type: ignore[union-attr]
120
+ assert set(actions) == {"create", "list", "show", "entries"}
121
+ entries = actions["entries"]._subparsers._group_actions[0].choices # type: ignore[union-attr]
122
+ assert set(entries) == {"list", "create", "delete"}
@@ -496,7 +496,7 @@ wheels = [
496
496
 
497
497
  [[package]]
498
498
  name = "cmpnd"
499
- version = "0.19.0"
499
+ version = "0.20.0"
500
500
  source = { editable = "." }
501
501
  dependencies = [
502
502
  { name = "dspy" },
File without changes