sorethumb-ml 0.1.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 (336) hide show
  1. sorethumb_ml-0.1.0/.gitignore +51 -0
  2. sorethumb_ml-0.1.0/CHANGELOG.md +378 -0
  3. sorethumb_ml-0.1.0/CODE_OF_CONDUCT.md +40 -0
  4. sorethumb_ml-0.1.0/CONTRIBUTING.md +175 -0
  5. sorethumb_ml-0.1.0/LICENSE +202 -0
  6. sorethumb_ml-0.1.0/PKG-INFO +303 -0
  7. sorethumb_ml-0.1.0/README.md +245 -0
  8. sorethumb_ml-0.1.0/SECURITY.md +184 -0
  9. sorethumb_ml-0.1.0/docs/adapting-to-your-data.md +222 -0
  10. sorethumb_ml-0.1.0/docs/approximations.md +423 -0
  11. sorethumb_ml-0.1.0/docs/banner.svg +47 -0
  12. sorethumb_ml-0.1.0/docs/benchmark-results/backtests/composite-agreement-auto-v1.json +7560 -0
  13. sorethumb_ml-0.1.0/docs/benchmark-results/backtests/composite-agreement-auto-v2.json +7528 -0
  14. sorethumb_ml-0.1.0/docs/benchmark-results/backtests/composite-agreement-v1.json +6956 -0
  15. sorethumb_ml-0.1.0/docs/benchmark-results/backtests/composite-agreement-v2.json +7403 -0
  16. sorethumb_ml-0.1.0/docs/benchmark-results/backtests/discrete-budget-v1.json +7374 -0
  17. sorethumb_ml-0.1.0/docs/benchmark-results/backtests/discrete-budget-v2.json +7364 -0
  18. sorethumb_ml-0.1.0/docs/benchmark-results/backtests/discrete-composite-v1.json +6893 -0
  19. sorethumb_ml-0.1.0/docs/benchmark-results/policy-matrix.json +2186 -0
  20. sorethumb_ml-0.1.0/docs/benchmark-results/real-backtests/composite-agreement-real-auto-v1.json +1495 -0
  21. sorethumb_ml-0.1.0/docs/benchmark-results/real-backtests/composite-agreement-real-v1.json +1482 -0
  22. sorethumb_ml-0.1.0/docs/benchmark-results/scale.json +474 -0
  23. sorethumb_ml-0.1.0/docs/benchmarks.md +498 -0
  24. sorethumb_ml-0.1.0/docs/check_readme_snippets.py +245 -0
  25. sorethumb_ml-0.1.0/docs/cli_reference.md +968 -0
  26. sorethumb_ml-0.1.0/docs/configuration-examples.md +722 -0
  27. sorethumb_ml-0.1.0/docs/configuration.md +229 -0
  28. sorethumb_ml-0.1.0/docs/ensemble-policy.md +613 -0
  29. sorethumb_ml-0.1.0/docs/example-runs.md +377 -0
  30. sorethumb_ml-0.1.0/docs/explanations.md +376 -0
  31. sorethumb_ml-0.1.0/docs/generate_config_docs.py +383 -0
  32. sorethumb_ml-0.1.0/docs/getting-started.md +222 -0
  33. sorethumb_ml-0.1.0/docs/index.md +72 -0
  34. sorethumb_ml-0.1.0/docs/limitations.md +91 -0
  35. sorethumb_ml-0.1.0/docs/logo.svg +8 -0
  36. sorethumb_ml-0.1.0/docs/models.md +432 -0
  37. sorethumb_ml-0.1.0/docs/platforms.md +29 -0
  38. sorethumb_ml-0.1.0/docs/releasing.md +80 -0
  39. sorethumb_ml-0.1.0/docs/scale.md +43 -0
  40. sorethumb_ml-0.1.0/docs/stability.md +50 -0
  41. sorethumb_ml-0.1.0/docs/trends.md +163 -0
  42. sorethumb_ml-0.1.0/pyproject.toml +285 -0
  43. sorethumb_ml-0.1.0/src/sorethumb_ml/__init__.py +95 -0
  44. sorethumb_ml-0.1.0/src/sorethumb_ml/_atomic.py +180 -0
  45. sorethumb_ml-0.1.0/src/sorethumb_ml/_determinism.py +24 -0
  46. sorethumb_ml-0.1.0/src/sorethumb_ml/_pipeline.py +3183 -0
  47. sorethumb_ml-0.1.0/src/sorethumb_ml/analysis/__init__.py +1 -0
  48. sorethumb_ml-0.1.0/src/sorethumb_ml/analysis/contrast.py +138 -0
  49. sorethumb_ml-0.1.0/src/sorethumb_ml/cli.py +3594 -0
  50. sorethumb_ml-0.1.0/src/sorethumb_ml/config.py +744 -0
  51. sorethumb_ml-0.1.0/src/sorethumb_ml/detectors/__init__.py +80 -0
  52. sorethumb_ml-0.1.0/src/sorethumb_ml/detectors/_hyperparams.py +142 -0
  53. sorethumb_ml-0.1.0/src/sorethumb_ml/detectors/_protocol.py +205 -0
  54. sorethumb_ml-0.1.0/src/sorethumb_ml/detectors/ecod.py +110 -0
  55. sorethumb_ml-0.1.0/src/sorethumb_ml/detectors/hbos.py +153 -0
  56. sorethumb_ml-0.1.0/src/sorethumb_ml/detectors/isolation_forest.py +89 -0
  57. sorethumb_ml-0.1.0/src/sorethumb_ml/detectors/kmeans_distance.py +315 -0
  58. sorethumb_ml-0.1.0/src/sorethumb_ml/detectors/lof.py +96 -0
  59. sorethumb_ml-0.1.0/src/sorethumb_ml/detectors/one_class_svm.py +127 -0
  60. sorethumb_ml-0.1.0/src/sorethumb_ml/errors.py +378 -0
  61. sorethumb_ml-0.1.0/src/sorethumb_ml/evaluate/__init__.py +1 -0
  62. sorethumb_ml-0.1.0/src/sorethumb_ml/evaluate/backtest.py +517 -0
  63. sorethumb_ml-0.1.0/src/sorethumb_ml/evaluate/benchmark.py +667 -0
  64. sorethumb_ml-0.1.0/src/sorethumb_ml/evaluate/budgets.py +155 -0
  65. sorethumb_ml-0.1.0/src/sorethumb_ml/evaluate/candidates.py +121 -0
  66. sorethumb_ml-0.1.0/src/sorethumb_ml/evaluate/metrics.py +281 -0
  67. sorethumb_ml-0.1.0/src/sorethumb_ml/evaluate/pipeline_benchmark.py +1222 -0
  68. sorethumb_ml-0.1.0/src/sorethumb_ml/evaluate/real_backtest.py +269 -0
  69. sorethumb_ml-0.1.0/src/sorethumb_ml/evaluate/scale_benchmark.py +617 -0
  70. sorethumb_ml-0.1.0/src/sorethumb_ml/evaluate/scenarios.py +504 -0
  71. sorethumb_ml-0.1.0/src/sorethumb_ml/evaluate/user_backtest.py +867 -0
  72. sorethumb_ml-0.1.0/src/sorethumb_ml/explain/__init__.py +30 -0
  73. sorethumb_ml-0.1.0/src/sorethumb_ml/explain/blend.py +155 -0
  74. sorethumb_ml-0.1.0/src/sorethumb_ml/explain/centroid.py +84 -0
  75. sorethumb_ml-0.1.0/src/sorethumb_ml/explain/gradient.py +370 -0
  76. sorethumb_ml-0.1.0/src/sorethumb_ml/explain/native.py +73 -0
  77. sorethumb_ml-0.1.0/src/sorethumb_ml/explain/project.py +244 -0
  78. sorethumb_ml-0.1.0/src/sorethumb_ml/explain/record.py +327 -0
  79. sorethumb_ml-0.1.0/src/sorethumb_ml/explain/reference.py +273 -0
  80. sorethumb_ml-0.1.0/src/sorethumb_ml/explain/shap_tree.py +178 -0
  81. sorethumb_ml-0.1.0/src/sorethumb_ml/features/__init__.py +1 -0
  82. sorethumb_ml-0.1.0/src/sorethumb_ml/features/build.py +522 -0
  83. sorethumb_ml-0.1.0/src/sorethumb_ml/features/correlate.py +159 -0
  84. sorethumb_ml-0.1.0/src/sorethumb_ml/features/encode.py +259 -0
  85. sorethumb_ml-0.1.0/src/sorethumb_ml/features/reduce.py +120 -0
  86. sorethumb_ml-0.1.0/src/sorethumb_ml/features/scale.py +156 -0
  87. sorethumb_ml-0.1.0/src/sorethumb_ml/features/space.py +35 -0
  88. sorethumb_ml-0.1.0/src/sorethumb_ml/history/__init__.py +1 -0
  89. sorethumb_ml-0.1.0/src/sorethumb_ml/history/anchor.py +227 -0
  90. sorethumb_ml-0.1.0/src/sorethumb_ml/history/ledger.py +215 -0
  91. sorethumb_ml-0.1.0/src/sorethumb_ml/history/period_stats.py +267 -0
  92. sorethumb_ml-0.1.0/src/sorethumb_ml/history/periods.py +161 -0
  93. sorethumb_ml-0.1.0/src/sorethumb_ml/history/trend.py +212 -0
  94. sorethumb_ml-0.1.0/src/sorethumb_ml/history/windows.py +596 -0
  95. sorethumb_ml-0.1.0/src/sorethumb_ml/io/__init__.py +1 -0
  96. sorethumb_ml-0.1.0/src/sorethumb_ml/io/fingerprint.py +98 -0
  97. sorethumb_ml-0.1.0/src/sorethumb_ml/io/nested.py +133 -0
  98. sorethumb_ml-0.1.0/src/sorethumb_ml/io/readers.py +302 -0
  99. sorethumb_ml-0.1.0/src/sorethumb_ml/io/source.py +771 -0
  100. sorethumb_ml-0.1.0/src/sorethumb_ml/io/toml_write.py +103 -0
  101. sorethumb_ml-0.1.0/src/sorethumb_ml/io/uri.py +176 -0
  102. sorethumb_ml-0.1.0/src/sorethumb_ml/logging.py +18 -0
  103. sorethumb_ml-0.1.0/src/sorethumb_ml/profiling/__init__.py +1 -0
  104. sorethumb_ml-0.1.0/src/sorethumb_ml/profiling/classify.py +369 -0
  105. sorethumb_ml-0.1.0/src/sorethumb_ml/profiling/missing.py +31 -0
  106. sorethumb_ml-0.1.0/src/sorethumb_ml/profiling/plan.py +559 -0
  107. sorethumb_ml-0.1.0/src/sorethumb_ml/profiling/profile.py +149 -0
  108. sorethumb_ml-0.1.0/src/sorethumb_ml/profiling/temporal.py +348 -0
  109. sorethumb_ml-0.1.0/src/sorethumb_ml/py.typed +0 -0
  110. sorethumb_ml-0.1.0/src/sorethumb_ml/report/__init__.py +1 -0
  111. sorethumb_ml-0.1.0/src/sorethumb_ml/report/csv.py +79 -0
  112. sorethumb_ml-0.1.0/src/sorethumb_ml/report/html.py +811 -0
  113. sorethumb_ml-0.1.0/src/sorethumb_ml/scoring/__init__.py +6 -0
  114. sorethumb_ml-0.1.0/src/sorethumb_ml/scoring/_finite.py +22 -0
  115. sorethumb_ml-0.1.0/src/sorethumb_ml/scoring/calibrate.py +246 -0
  116. sorethumb_ml-0.1.0/src/sorethumb_ml/scoring/combine.py +706 -0
  117. sorethumb_ml-0.1.0/src/sorethumb_ml/scoring/policies.py +144 -0
  118. sorethumb_ml-0.1.0/src/sorethumb_ml/store/__init__.py +6 -0
  119. sorethumb_ml-0.1.0/src/sorethumb_ml/store/db.py +1224 -0
  120. sorethumb_ml-0.1.0/src/sorethumb_ml/store/identifiers.py +30 -0
  121. sorethumb_ml-0.1.0/src/sorethumb_ml/store/migrations/001_initial.sql +95 -0
  122. sorethumb_ml-0.1.0/src/sorethumb_ml/store/migrations/002_totals_config_hash.sql +33 -0
  123. sorethumb_ml-0.1.0/src/sorethumb_ml/store/migrations/003_artifact_run_id.sql +19 -0
  124. sorethumb_ml-0.1.0/src/sorethumb_ml/store/migrations/004_run_source.sql +11 -0
  125. sorethumb_ml-0.1.0/src/sorethumb_ml/store/migrations/005_dataset_identity.sql +44 -0
  126. sorethumb_ml-0.1.0/src/sorethumb_ml/store/migrations/006_period_execution.sql +33 -0
  127. sorethumb_ml-0.1.0/src/sorethumb_ml/store/migrations/007_run_group_warnings.sql +11 -0
  128. sorethumb_ml-0.1.0/src/sorethumb_ml/store/migrations/008_query_indexes.sql +40 -0
  129. sorethumb_ml-0.1.0/src/sorethumb_ml/store/migrations/009_source_digest.sql +21 -0
  130. sorethumb_ml-0.1.0/src/sorethumb_ml/store/migrations/010_run_ensemble_policy.sql +9 -0
  131. sorethumb_ml-0.1.0/src/sorethumb_ml/store/migrations/011_policy_evidence.sql +38 -0
  132. sorethumb_ml-0.1.0/src/sorethumb_ml/store/migrations/012_explain_status.sql +15 -0
  133. sorethumb_ml-0.1.0/src/sorethumb_ml/store/migrations/013_period_stats.sql +20 -0
  134. sorethumb_ml-0.1.0/src/sorethumb_ml/store/migrations/__init__.py +1 -0
  135. sorethumb_ml-0.1.0/src/sorethumb_ml/store/models.py +497 -0
  136. sorethumb_ml-0.1.0/src/sorethumb_ml/store/results.py +305 -0
  137. sorethumb_ml-0.1.0/src/sorethumb_ml/store/storage.py +131 -0
  138. sorethumb_ml-0.1.0/src/sorethumb_ml/store/workspace.py +335 -0
  139. sorethumb_ml-0.1.0/tests/README.md +65 -0
  140. sorethumb_ml-0.1.0/tests/__init__.py +0 -0
  141. sorethumb_ml-0.1.0/tests/benchmark/__init__.py +1 -0
  142. sorethumb_ml-0.1.0/tests/benchmark/budgets-archive/v2.toml +170 -0
  143. sorethumb_ml-0.1.0/tests/benchmark/budgets-archive/v3.toml +191 -0
  144. sorethumb_ml-0.1.0/tests/benchmark/budgets.toml +204 -0
  145. sorethumb_ml-0.1.0/tests/benchmark/preregistrations/composite-agreement-auto-v1.toml +27 -0
  146. sorethumb_ml-0.1.0/tests/benchmark/preregistrations/composite-agreement-auto-v2.toml +33 -0
  147. sorethumb_ml-0.1.0/tests/benchmark/preregistrations/composite-agreement-v1.toml +26 -0
  148. sorethumb_ml-0.1.0/tests/benchmark/preregistrations/composite-agreement-v2.toml +27 -0
  149. sorethumb_ml-0.1.0/tests/benchmark/preregistrations/discrete-budget-v1.toml +24 -0
  150. sorethumb_ml-0.1.0/tests/benchmark/preregistrations/discrete-budget-v2.toml +30 -0
  151. sorethumb_ml-0.1.0/tests/benchmark/preregistrations/discrete-composite-v1.toml +24 -0
  152. sorethumb_ml-0.1.0/tests/benchmark/preregistrations/real/composite-agreement-real-auto-v1.toml +23 -0
  153. sorethumb_ml-0.1.0/tests/benchmark/preregistrations/real/composite-agreement-real-v1.toml +24 -0
  154. sorethumb_ml-0.1.0/tests/benchmark/test_accuracy_floors.py +97 -0
  155. sorethumb_ml-0.1.0/tests/benchmark/test_benchmark_harness.py +225 -0
  156. sorethumb_ml-0.1.0/tests/benchmark/test_memory_budget.py +230 -0
  157. sorethumb_ml-0.1.0/tests/benchmark/test_pipeline_accuracy_floors.py +271 -0
  158. sorethumb_ml-0.1.0/tests/benchmark/test_pipeline_benchmark_harness.py +213 -0
  159. sorethumb_ml-0.1.0/tests/benchmark/test_policy_budgets.py +62 -0
  160. sorethumb_ml-0.1.0/tests/benchmark/test_real_dataset_smoke.py +90 -0
  161. sorethumb_ml-0.1.0/tests/benchmark/test_scale_benchmark_smoke.py +63 -0
  162. sorethumb_ml-0.1.0/tests/conftest.py +88 -0
  163. sorethumb_ml-0.1.0/tests/contract/__init__.py +0 -0
  164. sorethumb_ml-0.1.0/tests/contract/test_cli.py +1126 -0
  165. sorethumb_ml-0.1.0/tests/contract/test_cli_exit_codes.py +364 -0
  166. sorethumb_ml-0.1.0/tests/contract/test_cli_tracebacks.py +104 -0
  167. sorethumb_ml-0.1.0/tests/contract/test_config_hash_consistency.py +115 -0
  168. sorethumb_ml-0.1.0/tests/contract/test_database_schema.py +784 -0
  169. sorethumb_ml-0.1.0/tests/contract/test_detector_plugins.py +729 -0
  170. sorethumb_ml-0.1.0/tests/contract/test_feature_plan_compat.py +131 -0
  171. sorethumb_ml-0.1.0/tests/contract/test_model_manifests.py +327 -0
  172. sorethumb_ml-0.1.0/tests/contract/test_packaging.py +71 -0
  173. sorethumb_ml-0.1.0/tests/contract/test_public_api.py +298 -0
  174. sorethumb_ml-0.1.0/tests/factories/__init__.py +8 -0
  175. sorethumb_ml-0.1.0/tests/factories/benchmark_rows.py +34 -0
  176. sorethumb_ml-0.1.0/tests/factories/configs.py +74 -0
  177. sorethumb_ml-0.1.0/tests/factories/detectors.py +106 -0
  178. sorethumb_ml-0.1.0/tests/factories/frames.py +154 -0
  179. sorethumb_ml-0.1.0/tests/factories/golden.py +51 -0
  180. sorethumb_ml-0.1.0/tests/factories/hypothesis_profiles.py +36 -0
  181. sorethumb_ml-0.1.0/tests/factories/pipeline_benchmark_rows.py +35 -0
  182. sorethumb_ml-0.1.0/tests/factories/policy_evidence.py +78 -0
  183. sorethumb_ml-0.1.0/tests/factories/results.py +25 -0
  184. sorethumb_ml-0.1.0/tests/factories/runs.py +54 -0
  185. sorethumb_ml-0.1.0/tests/factories/source_uris.py +79 -0
  186. sorethumb_ml-0.1.0/tests/factories/workspaces.py +35 -0
  187. sorethumb_ml-0.1.0/tests/golden/cli_config_schema.json +742 -0
  188. sorethumb_ml-0.1.0/tests/golden/cli_detectors.json +32 -0
  189. sorethumb_ml-0.1.0/tests/golden/ensemble_policies.json +196 -0
  190. sorethumb_ml-0.1.0/tests/golden/report_index.html +60 -0
  191. sorethumb_ml-0.1.0/tests/golden/report_reason_detail.html +1 -0
  192. sorethumb_ml-0.1.0/tests/integration/__init__.py +0 -0
  193. sorethumb_ml-0.1.0/tests/integration/test_anchor_decomposition.py +284 -0
  194. sorethumb_ml-0.1.0/tests/integration/test_attribution_failure.py +161 -0
  195. sorethumb_ml-0.1.0/tests/integration/test_cli.py +1068 -0
  196. sorethumb_ml-0.1.0/tests/integration/test_cli_file_handles.py +179 -0
  197. sorethumb_ml-0.1.0/tests/integration/test_cli_home_expansion.py +54 -0
  198. sorethumb_ml-0.1.0/tests/integration/test_cli_markup.py +123 -0
  199. sorethumb_ml-0.1.0/tests/integration/test_cli_output_encoding.py +98 -0
  200. sorethumb_ml-0.1.0/tests/integration/test_cli_stdin.py +44 -0
  201. sorethumb_ml-0.1.0/tests/integration/test_cli_storage_warning.py +44 -0
  202. sorethumb_ml-0.1.0/tests/integration/test_cli_zero_config.py +122 -0
  203. sorethumb_ml-0.1.0/tests/integration/test_config_wiring.py +503 -0
  204. sorethumb_ml-0.1.0/tests/integration/test_ensemble_policy_provenance.py +110 -0
  205. sorethumb_ml-0.1.0/tests/integration/test_explain_isolation.py +307 -0
  206. sorethumb_ml-0.1.0/tests/integration/test_explain_record.py +245 -0
  207. sorethumb_ml-0.1.0/tests/integration/test_explain_reference.py +173 -0
  208. sorethumb_ml-0.1.0/tests/integration/test_explain_without_shap.py +93 -0
  209. sorethumb_ml-0.1.0/tests/integration/test_feature_matrix_lifetime.py +63 -0
  210. sorethumb_ml-0.1.0/tests/integration/test_files_in_use.py +183 -0
  211. sorethumb_ml-0.1.0/tests/integration/test_first_run_guidance.py +222 -0
  212. sorethumb_ml-0.1.0/tests/integration/test_group_selection.py +95 -0
  213. sorethumb_ml-0.1.0/tests/integration/test_history.py +980 -0
  214. sorethumb_ml-0.1.0/tests/integration/test_id_column_identity.py +239 -0
  215. sorethumb_ml-0.1.0/tests/integration/test_include_only.py +38 -0
  216. sorethumb_ml-0.1.0/tests/integration/test_non_utf8_source.py +53 -0
  217. sorethumb_ml-0.1.0/tests/integration/test_optional_packages.py +133 -0
  218. sorethumb_ml-0.1.0/tests/integration/test_period_stats_pipeline.py +146 -0
  219. sorethumb_ml-0.1.0/tests/integration/test_pipeline_detection.py +203 -0
  220. sorethumb_ml-0.1.0/tests/integration/test_pipeline_explain.py +368 -0
  221. sorethumb_ml-0.1.0/tests/integration/test_pipeline_resume.py +182 -0
  222. sorethumb_ml-0.1.0/tests/integration/test_policy_evidence_gate.py +582 -0
  223. sorethumb_ml-0.1.0/tests/integration/test_rank_ties.py +150 -0
  224. sorethumb_ml-0.1.0/tests/integration/test_report.py +581 -0
  225. sorethumb_ml-0.1.0/tests/integration/test_report_reason_detail.py +168 -0
  226. sorethumb_ml-0.1.0/tests/integration/test_row_identity.py +224 -0
  227. sorethumb_ml-0.1.0/tests/integration/test_scale_benchmark_inprocess.py +86 -0
  228. sorethumb_ml-0.1.0/tests/integration/test_score_forward.py +521 -0
  229. sorethumb_ml-0.1.0/tests/integration/test_source_provenance.py +570 -0
  230. sorethumb_ml-0.1.0/tests/integration/test_store_workflows.py +510 -0
  231. sorethumb_ml-0.1.0/tests/integration/test_temporal_columns.py +231 -0
  232. sorethumb_ml-0.1.0/tests/integration/test_trend_report.py +251 -0
  233. sorethumb_ml-0.1.0/tests/integration/test_trend_windows.py +315 -0
  234. sorethumb_ml-0.1.0/tests/integration/test_uninformative_boundary_pipeline.py +134 -0
  235. sorethumb_ml-0.1.0/tests/integration/test_workspace_reset_portability.py +119 -0
  236. sorethumb_ml-0.1.0/tests/property/__init__.py +1 -0
  237. sorethumb_ml-0.1.0/tests/property/test_atomic_write_properties.py +46 -0
  238. sorethumb_ml-0.1.0/tests/property/test_calibration_properties.py +71 -0
  239. sorethumb_ml-0.1.0/tests/property/test_detector_properties.py +74 -0
  240. sorethumb_ml-0.1.0/tests/property/test_feature_properties.py +85 -0
  241. sorethumb_ml-0.1.0/tests/property/test_grouping_properties.py +49 -0
  242. sorethumb_ml-0.1.0/tests/property/test_profiling_properties.py +203 -0
  243. sorethumb_ml-0.1.0/tests/repo_check/__init__.py +0 -0
  244. sorethumb_ml-0.1.0/tests/repo_check/test_changelog_commands.py +88 -0
  245. sorethumb_ml-0.1.0/tests/repo_check/test_ci_platforms.py +39 -0
  246. sorethumb_ml-0.1.0/tests/repo_check/test_dependency_floors.py +92 -0
  247. sorethumb_ml-0.1.0/tests/repo_check/test_dist_artifacts.py +477 -0
  248. sorethumb_ml-0.1.0/tests/repo_check/test_docs_checks.py +126 -0
  249. sorethumb_ml-0.1.0/tests/repo_check/test_license.py +78 -0
  250. sorethumb_ml-0.1.0/tests/repo_check/test_line_endings.py +46 -0
  251. sorethumb_ml-0.1.0/tests/repo_check/test_newest_dependencies.py +75 -0
  252. sorethumb_ml-0.1.0/tests/repo_check/test_no_private_references.py +119 -0
  253. sorethumb_ml-0.1.0/tests/repo_check/test_no_review_documents.py +82 -0
  254. sorethumb_ml-0.1.0/tests/repo_check/test_platform_claims.py +52 -0
  255. sorethumb_ml-0.1.0/tests/repo_check/test_policy_contacts.py +48 -0
  256. sorethumb_ml-0.1.0/tests/repo_check/test_python_versions.py +73 -0
  257. sorethumb_ml-0.1.0/tests/repo_check/test_readme_structure.py +132 -0
  258. sorethumb_ml-0.1.0/tests/repo_check/test_release_gate.py +153 -0
  259. sorethumb_ml-0.1.0/tests/repo_check/test_shell_neutral_commands.py +38 -0
  260. sorethumb_ml-0.1.0/tests/repo_check/test_usage_guidance.py +68 -0
  261. sorethumb_ml-0.1.0/tests/repo_check/test_workspace_docs.py +147 -0
  262. sorethumb_ml-0.1.0/tests/scripts/__init__.py +0 -0
  263. sorethumb_ml-0.1.0/tests/scripts/test_check_installed_dist.py +192 -0
  264. sorethumb_ml-0.1.0/tests/scripts/test_check_release_state.py +306 -0
  265. sorethumb_ml-0.1.0/tests/scripts/test_rehearsal_version.py +54 -0
  266. sorethumb_ml-0.1.0/tests/scripts/test_run_benchmark.py +136 -0
  267. sorethumb_ml-0.1.0/tests/scripts/test_summarise_junit.py +99 -0
  268. sorethumb_ml-0.1.0/tests/scripts/validation/__init__.py +0 -0
  269. sorethumb_ml-0.1.0/tests/scripts/validation/test_data.py +81 -0
  270. sorethumb_ml-0.1.0/tests/scripts/validation/test_identity.py +40 -0
  271. sorethumb_ml-0.1.0/tests/scripts/validation/test_planner.py +213 -0
  272. sorethumb_ml-0.1.0/tests/scripts/validation/test_runner_integration.py +175 -0
  273. sorethumb_ml-0.1.0/tests/scripts/validation/test_schema.py +78 -0
  274. sorethumb_ml-0.1.0/tests/synth/__init__.py +115 -0
  275. sorethumb_ml-0.1.0/tests/unit/__init__.py +0 -0
  276. sorethumb_ml-0.1.0/tests/unit/analysis/__init__.py +0 -0
  277. sorethumb_ml-0.1.0/tests/unit/analysis/test_contrast_scale.py +109 -0
  278. sorethumb_ml-0.1.0/tests/unit/config/__init__.py +0 -0
  279. sorethumb_ml-0.1.0/tests/unit/config/test_config.py +79 -0
  280. sorethumb_ml-0.1.0/tests/unit/detectors/__init__.py +0 -0
  281. sorethumb_ml-0.1.0/tests/unit/detectors/test_detectors.py +800 -0
  282. sorethumb_ml-0.1.0/tests/unit/detectors/test_kmeans_determinism.py +51 -0
  283. sorethumb_ml-0.1.0/tests/unit/evaluation/__init__.py +0 -0
  284. sorethumb_ml-0.1.0/tests/unit/evaluation/test_backtest.py +376 -0
  285. sorethumb_ml-0.1.0/tests/unit/evaluation/test_budgets.py +125 -0
  286. sorethumb_ml-0.1.0/tests/unit/evaluation/test_candidates.py +72 -0
  287. sorethumb_ml-0.1.0/tests/unit/evaluation/test_evaluate.py +579 -0
  288. sorethumb_ml-0.1.0/tests/unit/evaluation/test_peak_memory.py +38 -0
  289. sorethumb_ml-0.1.0/tests/unit/evaluation/test_pipeline_benchmark_cells.py +159 -0
  290. sorethumb_ml-0.1.0/tests/unit/evaluation/test_pipeline_benchmark_matrix.py +184 -0
  291. sorethumb_ml-0.1.0/tests/unit/evaluation/test_real_backtest.py +133 -0
  292. sorethumb_ml-0.1.0/tests/unit/evaluation/test_scale_benchmark.py +190 -0
  293. sorethumb_ml-0.1.0/tests/unit/evaluation/test_scenarios.py +198 -0
  294. sorethumb_ml-0.1.0/tests/unit/evaluation/test_user_backtest.py +124 -0
  295. sorethumb_ml-0.1.0/tests/unit/explain/__init__.py +0 -0
  296. sorethumb_ml-0.1.0/tests/unit/explain/test_attribution_reference.py +209 -0
  297. sorethumb_ml-0.1.0/tests/unit/explain/test_explain.py +736 -0
  298. sorethumb_ml-0.1.0/tests/unit/explain/test_record.py +351 -0
  299. sorethumb_ml-0.1.0/tests/unit/explain/test_reference.py +353 -0
  300. sorethumb_ml-0.1.0/tests/unit/features/__init__.py +0 -0
  301. sorethumb_ml-0.1.0/tests/unit/features/test_features.py +1226 -0
  302. sorethumb_ml-0.1.0/tests/unit/features/test_missing_and_nonfinite.py +243 -0
  303. sorethumb_ml-0.1.0/tests/unit/history/__init__.py +0 -0
  304. sorethumb_ml-0.1.0/tests/unit/history/test_anchor_rule.py +74 -0
  305. sorethumb_ml-0.1.0/tests/unit/history/test_period_stats.py +217 -0
  306. sorethumb_ml-0.1.0/tests/unit/history/test_periods.py +177 -0
  307. sorethumb_ml-0.1.0/tests/unit/io/__init__.py +0 -0
  308. sorethumb_ml-0.1.0/tests/unit/io/test_io.py +2085 -0
  309. sorethumb_ml-0.1.0/tests/unit/io/test_source_uri.py +162 -0
  310. sorethumb_ml-0.1.0/tests/unit/io/test_toml_write.py +112 -0
  311. sorethumb_ml-0.1.0/tests/unit/io/test_uri.py +128 -0
  312. sorethumb_ml-0.1.0/tests/unit/pipeline/__init__.py +0 -0
  313. sorethumb_ml-0.1.0/tests/unit/pipeline/test_grouping.py +146 -0
  314. sorethumb_ml-0.1.0/tests/unit/pipeline/test_ranking.py +160 -0
  315. sorethumb_ml-0.1.0/tests/unit/profiling/__init__.py +0 -0
  316. sorethumb_ml-0.1.0/tests/unit/profiling/test_include_only.py +67 -0
  317. sorethumb_ml-0.1.0/tests/unit/profiling/test_profiling.py +813 -0
  318. sorethumb_ml-0.1.0/tests/unit/profiling/test_temporal.py +365 -0
  319. sorethumb_ml-0.1.0/tests/unit/report/__init__.py +0 -0
  320. sorethumb_ml-0.1.0/tests/unit/report/test_csv_bom.py +41 -0
  321. sorethumb_ml-0.1.0/tests/unit/scoring/__init__.py +0 -0
  322. sorethumb_ml-0.1.0/tests/unit/scoring/test_policies.py +136 -0
  323. sorethumb_ml-0.1.0/tests/unit/scoring/test_scoring.py +1014 -0
  324. sorethumb_ml-0.1.0/tests/unit/scoring/test_uninformative_boundary.py +128 -0
  325. sorethumb_ml-0.1.0/tests/unit/store/__init__.py +0 -0
  326. sorethumb_ml-0.1.0/tests/unit/store/test_storage_detection.py +81 -0
  327. sorethumb_ml-0.1.0/tests/unit/store/test_store_helpers.py +81 -0
  328. sorethumb_ml-0.1.0/tests/unit/test_atomic_durability.py +232 -0
  329. sorethumb_ml-0.1.0/tests/unit/test_cli_guards.py +62 -0
  330. sorethumb_ml-0.1.0/tests/unit/test_cli_output_streams.py +87 -0
  331. sorethumb_ml-0.1.0/tests/unit/test_file_in_use.py +125 -0
  332. sorethumb_ml-0.1.0/tests/unit/test_log_rollover.py +97 -0
  333. sorethumb_ml-0.1.0/tests/unit/test_path_length.py +80 -0
  334. sorethumb_ml-0.1.0/tests/unit/test_smoke.py +90 -0
  335. sorethumb_ml-0.1.0/tests/unit/test_warning_records.py +81 -0
  336. sorethumb_ml-0.1.0/uv.lock +2689 -0
