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,1099 @@
1
+ """Trusted immutable repair execution and independent closure coordination."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import threading
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass, replace
9
+ from typing import Any, Protocol
10
+
11
+ from cryptography.exceptions import InvalidSignature
12
+
13
+ from mostlyright.data_harness.canonical import canonical_json_bytes, canonical_sha256
14
+ from mostlyright.data_harness.preparation.contracts import (
15
+ AsOfJoinSpec,
16
+ EqualityJoinSpec,
17
+ OperationSpec,
18
+ PreparationPlan,
19
+ )
20
+ from mostlyright.data_harness.preparation.engine import (
21
+ PreparationResult,
22
+ execute_preparation,
23
+ )
24
+ from mostlyright.data_harness.preparation.table import TableState
25
+ from mostlyright.data_harness.repair.adapters import (
26
+ append_join,
27
+ append_operation,
28
+ candidate_digest,
29
+ input_set_digest,
30
+ output_columns_digest,
31
+ plan_digest,
32
+ preparation_value,
33
+ replace_join,
34
+ replace_operation,
35
+ validation_policy_digest,
36
+ )
37
+ from mostlyright.data_harness.repair.contracts import (
38
+ ALL_QUALITY_CHECK_IDS,
39
+ BLOCKING_SEVERITIES,
40
+ CLOSURE_DECISION_VERSION,
41
+ CLOSURE_REPORT_VERSION,
42
+ CLOSURE_SCOPE_VERSION,
43
+ CYCLE_ANCHOR_VERSION,
44
+ MAX_FAILED_CANDIDATES,
45
+ REPAIR_ASSIGNMENT_VERSION,
46
+ REPAIR_SUCCESSOR_VERSION,
47
+ REQUIRED_CLOSURE_LENSES,
48
+ CandidateReference,
49
+ ClosureDecision,
50
+ ClosureReviewReport,
51
+ ClosureReviewScope,
52
+ CycleAnchor,
53
+ FindingInventory,
54
+ RepairAction,
55
+ RepairAssignment,
56
+ RepairFinding,
57
+ RepairSuccessor,
58
+ ResolutionEvidence,
59
+ ReviewerEnrollment,
60
+ load_public_key,
61
+ public_key_text,
62
+ )
63
+ from mostlyright.data_harness.repair.errors import RepairError
64
+ from mostlyright.data_harness.signing import TrustedSigner
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class RepairExecution:
69
+ """The frozen successor plan, deterministic evidence, and lifecycle envelope."""
70
+
71
+ plan: PreparationPlan
72
+ result: PreparationResult
73
+ successor: RepairSuccessor
74
+
75
+
76
+ class TrustedCycleAnchorStore(Protocol):
77
+ """External linearizable persistence for signed repair-cycle anchors."""
78
+
79
+ def load(self, run_id: str) -> CycleAnchor | None:
80
+ """Return the authoritative current anchor for one run."""
81
+
82
+ def compare_and_swap(
83
+ self,
84
+ run_id: str,
85
+ expected_anchor_digest: str | None,
86
+ successor: CycleAnchor,
87
+ ) -> None:
88
+ """Persist only when the external current digest equals ``expected``."""
89
+
90
+
91
+ def bind_candidate(
92
+ plan: PreparationPlan,
93
+ result: PreparationResult,
94
+ *,
95
+ producer_principal: str,
96
+ producer_attempt: str,
97
+ status: str,
98
+ failed_candidate_count: int,
99
+ inputs: Mapping[str, TableState],
100
+ ) -> CandidateReference:
101
+ """Bind a preparation result to one exact immutable repair candidate."""
102
+
103
+ if not isinstance(result, PreparationResult):
104
+ raise RepairError("REPAIR_RESULT_TYPE", "result", "must be a PreparationResult")
105
+ if status in {"pending_independent_review", "reviewed_successor"} and not result.analysis_ready:
106
+ raise RepairError(
107
+ "REPAIR_STATUS",
108
+ "status",
109
+ "a deterministic failure cannot enter review or reviewed state",
110
+ )
111
+ if status == "human_direction_required" and failed_candidate_count != 3:
112
+ raise RepairError(
113
+ "REPAIR_STATUS",
114
+ "status",
115
+ "human direction requires exactly three failed candidates",
116
+ )
117
+ if status not in {
118
+ "fixes_required",
119
+ "human_direction_required",
120
+ "pending_independent_review",
121
+ "reviewed_successor",
122
+ }:
123
+ raise RepairError("REPAIR_STATUS", "status", "is unsupported")
124
+ return CandidateReference(
125
+ candidate_digest=candidate_digest(plan, result, inputs),
126
+ plan_digest=plan_digest(plan),
127
+ validation_policy_digest=validation_policy_digest(plan),
128
+ input_set_digest=input_set_digest(inputs),
129
+ evidence_digest=result.evidence_digest,
130
+ producer_principal=producer_principal,
131
+ producer_attempt=producer_attempt,
132
+ status=status,
133
+ failed_candidate_count=failed_candidate_count,
134
+ )
135
+
136
+
137
+ def make_closure_report(
138
+ scope: ClosureReviewScope,
139
+ enrollment: ReviewerEnrollment,
140
+ signer: TrustedSigner,
141
+ *,
142
+ decision: str,
143
+ verified_resolutions: tuple[tuple[str, str], ...],
144
+ findings: tuple[RepairFinding, ...] = (),
145
+ ) -> ClosureReviewReport:
146
+ """Sign one exact closure report without exposing reviewer private key bytes."""
147
+
148
+ if enrollment not in scope.reviewers:
149
+ raise RepairError(
150
+ "REPAIR_REVIEWER_SCOPE",
151
+ "enrollment",
152
+ "is not enrolled for the closure scope",
153
+ )
154
+ try:
155
+ signer_public = public_key_text(signer.public_key())
156
+ except Exception as exc:
157
+ raise RepairError(
158
+ "REPAIR_SIGNER",
159
+ "signer",
160
+ "did not expose a valid public verification key",
161
+ ) from exc
162
+ if signer_public != enrollment.public_key:
163
+ raise RepairError(
164
+ "REPAIR_SIGNER",
165
+ "signer",
166
+ "does not match the enrolled reviewer key",
167
+ )
168
+ unsigned = {
169
+ "schema_version": CLOSURE_REPORT_VERSION,
170
+ "scope_digest": scope.digest,
171
+ "successor_candidate_digest": scope.successor_candidate_digest,
172
+ "successor_evidence_digest": scope.successor_evidence_digest,
173
+ "reviewer_principal": enrollment.principal_id,
174
+ "reviewer_attempt": enrollment.attempt_id,
175
+ "reviewer_key_id": enrollment.key_id,
176
+ "lens": enrollment.lens,
177
+ "decision": decision,
178
+ "verified_resolutions": [list(item) for item in verified_resolutions],
179
+ "findings": [item.to_dict() for item in findings],
180
+ }
181
+ try:
182
+ signature = base64.b64encode(signer.sign(canonical_json_bytes(unsigned))).decode("ascii")
183
+ except Exception as exc:
184
+ raise RepairError(
185
+ "REPAIR_SIGNER",
186
+ "signer",
187
+ "could not sign the exact closure report",
188
+ ) from exc
189
+ return ClosureReviewReport(
190
+ schema_version=CLOSURE_REPORT_VERSION,
191
+ scope_digest=scope.digest,
192
+ successor_candidate_digest=scope.successor_candidate_digest,
193
+ successor_evidence_digest=scope.successor_evidence_digest,
194
+ reviewer_principal=enrollment.principal_id,
195
+ reviewer_attempt=enrollment.attempt_id,
196
+ reviewer_key_id=enrollment.key_id,
197
+ lens=enrollment.lens,
198
+ decision=decision,
199
+ verified_resolutions=verified_resolutions,
200
+ findings=findings,
201
+ signature=signature,
202
+ )
203
+
204
+
205
+ class ImmutableRepairCoordinator:
206
+ """Linearizes one externally signed, append-only runtime repair chain."""
207
+
208
+ def __init__(
209
+ self,
210
+ *,
211
+ run_id: str,
212
+ predecessor: CandidateReference,
213
+ inventory: FindingInventory,
214
+ authority_key_id: str,
215
+ authority_signer: TrustedSigner,
216
+ anchor_store: TrustedCycleAnchorStore,
217
+ ) -> None:
218
+ if predecessor.status not in {"fixes_required", "human_direction_required"}:
219
+ raise RepairError(
220
+ "REPAIR_LIFECYCLE",
221
+ "predecessor.status",
222
+ "must be a failed candidate",
223
+ )
224
+ if not callable(getattr(authority_signer, "sign", None)) or not callable(
225
+ getattr(authority_signer, "public_key", None)
226
+ ):
227
+ raise RepairError(
228
+ "REPAIR_SIGNER",
229
+ "authority_signer",
230
+ "must be an opaque trusted signer capability",
231
+ )
232
+ self._lock = threading.RLock()
233
+ self._authority_signer = authority_signer
234
+ if not callable(getattr(anchor_store, "load", None)) or not callable(
235
+ getattr(anchor_store, "compare_and_swap", None)
236
+ ):
237
+ raise RepairError(
238
+ "REPAIR_ANCHOR_STORE",
239
+ "anchor_store",
240
+ "must provide trusted load and compare-and-swap operations",
241
+ )
242
+ self._anchor_store = anchor_store
243
+ self._authority_key_id = authority_key_id
244
+ try:
245
+ self._authority_public_key = authority_signer.public_key()
246
+ self._authority_public_text = public_key_text(self._authority_public_key)
247
+ except Exception as exc:
248
+ raise RepairError(
249
+ "REPAIR_SIGNER",
250
+ "authority_signer",
251
+ "did not expose a valid public key",
252
+ ) from exc
253
+ self._run_id = run_id
254
+ existing_anchor = self._load_external_anchor()
255
+ self._root_candidate_digest = (
256
+ predecessor.candidate_digest
257
+ if existing_anchor is None
258
+ else existing_anchor.root_candidate_digest
259
+ )
260
+ self._current = predecessor
261
+ self._inventory = inventory
262
+ self._issued_assignments: dict[str, RepairAssignment] = {}
263
+ self._consumed_assignments: set[str] = set()
264
+ self._pending_execution: RepairExecution | None = None
265
+ self._issued_scopes: dict[str, ClosureReviewScope] = {}
266
+ self._consumed_scopes: set[str] = set()
267
+ self._closed = False
268
+ if existing_anchor is None:
269
+ self._anchor = self._sign_anchor(
270
+ current=predecessor,
271
+ inventory_digest=inventory.digest,
272
+ sequence=0,
273
+ predecessor_anchor_digest=None,
274
+ )
275
+ self._commit_external_anchor(None, self._anchor)
276
+ else:
277
+ self._anchor = existing_anchor
278
+ if self._anchor.inventory_digest != inventory.digest:
279
+ raise RepairError(
280
+ "REPAIR_ANCHOR_STATE",
281
+ "inventory",
282
+ "does not match externally anchored current findings",
283
+ )
284
+ self._verify_current_anchor()
285
+
286
+ @property
287
+ def current_anchor(self) -> CycleAnchor:
288
+ return self._anchor
289
+
290
+ @property
291
+ def current_candidate(self) -> CandidateReference:
292
+ return self._current
293
+
294
+ def issue_repair_assignment(
295
+ self,
296
+ *,
297
+ assignment_id: str,
298
+ fixer_principal: str,
299
+ fixer_attempt: str,
300
+ actions: tuple[RepairAction, ...],
301
+ affected_check_ids: tuple[str, ...] = ALL_QUALITY_CHECK_IDS,
302
+ ) -> RepairAssignment:
303
+ """Bind one exact fixer and closed repair proposal to current trusted state."""
304
+
305
+ with self._lock:
306
+ self._verify_current_anchor()
307
+ if self._closed:
308
+ raise RepairError(
309
+ "REPAIR_LIFECYCLE",
310
+ "coordinator",
311
+ "the successor is already independently reviewed",
312
+ )
313
+ if self._pending_execution is not None:
314
+ raise RepairError(
315
+ "REPAIR_LIFECYCLE",
316
+ "coordinator",
317
+ "a successor awaits independent review",
318
+ )
319
+ if self._current.status == "human_direction_required":
320
+ raise RepairError(
321
+ "REPAIR_CYCLE_CAP",
322
+ "coordinator",
323
+ "three failed candidates require human direction",
324
+ )
325
+ if self._current.status != "fixes_required":
326
+ raise RepairError(
327
+ "REPAIR_LIFECYCLE",
328
+ "coordinator",
329
+ "current candidate does not accept automatic repair",
330
+ )
331
+ if fixer_principal in {
332
+ item.reviewer_principal
333
+ for item in (*self._inventory.blocking, *self._inventory.backlog)
334
+ }:
335
+ raise RepairError(
336
+ "REPAIR_IDENTITY",
337
+ "fixer_principal",
338
+ "fixer must differ from predecessor finding reviewers",
339
+ )
340
+ assignment = RepairAssignment(
341
+ schema_version=REPAIR_ASSIGNMENT_VERSION,
342
+ assignment_id=assignment_id,
343
+ run_id=self._run_id,
344
+ predecessor=self._current,
345
+ inventory=self._inventory,
346
+ anchor_digest=self._anchor.digest,
347
+ anchor_sequence=self._anchor.sequence,
348
+ fixer_principal=fixer_principal,
349
+ fixer_attempt=fixer_attempt,
350
+ actions=actions,
351
+ affected_check_ids=affected_check_ids,
352
+ )
353
+ if assignment.digest in self._issued_assignments:
354
+ raise RepairError(
355
+ "REPAIR_REPLAY",
356
+ "assignment",
357
+ "this exact assignment was already issued",
358
+ )
359
+ self._issued_assignments[assignment.digest] = assignment
360
+ return assignment
361
+
362
+ def execute_repair(
363
+ self,
364
+ assignment: RepairAssignment,
365
+ predecessor_plan: PreparationPlan,
366
+ inputs: Mapping[str, TableState],
367
+ ) -> RepairExecution:
368
+ """Create a distinct successor and rerun the complete deterministic gate set."""
369
+
370
+ with self._lock:
371
+ self._verify_current_anchor()
372
+ issued = self._issued_assignments.get(assignment.digest)
373
+ if issued is None or issued != assignment:
374
+ raise RepairError(
375
+ "REPAIR_ASSIGNMENT_TRUST",
376
+ "assignment",
377
+ "is not the exact coordinator-issued assignment",
378
+ )
379
+ if assignment.digest in self._consumed_assignments:
380
+ raise RepairError(
381
+ "REPAIR_REPLAY",
382
+ "assignment",
383
+ "has already been consumed",
384
+ )
385
+ if (
386
+ assignment.anchor_digest != self._anchor.digest
387
+ or assignment.anchor_sequence != self._anchor.sequence
388
+ or assignment.predecessor != self._current
389
+ or assignment.inventory != self._inventory
390
+ ):
391
+ raise RepairError(
392
+ "REPAIR_STALE_ASSIGNMENT",
393
+ "assignment",
394
+ "does not bind the current monotonic coordinator state",
395
+ )
396
+ if plan_digest(predecessor_plan) != assignment.predecessor.plan_digest:
397
+ raise RepairError(
398
+ "REPAIR_PREDECESSOR",
399
+ "predecessor_plan",
400
+ "digest does not match the sealed predecessor",
401
+ )
402
+ if (
403
+ validation_policy_digest(predecessor_plan)
404
+ != assignment.predecessor.validation_policy_digest
405
+ ):
406
+ raise RepairError(
407
+ "REPAIR_POLICY",
408
+ "predecessor_plan",
409
+ "validation policy differs from the sealed predecessor",
410
+ )
411
+ if input_set_digest(inputs) != assignment.predecessor.input_set_digest:
412
+ raise RepairError(
413
+ "REPAIR_INPUT_SUBSTITUTION",
414
+ "inputs",
415
+ "exact input tables differ from the sealed predecessor",
416
+ )
417
+ successor_plan = predecessor_plan
418
+ for action in assignment.actions:
419
+ successor_plan = self._apply_action(successor_plan, action)
420
+ if plan_digest(successor_plan) == assignment.predecessor.plan_digest:
421
+ raise RepairError(
422
+ "REPAIR_NO_SUCCESSOR",
423
+ "assignment.actions",
424
+ "did not create a distinct successor plan",
425
+ )
426
+ if (
427
+ validation_policy_digest(successor_plan)
428
+ != assignment.predecessor.validation_policy_digest
429
+ ):
430
+ raise RepairError(
431
+ "REPAIR_POLICY",
432
+ "successor_plan",
433
+ "repair actions cannot change quality, temporal, or target policy",
434
+ )
435
+ result = execute_preparation(successor_plan, inputs)
436
+ checks = {item.check_id: item for item in result.quality_report.checks}
437
+ if tuple(sorted(checks)) != ALL_QUALITY_CHECK_IDS:
438
+ raise RepairError(
439
+ "REPAIR_CHECK_COVERAGE",
440
+ "result.quality_report",
441
+ "does not contain the complete expected check inventory",
442
+ )
443
+ check_evidence = tuple(
444
+ (
445
+ check_id,
446
+ checks[check_id].status,
447
+ canonical_sha256(checks[check_id].to_dict()),
448
+ )
449
+ for check_id in ALL_QUALITY_CHECK_IDS
450
+ )
451
+ resolutions = self._resolution_evidence(
452
+ assignment.inventory.blocking,
453
+ checks,
454
+ )
455
+ failed_count = assignment.predecessor.failed_candidate_count
456
+ if result.analysis_ready:
457
+ status = "pending_independent_review"
458
+ else:
459
+ failed_count += 1
460
+ status = (
461
+ "human_direction_required"
462
+ if failed_count >= MAX_FAILED_CANDIDATES
463
+ else "fixes_required"
464
+ )
465
+ candidate = bind_candidate(
466
+ successor_plan,
467
+ result,
468
+ producer_principal=assignment.fixer_principal,
469
+ producer_attempt=assignment.fixer_attempt,
470
+ status=status,
471
+ failed_candidate_count=failed_count,
472
+ inputs=inputs,
473
+ )
474
+ if candidate.input_set_digest != assignment.predecessor.input_set_digest:
475
+ raise RepairError(
476
+ "REPAIR_INPUT_SUBSTITUTION",
477
+ "successor",
478
+ "successor is not bound to the predecessor input set",
479
+ )
480
+ successor = RepairSuccessor(
481
+ schema_version=REPAIR_SUCCESSOR_VERSION,
482
+ assignment_digest=assignment.digest,
483
+ predecessor_candidate_digest=assignment.predecessor.candidate_digest,
484
+ candidate=candidate,
485
+ cycle_number=assignment.predecessor.failed_candidate_count + 1,
486
+ fixer_principal=assignment.fixer_principal,
487
+ fixer_attempt=assignment.fixer_attempt,
488
+ affected_check_ids=assignment.affected_check_ids,
489
+ check_evidence=check_evidence,
490
+ resolution_evidence=resolutions,
491
+ carried_backlog=assignment.inventory.backlog,
492
+ )
493
+ execution = RepairExecution(
494
+ plan=successor_plan,
495
+ result=result,
496
+ successor=successor,
497
+ )
498
+ self._consumed_assignments.add(assignment.digest)
499
+ self._current = candidate
500
+ if result.analysis_ready:
501
+ self._pending_execution = execution
502
+ next_inventory = assignment.inventory
503
+ else:
504
+ next_inventory = self._failed_inventory(
505
+ predecessor_inventory=assignment.inventory,
506
+ candidate=candidate,
507
+ checks=checks,
508
+ cycle_number=successor.cycle_number,
509
+ )
510
+ self._inventory = next_inventory
511
+ self._advance_anchor(candidate, next_inventory.digest)
512
+ return execution
513
+
514
+ def issue_closure_scope(
515
+ self,
516
+ execution: RepairExecution,
517
+ *,
518
+ scope_id: str,
519
+ reviewers: tuple[ReviewerEnrollment, ...],
520
+ ) -> ClosureReviewScope:
521
+ """Enroll two fresh independent reviewers over the exact successor."""
522
+
523
+ with self._lock:
524
+ self._verify_current_anchor()
525
+ if self._pending_execution != execution:
526
+ raise RepairError(
527
+ "REPAIR_REVIEW_SCOPE",
528
+ "execution",
529
+ "is not the exact pending successor",
530
+ )
531
+ successor = execution.successor
532
+ if successor.candidate.status != "pending_independent_review":
533
+ raise RepairError(
534
+ "REPAIR_REVIEW_SCOPE",
535
+ "successor",
536
+ "did not pass deterministic affected checks",
537
+ )
538
+ assignment = self._issued_assignments[successor.assignment_digest]
539
+ forbidden_principals = {
540
+ assignment.predecessor.producer_principal,
541
+ assignment.fixer_principal,
542
+ }
543
+ forbidden_attempts = {
544
+ assignment.predecessor.producer_attempt,
545
+ assignment.fixer_attempt,
546
+ }
547
+ if any(item.principal_id in forbidden_principals for item in reviewers):
548
+ raise RepairError(
549
+ "REPAIR_SELF_REVIEW",
550
+ "reviewers",
551
+ "producer or fixer cannot review the successor",
552
+ )
553
+ if any(item.attempt_id in forbidden_attempts for item in reviewers):
554
+ raise RepairError(
555
+ "REPAIR_SELF_REVIEW",
556
+ "reviewers",
557
+ "review attempts must be fresh",
558
+ )
559
+ blocking_ids = tuple(sorted(item.finding_id for item in assignment.inventory.blocking))
560
+ resolution_ids = tuple(item.finding_id for item in successor.resolution_evidence)
561
+ if resolution_ids != blocking_ids:
562
+ raise RepairError(
563
+ "REPAIR_RESOLUTION",
564
+ "successor.resolution_evidence",
565
+ "does not cover every carried blocking finding",
566
+ )
567
+ scope = ClosureReviewScope(
568
+ schema_version=CLOSURE_SCOPE_VERSION,
569
+ scope_id=scope_id,
570
+ run_id=self._run_id,
571
+ assignment_digest=successor.assignment_digest,
572
+ predecessor_candidate_digest=successor.predecessor_candidate_digest,
573
+ successor_candidate_digest=successor.candidate.candidate_digest,
574
+ successor_evidence_digest=successor.candidate.evidence_digest,
575
+ resolution_evidence_digest=successor.resolution_evidence_digest,
576
+ cycle_number=successor.cycle_number,
577
+ blocking_finding_ids=blocking_ids,
578
+ backlog_digest=canonical_sha256(
579
+ [item.to_dict() for item in successor.carried_backlog]
580
+ ),
581
+ reviewers=reviewers,
582
+ )
583
+ if scope.digest in self._issued_scopes:
584
+ raise RepairError(
585
+ "REPAIR_REPLAY",
586
+ "scope",
587
+ "this exact closure scope was already issued",
588
+ )
589
+ self._issued_scopes[scope.digest] = scope
590
+ return scope
591
+
592
+ def finalize_closure(
593
+ self,
594
+ scope: ClosureReviewScope,
595
+ reports: tuple[ClosureReviewReport, ...],
596
+ ) -> ClosureDecision:
597
+ """Verify both signatures and derive closure or the next bounded failed state."""
598
+
599
+ with self._lock:
600
+ self._verify_current_anchor()
601
+ issued = self._issued_scopes.get(scope.digest)
602
+ if issued is None or issued != scope:
603
+ raise RepairError(
604
+ "REPAIR_SCOPE_TRUST",
605
+ "scope",
606
+ "is not the exact coordinator-issued closure scope",
607
+ )
608
+ if scope.digest in self._consumed_scopes:
609
+ raise RepairError(
610
+ "REPAIR_REPLAY",
611
+ "scope",
612
+ "has already been finalized",
613
+ )
614
+ if self._pending_execution is None:
615
+ raise RepairError(
616
+ "REPAIR_LIFECYCLE",
617
+ "scope",
618
+ "there is no pending successor",
619
+ )
620
+ if not isinstance(reports, tuple) or len(reports) != 2:
621
+ raise RepairError(
622
+ "REPAIR_REPORTS",
623
+ "reports",
624
+ "exactly two signed reports are required",
625
+ )
626
+ by_lens = {report.lens: report for report in reports}
627
+ if len(by_lens) != 2 or tuple(sorted(by_lens)) != REQUIRED_CLOSURE_LENSES:
628
+ raise RepairError(
629
+ "REPAIR_REPORTS",
630
+ "reports",
631
+ "must cover both required lenses exactly once",
632
+ )
633
+ execution = self._pending_execution
634
+ expected_resolutions = tuple(
635
+ (item.finding_id, item.digest) for item in execution.successor.resolution_evidence
636
+ )
637
+ all_findings: list[RepairFinding] = []
638
+ for enrollment in scope.reviewers:
639
+ report = by_lens[enrollment.lens]
640
+ self._verify_report(
641
+ report,
642
+ scope=scope,
643
+ enrollment=enrollment,
644
+ expected_resolutions=expected_resolutions,
645
+ )
646
+ all_findings.extend(report.findings)
647
+ finding_ids = tuple(item.finding_id for item in all_findings)
648
+ if len(finding_ids) != len(set(finding_ids)):
649
+ raise RepairError(
650
+ "REPAIR_FINDINGS",
651
+ "reports.findings",
652
+ "finding IDs must be unique across reviewers",
653
+ )
654
+ new_blocking = tuple(
655
+ sorted(
656
+ (
657
+ finding
658
+ for finding in all_findings
659
+ if finding.severity in BLOCKING_SEVERITIES
660
+ ),
661
+ key=lambda item: item.finding_id,
662
+ )
663
+ )
664
+ rejected = any(report.decision == "reject" for report in reports)
665
+ if rejected and not new_blocking:
666
+ raise RepairError(
667
+ "REPAIR_REVIEW_DECISION",
668
+ "reports",
669
+ "a rejection requires at least one evidence-bound blocking finding",
670
+ )
671
+ if not rejected and new_blocking:
672
+ raise RepairError(
673
+ "REPAIR_REVIEW_DECISION",
674
+ "reports",
675
+ "an accepting report cannot contain a blocking finding",
676
+ )
677
+ new_backlog = tuple(
678
+ sorted(
679
+ (
680
+ *execution.successor.carried_backlog,
681
+ *(
682
+ finding
683
+ for finding in all_findings
684
+ if finding.severity not in BLOCKING_SEVERITIES
685
+ ),
686
+ ),
687
+ key=lambda item: item.finding_id,
688
+ )
689
+ )
690
+ backlog_ids = tuple(item.finding_id for item in new_backlog)
691
+ if len(backlog_ids) != len(set(backlog_ids)):
692
+ raise RepairError(
693
+ "REPAIR_BACKLOG",
694
+ "reports.findings",
695
+ "cannot replace or duplicate a carried backlog item",
696
+ )
697
+ report_digests = tuple(
698
+ canonical_sha256(preparation_value(by_lens[lens]))
699
+ for lens in REQUIRED_CLOSURE_LENSES
700
+ )
701
+ if not new_blocking:
702
+ candidate = replace(
703
+ execution.successor.candidate,
704
+ status="reviewed_successor",
705
+ )
706
+ status = "reviewed_successor"
707
+ self._closed = True
708
+ self._current = candidate
709
+ inventory_digest = canonical_sha256(
710
+ {
711
+ "blocking": [],
712
+ "backlog": [item.to_dict() for item in new_backlog],
713
+ }
714
+ )
715
+ else:
716
+ failed_count = execution.successor.candidate.failed_candidate_count + 1
717
+ status = (
718
+ "human_direction_required"
719
+ if failed_count >= MAX_FAILED_CANDIDATES
720
+ else "fixes_required"
721
+ )
722
+ candidate = replace(
723
+ execution.successor.candidate,
724
+ status=status,
725
+ failed_candidate_count=failed_count,
726
+ )
727
+ self._inventory = FindingInventory(
728
+ blocking=new_blocking,
729
+ backlog=new_backlog,
730
+ )
731
+ self._current = candidate
732
+ inventory_digest = self._inventory.digest
733
+ decision = ClosureDecision(
734
+ schema_version=CLOSURE_DECISION_VERSION,
735
+ scope_digest=scope.digest,
736
+ candidate=candidate,
737
+ status=status,
738
+ report_digests=report_digests,
739
+ preserved_backlog=new_backlog,
740
+ remaining_blocking=new_blocking,
741
+ )
742
+ self._pending_execution = None
743
+ self._consumed_scopes.add(scope.digest)
744
+ self._advance_anchor(candidate, inventory_digest)
745
+ return decision
746
+
747
+ def _apply_action(
748
+ self,
749
+ plan: PreparationPlan,
750
+ action: RepairAction,
751
+ ) -> PreparationPlan:
752
+ if action.kind == "append_operation":
753
+ if not isinstance(action.replacement, OperationSpec):
754
+ raise RepairError(
755
+ "REPAIR_ACTION_SHAPE",
756
+ action.action_id,
757
+ "append action lost its validated operation replacement",
758
+ )
759
+ return append_operation(plan, action.replacement)
760
+ if action.kind in {"replace_operation", "remove_operation"}:
761
+ if action.expected_before_digest is None:
762
+ raise RepairError(
763
+ "REPAIR_ACTION_SHAPE",
764
+ action.action_id,
765
+ "operation edit lost its predecessor digest",
766
+ )
767
+ replacement = (
768
+ action.replacement
769
+ if action.kind == "replace_operation"
770
+ and isinstance(action.replacement, OperationSpec)
771
+ else None
772
+ )
773
+ if action.kind == "replace_operation" and replacement is None:
774
+ raise RepairError(
775
+ "REPAIR_ACTION_SHAPE",
776
+ action.action_id,
777
+ "replacement action lost its validated operation",
778
+ )
779
+ return replace_operation(
780
+ plan,
781
+ target_id=action.target_id,
782
+ expected_digest=action.expected_before_digest,
783
+ replacement=replacement,
784
+ )
785
+ if action.kind == "append_join":
786
+ if not isinstance(action.replacement, (EqualityJoinSpec, AsOfJoinSpec)):
787
+ raise RepairError(
788
+ "REPAIR_ACTION_SHAPE",
789
+ action.action_id,
790
+ "append action lost its validated join replacement",
791
+ )
792
+ return append_join(plan, action.replacement)
793
+ if action.kind in {"replace_join", "remove_join"}:
794
+ if action.expected_before_digest is None:
795
+ raise RepairError(
796
+ "REPAIR_ACTION_SHAPE",
797
+ action.action_id,
798
+ "join edit lost its predecessor digest",
799
+ )
800
+ replacement = (
801
+ action.replacement
802
+ if action.kind == "replace_join"
803
+ and isinstance(action.replacement, (EqualityJoinSpec, AsOfJoinSpec))
804
+ else None
805
+ )
806
+ if action.kind == "replace_join" and replacement is None:
807
+ raise RepairError(
808
+ "REPAIR_ACTION_SHAPE",
809
+ action.action_id,
810
+ "replacement action lost its validated join",
811
+ )
812
+ return replace_join(
813
+ plan,
814
+ target_id=action.target_id,
815
+ expected_digest=action.expected_before_digest,
816
+ replacement=replacement,
817
+ )
818
+ if action.kind == "replace_output_columns":
819
+ if action.expected_before_digest is None or not isinstance(
820
+ action.replacement,
821
+ tuple,
822
+ ):
823
+ raise RepairError(
824
+ "REPAIR_ACTION_SHAPE",
825
+ action.action_id,
826
+ "output edit lost its validated predecessor or replacement",
827
+ )
828
+ if output_columns_digest(plan.output_columns) != action.expected_before_digest:
829
+ raise RepairError(
830
+ "REPAIR_STALE_ACTION",
831
+ action.target_id,
832
+ "output-column digest differs from the assigned predecessor",
833
+ )
834
+ return replace(plan, output_columns=action.replacement)
835
+ raise RepairError("REPAIR_ACTION_KIND", action.kind, "is not allowlisted")
836
+
837
+ def _resolution_evidence(
838
+ self,
839
+ findings: tuple[RepairFinding, ...],
840
+ checks: dict[str, Any],
841
+ ) -> tuple[ResolutionEvidence, ...]:
842
+ resolutions: list[ResolutionEvidence] = []
843
+ for finding in findings:
844
+ selected = tuple(checks[check_id] for check_id in finding.affected_check_ids)
845
+ if any(check.status != "passed" for check in selected):
846
+ continue
847
+ resolutions.append(
848
+ ResolutionEvidence(
849
+ finding_id=finding.finding_id,
850
+ predecessor_evidence_digest=finding.evidence_digest,
851
+ acceptance_condition_digest=canonical_sha256(
852
+ {"acceptance_condition": finding.acceptance_condition}
853
+ ),
854
+ check_evidence=tuple(
855
+ (
856
+ check.check_id,
857
+ canonical_sha256(check.to_dict()),
858
+ )
859
+ for check in selected
860
+ ),
861
+ )
862
+ )
863
+ return tuple(sorted(resolutions, key=lambda item: item.finding_id))
864
+
865
+ def _failed_inventory(
866
+ self,
867
+ *,
868
+ predecessor_inventory: FindingInventory,
869
+ candidate: CandidateReference,
870
+ checks: dict[str, Any],
871
+ cycle_number: int,
872
+ ) -> FindingInventory:
873
+ blockers = list(predecessor_inventory.blocking)
874
+ covered = {
875
+ check_id
876
+ for finding in predecessor_inventory.blocking
877
+ for check_id in finding.affected_check_ids
878
+ }
879
+ for check_id in ALL_QUALITY_CHECK_IDS:
880
+ check = checks[check_id]
881
+ if check.status == "passed" or check_id in covered:
882
+ continue
883
+ blockers.append(
884
+ RepairFinding(
885
+ finding_id=f"H4.C{cycle_number}.{check_id}",
886
+ severity=check.severity,
887
+ invariant=f"Quality check {check_id} must pass on the immutable successor.",
888
+ evidence_digest=canonical_sha256(check.to_dict()),
889
+ affected_artifact_digest=candidate.candidate_digest,
890
+ acceptance_condition=(
891
+ f"Rerun the policy-owned {check.threshold_id} check and obtain passed "
892
+ "evidence on a distinct successor."
893
+ ),
894
+ affected_check_ids=(check_id,),
895
+ reviewer_principal="deterministic_validator",
896
+ )
897
+ )
898
+ return FindingInventory(
899
+ blocking=tuple(sorted(blockers, key=lambda item: item.finding_id)),
900
+ backlog=predecessor_inventory.backlog,
901
+ )
902
+
903
+ def _verify_report(
904
+ self,
905
+ report: ClosureReviewReport,
906
+ *,
907
+ scope: ClosureReviewScope,
908
+ enrollment: ReviewerEnrollment,
909
+ expected_resolutions: tuple[tuple[str, str], ...],
910
+ ) -> None:
911
+ expected = {
912
+ "scope_digest": scope.digest,
913
+ "successor_candidate_digest": scope.successor_candidate_digest,
914
+ "successor_evidence_digest": scope.successor_evidence_digest,
915
+ "reviewer_principal": enrollment.principal_id,
916
+ "reviewer_attempt": enrollment.attempt_id,
917
+ "reviewer_key_id": enrollment.key_id,
918
+ "lens": enrollment.lens,
919
+ }
920
+ for name, value in expected.items():
921
+ if getattr(report, name) != value:
922
+ raise RepairError(
923
+ "REPAIR_REPORT_SCOPE",
924
+ f"report.{name}",
925
+ "does not match the enrolled closure scope",
926
+ )
927
+ if report.verified_resolutions != expected_resolutions:
928
+ raise RepairError(
929
+ "REPAIR_WAIVER",
930
+ "report.verified_resolutions",
931
+ "must verify every exact carried resolution; omission is not a waiver",
932
+ )
933
+ if any(item.reviewer_principal != enrollment.principal_id for item in report.findings):
934
+ raise RepairError(
935
+ "REPAIR_FINDING_AUTHOR",
936
+ "report.findings",
937
+ "finding reviewer must match the signed report reviewer",
938
+ )
939
+ if any(
940
+ item.affected_artifact_digest != scope.successor_candidate_digest
941
+ for item in report.findings
942
+ ):
943
+ raise RepairError(
944
+ "REPAIR_FINDING_SCOPE",
945
+ "report.findings",
946
+ "every finding must bind the exact successor candidate",
947
+ )
948
+ report_has_blocking = any(item.severity in BLOCKING_SEVERITIES for item in report.findings)
949
+ if (report.decision == "reject") != report_has_blocking:
950
+ raise RepairError(
951
+ "REPAIR_REVIEW_DECISION",
952
+ "report.decision",
953
+ "must reconcile with this reviewer's blocking findings",
954
+ )
955
+ try:
956
+ signature = base64.b64decode(report.signature, validate=True)
957
+ public_key = next(item for item in scope.reviewers if item.lens == report.lens)
958
+ load_public_key(public_key.public_key).verify(
959
+ signature,
960
+ canonical_json_bytes(report.unsigned_dict()),
961
+ )
962
+ except (InvalidSignature, ValueError, TypeError, StopIteration) as exc:
963
+ raise RepairError(
964
+ "REPAIR_REPORT_SIGNATURE",
965
+ "report.signature",
966
+ "does not verify for the enrolled key and exact payload",
967
+ ) from exc
968
+
969
+ def _advance_anchor(
970
+ self,
971
+ current: CandidateReference,
972
+ inventory_digest: str,
973
+ ) -> None:
974
+ predecessor_digest = self._anchor.digest
975
+ successor = self._sign_anchor(
976
+ current=current,
977
+ inventory_digest=inventory_digest,
978
+ sequence=self._anchor.sequence + 1,
979
+ predecessor_anchor_digest=predecessor_digest,
980
+ )
981
+ self._commit_external_anchor(predecessor_digest, successor)
982
+ self._anchor = successor
983
+
984
+ def _sign_anchor(
985
+ self,
986
+ *,
987
+ current: CandidateReference,
988
+ inventory_digest: str,
989
+ sequence: int,
990
+ predecessor_anchor_digest: str | None,
991
+ ) -> CycleAnchor:
992
+ unsigned = {
993
+ "schema_version": CYCLE_ANCHOR_VERSION,
994
+ "run_id": self._run_id,
995
+ "root_candidate_digest": self._root_candidate_digest,
996
+ "current_candidate_digest": current.candidate_digest,
997
+ "current_state_digest": canonical_sha256(current.to_dict()),
998
+ "inventory_digest": inventory_digest,
999
+ "failed_candidate_count": current.failed_candidate_count,
1000
+ "sequence": sequence,
1001
+ "predecessor_anchor_digest": predecessor_anchor_digest,
1002
+ "authority_key_id": self._authority_key_id,
1003
+ "authority_public_key": self._authority_public_text,
1004
+ }
1005
+ try:
1006
+ signature = base64.b64encode(
1007
+ self._authority_signer.sign(canonical_json_bytes(unsigned))
1008
+ ).decode("ascii")
1009
+ except Exception as exc:
1010
+ raise RepairError(
1011
+ "REPAIR_SIGNER",
1012
+ "authority_signer",
1013
+ "could not sign the monotonic cycle anchor",
1014
+ ) from exc
1015
+ return CycleAnchor(**unsigned, signature=signature)
1016
+
1017
+ def _verify_current_anchor(self) -> None:
1018
+ external = self._load_external_anchor()
1019
+ if external is None or external.digest != self._anchor.digest:
1020
+ raise RepairError(
1021
+ "REPAIR_ANCHOR_STORE",
1022
+ "anchor_store",
1023
+ "external current anchor differs; restart from authoritative state",
1024
+ )
1025
+ if self._anchor.authority_public_key != self._authority_public_text:
1026
+ raise RepairError(
1027
+ "REPAIR_ANCHOR_AUTHORITY",
1028
+ "anchor.authority_public_key",
1029
+ "does not match the coordinator authority",
1030
+ )
1031
+ try:
1032
+ self._authority_public_key.verify(
1033
+ base64.b64decode(self._anchor.signature, validate=True),
1034
+ canonical_json_bytes(self._anchor.unsigned_dict()),
1035
+ )
1036
+ except (InvalidSignature, ValueError, TypeError) as exc:
1037
+ raise RepairError(
1038
+ "REPAIR_ANCHOR_SIGNATURE",
1039
+ "anchor.signature",
1040
+ "does not verify for the trusted coordinator authority",
1041
+ ) from exc
1042
+ if (
1043
+ self._anchor.current_candidate_digest != self._current.candidate_digest
1044
+ or self._anchor.current_state_digest != canonical_sha256(self._current.to_dict())
1045
+ or self._anchor.failed_candidate_count != self._current.failed_candidate_count
1046
+ ):
1047
+ raise RepairError(
1048
+ "REPAIR_ANCHOR_STATE",
1049
+ "anchor",
1050
+ "does not bind the coordinator's current candidate state",
1051
+ )
1052
+
1053
+ def _load_external_anchor(self) -> CycleAnchor | None:
1054
+ try:
1055
+ anchor = self._anchor_store.load(self._run_id)
1056
+ except RepairError:
1057
+ raise
1058
+ except Exception as exc:
1059
+ raise RepairError(
1060
+ "REPAIR_ANCHOR_STORE",
1061
+ "anchor_store",
1062
+ "could not load authoritative cycle state",
1063
+ ) from exc
1064
+ if anchor is not None and not isinstance(anchor, CycleAnchor):
1065
+ raise RepairError(
1066
+ "REPAIR_ANCHOR_STORE",
1067
+ "anchor_store",
1068
+ "returned an invalid cycle anchor",
1069
+ )
1070
+ return anchor
1071
+
1072
+ def _commit_external_anchor(
1073
+ self,
1074
+ expected_anchor_digest: str | None,
1075
+ successor: CycleAnchor,
1076
+ ) -> None:
1077
+ try:
1078
+ self._anchor_store.compare_and_swap(
1079
+ self._run_id,
1080
+ expected_anchor_digest,
1081
+ successor,
1082
+ )
1083
+ except RepairError:
1084
+ raise
1085
+ except Exception as exc:
1086
+ raise RepairError(
1087
+ "REPAIR_ANCHOR_STORE",
1088
+ "anchor_store",
1089
+ "rejected the monotonic compare-and-swap",
1090
+ ) from exc
1091
+
1092
+
1093
+ __all__ = [
1094
+ "ImmutableRepairCoordinator",
1095
+ "RepairExecution",
1096
+ "TrustedCycleAnchorStore",
1097
+ "bind_candidate",
1098
+ "make_closure_report",
1099
+ ]