mostlyright-data 0.9.0__py3-none-any.whl

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 (314) hide show
  1. mostlyright/data_harness/__init__.py +158 -0
  2. mostlyright/data_harness/acquisition/__init__.py +55 -0
  3. mostlyright/data_harness/acquisition/http.py +2773 -0
  4. mostlyright/data_harness/acquisition/parsing.py +809 -0
  5. mostlyright/data_harness/acquisition/ranges.py +495 -0
  6. mostlyright/data_harness/acquisition/result_download.py +360 -0
  7. mostlyright/data_harness/acquisition/retention_admission.py +248 -0
  8. mostlyright/data_harness/acquisition/sandbox.py +4888 -0
  9. mostlyright/data_harness/acquisition/url_policy.py +530 -0
  10. mostlyright/data_harness/agent_runtime.py +2743 -0
  11. mostlyright/data_harness/assets/logo-ink.svg +31 -0
  12. mostlyright/data_harness/backends/__init__.py +28 -0
  13. mostlyright/data_harness/backends/pandas_backend.py +350 -0
  14. mostlyright/data_harness/backends/polars_backend.py +366 -0
  15. mostlyright/data_harness/backends/protocol.py +124 -0
  16. mostlyright/data_harness/backends/reference.py +83 -0
  17. mostlyright/data_harness/backends/registry.py +55 -0
  18. mostlyright/data_harness/backends/restrictions.py +126 -0
  19. mostlyright/data_harness/canonical.py +333 -0
  20. mostlyright/data_harness/catalog_job.py +625 -0
  21. mostlyright/data_harness/cli.py +5398 -0
  22. mostlyright/data_harness/contracts.py +53 -0
  23. mostlyright/data_harness/coordinator.py +1307 -0
  24. mostlyright/data_harness/deploy.py +924 -0
  25. mostlyright/data_harness/deploy_target.py +312 -0
  26. mostlyright/data_harness/deployment_evidence.py +1067 -0
  27. mostlyright/data_harness/event_presentation.py +576 -0
  28. mostlyright/data_harness/events.py +2152 -0
  29. mostlyright/data_harness/fast_delimited.py +239 -0
  30. mostlyright/data_harness/fleet.py +237 -0
  31. mostlyright/data_harness/formats.py +236 -0
  32. mostlyright/data_harness/governors.py +1163 -0
  33. mostlyright/data_harness/hosted_bootstrap.py +972 -0
  34. mostlyright/data_harness/hosted_crawler.py +1115 -0
  35. mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
  36. mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
  37. mostlyright/data_harness/hosted_crawler_job.py +1277 -0
  38. mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
  39. mostlyright/data_harness/hosted_dataset.py +1500 -0
  40. mostlyright/data_harness/hosted_deploy.py +3037 -0
  41. mostlyright/data_harness/hosted_handoff.py +62 -0
  42. mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
  43. mostlyright/data_harness/hosted_ingestion_job.py +356 -0
  44. mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
  45. mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
  46. mostlyright/data_harness/hosted_session_worker.py +3554 -0
  47. mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
  48. mostlyright/data_harness/hosted_worker.py +6784 -0
  49. mostlyright/data_harness/ingestion/__init__.py +56 -0
  50. mostlyright/data_harness/ingestion/contracts.py +461 -0
  51. mostlyright/data_harness/ingestion/faults.py +42 -0
  52. mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
  53. mostlyright/data_harness/ingestion/spool.py +130 -0
  54. mostlyright/data_harness/ingestion/store.py +885 -0
  55. mostlyright/data_harness/key_seam.py +434 -0
  56. mostlyright/data_harness/linux_process_boundary.py +262 -0
  57. mostlyright/data_harness/local_contracts.py +2880 -0
  58. mostlyright/data_harness/local_search/__init__.py +5 -0
  59. mostlyright/data_harness/local_search/build_index.py +1087 -0
  60. mostlyright/data_harness/local_search/contracts.py +920 -0
  61. mostlyright/data_harness/local_search/query_trace.py +266 -0
  62. mostlyright/data_harness/local_search/retrieval.py +700 -0
  63. mostlyright/data_harness/local_search/sealed.py +474 -0
  64. mostlyright/data_harness/local_search/service.py +784 -0
  65. mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
  66. mostlyright/data_harness/nbrender/__init__.py +12 -0
  67. mostlyright/data_harness/nbrender/chrome.py +359 -0
  68. mostlyright/data_harness/nbrender/code_body.py +266 -0
  69. mostlyright/data_harness/nbrender/document.py +407 -0
  70. mostlyright/data_harness/nbrender/frame.py +275 -0
  71. mostlyright/data_harness/nbrender/interactive.py +337 -0
  72. mostlyright/data_harness/nbrender/markdown_body.py +477 -0
  73. mostlyright/data_harness/nbrender/mr_components.py +134 -0
  74. mostlyright/data_harness/nbrender/outputs_data.py +595 -0
  75. mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
  76. mostlyright/data_harness/nbrender/outputs_source.py +260 -0
  77. mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
  78. mostlyright/data_harness/nbrender/outputs_text.py +400 -0
  79. mostlyright/data_harness/nbrender/parse.py +394 -0
  80. mostlyright/data_harness/nbrender/status.py +40 -0
  81. mostlyright/data_harness/nbrender/tokens.py +1295 -0
  82. mostlyright/data_harness/notebook.py +1710 -0
  83. mostlyright/data_harness/offline.py +2049 -0
  84. mostlyright/data_harness/operation_registry.py +1007 -0
  85. mostlyright/data_harness/operator_setup.py +239 -0
  86. mostlyright/data_harness/pipeline.py +6428 -0
  87. mostlyright/data_harness/plan_graph.py +2026 -0
  88. mostlyright/data_harness/preparation/__init__.py +104 -0
  89. mostlyright/data_harness/preparation/contracts.py +1017 -0
  90. mostlyright/data_harness/preparation/engine.py +221 -0
  91. mostlyright/data_harness/preparation/errors.py +14 -0
  92. mostlyright/data_harness/preparation/gates.py +751 -0
  93. mostlyright/data_harness/preparation/joins.py +574 -0
  94. mostlyright/data_harness/preparation/profile.py +384 -0
  95. mostlyright/data_harness/preparation/table.py +217 -0
  96. mostlyright/data_harness/preparation/transforms.py +568 -0
  97. mostlyright/data_harness/progress_events.py +534 -0
  98. mostlyright/data_harness/readers/__init__.py +46 -0
  99. mostlyright/data_harness/readers/containers.py +963 -0
  100. mostlyright/data_harness/readers/contracts.py +542 -0
  101. mostlyright/data_harness/readers/delimited.py +257 -0
  102. mostlyright/data_harness/readers/grib2/__init__.py +33 -0
  103. mostlyright/data_harness/readers/grib2/admission.py +722 -0
  104. mostlyright/data_harness/readers/grib2/decode.py +1009 -0
  105. mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
  106. mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
  107. mostlyright/data_harness/readers/json_tabular.py +485 -0
  108. mostlyright/data_harness/readers/registry.py +514 -0
  109. mostlyright/data_harness/readers/samples/README.md +110 -0
  110. mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
  111. mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
  112. mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
  113. mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
  114. mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
  115. mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
  116. mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
  117. mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
  118. mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
  119. mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
  120. mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
  121. mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
  122. mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
  123. mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
  124. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
  125. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
  126. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
  127. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
  128. mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
  129. mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
  130. mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
  131. mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
  132. mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
  133. mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
  134. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
  135. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
  136. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
  137. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
  138. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
  139. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
  140. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
  141. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
  142. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
  143. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
  144. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
  145. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
  146. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
  147. mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
  148. mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
  149. mostlyright/data_harness/readers/samples.py +582 -0
  150. mostlyright/data_harness/readers/spreadsheet.py +803 -0
  151. mostlyright/data_harness/readers/tabular.py +510 -0
  152. mostlyright/data_harness/recipe.py +5321 -0
  153. mostlyright/data_harness/repair/__init__.py +78 -0
  154. mostlyright/data_harness/repair/adapters.py +274 -0
  155. mostlyright/data_harness/repair/contracts.py +872 -0
  156. mostlyright/data_harness/repair/coordinator.py +1099 -0
  157. mostlyright/data_harness/repair/errors.py +16 -0
  158. mostlyright/data_harness/review.py +2533 -0
  159. mostlyright/data_harness/rowset.py +283 -0
  160. mostlyright/data_harness/serving.py +1975 -0
  161. mostlyright/data_harness/serving_edge.py +590 -0
  162. mostlyright/data_harness/serving_http.py +1031 -0
  163. mostlyright/data_harness/session_probes.py +759 -0
  164. mostlyright/data_harness/signing.py +101 -0
  165. mostlyright/data_harness/source_discovery.py +898 -0
  166. mostlyright/data_harness/sources/__init__.py +209 -0
  167. mostlyright/data_harness/sources/_adapter_steps.py +213 -0
  168. mostlyright/data_harness/sources/adapters.py +1214 -0
  169. mostlyright/data_harness/sources/cadence.py +1428 -0
  170. mostlyright/data_harness/sources/cadence_emission.py +453 -0
  171. mostlyright/data_harness/sources/cadence_history.py +546 -0
  172. mostlyright/data_harness/sources/catalog/__init__.py +17 -0
  173. mostlyright/data_harness/sources/catalog/admission.py +477 -0
  174. mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
  175. mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
  176. mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
  177. mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
  178. mostlyright/data_harness/sources/catalog/channel.py +523 -0
  179. mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
  180. mostlyright/data_harness/sources/catalog/contracts.py +825 -0
  181. mostlyright/data_harness/sources/catalog/coverage.py +137 -0
  182. mostlyright/data_harness/sources/catalog/delta.py +1340 -0
  183. mostlyright/data_harness/sources/catalog/embedding.py +532 -0
  184. mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
  185. mostlyright/data_harness/sources/catalog/fill.py +3889 -0
  186. mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
  187. mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
  188. mostlyright/data_harness/sources/catalog/gating.py +374 -0
  189. mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
  190. mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
  191. mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
  192. mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
  193. mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
  194. mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
  195. mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
  196. mostlyright/data_harness/sources/catalog/health.py +447 -0
  197. mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
  198. mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
  199. mostlyright/data_harness/sources/catalog/neural.py +1618 -0
  200. mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
  201. mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
  202. mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
  203. mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
  204. mostlyright/data_harness/sources/catalog/recommend.py +171 -0
  205. mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
  206. mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
  207. mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
  208. mostlyright/data_harness/sources/catalog/sealed.py +560 -0
  209. mostlyright/data_harness/sources/catalog/search.py +230 -0
  210. mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
  211. mostlyright/data_harness/sources/catalog/update.py +891 -0
  212. mostlyright/data_harness/sources/collections.py +815 -0
  213. mostlyright/data_harness/sources/contracts.py +2223 -0
  214. mostlyright/data_harness/sources/deletion.py +761 -0
  215. mostlyright/data_harness/sources/fitness.py +162 -0
  216. mostlyright/data_harness/sources/governance.py +163 -0
  217. mostlyright/data_harness/sources/hosted.py +173 -0
  218. mostlyright/data_harness/sources/integration.py +218 -0
  219. mostlyright/data_harness/sources/range_reader.py +418 -0
  220. mostlyright/data_harness/sources/registry.py +514 -0
  221. mostlyright/data_harness/sources/rights_rule.py +59 -0
  222. mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
  223. mostlyright/data_harness/sources/sports.py +521 -0
  224. mostlyright/data_harness/sources/stream.py +524 -0
  225. mostlyright/data_harness/sources/stream_connector.py +418 -0
  226. mostlyright/data_harness/sources/stream_recorder.py +1404 -0
  227. mostlyright/data_harness/studio_boundary.py +2019 -0
  228. mostlyright/data_harness/thin/__init__.py +37 -0
  229. mostlyright/data_harness/thin/acquire.py +1137 -0
  230. mostlyright/data_harness/thin/acquire_cancel.py +579 -0
  231. mostlyright/data_harness/thin/approvals.py +617 -0
  232. mostlyright/data_harness/thin/commands.py +406 -0
  233. mostlyright/data_harness/thin/download.py +194 -0
  234. mostlyright/data_harness/thin/narrative.py +589 -0
  235. mostlyright/data_harness/thin/parity.py +1070 -0
  236. mostlyright/data_harness/thin/propose.py +2759 -0
  237. mostlyright/data_harness/thin/research.py +1663 -0
  238. mostlyright/data_harness/thin/router.py +924 -0
  239. mostlyright/data_harness/thin/runs.py +519 -0
  240. mostlyright/data_harness/thin/session.py +281 -0
  241. mostlyright/data_harness/thin/stream.py +501 -0
  242. mostlyright/data_harness/thin/transport.py +187 -0
  243. mostlyright/data_harness/thin/vocabulary.py +368 -0
  244. mostlyright/data_harness/thin/workers.py +164 -0
  245. mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
  246. mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
  247. mostlyright/data_harness/unit_flow.py +927 -0
  248. mostlyright/data_harness/units.py +572 -0
  249. mostlyright/data_harness/ux/__init__.py +9 -0
  250. mostlyright/data_harness/ux/approve.py +485 -0
  251. mostlyright/data_harness/ux/author_yaml.py +597 -0
  252. mostlyright/data_harness/ux/cloud_auth.py +447 -0
  253. mostlyright/data_harness/ux/commands/__init__.py +260 -0
  254. mostlyright/data_harness/ux/commands/approve.py +136 -0
  255. mostlyright/data_harness/ux/commands/auth.py +744 -0
  256. mostlyright/data_harness/ux/commands/author.py +79 -0
  257. mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
  258. mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
  259. mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
  260. mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
  261. mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
  262. mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
  263. mostlyright/data_harness/ux/commands/deploy.py +134 -0
  264. mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
  265. mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
  266. mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
  267. mostlyright/data_harness/ux/commands/diff.py +74 -0
  268. mostlyright/data_harness/ux/commands/index.py +84 -0
  269. mostlyright/data_harness/ux/commands/inventory.py +47 -0
  270. mostlyright/data_harness/ux/commands/list_builds.py +143 -0
  271. mostlyright/data_harness/ux/commands/login.py +63 -0
  272. mostlyright/data_harness/ux/commands/peek.py +236 -0
  273. mostlyright/data_harness/ux/commands/plan_check.py +90 -0
  274. mostlyright/data_harness/ux/commands/preflight.py +97 -0
  275. mostlyright/data_harness/ux/commands/record.py +107 -0
  276. mostlyright/data_harness/ux/commands/review_setup.py +47 -0
  277. mostlyright/data_harness/ux/commands/search.py +440 -0
  278. mostlyright/data_harness/ux/commands/show.py +61 -0
  279. mostlyright/data_harness/ux/commands/whoami.py +37 -0
  280. mostlyright/data_harness/ux/credential_native.py +551 -0
  281. mostlyright/data_harness/ux/credential_store.py +1055 -0
  282. mostlyright/data_harness/ux/credentials.py +631 -0
  283. mostlyright/data_harness/ux/diffing.py +444 -0
  284. mostlyright/data_harness/ux/headline.py +671 -0
  285. mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
  286. mostlyright/data_harness/ux/hosted_run_status.py +619 -0
  287. mostlyright/data_harness/ux/inventory.py +427 -0
  288. mostlyright/data_harness/ux/local_review.py +375 -0
  289. mostlyright/data_harness/ux/login.py +691 -0
  290. mostlyright/data_harness/ux/path_kind.py +147 -0
  291. mostlyright/data_harness/ux/peek.py +1000 -0
  292. mostlyright/data_harness/ux/plain_file.py +178 -0
  293. mostlyright/data_harness/ux/plan_check.py +311 -0
  294. mostlyright/data_harness/ux/preflight.py +918 -0
  295. mostlyright/data_harness/ux/readers.py +1124 -0
  296. mostlyright/data_harness/ux/remediation.py +2195 -0
  297. mostlyright/data_harness/ux/render.py +657 -0
  298. mostlyright/data_harness/ux/workload.py +1077 -0
  299. mostlyright/data_harness/viewer.py +3713 -0
  300. mostlyright/data_harness/visual_run/__init__.py +83 -0
  301. mostlyright/data_harness/visual_run/authoring.py +235 -0
  302. mostlyright/data_harness/visual_run/contracts.py +673 -0
  303. mostlyright/data_harness/visual_run/materialize.py +486 -0
  304. mostlyright/data_harness/visual_run/observations.py +874 -0
  305. mostlyright/data_harness/visual_run/query.py +259 -0
  306. mostlyright/data_harness/visual_run/reducer.py +280 -0
  307. mostlyright/data_harness/visual_run/sdk.py +892 -0
  308. mostlyright/data_harness/visual_run/store.py +584 -0
  309. mostlyright/data_harness/visual_run/transport.py +239 -0
  310. mostlyright/data_harness/watch.py +2999 -0
  311. mostlyright_data-0.9.0.dist-info/METADATA +607 -0
  312. mostlyright_data-0.9.0.dist-info/RECORD +314 -0
  313. mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
  314. mostlyright_data-0.9.0.dist-info/entry_points.txt +12 -0