@@ -0,0 +1,51 @@
1
+ data-samples/
2
+ work-dir/
3
+ .venv/
4
+ __pycache__/
5
+ *.py[cod]
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .pytest_cache/
12
+ .hypothesis/
13
+ .claude/
14
+ .coverage
15
+ coverage.xml
16
+ htmlcov/
17
+ *.log
18
+ *.joblib
19
+ # Default sorethumb workspace and its artefacts. Anchored: a
20
+ # workspace explicitly created at "." (the previous default, or via
21
+ # `--workdir .`) produces these same top-level names, and unanchored
22
+ # patterns would also match same-named directories anywhere else in the tree.
23
+ /sorethumb-workspace/
24
+ /sorethumb.db
25
+ /models/
26
+ /results/
27
+ /reports/
28
+ /logs/
29
+ /cache/
30
+ .sorethumb_benchmark_cache/
31
+ benchmark_results/
32
+ /validation/
33
+ /tmp/
34
+ .idea/
35
+ *.iml
36
+ prompts/
37
+ PLAN.md
38
+
39
+ # Generated or hand-written review documents (code reviews, audits of the repository,
40
+ # pre-release critiques). They are working material about the project, not part of it, and
41
+ # were never meant to be published; the [Rr][Ee]... classes make the match case-insensitive.
42
+ /reviews/
43
+ *[Rr][Ee][Vv][Ii][Ee][Ww]*.md
44
+ *[Rr][Ee][Vv][Ii][Ee][Ww]*.docx
45
+ *[Rr][Ee][Vv][Ii][Ee][Ww]*.pdf
46
+
47
+ # Delete-pending placeholders an SMB/CIFS share leaves while a deleted file is still open
48
+ .smbdelete*
49
+
50
+ # scripts/run_scale_benchmark.py default scratch directory
51
+ scale_benchmark_scratch/
@@ -0,0 +1,378 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
+ Versioning: [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-10-20
10
+
11
+ First release.
12
+
13
+ ### Added
14
+
15
+ - **Names**: install the `sorethumb-ml` distribution (`pip install sorethumb-ml`;
16
+ `sorethumb` on PyPI is an unrelated package), import `sorethumb_ml`, and run
17
+ the `sorethumb` command. Third-party detectors register through the
18
+ `sorethumb_ml.detectors` entry-point group. Python 3.11–3.14 on Linux,
19
+ macOS and Windows (x64), each tested in CI; `requires-python` is `>=3.11,<3.15`,
20
+ so pip will not install on a CPython that has not been tested. The package ships
21
+ `py.typed`.
22
+ - **Pipeline**: profile every column, encode, impute and derive features, fit
23
+ an ensemble of detectors, percentile-calibrate their scores, combine them into
24
+ a ranking and a flag decision, and explain each flagged row in terms of the
25
+ original columns. It ranks a review shortlist: `scoring.contamination` is a
26
+ review budget, not an estimate of how many anomalies the data contains, and
27
+ each run shows the per-detector flag rates alongside the flagged count. How many
28
+ rows are flagged depends on the ensemble policy as well as on `contamination`.
29
+ By default (`intersection` at `"auto"`) it is whatever every detector's own
30
+ boundary agrees on, not a number you choose.
31
+ - **Input data**: a file with a single column, or with every column as text
32
+ (all-categorical data), is valid input. A CSV/TSV is refused as
33
+ mis-delimited only when another delimiter character appears in the header
34
+ and the same number of times in most data rows; an all-text schema is
35
+ re-scanned (up to 1,000,000 rows) and refused, naming
36
+ `source.read_options['infer_schema_length']`, if a numeric column was hidden
37
+ by blank leading rows. A source path that is a directory is rejected with a
38
+ clear message. A column is never dropped for having few distinct values
39
+ (only a constant one is), since a rare level of a low-cardinality column can
40
+ be exactly the anomaly; `profiling.near_constant_distinct` can opt in to
41
+ dropping them. `columns.include_only` restricts
42
+ the features to the listed columns plus `time_column`, `group_by`,
43
+ `id_column` and `reference_column`; a listed name not in the dataset is a
44
+ `ConfigError`.
45
+ - **Categorical columns**: up to `features.one_hot_max_cardinality` levels are
46
+ one-hot encoded, more are frequency-encoded. One-hot dummies are not scaled
47
+ like numeric columns: two rows that differ only in one category are
48
+ `features.categorical_distance` apart (default 0.5, in the interquartile-range
49
+ units numeric features use), so a categorical column of noise cannot dominate
50
+ the distance-based detectors (KMeans, One-Class SVM, LOF) while a rare level
51
+ still stands out. Isolation Forest is unaffected.
52
+ - **Dates and times stored as text**: a text column holding ISO dates or
53
+ datetimes, a time of day, or a day/month/year date is parsed into a real
54
+ date/time before profiling, so it gets derived features (`hour`,
55
+ `dayofweek`, ...) and a text `columns.time_column` filters period windows by
56
+ date rather than as a string. Every date/time column gets derived features,
57
+ not only `time_column`. `group_by`, `id_column` and `reference_column`
58
+ columns, and columns an `ignore` pattern or `include_only` excludes, are
59
+ left as text. An ambiguous day/month order (every day and month <= 12) is
60
+ never guessed: the column stays text, with a warning that `run.strict`
61
+ does not turn into an error. `columns.datetime_formats` forces a column's
62
+ format, or `"epoch_s"`/`"epoch_ms"` for a Unix-timestamp integer or float
63
+ column. A value that fails to parse becomes missing. `Duration` columns
64
+ become a numeric seconds feature. `run`, `score --from-run`, `inspect`,
65
+ `explain-plan` and the Python API's `build_feature_plan`/`apply_feature_plan`
66
+ all parse the same way, and the parse is stored with the run so
67
+ `score --from-run` replays it exactly.
68
+ - **Detectors**: `isolation_forest`, `kmeans_distance` (CBLOF-style) and
69
+ `one_class_svm` form the default ensemble; `ecod`, `hbos` and `lof` are also
70
+ available. A detector's `extra_params` forwards extra constructor keywords to
71
+ the underlying scikit-learn estimator. On discrete features (for example
72
+ all-categorical data: at most 50 distinct feature rows, and at most half
73
+ as many as there are rows), `kmeans_distance` gives each distinct row its
74
+ own cluster so a rare category is flagged; its `k_max` defaults to automatic
75
+ (8 on continuous data), and an explicitly set `k_max` is a hard cap.
76
+ - **Detector output contract**: `score_samples` must return a finite 1-D numeric
77
+ `ndarray` with one value per input row, and `natural_flag` a 1-D `bool`
78
+ `ndarray` of the same length. Every call is checked immediately, for a
79
+ freshly fitted detector, a `run.reuse_models` model and a `score --from-run`
80
+ model alike. A violation, or an exception raised inside a plugin's method,
81
+ fails the group with a `DetectorError` naming the detector, the failing
82
+ condition, the expected contract and the actual dtype/shape, and the model is
83
+ not saved. `ScoreEnsemble.combine()` and `Calibrator.fit()`/`transform()` also
84
+ reject malformed or non-finite input themselves. See
85
+ [Writing a detector plugin](docs/models.md#writing-a-detector-plugin).
86
+ - **Ensemble combination**: `intersection` (the default: every configured
87
+ detector must flag a row; see Known limitations), `union`, and `composite`
88
+ (a weighted average with a guard that drops an anti-correlated member, and
89
+ optional agreement-based weighting). Each is a versioned *policy*
90
+ (`intersection@1`, `union@1`, `composite-equal@1`, `composite-agreement@1`,
91
+ `composite-manual@1`) recorded on every run (`run --json`, `show`); a change
92
+ to what a policy flags is a new version. The default is a deliberate design
93
+ decision and is not changing for now; see "Where things stand" in
94
+ `docs/ensemble-policy.md` for the evidence and its limits.
95
+ - **Explanations**: up to `explain.top_n` reasons per flagged row
96
+ (`reason_1`, `reason_2`, ...), labelled by `attribution_kind`: `exact` for ECOD
97
+ and HBOS, `model_specific` for Isolation Forest (interventional TreeSHAP, with
98
+ the `explain` extra) and `heuristic` otherwise (centroid distance, input
99
+ gradient, KernelSHAP, or the reference-scaled deviation used where a score is
100
+ flat). See `docs/explanations.md`.
101
+ - **Detection first.** Each group's detection output (`anomalies.parquet`:
102
+ scores, flags, ranks) is written, and its SHA-256 recorded as
103
+ `detection_digest`, before any explanation is computed. Nothing the
104
+ explanation stage does, including failing, changes which rows are flagged.
105
+ Detection always uses `features.dtype`, whatever the explain settings.
106
+ - **Explained against normal rows.** Every method measures a flagged row
107
+ against a reference of rows no detector flags (falling back to the less
108
+ anomalous half of the unflagged rows, and to no explanation when neither has
109
+ 10 rows), never against the anomalies. A fitting run saves the reference
110
+ summary (`models/<run>/<group>/reference.json`, plain JSON), and
111
+ `score --from-run` explains against it.
112
+ - **Per-row status.** Each flagged row has an `explain_status` (`ok`,
113
+ `partial`, `unavailable`) and an `explain_issue` saying what is missing; an
114
+ unavailable row has no `reason_<k>`. Each group records `explain_status`,
115
+ `n_explained` and `n_explain_unavailable` (`run --json`, `show`).
116
+ - **The record behind each reason** (`attributions.parquet`): original field,
117
+ raw value, reference value and direction, signed contribution (positive
118
+ toward anomaly) and the magnitude reasons are ranked by, each detector's
119
+ method, weight and share, the transformed features behind the field, every
120
+ approximation caveat (`approximation_flags`), `explainer_version`
121
+ (`explain@2`) and `reference_id`. Shown by `sorethumb anomalies --detail` and
122
+ the HTML report's "Explanation detail" section; `index.json` carries it under
123
+ `attributions`.
124
+ - **Failures are reported, not hidden.** An explanation failure is a warning
125
+ with a stable code (`explain.stage_failed`, `explain.backend_failed`,
126
+ `explain.no_signal`, `explain.reference_unavailable`, ...), never a group
127
+ failure. A failed explanation stage makes the run exit `partial`, as does any
128
+ explanation warning under `run.strict`.
129
+ - **Structured warnings**: every warning carries a stable `code`, its stage and,
130
+ where they apply, the detector and rows affected. They are stored with each
131
+ group, shown with their codes in the run summary, `show` and the report, and
132
+ listed in `run --json` (`warnings`).
133
+ - **CLI**: `init`, `inspect`, `run`, `score`, `report`, `backfill`, `history`,
134
+ `backtest`, `evidence`, `runs`, `show`, `anomalies`, `explain-plan`, `detectors`,
135
+ `config` (`check`, `schema`, `show`) and `workspace` (`ls`, `du`, `prune`,
136
+ `vacuum`, `migrate`, `reset`). `sorethumb run data.csv` works with no config file, and
137
+ the read-only commands after it need none either. Runs are resumable and
138
+ idempotent, tracked in a SQLite workspace. Exit codes are stable (`0` ok,
139
+ `1` runtime failure, `2` pre-flight rejection, `3` not found, `4` partial
140
+ success) and every `--json` command emits one document per outcome; see
141
+ `docs/cli_reference.md`. A group is selected by its label (`run --only-group`,
142
+ `show --group`, `history --group`; the last two also take its key).
143
+ - **Reproducible fits**: KMeans (the detector, its choice of k, and the explanation
144
+ reference) fits on one OpenMP thread, so the same data and seed give the same
145
+ centroids every run; scikit-learn's parallel partial sums otherwise varied by a
146
+ rounding step, which on data with many duplicate rows moved whole tied blocks across
147
+ the KMeans flag fence (up to 90 rows flagged more or fewer between identical KDD Cup
148
+ 1999 runs). About 18% more KMeans fit time at its 200,000-row training cap.
149
+ `threadpoolctl` is now a declared dependency. The real-data backtest pins the KDD
150
+ SA subset's random draw (`random_state=0`), which was different on every load.
151
+ - **Uninformative detector boundaries**: at `scoring.contamination = "auto"`, a
152
+ detector whose own boundary flagged more than half the rows it was fitted on is left
153
+ out of the flag vote (the intersection or union, and the composite's
154
+ median-of-rates cut), with an `UninformativeBoundaryWarning`
155
+ (`scoring.boundary_uninformative`); its scores still rank. On a table of
156
+ low-cardinality categoricals, Isolation Forest's boundary flagged every row (so
157
+ Isolation Forest alone flagged every row, and `union` did too) and One-Class SVM's
158
+ sat on every row, where the side a row fell on was floating-point noise and differed
159
+ by platform. The fit-time rate is saved in each model's manifest
160
+ (`boundary_flag_rate`), so `score --from-run` applies the same verdict; models
161
+ saved before have none and keep their vote. `GroupSummary.abstaining_detectors` and
162
+ `groups[].abstaining_detectors` in `--json` name them. A numeric contamination is
163
+ unaffected.
164
+ - **Reading a run: the ranking first**: every `run` and `score` summary ends with
165
+ the top-ranked rows, flagged or not, and each group keeps its top 20 by combined
166
+ score in `ranked.parquet` (`GroupSummary.top_ranked`, `groups[].top_ranked` in
167
+ `--json`, `sorethumb anomalies --ranked`, and in the report). At the default
168
+ `scoring.contamination = "auto"` the detectors' own boundaries set how many rows
169
+ are flagged, so every run says the size was not chosen (`shortlist_size_chosen:
170
+ false` in `--json`), and a workspace's first run, or any run without a
171
+ `sorethumb.toml`, says it prominently with the count it came to and how to set a
172
+ review budget. The default policy is unchanged. A run, `anomalies` or a report
173
+ with no flagged rows says that is the policy's decision, not a finding that the
174
+ data holds no anomalies, and shows the ranking; a missing results file is
175
+ reported as missing.
176
+ - **Score-forward**: `sorethumb score --from-run RUN_ID` scores new data with a
177
+ fitted run's persisted models and calibrators, without refitting, so scores
178
+ stay on one scale across calls. A config whose fit-time settings differ from
179
+ the source run's is rejected.
180
+ - **History**: `sorethumb backfill` fills missing periods and `sorethumb history`
181
+ reports how the data is changing, scoped to the dataset and the exact
182
+ configuration that produced each period. A period window that selects no rows
183
+ fails immediately, naming the window, the time column and the data's available
184
+ range. See `docs/trends.md`.
185
+ - Four signals, each with a verdict per window (`ok`, caveats with reasons, or
186
+ n/a with why): **population** drift (PSI per input field, and volume; no
187
+ model involved), **scores** and **threshold** (with an anchor run, below),
188
+ and **counts** (the anomaly rate per window with Wilson and Newcombe 95%
189
+ intervals, coverage and matched groups).
190
+ - Under `composite` with a numeric contamination the rate is set by the review
191
+ budget, so no change is shown.
192
+ - **Anchor run** (`history.anchor_run`): each period run also scores its rows
193
+ with one fixed run's models, calibrators and flag rule, so a change in
194
+ anomaly count splits exactly into volume, score drift and threshold drift.
195
+ Every fitting run saves its rule (`anchor_rule.json`, plain JSON), so any
196
+ complete fitting run can be an anchor.
197
+ - Each period run records per-period statistics (`period_stats`): per input
198
+ field, its missing count and percentiles or top values; per detector, score
199
+ percentiles and flag count.
200
+ - `history --json` prints the whole report; each period run's HTML report shows
201
+ it per group, and `index.json` carries it under `trend`.
202
+ - **Reports**: self-contained HTML plus CSV and JSON, re-renderable from
203
+ persisted state with `sorethumb report`.
204
+ - **Windows** (10/11 x64): every command, the Python API and reports work as
205
+ on Linux and macOS, from PowerShell, Windows PowerShell 5.1 or cmd.exe; CI
206
+ runs the unit, contract and integration suites on `windows-latest` for
207
+ every change, plus installs of the built wheel and sdist. Spellings of one Windows
208
+ file that differ only in letter case, separators or a `file://` prefix
209
+ (`C:\Data\x.csv`, `c:/data/x.csv`, `file:///C:/Data/x.csv`) are one dataset
210
+ for history and one configuration for run ids and model reuse; a
211
+ drive-relative path such as `C:data.csv` is rejected with a clear message.
212
+ `~` in `--config`, `--workdir` and `init` paths is expanded by sorethumb
213
+ itself, so it works in cmd.exe too, and a command started with stdin from
214
+ `NUL` (a scheduled task, `subprocess.DEVNULL`) never prompts.
215
+ - **Files held open by another program** (a spreadsheet, a file-sync client,
216
+ antivirus): replacing or deleting a report, model or downloaded file is
217
+ retried briefly, then fails with a `FileInUseError` naming the file.
218
+ `sorethumb workspace prune` deletes everything else it can, keeps a locked
219
+ file indexed for the next prune, and exits non-zero.
220
+ - **Workspace checks before a run**: without Windows long-path support, `init`,
221
+ `run` and `score` stop before doing any work (exit code `2`,
222
+ `PathTooLongError`) when the workspace is too deep for the 260-character
223
+ path limit. A workspace on a network filesystem, a mapped drive or UNC
224
+ path, or in a OneDrive, Dropbox, Google Drive or iCloud folder gets a
225
+ one-line warning.
226
+ - **Text encodings**: a CSV that isn't UTF-8 (Excel's plain "CSV" on Windows
227
+ writes cp1252) fails with a message saying so and how to fix it.
228
+ `report.csv_bom` starts report CSVs with the byte-order mark Excel needs to
229
+ show non-ASCII text. Output redirected to a pipe or file is UTF-8 unless
230
+ `PYTHONIOENCODING` says otherwise, and every `--json` output is
231
+ ASCII-escaped, so it is lossless under any output encoding.
232
+ - **Extras**: `explain` (shap, numba) for TreeSHAP and KernelSHAP attributions;
233
+ `benchmark` (datasets, pandas, pyarrow) for the benchmark harnesses.
234
+ - **Docs**: the README is an entry page: install, the quickstart (the command line
235
+ first, then Python), what it does not do, whether it is the right tool and which
236
+ setup to use, and the platform table. The rest is behind `docs/index.md`:
237
+ `docs/getting-started.md` (the command line step by step, and file formats),
238
+ `docs/limitations.md`, `docs/scale.md` and `docs/platforms.md`, with the benchmark
239
+ headline summary in `docs/benchmarks.md`. `docs/stability.md` sets out the pre-1.0
240
+ API and stability policy;
241
+ `docs/benchmarks.md` holds the benchmark results and their accuracy floors,
242
+ and an ensemble policy matrix: every scenario under every policy, with
243
+ precision, recall, ranking, stability and false-positive figures checked
244
+ against versioned per-anomaly-type budgets, where every budget the default
245
+ misses is listed as an accepted limitation with its reason.
246
+ `docs/ensemble-policy.md` sets out the backtest protocol a candidate policy
247
+ must pass before it can be said to outperform the default (pre-registration,
248
+ disjoint confirmation seeds, paired bootstrap with Holm correction, no budget
249
+ or stability regressions, confirmation in rounds of fresh seeds) and
250
+ publishes each confirmation result. An end-to-end scale benchmark
251
+ (`scripts/run_scale_benchmark.py`) runs a zero-config `sorethumb run` and a
252
+ `score --from-run` at 10,000, 100,000 and 1,000,000 rows, and publishes time per
253
+ stage, peak memory, feature width, each detector's training sample, explanation
254
+ cost and output size. `docs/explanations.md` and
255
+ `docs/trends.md` say what each explanation and trend figure means and where it
256
+ stops.
257
+ - **`sorethumb backtest`**: compares every registered ensemble policy with the
258
+ default on your own data, using `columns.reference_column` labels, and records
259
+ each verdict in the workspace (migration 011, `policy_evidence`). A `run` or
260
+ `score` with a non-default policy that has no `outperforms` record for the
261
+ dataset and configuration warns (`UnevidencedPolicyWarning`); `run.strict`
262
+ refuses it (`UnevidencedPolicyError`, exit code 2). The evidence a run relied
263
+ on is recorded as `run.policy_evidence_id` and in `--json` output, and shown by
264
+ `sorethumb show` and the run summary; `sorethumb evidence` lists the recorded
265
+ evidence and which records apply to the current configuration.
266
+ `sorethumb backtest --forward` tests out of time instead: each period is fitted
267
+ and the next scored with those fits, as `score --from-run` does. A candidate
268
+ must also rank the anomalies no worse than the default (mean average precision
269
+ within `--ranking-tolerance`, 10%): decision rule 2, which `run` requires of the
270
+ evidence it relies on. With `scoring.contamination = "auto"`, `--review-budget`
271
+ states the budget flags are judged against.
272
+ - **Real-data policy evidence**: the pre-registered backtest protocol applied to
273
+ KDD Cup 1999 (SA), Covertype (2 vs 4) and a time-ordered credit-card fraud
274
+ dataset, with disjoint development and confirmation slices; results in
275
+ `docs/ensemble-policy.md`.
276
+ Composite policies must be paired with a numeric `scoring.contamination`: at
277
+ `"auto"`, `composite-agreement@1` flags several times the review budget and
278
+ does not outperform the default (pre-registered, synthetic and real).
279
+ `docs/ensemble-policy.md` gives a recommended setup for users with a review
280
+ budget, and qualifies the default's precision-first rationale: it holds at the
281
+ shipped `"auto"`, and the default's real weakness is unpredictable volume.
282
+
283
+ ### Security
284
+
285
+ - CSV report output is neutralised against spreadsheet formula injection.
286
+ - `sorethumb workspace reset` refuses any target that does not open as a
287
+ sorethumb workspace, and always refuses a filesystem root, the home
288
+ directory, the current directory, a git repository root or a very shallow
289
+ path. It deletes only the entries sorethumb creates, so other files in the
290
+ directory are kept, and removes a symlink or Windows directory junction
291
+ among them without following it. The typed confirmation accepts the path
292
+ however it is spelled: quoted (as Windows Explorer's "Copy as path" gives
293
+ it), with a trailing separator, or on Windows in a different letter case
294
+ or with forward slashes.
295
+ - Credentials are never sent over plaintext HTTP. `source.auth` or
296
+ `user:password@` in an `http://` `source.uri` is rejected at config load, and
297
+ a credentialed request is refused before it is sent on every hop, including
298
+ the first request of an HTTP-to-HTTPS redirect. There is no loopback
299
+ exception.
300
+ - A config validation error never echoes a `user:password@` from
301
+ `source.uri`, and download error messages redact it.
302
+ - Authenticated HTTP(S) downloads (`source.auth`) send the `Authorization`
303
+ header only to the configured origin: never across a redirect to another host
304
+ or port. An HTTPS-to-HTTP redirect is refused, and so is a redirect to a
305
+ different host that resolves to a loopback, link-local, private, reserved
306
+ or multicast address (such as a cloud metadata endpoint). The host you
307
+ configure yourself is never blocked, even an internal one; see SECURITY.md.
308
+ - An unexpected CLI crash never prints local variables, so a traceback cannot
309
+ reveal the download credential.
310
+ - Source URI query values are never stored, logged or reported. The persisted
311
+ and displayed form keeps the scheme, host, non-default port, path and query
312
+ key names and replaces every query value with `REDACTED` (no name list, so
313
+ `client_secret`, `jwt`, `accessKey` and unlisted names are covered); userinfo
314
+ becomes `***@` and the fragment is dropped. This covers `dataset.source_uri`,
315
+ `run.config_json`, `RunResult.dataset_uri`, `run --json`, `show`, reports,
316
+ the `httpx` request log line and download errors, with no opt-out. A new
317
+ `source_digest` (SHA-256 of the canonical full URI) on `dataset` and `run`
318
+ keeps query-differing sources distinguishable. A workspace created by a
319
+ pre-release build is scrubbed when opened (migration 009; see SECURITY.md for
320
+ what it does not reach). Without
321
+ `source.dataset_id`, a run whose query key set differs from the dataset's
322
+ previous run is refused, naming the `dataset_id` that keeps its history.
323
+ - **Persisted-model trust boundary**: a workspace's fitted models are
324
+ `joblib`/pickle files, and `score --from-run`, `run.reuse_models` and
325
+ `history.anchor_run` unpickle them with no sandboxing. That is arbitrary code execution, not safe data
326
+ loading. The SHA-256 digests checked on load catch corruption or a swapped
327
+ file; they are **not** a security boundary against a deliberately malicious
328
+ one. Only use workspaces you created or fully trust. See
329
+ [SECURITY.md](https://github.com/tarne75/sorethumb/blob/main/SECURITY.md).
330
+
331
+ ### Known limitations
332
+
333
+ - **The default `combination="intersection"`** flags a row only when every
334
+ configured detector flags it independently. That is deliberately
335
+ conservative and, on some data shapes, can flag very few true anomalies or
336
+ none. At the default `scoring.contamination = "auto"` you do not control how
337
+ many rows it flags: 0.18x to 8.4x a review budget on the real datasets tested.
338
+ With a fixed review budget, `composite-agreement@1` at that budget is the
339
+ evidenced choice. See `docs/ensemble-policy.md` and `docs/approximations.md`.
340
+ - **Policy evidence has limits.** It covers a maintainer-written synthetic suite
341
+ and three real datasets (one confirmation slice each), with the default three
342
+ detectors at stated review budgets. `rule:discrete-budget@2` won on synthetic
343
+ data only and is not a selectable setting. No policy recovers contextual
344
+ anomalies. Full list: "Where things stand" in `docs/ensemble-policy.md`.
345
+ - **Without the `explain` extra**, Isolation Forest explanations are
346
+ `heuristic` rather than `model_specific`. Explanations are exact only for ECOD
347
+ and HBOS; TreeSHAP decomposes Isolation Forest's path length, not its score.
348
+ - **`run.max_memory_mb` is a projection, not a limit**: it refuses a run whose
349
+ estimated feature-matrix size exceeds it, but cannot cap actual memory use.
350
+ - **How much a categorical column counts is a prior.** One-hot dummies sit at
351
+ a fixed scale (`features.categorical_distance`, 0.5 interquartile ranges per
352
+ change of category), chosen so uninformative categories cannot swamp the
353
+ distance-based detectors; it is not learned from your data. See
354
+ `docs/approximations.md`.
355
+ - Every detector fits on the same unlabelled data it scores; there is no
356
+ held-out "known normal" reference. Independent runs are not on a common score
357
+ scale: `sorethumb history` separates score drift from threshold drift only
358
+ with an anchor run, and `score --from-run` reuses a fitted run's scale. See
359
+ `docs/approximations.md` for this and the other approximations (PCA,
360
+ correlation pruning, row-capped detector training, zero-inflated scaling).
361
+ - **The `swamping` benchmark scenario** found no false-positive inflation from
362
+ unlabelled training contamination; measured on matched data, the effect runs
363
+ the other way (mildly reduced sensitivity).
364
+ - Real-dataset benchmark figures (KDDCup99, Covtype) are capped at 20,000 rows
365
+ for CI runtime, as `docs/benchmarks.md` states where the tables appear.
366
+ - **Dataset size**: measured end to end up to 1,000,000 rows (with the defaults, 6.9
367
+ minutes and 4.9 GB peak memory on an 8-core, 16 GB laptop); larger datasets are
368
+ untested. Explanations are most of the time and memory, and peak memory is set by
369
+ loading, profiling and explanations, not by the projected feature matrix that
370
+ `run.max_memory_mb` checks. See `docs/scale.md`.
371
+ - **Platforms**: native Windows on ARM is untested (the core installs from
372
+ Windows ARM64 wheels, but no CI job runs there, and the `benchmark` extra's
373
+ pyarrow has no such wheel; an x64 Python under emulation is the tested
374
+ route), and on any OS a workspace needs a local drive, not a network share or
375
+ synced folder. See `docs/platforms.md`.
376
+
377
+ [Unreleased]: https://github.com/tarne75/sorethumb/compare/v0.1.0...HEAD
378
+ [0.1.0]: https://github.com/tarne75/sorethumb/releases/tag/v0.1.0
@@ -0,0 +1,40 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to a positive environment:
15
+
16
+ * Demonstrating empathy and kindness toward other people
17
+ * Being respectful of differing opinions, viewpoints, and experiences
18
+ * Giving and gracefully accepting constructive feedback
19
+ * Accepting responsibility and apologizing to those affected by our mistakes
20
+
21
+ Examples of unacceptable behavior:
22
+
23
+ * The use of sexualized language or imagery, and sexual attention or advances
24
+ * Trolling, insulting or derogatory comments, and personal or political attacks
25
+ * Public or private harassment
26
+ * Publishing others' private information without their explicit permission
27
+
28
+ ## Enforcement
29
+
30
+ Community leaders are responsible for clarifying and enforcing our standards.
31
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
32
+ reported to the project maintainer, Tarne Westcott, by emailing
33
+ hello@t4-digital.uk (the project's contact address) with the subject line "sorethumb code of conduct". All complaints will be reviewed and
34
+ investigated promptly and fairly, and reporter identity will be kept
35
+ confidential.
36
+
37
+ ## Attribution
38
+
39
+ This Code of Conduct is adapted from the
40
+ [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
@@ -0,0 +1,175 @@
1
+ # Contributing to sorethumb
2
+
3
+ ## From zero to a passing test suite
4
+
5
+ ```bash
6
+ # 1. Clone and enter the repo
7
+ git clone https://github.com/tarne75/sorethumb.git
8
+ cd sorethumb
9
+
10
+ # 2. Install uv (if not already installed)
11
+ curl -LsSf https://astral.sh/uv/install.sh | sh
12
+
13
+ # 3. Create the virtualenv and install every extra (--frozen: use the
14
+ # committed uv.lock as-is, matching what CI installs)
15
+ uv sync --all-extras --frozen
16
+
17
+ # 4. Install pre-commit hooks
18
+ uv run pre-commit install
19
+
20
+ # 5. Verify everything passes (mirrors CI's required PR lanes)
21
+ uv run ruff check src/ tests/
22
+ uv run ruff format --check src/ tests/
23
+ uv run mypy src/
24
+ uv run python docs/generate_config_docs.py --check
25
+ uv run pytest -m "unit or contract"
26
+ uv run pytest -m integration
27
+ uv run pytest -m property
28
+ uv run pytest -m repo_check
29
+ ```
30
+
31
+ ### Developing on Windows
32
+
33
+ The same steps work in PowerShell, with two differences:
34
+
35
+ - Install uv with `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`
36
+ instead of the `curl` line.
37
+ - `uv run ...` needs no activated virtualenv. To activate one anyway, it is
38
+ `.venv\Scripts\Activate.ps1` (PowerShell may first need
39
+ `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`).
40
+
41
+ Clone onto a local drive, not inside a OneDrive or other synced folder: sync
42
+ clients lock files mid-write, which shows up as intermittent "file is open in
43
+ another program" failures. The repository is checked out with LF line endings
44
+ on every platform (`.gitattributes`), whatever your `core.autocrlf` setting.
45
+
46
+ `scripts/release.sh` is a bash script for maintainers cutting a release; run it
47
+ from macOS, Linux or WSL. Everything else in this guide works natively.
48
+
49
+ If you changed `pyproject.toml`'s dependencies, run `uv lock` to update
50
+ `uv.lock` and commit both together — CI runs `uv lock --check` and will fail
51
+ a PR where they've drifted apart.
52
+
53
+ `uv.lock` pins what CI tests, not what a fresh `pip install sorethumb-ml` gets: that
54
+ resolves the newest release of every dependency. The `Newest dependencies` workflow
55
+ (`.github/workflows/newest-deps.yml`) re-resolves everything at its newest release
56
+ and runs the suite daily. When it fails, an upstream release broke something:
57
+ re-lock (`uv lock --upgrade-package NAME`) and fix the code, or add an upper bound
58
+ in `pyproject.toml` until you can.
59
+
60
+ ## Optional extras
61
+
62
+ The mandatory install (`pip install sorethumb-ml` / `uv sync`) is deliberately
63
+ minimal. Everything below is opt-in, matched to the feature it enables:
64
+
65
+ | Extra | Adds | Enables |
66
+ |---|---|---|
67
+ | `explain` | shap, numba | TreeSHAP / KernelSHAP explanations. Without it, explanations fall back to the pure-numpy gradient method with a warning — the run itself never fails. |
68
+ | `benchmark` | datasets, pandas, pyarrow | `scripts/run_benchmark.py` (both the real-dataset and full-pipeline-scenario suites) and `pytest -m benchmark`. |
69
+ | `dev` | pytest, ruff, mypy, pre-commit, hypothesis, ... | Everything needed to run the test suite and quality checks in this repo. |
70
+
71
+ `uv sync --all-extras --frozen` installs all three, which is what you want for
72
+ contributing. A production install that only ever calls `run_detection`
73
+ without SHAP explanations can skip straight to core: `pip install sorethumb-ml`.
74
+
75
+ ## Running specific test groups
76
+
77
+ Every test carries exactly one marker (see `[tool.pytest.ini_options]` in
78
+ `pyproject.toml`); bare `uv run pytest` runs only `unit`/`contract` by
79
+ default (its `addopts` excludes the rest). These mirror CI's own job-by-job
80
+ `-m` selection (`.github/workflows/ci.yml`) — nothing here should drift from
81
+ what a job actually runs.
82
+
83
+ ```bash
84
+ # Default: unit + contract only — fast, deterministic, no filesystem/
85
+ # network/subprocess/model-serialisation I/O. Matches CI's fast-tests job.
86
+ uv run pytest
87
+
88
+ # Integration: real workspace, SQLite, CLI process, full pipeline, report
89
+ # rendering. May touch the local filesystem and subprocesses, not the network.
90
+ uv run pytest -m integration
91
+
92
+ # Hypothesis-driven property tests.
93
+ uv run pytest -m property
94
+
95
+ # Repository/docs consistency checks (generated docs, README snippets).
96
+ uv run pytest -m repo_check
97
+
98
+ # Benchmark suite (opt-in, measures accuracy on real or synthetic datasets).
99
+ uv run pytest -m benchmark
100
+
101
+ # Every required-PR-lane test in one invocation (matches CI's coverage job).
102
+ uv run pytest -m "unit or contract or integration or property or repo_check"
103
+
104
+ # With coverage. Start coverage.py before pytest: pytest's own startup imports
105
+ # sorethumb_ml, so `pytest --cov` reports every import-time line as untested.
106
+ # `combine` merges in the CLI subprocesses the tests start.
107
+ uv run coverage run -m pytest && uv run coverage combine && uv run coverage report
108
+ ```
109
+
110
+ ## Adding a detector
111
+
112
+ `sorethumb` discovers detectors via the `sorethumb_ml.detectors` entry-point group. You can
113
+ add a detector in a separate package without modifying this repository.
114
+
115
+ ### 1. Implement the `Detector` protocol
116
+
117
+ ```python
118
+ # my_package/my_detector.py
119
+ from typing import ClassVar, Any
120
+ import numpy as np
121
+
122
+
123
+ class MyDetector:
124
+ name: ClassVar[str] = "my_detector"
125
+ supports_tree_shap: ClassVar[bool] = False
126
+ default_train_row_cap: ClassVar[int] = 100_000
127
+
128
+ def fit(self, X: np.ndarray, *, seed: int) -> None:
129
+ # fit your model; store it on self
130
+ ...
131
+
132
+ def score_samples(self, X: np.ndarray) -> np.ndarray:
133
+ # MUST return a finite 1-D numeric ndarray of shape (len(X),), one score
134
+ # per row in input order, higher = MORE NORMAL (sklearn's convention).
135
+ # No NaN/inf, no (n, 1) arrays, no lists.
136
+ ...
137
+
138
+ def natural_flag(self, scores: np.ndarray) -> np.ndarray:
139
+ # MUST return a 1-D ndarray of dtype bool (not int 0/1), same length as
140
+ # `scores`: True = anomaly, using the model's own boundary
141
+ ...
142
+
143
+ def get_params(self) -> dict[str, Any]:
144
+ return {}
145
+ ```
146
+
147
+ ### 2. Register it via entry points in your package's `pyproject.toml`
148
+
149
+ ```toml
150
+ [project.entry-points."sorethumb_ml.detectors"]
151
+ my_detector = "my_package.my_detector:MyDetector"
152
+ ```
153
+
154
+ ### 3. Verify it appears
155
+
156
+ ```bash
157
+ pip install -e .
158
+ sorethumb detectors
159
+ ```
160
+
161
+ Your detector should appear in the list alongside the built-ins.
162
+
163
+ The exact output contract (shape, dtype, finiteness, fitted-state rules) and the
164
+ `DetectorError` raised when it is broken are in
165
+ [Writing a detector plugin](docs/models.md#writing-a-detector-plugin).
166
+
167
+ ## Code standards
168
+
169
+ - ruff for linting and formatting (`uv run ruff check --fix src/`, then `uv run ruff format src/`)
170
+ - mypy strict on `src/` (`uv run mypy src/`)
171
+ - No `print` in `src/sorethumb_ml/` — use `logging.getLogger(__name__)`
172
+ - A threshold a user may need to change is a field in `config.py`, with a
173
+ description; a fixed internal constant is a named module-level constant with a
174
+ comment saying why it has that value, never a bare number in the logic
175
+ - Every new degradation point gets a named `SorethumbWarning` subclass in `errors.py`