@@ -0,0 +1,872 @@
1
+ """Closed contracts for immutable, bounded repair and closure review."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import re
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ from cryptography.hazmat.primitives import serialization
11
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
12
+
13
+ from mostlyright.data_harness.canonical import canonical_sha256
14
+ from mostlyright.data_harness.preparation.contracts import (
15
+ QUALITY_CHECK_POLICY,
16
+ AsOfJoinSpec,
17
+ EqualityJoinSpec,
18
+ OperationSpec,
19
+ )
20
+ from mostlyright.data_harness.repair.adapters import (
21
+ JoinSpec,
22
+ preparation_value,
23
+ )
24
+ from mostlyright.data_harness.repair.errors import RepairError
25
+
26
+ REPAIR_CONTRACT_VERSION = "repair.v1"
27
+ REPAIR_ASSIGNMENT_VERSION = "repair-assignment.v1"
28
+ REPAIR_SUCCESSOR_VERSION = "repair-successor.v1"
29
+ CYCLE_ANCHOR_VERSION = "repair-cycle-anchor.v1"
30
+ CLOSURE_SCOPE_VERSION = "repair-closure-scope.v1"
31
+ CLOSURE_REPORT_VERSION = "repair-closure-report.v1"
32
+ CLOSURE_DECISION_VERSION = "repair-closure-decision.v1"
33
+ MAX_FAILED_CANDIDATES = 3
34
+ MAX_REPAIR_ACTIONS = 32
35
+ MAX_FINDINGS = 256
36
+
37
+ BLOCKING_SEVERITIES = frozenset({"blocker", "critical", "high"})
38
+ SEVERITIES = frozenset({*BLOCKING_SEVERITIES, "medium", "low"})
39
+ REPAIR_ACTION_KINDS = frozenset(
40
+ {
41
+ "append_operation",
42
+ "replace_operation",
43
+ "remove_operation",
44
+ "append_join",
45
+ "replace_join",
46
+ "remove_join",
47
+ "replace_output_columns",
48
+ }
49
+ )
50
+ # Repair contracts require canonical identifier order, while quality reports retain the
51
+ # policy-owned presentation order.
52
+ ALL_QUALITY_CHECK_IDS = tuple(sorted(QUALITY_CHECK_POLICY))
53
+ REQUIRED_CLOSURE_LENSES = ("data_quality_temporal", "repair_correctness")
54
+
55
+ _IDENT = re.compile(r"\A[A-Za-z][A-Za-z0-9_.-]{0,127}\Z")
56
+ _DIGEST = re.compile(r"\A[0-9a-f]{64}\Z")
57
+
58
+
59
+ def _identifier(value: Any, path: str) -> str:
60
+ if not isinstance(value, str) or _IDENT.fullmatch(value) is None:
61
+ raise RepairError("REPAIR_IDENTIFIER", path, "must be a canonical identifier")
62
+ return value
63
+
64
+
65
+ def _digest(value: Any, path: str) -> str:
66
+ if not isinstance(value, str) or _DIGEST.fullmatch(value) is None:
67
+ raise RepairError("REPAIR_DIGEST", path, "must be a lowercase SHA-256 digest")
68
+ return value
69
+
70
+
71
+ def _text(value: Any, path: str, *, maximum: int = 4_096) -> str:
72
+ if not isinstance(value, str) or not value or len(value) > maximum:
73
+ raise RepairError("REPAIR_TEXT", path, f"must be 1..{maximum} characters")
74
+ if any(0xD800 <= ord(character) <= 0xDFFF for character in value):
75
+ raise RepairError("REPAIR_UNICODE", path, "contains an unpaired surrogate")
76
+ return value
77
+
78
+
79
+ def _identifiers(
80
+ value: Any,
81
+ path: str,
82
+ *,
83
+ minimum: int = 1,
84
+ allowed: frozenset[str] | None = None,
85
+ ) -> tuple[str, ...]:
86
+ if not isinstance(value, tuple) or not minimum <= len(value) <= 256:
87
+ raise RepairError(
88
+ "REPAIR_COLLECTION",
89
+ path,
90
+ f"must be an immutable tuple with {minimum}..256 members",
91
+ )
92
+ result = tuple(_identifier(item, f"{path}[{index}]") for index, item in enumerate(value))
93
+ if result != tuple(sorted(set(result))):
94
+ raise RepairError("REPAIR_ORDER", path, "must be unique and canonically sorted")
95
+ if allowed is not None and any(item not in allowed for item in result):
96
+ raise RepairError("REPAIR_VALUE", path, "contains an unsupported identifier")
97
+ return result
98
+
99
+
100
+ def _public_key(value: Any, path: str) -> Ed25519PublicKey:
101
+ if not isinstance(value, str):
102
+ raise RepairError("REPAIR_PUBLIC_KEY", path, "must be base64 text")
103
+ try:
104
+ raw = base64.b64decode(value, validate=True)
105
+ return Ed25519PublicKey.from_public_bytes(raw)
106
+ except (ValueError, TypeError) as exc:
107
+ raise RepairError("REPAIR_PUBLIC_KEY", path, "is not an Ed25519 public key") from exc
108
+
109
+
110
+ def public_key_text(public_key: Ed25519PublicKey) -> str:
111
+ raw = public_key.public_bytes(
112
+ serialization.Encoding.Raw,
113
+ serialization.PublicFormat.Raw,
114
+ )
115
+ return base64.b64encode(raw).decode("ascii")
116
+
117
+
118
+ def load_public_key(value: str) -> Ed25519PublicKey:
119
+ """Load one enrolled public key without exposing private material."""
120
+
121
+ return _public_key(value, "public_key")
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class CandidateReference:
126
+ """Exact immutable candidate state known to the trusted coordinator."""
127
+
128
+ candidate_digest: str
129
+ plan_digest: str
130
+ validation_policy_digest: str
131
+ input_set_digest: str
132
+ evidence_digest: str
133
+ producer_principal: str
134
+ producer_attempt: str
135
+ status: str
136
+ failed_candidate_count: int
137
+
138
+ def __post_init__(self) -> None:
139
+ for name in (
140
+ "candidate_digest",
141
+ "plan_digest",
142
+ "validation_policy_digest",
143
+ "input_set_digest",
144
+ "evidence_digest",
145
+ ):
146
+ _digest(getattr(self, name), f"candidate.{name}")
147
+ _identifier(self.producer_principal, "candidate.producer_principal")
148
+ _identifier(self.producer_attempt, "candidate.producer_attempt")
149
+ if self.status not in {
150
+ "fixes_required",
151
+ "human_direction_required",
152
+ "pending_independent_review",
153
+ "reviewed_successor",
154
+ }:
155
+ raise RepairError("REPAIR_STATUS", "candidate.status", "is unsupported")
156
+ if (
157
+ type(self.failed_candidate_count) is not int
158
+ or not 0 <= self.failed_candidate_count <= MAX_FAILED_CANDIDATES
159
+ ):
160
+ raise RepairError(
161
+ "REPAIR_COUNTER",
162
+ "candidate.failed_candidate_count",
163
+ "must be between zero and three",
164
+ )
165
+ if self.status == "fixes_required" and self.failed_candidate_count not in {1, 2}:
166
+ raise RepairError(
167
+ "REPAIR_COUNTER",
168
+ "candidate.failed_candidate_count",
169
+ "fixes_required candidates must have failed count one or two",
170
+ )
171
+ if (
172
+ self.status == "human_direction_required"
173
+ and self.failed_candidate_count != MAX_FAILED_CANDIDATES
174
+ ):
175
+ raise RepairError(
176
+ "REPAIR_COUNTER",
177
+ "candidate.failed_candidate_count",
178
+ "human direction requires exactly three failed candidates",
179
+ )
180
+
181
+ def to_dict(self) -> dict[str, Any]:
182
+ return preparation_value(self)
183
+
184
+
185
+ @dataclass(frozen=True)
186
+ class RepairFinding:
187
+ """One stable finding; closure is evidence-derived, never a caller Boolean."""
188
+
189
+ finding_id: str
190
+ severity: str
191
+ invariant: str
192
+ evidence_digest: str
193
+ affected_artifact_digest: str
194
+ acceptance_condition: str
195
+ affected_check_ids: tuple[str, ...]
196
+ reviewer_principal: str
197
+
198
+ def __post_init__(self) -> None:
199
+ _identifier(self.finding_id, "finding.finding_id")
200
+ if self.severity not in SEVERITIES:
201
+ raise RepairError("REPAIR_SEVERITY", "finding.severity", "is unsupported")
202
+ _text(self.invariant, "finding.invariant")
203
+ _digest(self.evidence_digest, "finding.evidence_digest")
204
+ _digest(self.affected_artifact_digest, "finding.affected_artifact_digest")
205
+ _text(self.acceptance_condition, "finding.acceptance_condition")
206
+ _identifiers(
207
+ self.affected_check_ids,
208
+ "finding.affected_check_ids",
209
+ allowed=frozenset(ALL_QUALITY_CHECK_IDS),
210
+ )
211
+ _identifier(self.reviewer_principal, "finding.reviewer_principal")
212
+
213
+ def to_dict(self) -> dict[str, Any]:
214
+ return preparation_value(self)
215
+
216
+
217
+ @dataclass(frozen=True)
218
+ class FindingInventory:
219
+ blocking: tuple[RepairFinding, ...]
220
+ backlog: tuple[RepairFinding, ...]
221
+
222
+ def __post_init__(self) -> None:
223
+ if not isinstance(self.blocking, tuple) or not 1 <= len(self.blocking) <= MAX_FINDINGS:
224
+ raise RepairError(
225
+ "REPAIR_FINDINGS",
226
+ "inventory.blocking",
227
+ "must contain 1..256 findings",
228
+ )
229
+ if not isinstance(self.backlog, tuple) or len(self.backlog) > MAX_FINDINGS:
230
+ raise RepairError(
231
+ "REPAIR_FINDINGS",
232
+ "inventory.backlog",
233
+ "must contain at most 256 findings",
234
+ )
235
+ if any(item.severity not in BLOCKING_SEVERITIES for item in self.blocking):
236
+ raise RepairError(
237
+ "REPAIR_FINDINGS",
238
+ "inventory.blocking",
239
+ "contains a non-blocking severity",
240
+ )
241
+ if any(item.severity in BLOCKING_SEVERITIES for item in self.backlog):
242
+ raise RepairError(
243
+ "REPAIR_FINDINGS",
244
+ "inventory.backlog",
245
+ "contains a blocking severity",
246
+ )
247
+ blocking_ids = tuple(item.finding_id for item in self.blocking)
248
+ backlog_ids = tuple(item.finding_id for item in self.backlog)
249
+ if blocking_ids != tuple(sorted(set(blocking_ids))) or backlog_ids != tuple(
250
+ sorted(set(backlog_ids))
251
+ ):
252
+ raise RepairError(
253
+ "REPAIR_ORDER",
254
+ "inventory",
255
+ "finding partitions must be canonically sorted",
256
+ )
257
+ if set(blocking_ids) & set(backlog_ids):
258
+ raise RepairError(
259
+ "REPAIR_FINDINGS",
260
+ "inventory",
261
+ "finding IDs must be globally unique",
262
+ )
263
+
264
+ @property
265
+ def digest(self) -> str:
266
+ return canonical_sha256(self.to_dict())
267
+
268
+ def to_dict(self) -> dict[str, Any]:
269
+ return {
270
+ "blocking": [item.to_dict() for item in self.blocking],
271
+ "backlog": [item.to_dict() for item in self.backlog],
272
+ }
273
+
274
+
275
+ @dataclass(frozen=True)
276
+ class RepairAction:
277
+ """One operation from the closed repair-plan edit registry."""
278
+
279
+ action_id: str
280
+ kind: str
281
+ target_id: str
282
+ expected_before_digest: str | None
283
+ replacement: OperationSpec | JoinSpec | tuple[str, ...] | None
284
+
285
+ def __post_init__(self) -> None:
286
+ _identifier(self.action_id, "action.action_id")
287
+ if self.kind not in REPAIR_ACTION_KINDS:
288
+ raise RepairError("REPAIR_ACTION_KIND", "action.kind", "is not allowlisted")
289
+ _identifier(self.target_id, "action.target_id")
290
+ append = self.kind in {"append_operation", "append_join"}
291
+ if append != (self.expected_before_digest is None):
292
+ raise RepairError(
293
+ "REPAIR_ACTION_SHAPE",
294
+ "action.expected_before_digest",
295
+ "must be absent only for append actions",
296
+ )
297
+ if self.expected_before_digest is not None:
298
+ _digest(self.expected_before_digest, "action.expected_before_digest")
299
+ if self.kind in {"append_operation", "replace_operation"}:
300
+ if not isinstance(self.replacement, OperationSpec):
301
+ raise RepairError(
302
+ "REPAIR_ACTION_SHAPE",
303
+ "action.replacement",
304
+ "must be an OperationSpec",
305
+ )
306
+ if self.replacement.operation_id != self.target_id:
307
+ raise RepairError(
308
+ "REPAIR_ACTION_SHAPE",
309
+ "action.target_id",
310
+ "must equal the replacement operation ID",
311
+ )
312
+ elif self.kind == "remove_operation":
313
+ if self.replacement is not None:
314
+ raise RepairError(
315
+ "REPAIR_ACTION_SHAPE",
316
+ "action.replacement",
317
+ "must be absent for removal",
318
+ )
319
+ elif self.kind in {"append_join", "replace_join"}:
320
+ if not isinstance(self.replacement, (EqualityJoinSpec, AsOfJoinSpec)):
321
+ raise RepairError(
322
+ "REPAIR_ACTION_SHAPE",
323
+ "action.replacement",
324
+ "must be a supported join",
325
+ )
326
+ if self.replacement.join_id != self.target_id:
327
+ raise RepairError(
328
+ "REPAIR_ACTION_SHAPE",
329
+ "action.target_id",
330
+ "must equal the replacement join ID",
331
+ )
332
+ elif self.kind == "remove_join":
333
+ if self.replacement is not None:
334
+ raise RepairError(
335
+ "REPAIR_ACTION_SHAPE",
336
+ "action.replacement",
337
+ "must be absent for removal",
338
+ )
339
+ elif self.kind == "replace_output_columns":
340
+ if not isinstance(self.replacement, tuple):
341
+ raise RepairError(
342
+ "REPAIR_ACTION_SHAPE",
343
+ "action.replacement",
344
+ "must be an immutable tuple",
345
+ )
346
+ values = tuple(
347
+ _identifier(item, f"action.replacement[{index}]")
348
+ for index, item in enumerate(self.replacement)
349
+ )
350
+ if not values or len(values) != len(set(values)):
351
+ raise RepairError(
352
+ "REPAIR_ACTION_SHAPE",
353
+ "action.replacement",
354
+ "must contain unique ordered output columns",
355
+ )
356
+ if self.target_id != "output_columns":
357
+ raise RepairError(
358
+ "REPAIR_ACTION_SHAPE",
359
+ "action.target_id",
360
+ "must be output_columns for an output replacement",
361
+ )
362
+
363
+ def to_dict(self) -> dict[str, Any]:
364
+ return preparation_value(self)
365
+
366
+
367
+ @dataclass(frozen=True)
368
+ class CycleAnchor:
369
+ """Externally signed monotonic coordinator state."""
370
+
371
+ schema_version: str
372
+ run_id: str
373
+ root_candidate_digest: str
374
+ current_candidate_digest: str
375
+ current_state_digest: str
376
+ inventory_digest: str
377
+ failed_candidate_count: int
378
+ sequence: int
379
+ predecessor_anchor_digest: str | None
380
+ authority_key_id: str
381
+ authority_public_key: str
382
+ signature: str
383
+
384
+ def __post_init__(self) -> None:
385
+ if self.schema_version != CYCLE_ANCHOR_VERSION:
386
+ raise RepairError("REPAIR_ANCHOR_VERSION", "anchor.schema_version", "is unsupported")
387
+ _identifier(self.run_id, "anchor.run_id")
388
+ for name in (
389
+ "root_candidate_digest",
390
+ "current_candidate_digest",
391
+ "current_state_digest",
392
+ "inventory_digest",
393
+ ):
394
+ _digest(getattr(self, name), f"anchor.{name}")
395
+ if (
396
+ type(self.failed_candidate_count) is not int
397
+ or not 0 <= self.failed_candidate_count <= MAX_FAILED_CANDIDATES
398
+ ):
399
+ raise RepairError("REPAIR_COUNTER", "anchor.failed_candidate_count", "is invalid")
400
+ if type(self.sequence) is not int or self.sequence < 0:
401
+ raise RepairError("REPAIR_COUNTER", "anchor.sequence", "is invalid")
402
+ if self.predecessor_anchor_digest is not None:
403
+ _digest(self.predecessor_anchor_digest, "anchor.predecessor_anchor_digest")
404
+ if (self.sequence == 0) != (self.predecessor_anchor_digest is None):
405
+ raise RepairError(
406
+ "REPAIR_ANCHOR_CHAIN",
407
+ "anchor.predecessor_anchor_digest",
408
+ "must be absent only for sequence zero",
409
+ )
410
+ _identifier(self.authority_key_id, "anchor.authority_key_id")
411
+ _public_key(self.authority_public_key, "anchor.authority_public_key")
412
+ if not isinstance(self.signature, str):
413
+ raise RepairError("REPAIR_SIGNATURE", "anchor.signature", "must be base64 text")
414
+ try:
415
+ base64.b64decode(self.signature, validate=True)
416
+ except (ValueError, TypeError) as exc:
417
+ raise RepairError(
418
+ "REPAIR_SIGNATURE",
419
+ "anchor.signature",
420
+ "is not canonical base64",
421
+ ) from exc
422
+
423
+ def unsigned_dict(self) -> dict[str, Any]:
424
+ value = preparation_value(self)
425
+ del value["signature"]
426
+ return value
427
+
428
+ @property
429
+ def digest(self) -> str:
430
+ return canonical_sha256(preparation_value(self))
431
+
432
+
433
+ @dataclass(frozen=True)
434
+ class RepairAssignment:
435
+ schema_version: str
436
+ assignment_id: str
437
+ run_id: str
438
+ predecessor: CandidateReference
439
+ inventory: FindingInventory
440
+ anchor_digest: str
441
+ anchor_sequence: int
442
+ fixer_principal: str
443
+ fixer_attempt: str
444
+ actions: tuple[RepairAction, ...]
445
+ affected_check_ids: tuple[str, ...]
446
+
447
+ def __post_init__(self) -> None:
448
+ if self.schema_version != REPAIR_ASSIGNMENT_VERSION:
449
+ raise RepairError(
450
+ "REPAIR_ASSIGNMENT_VERSION",
451
+ "assignment.schema_version",
452
+ "is unsupported",
453
+ )
454
+ _identifier(self.assignment_id, "assignment.assignment_id")
455
+ _identifier(self.run_id, "assignment.run_id")
456
+ if self.predecessor.status != "fixes_required":
457
+ raise RepairError(
458
+ "REPAIR_LIFECYCLE",
459
+ "assignment.predecessor",
460
+ "must be a fixes_required candidate",
461
+ )
462
+ _digest(self.anchor_digest, "assignment.anchor_digest")
463
+ if type(self.anchor_sequence) is not int or self.anchor_sequence < 0:
464
+ raise RepairError("REPAIR_COUNTER", "assignment.anchor_sequence", "is invalid")
465
+ _identifier(self.fixer_principal, "assignment.fixer_principal")
466
+ _identifier(self.fixer_attempt, "assignment.fixer_attempt")
467
+ if self.fixer_principal == self.predecessor.producer_principal:
468
+ raise RepairError(
469
+ "REPAIR_IDENTITY",
470
+ "assignment.fixer_principal",
471
+ "fixer must differ from the predecessor producer",
472
+ )
473
+ if self.fixer_attempt == self.predecessor.producer_attempt:
474
+ raise RepairError(
475
+ "REPAIR_IDENTITY",
476
+ "assignment.fixer_attempt",
477
+ "fixer attempt must differ from the predecessor producer attempt",
478
+ )
479
+ if not isinstance(self.actions, tuple) or not 1 <= len(self.actions) <= MAX_REPAIR_ACTIONS:
480
+ raise RepairError(
481
+ "REPAIR_ACTIONS",
482
+ "assignment.actions",
483
+ "must contain 1..32 actions",
484
+ )
485
+ action_ids = tuple(item.action_id for item in self.actions)
486
+ if len(set(action_ids)) != len(action_ids):
487
+ raise RepairError(
488
+ "REPAIR_ACTIONS",
489
+ "assignment.actions",
490
+ "contains duplicate action IDs",
491
+ )
492
+ # An assignment must claim the entire check inventory, not the subset a repair
493
+ # believes it touches: a fixer that under-declares would narrow the re-validation
494
+ # that closes the repair. A genuine subset is therefore rejected here, so
495
+ # ``affected_check_ids`` on an assignment is in practice always
496
+ # ``ALL_QUALITY_CHECK_IDS``. Per-finding subsets do exist and are validated on
497
+ # ``RepairFinding.affected_check_ids`` instead.
498
+ checked = _identifiers(
499
+ self.affected_check_ids,
500
+ "assignment.affected_check_ids",
501
+ allowed=frozenset(ALL_QUALITY_CHECK_IDS),
502
+ )
503
+ if checked != ALL_QUALITY_CHECK_IDS:
504
+ raise RepairError(
505
+ "REPAIR_CHECK_COVERAGE",
506
+ "assignment.affected_check_ids",
507
+ "must include every directly or transitively affected quality check",
508
+ )
509
+
510
+ @property
511
+ def digest(self) -> str:
512
+ return canonical_sha256(self.to_dict())
513
+
514
+ def to_dict(self) -> dict[str, Any]:
515
+ return preparation_value(self)
516
+
517
+
518
+ @dataclass(frozen=True)
519
+ class ResolutionEvidence:
520
+ finding_id: str
521
+ predecessor_evidence_digest: str
522
+ acceptance_condition_digest: str
523
+ check_evidence: tuple[tuple[str, str], ...]
524
+
525
+ def __post_init__(self) -> None:
526
+ _identifier(self.finding_id, "resolution.finding_id")
527
+ _digest(self.predecessor_evidence_digest, "resolution.predecessor_evidence_digest")
528
+ _digest(self.acceptance_condition_digest, "resolution.acceptance_condition_digest")
529
+ if not isinstance(self.check_evidence, tuple) or not self.check_evidence:
530
+ raise RepairError(
531
+ "REPAIR_RESOLUTION",
532
+ "resolution.check_evidence",
533
+ "must contain deterministic check evidence",
534
+ )
535
+ check_ids = tuple(item[0] for item in self.check_evidence)
536
+ if check_ids != tuple(sorted(set(check_ids))):
537
+ raise RepairError(
538
+ "REPAIR_ORDER",
539
+ "resolution.check_evidence",
540
+ "must be unique and sorted",
541
+ )
542
+ for check_id, evidence_digest in self.check_evidence:
543
+ _identifier(check_id, "resolution.check_evidence.check_id")
544
+ _digest(evidence_digest, "resolution.check_evidence.digest")
545
+
546
+ @property
547
+ def digest(self) -> str:
548
+ return canonical_sha256(preparation_value(self))
549
+
550
+
551
+ @dataclass(frozen=True)
552
+ class RepairSuccessor:
553
+ schema_version: str
554
+ assignment_digest: str
555
+ predecessor_candidate_digest: str
556
+ candidate: CandidateReference
557
+ cycle_number: int
558
+ fixer_principal: str
559
+ fixer_attempt: str
560
+ affected_check_ids: tuple[str, ...]
561
+ check_evidence: tuple[tuple[str, str, str], ...]
562
+ resolution_evidence: tuple[ResolutionEvidence, ...]
563
+ carried_backlog: tuple[RepairFinding, ...]
564
+
565
+ def __post_init__(self) -> None:
566
+ if self.schema_version != REPAIR_SUCCESSOR_VERSION:
567
+ raise RepairError(
568
+ "REPAIR_SUCCESSOR_VERSION",
569
+ "successor.schema_version",
570
+ "is unsupported",
571
+ )
572
+ _digest(self.assignment_digest, "successor.assignment_digest")
573
+ _digest(self.predecessor_candidate_digest, "successor.predecessor_candidate_digest")
574
+ if self.candidate.candidate_digest == self.predecessor_candidate_digest:
575
+ raise RepairError(
576
+ "REPAIR_IMMUTABILITY",
577
+ "successor.candidate",
578
+ "must have a distinct candidate identity",
579
+ )
580
+ if type(self.cycle_number) is not int or not 1 <= self.cycle_number <= 3:
581
+ raise RepairError("REPAIR_COUNTER", "successor.cycle_number", "is invalid")
582
+ _identifier(self.fixer_principal, "successor.fixer_principal")
583
+ _identifier(self.fixer_attempt, "successor.fixer_attempt")
584
+ _identifiers(
585
+ self.affected_check_ids,
586
+ "successor.affected_check_ids",
587
+ allowed=frozenset(ALL_QUALITY_CHECK_IDS),
588
+ )
589
+ check_ids = tuple(item[0] for item in self.check_evidence)
590
+ if check_ids != ALL_QUALITY_CHECK_IDS:
591
+ raise RepairError(
592
+ "REPAIR_CHECK_COVERAGE",
593
+ "successor.check_evidence",
594
+ "must contain the complete rerun check inventory",
595
+ )
596
+ for check_id, status, evidence_digest in self.check_evidence:
597
+ _identifier(check_id, "successor.check_evidence.check_id")
598
+ if status not in {"passed", "failed"}:
599
+ raise RepairError(
600
+ "REPAIR_STATUS",
601
+ "successor.check_evidence.status",
602
+ "is unsupported",
603
+ )
604
+ _digest(evidence_digest, "successor.check_evidence.digest")
605
+ resolution_ids = tuple(item.finding_id for item in self.resolution_evidence)
606
+ if resolution_ids != tuple(sorted(set(resolution_ids))):
607
+ raise RepairError(
608
+ "REPAIR_ORDER",
609
+ "successor.resolution_evidence",
610
+ "must be unique and sorted",
611
+ )
612
+ backlog_ids = tuple(item.finding_id for item in self.carried_backlog)
613
+ if backlog_ids != tuple(sorted(set(backlog_ids))):
614
+ raise RepairError(
615
+ "REPAIR_ORDER",
616
+ "successor.carried_backlog",
617
+ "must be unique and sorted",
618
+ )
619
+ if any(item.severity in BLOCKING_SEVERITIES for item in self.carried_backlog):
620
+ raise RepairError(
621
+ "REPAIR_BACKLOG",
622
+ "successor.carried_backlog",
623
+ "cannot contain a blocking finding",
624
+ )
625
+
626
+ @property
627
+ def resolution_evidence_digest(self) -> str:
628
+ return canonical_sha256([preparation_value(item) for item in self.resolution_evidence])
629
+
630
+
631
+ @dataclass(frozen=True)
632
+ class ReviewerEnrollment:
633
+ lens: str
634
+ principal_id: str
635
+ attempt_id: str
636
+ key_id: str
637
+ public_key: str
638
+
639
+ def __post_init__(self) -> None:
640
+ if self.lens not in REQUIRED_CLOSURE_LENSES:
641
+ raise RepairError("REPAIR_REVIEW_LENS", "reviewer.lens", "is unsupported")
642
+ _identifier(self.principal_id, "reviewer.principal_id")
643
+ _identifier(self.attempt_id, "reviewer.attempt_id")
644
+ _identifier(self.key_id, "reviewer.key_id")
645
+ _public_key(self.public_key, "reviewer.public_key")
646
+
647
+
648
+ @dataclass(frozen=True)
649
+ class ClosureReviewScope:
650
+ schema_version: str
651
+ scope_id: str
652
+ run_id: str
653
+ assignment_digest: str
654
+ predecessor_candidate_digest: str
655
+ successor_candidate_digest: str
656
+ successor_evidence_digest: str
657
+ resolution_evidence_digest: str
658
+ cycle_number: int
659
+ blocking_finding_ids: tuple[str, ...]
660
+ backlog_digest: str
661
+ reviewers: tuple[ReviewerEnrollment, ...]
662
+
663
+ def __post_init__(self) -> None:
664
+ if self.schema_version != CLOSURE_SCOPE_VERSION:
665
+ raise RepairError("REPAIR_SCOPE_VERSION", "scope.schema_version", "is unsupported")
666
+ _identifier(self.scope_id, "scope.scope_id")
667
+ _identifier(self.run_id, "scope.run_id")
668
+ for name in (
669
+ "assignment_digest",
670
+ "predecessor_candidate_digest",
671
+ "successor_candidate_digest",
672
+ "successor_evidence_digest",
673
+ "resolution_evidence_digest",
674
+ "backlog_digest",
675
+ ):
676
+ _digest(getattr(self, name), f"scope.{name}")
677
+ if self.predecessor_candidate_digest == self.successor_candidate_digest:
678
+ raise RepairError(
679
+ "REPAIR_IMMUTABILITY",
680
+ "scope.successor_candidate_digest",
681
+ "must differ from the predecessor",
682
+ )
683
+ if type(self.cycle_number) is not int or not 1 <= self.cycle_number <= 3:
684
+ raise RepairError("REPAIR_COUNTER", "scope.cycle_number", "is invalid")
685
+ _identifiers(self.blocking_finding_ids, "scope.blocking_finding_ids")
686
+ if not isinstance(self.reviewers, tuple) or len(self.reviewers) != 2:
687
+ raise RepairError(
688
+ "REPAIR_REVIEWERS",
689
+ "scope.reviewers",
690
+ "must contain exactly two reviewers",
691
+ )
692
+ if tuple(item.lens for item in self.reviewers) != REQUIRED_CLOSURE_LENSES:
693
+ raise RepairError(
694
+ "REPAIR_REVIEWERS",
695
+ "scope.reviewers",
696
+ "must cover both required lenses in canonical order",
697
+ )
698
+ for attribute in ("principal_id", "attempt_id", "key_id", "public_key"):
699
+ values = tuple(getattr(item, attribute) for item in self.reviewers)
700
+ if len(set(values)) != len(values):
701
+ raise RepairError(
702
+ "REPAIR_REVIEWERS",
703
+ f"scope.reviewers.{attribute}",
704
+ "must be distinct",
705
+ )
706
+
707
+ @property
708
+ def digest(self) -> str:
709
+ return canonical_sha256(preparation_value(self))
710
+
711
+
712
+ @dataclass(frozen=True)
713
+ class ClosureReviewReport:
714
+ schema_version: str
715
+ scope_digest: str
716
+ successor_candidate_digest: str
717
+ successor_evidence_digest: str
718
+ reviewer_principal: str
719
+ reviewer_attempt: str
720
+ reviewer_key_id: str
721
+ lens: str
722
+ decision: str
723
+ verified_resolutions: tuple[tuple[str, str], ...]
724
+ findings: tuple[RepairFinding, ...]
725
+ signature: str
726
+
727
+ def __post_init__(self) -> None:
728
+ if self.schema_version != CLOSURE_REPORT_VERSION:
729
+ raise RepairError("REPAIR_REPORT_VERSION", "report.schema_version", "is unsupported")
730
+ _digest(self.scope_digest, "report.scope_digest")
731
+ _digest(self.successor_candidate_digest, "report.successor_candidate_digest")
732
+ _digest(self.successor_evidence_digest, "report.successor_evidence_digest")
733
+ _identifier(self.reviewer_principal, "report.reviewer_principal")
734
+ _identifier(self.reviewer_attempt, "report.reviewer_attempt")
735
+ _identifier(self.reviewer_key_id, "report.reviewer_key_id")
736
+ if self.lens not in REQUIRED_CLOSURE_LENSES:
737
+ raise RepairError("REPAIR_REVIEW_LENS", "report.lens", "is unsupported")
738
+ if self.decision not in {"accept", "reject"}:
739
+ raise RepairError("REPAIR_REVIEW_DECISION", "report.decision", "is unsupported")
740
+ ids = tuple(item[0] for item in self.verified_resolutions)
741
+ if ids != tuple(sorted(set(ids))):
742
+ raise RepairError(
743
+ "REPAIR_ORDER",
744
+ "report.verified_resolutions",
745
+ "must be unique and sorted",
746
+ )
747
+ for finding_id, resolution_digest in self.verified_resolutions:
748
+ _identifier(finding_id, "report.verified_resolutions.finding_id")
749
+ _digest(resolution_digest, "report.verified_resolutions.digest")
750
+ finding_ids = tuple(item.finding_id for item in self.findings)
751
+ if finding_ids != tuple(sorted(set(finding_ids))):
752
+ raise RepairError(
753
+ "REPAIR_ORDER",
754
+ "report.findings",
755
+ "must be unique and sorted",
756
+ )
757
+ if not isinstance(self.signature, str):
758
+ raise RepairError("REPAIR_SIGNATURE", "report.signature", "must be base64 text")
759
+ try:
760
+ base64.b64decode(self.signature, validate=True)
761
+ except (ValueError, TypeError) as exc:
762
+ raise RepairError(
763
+ "REPAIR_SIGNATURE",
764
+ "report.signature",
765
+ "is not canonical base64",
766
+ ) from exc
767
+
768
+ def unsigned_dict(self) -> dict[str, Any]:
769
+ value = preparation_value(self)
770
+ del value["signature"]
771
+ return value
772
+
773
+
774
+ @dataclass(frozen=True)
775
+ class ClosureDecision:
776
+ schema_version: str
777
+ scope_digest: str
778
+ candidate: CandidateReference
779
+ status: str
780
+ report_digests: tuple[str, ...]
781
+ preserved_backlog: tuple[RepairFinding, ...]
782
+ remaining_blocking: tuple[RepairFinding, ...]
783
+
784
+ def __post_init__(self) -> None:
785
+ if self.schema_version != CLOSURE_DECISION_VERSION:
786
+ raise RepairError(
787
+ "REPAIR_DECISION_VERSION",
788
+ "decision.schema_version",
789
+ "is unsupported",
790
+ )
791
+ _digest(self.scope_digest, "decision.scope_digest")
792
+ if self.status not in {
793
+ "reviewed_successor",
794
+ "fixes_required",
795
+ "human_direction_required",
796
+ }:
797
+ raise RepairError("REPAIR_STATUS", "decision.status", "is unsupported")
798
+ if not isinstance(self.report_digests, tuple) or len(self.report_digests) != 2:
799
+ raise RepairError(
800
+ "REPAIR_REPORTS",
801
+ "decision.report_digests",
802
+ "must contain two report digests",
803
+ )
804
+ for digest in self.report_digests:
805
+ _digest(digest, "decision.report_digests")
806
+ if self.status == "reviewed_successor" and self.remaining_blocking:
807
+ raise RepairError(
808
+ "REPAIR_CLOSURE",
809
+ "decision.remaining_blocking",
810
+ "a reviewed successor cannot retain blocking findings",
811
+ )
812
+ if self.status != self.candidate.status:
813
+ raise RepairError(
814
+ "REPAIR_STATUS",
815
+ "decision.candidate.status",
816
+ "must match the derived decision status",
817
+ )
818
+ backlog_ids = tuple(item.finding_id for item in self.preserved_backlog)
819
+ blocking_ids = tuple(item.finding_id for item in self.remaining_blocking)
820
+ if backlog_ids != tuple(sorted(set(backlog_ids))) or blocking_ids != tuple(
821
+ sorted(set(blocking_ids))
822
+ ):
823
+ raise RepairError(
824
+ "REPAIR_ORDER",
825
+ "decision",
826
+ "finding partitions must be unique and sorted",
827
+ )
828
+ if any(item.severity in BLOCKING_SEVERITIES for item in self.preserved_backlog):
829
+ raise RepairError(
830
+ "REPAIR_BACKLOG",
831
+ "decision.preserved_backlog",
832
+ "cannot contain a blocking finding",
833
+ )
834
+ if any(item.severity not in BLOCKING_SEVERITIES for item in self.remaining_blocking):
835
+ raise RepairError(
836
+ "REPAIR_FINDINGS",
837
+ "decision.remaining_blocking",
838
+ "cannot contain a non-blocking finding",
839
+ )
840
+
841
+ @property
842
+ def digest(self) -> str:
843
+ return canonical_sha256(preparation_value(self))
844
+
845
+
846
+ __all__ = [
847
+ "ALL_QUALITY_CHECK_IDS",
848
+ "BLOCKING_SEVERITIES",
849
+ "CLOSURE_DECISION_VERSION",
850
+ "CLOSURE_REPORT_VERSION",
851
+ "CLOSURE_SCOPE_VERSION",
852
+ "CYCLE_ANCHOR_VERSION",
853
+ "MAX_FAILED_CANDIDATES",
854
+ "REPAIR_ASSIGNMENT_VERSION",
855
+ "REPAIR_CONTRACT_VERSION",
856
+ "REPAIR_SUCCESSOR_VERSION",
857
+ "REQUIRED_CLOSURE_LENSES",
858
+ "CandidateReference",
859
+ "ClosureDecision",
860
+ "ClosureReviewReport",
861
+ "ClosureReviewScope",
862
+ "CycleAnchor",
863
+ "FindingInventory",
864
+ "RepairAction",
865
+ "RepairAssignment",
866
+ "RepairFinding",
867
+ "RepairSuccessor",
868
+ "ResolutionEvidence",
869
+ "ReviewerEnrollment",
870
+ "load_public_key",
871
+ "public_key_text",
872
+ ]