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,892 @@
1
+ """The supported agent-facing API for durable, observable research work."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from collections.abc import Callable, Mapping
7
+ from datetime import UTC, datetime
8
+ from typing import Any
9
+
10
+ from mostlyright.data_harness.canonical import canonical_json_bytes
11
+ from mostlyright.data_harness.visual_run.contracts import (
12
+ ACTORS,
13
+ ArtifactReference,
14
+ TerminalError,
15
+ VisualRunDraft,
16
+ VisualRunError,
17
+ VisualRunEvent,
18
+ )
19
+ from mostlyright.data_harness.visual_run.materialize import MAX_MATERIALIZED_NOTEBOOK_BYTES
20
+ from mostlyright.data_harness.visual_run.observations import (
21
+ scrub_for_display,
22
+ validate_stage_observation,
23
+ )
24
+ from mostlyright.data_harness.visual_run.reducer import CellState, reduce_events
25
+ from mostlyright.data_harness.visual_run.store import VisualRunStore
26
+
27
+
28
+ def validate_markdown_checkpoint(source: str) -> None:
29
+ """Load renderer-backed authoring policy only when an agent writes markdown."""
30
+
31
+ from mostlyright.data_harness.visual_run.authoring import validate_markdown_checkpoint
32
+
33
+ validate_markdown_checkpoint(source)
34
+
35
+
36
+ def validate_markdown_completion(source: str) -> None:
37
+ """Load renderer-backed completion policy only when an agent completes markdown."""
38
+
39
+ from mostlyright.data_harness.visual_run.authoring import validate_markdown_completion
40
+
41
+ validate_markdown_completion(source)
42
+
43
+
44
+ def validate_markdown_document(source: str) -> None:
45
+ """Load the full-document authoring rule only for agent markdown writes."""
46
+
47
+ from mostlyright.data_harness.visual_run.authoring import validate_markdown_document
48
+
49
+ validate_markdown_document(source)
50
+
51
+
52
+ def _utc_now() -> str:
53
+ return datetime.now(UTC).isoformat(timespec="microseconds").replace("+00:00", "Z")
54
+
55
+
56
+ def next_attempt_coordinate(store: VisualRunStore, scope: str, attempt: str) -> str:
57
+ """Return the active or next durable attempt coordinate for a retryable command.
58
+
59
+ Producer attempt ids are fences and intentionally remain stable across process retries. A
60
+ visual phase, however, is one concrete invocation. Reusing the fence as the phase identity
61
+ caused a later failure to collide with the first invocation's terminal event and mask the
62
+ real error. Active phases are resumed idempotently; completed/failed phases advance to a
63
+ deterministic retry coordinate derived only from the durable log.
64
+ """
65
+
66
+ if not scope or ":" in scope:
67
+ raise ValueError("attempt scope must be a non-empty coordinate segment")
68
+ state = reduce_events(store.events())
69
+ ordinal = 0
70
+ while True:
71
+ base = f"{scope}:{attempt}"
72
+ coordinate = base if ordinal == 0 else f"{base}:retry-{ordinal}"
73
+ object_id = VisualWork._attempt_identity(coordinate)
74
+ existing = state.objects.get(object_id)
75
+ if existing is None or existing.status == "active":
76
+ return coordinate
77
+ ordinal += 1
78
+
79
+
80
+ def attempt_event_key(scope: str, coordinate: str, transition: str) -> str:
81
+ """Bind an attempt transition key to its invocation without exposing long coordinates."""
82
+
83
+ if transition not in {"started", "completed", "failed"}:
84
+ raise ValueError("unknown attempt transition")
85
+ digest = hashlib.sha256(coordinate.encode("utf-8")).hexdigest()[:24]
86
+ return f"{scope}:attempt:{digest}:{transition}"
87
+
88
+
89
+ class VisualWork:
90
+ """Persist meaningful agent cell phases through one versioned API.
91
+
92
+ Callers provide complete source/output states at real checkpoints. The SDK never sleeps,
93
+ slices text, invents progress, or derives ordering from timestamps.
94
+ """
95
+
96
+ @staticmethod
97
+ def _run_identity(run_id: str) -> str:
98
+ return f"run:{hashlib.sha256(run_id.encode('utf-8')).hexdigest()[:24]}"
99
+
100
+ def _run_object_id(self) -> str:
101
+ """Use the namespaced identity, while preserving already-durable v1 run objects."""
102
+
103
+ state = self._state()
104
+ existing = [object_id for object_id, kind in state.object_kinds.items() if kind == "run"]
105
+ if len(existing) > 1:
106
+ raise VisualRunError("VISUAL_RUN_TRANSITION", "run has multiple identity objects")
107
+ return existing[0] if existing else self._run_identity(self.store.run_id)
108
+
109
+ def __init__(
110
+ self,
111
+ store: VisualRunStore,
112
+ *,
113
+ observed_at: Callable[[], str] = _utc_now,
114
+ actor: str = "agent",
115
+ ) -> None:
116
+ if actor not in ACTORS:
117
+ raise ValueError("actor is outside the visual-run contract")
118
+ self.store = store
119
+ self._observed_at = observed_at
120
+ self._actor = actor
121
+
122
+ def _state(self) -> Any:
123
+ return reduce_events(self.store.events())
124
+
125
+ def _cell(self, cell_id: str) -> CellState:
126
+ cell = self._state().cells.get(cell_id)
127
+ if cell is None:
128
+ raise KeyError(cell_id)
129
+ return cell
130
+
131
+ def _append(
132
+ self,
133
+ event_type: str,
134
+ key: str,
135
+ *,
136
+ object_id: str,
137
+ object_version: int,
138
+ previous_object_version: int | None,
139
+ payload: Mapping[str, Any],
140
+ artifact: ArtifactReference | None = None,
141
+ status: str | None = None,
142
+ error: TerminalError | None = None,
143
+ phase: str = "researching",
144
+ stage: str = "authoring",
145
+ ) -> VisualRunEvent:
146
+ existing = self.store.event_for_key(key)
147
+ draft = VisualRunDraft.from_mapping(
148
+ {
149
+ "schema_version": "visual-run-event.v2",
150
+ "run_id": self.store.run_id,
151
+ "event_type": event_type,
152
+ "event_version": 1,
153
+ "actor": self._actor,
154
+ "phase": phase,
155
+ "stage": stage,
156
+ "object_id": object_id,
157
+ "object_version": object_version,
158
+ "previous_object_version": previous_object_version,
159
+ # Retries compare the durable timestamp, because time is not command intent.
160
+ "observed_at": existing.observed_at
161
+ if existing is not None
162
+ else self._observed_at(),
163
+ "idempotency_key": key,
164
+ "payload": dict(payload),
165
+ "artifact": artifact.to_mapping() if artifact is not None else None,
166
+ "status": status,
167
+ "error": error.to_mapping() if error is not None else None,
168
+ }
169
+ )
170
+ if existing is not None:
171
+ existing_value = VisualRunDraft.from_event(existing).to_mapping()
172
+ retry_value = draft.to_mapping()
173
+ existing_value.pop("observed_at")
174
+ retry_value.pop("observed_at")
175
+ if existing_value == retry_value:
176
+ return existing
177
+ raise VisualRunError(
178
+ "VISUAL_RUN_IDEMPOTENCY_CONFLICT", "idempotency key names another command"
179
+ )
180
+ return self.store.append(draft)
181
+
182
+ def start_run(self, *, key: str = "run-started") -> VisualRunEvent:
183
+ return self._append(
184
+ "run.started",
185
+ key,
186
+ # Run identity and object identity are separate namespaces. Hashing keeps an
187
+ # operator-selected run id such as ``build`` from colliding with fixed worker
188
+ # objects while the event's top-level ``run_id`` remains the public identity.
189
+ object_id=self._run_object_id(),
190
+ object_version=1,
191
+ previous_object_version=None,
192
+ payload={},
193
+ )
194
+
195
+ def start_cell(
196
+ self,
197
+ cell_id: str,
198
+ cell_type: str,
199
+ *,
200
+ key: str,
201
+ after_cell_id: str | None = None,
202
+ ) -> VisualRunEvent:
203
+ created = self._append(
204
+ "cell.created",
205
+ key,
206
+ object_id=cell_id,
207
+ object_version=1,
208
+ previous_object_version=None,
209
+ payload={"cell_type": cell_type},
210
+ )
211
+ if after_cell_id is not None:
212
+ self.move_after(cell_id, after_cell_id, key=f"{key}-position")
213
+ return created
214
+
215
+ def update_source(self, cell_id: str, source: str, *, key: str) -> VisualRunEvent:
216
+ if not isinstance(source, str):
217
+ raise TypeError("source must be text")
218
+ content = source.encode("utf-8")
219
+ existing = self.store.event_for_key(key)
220
+ if existing is not None:
221
+ reference = self._retry_artifact_reference(
222
+ existing,
223
+ content,
224
+ expected_schema="mostlyright.cell-source.v1",
225
+ )
226
+ return self._append(
227
+ "cell.source_extended",
228
+ key,
229
+ object_id=cell_id,
230
+ object_version=existing.object_version,
231
+ previous_object_version=existing.previous_object_version,
232
+ payload={"source_length": len(source)},
233
+ artifact=reference,
234
+ )
235
+ cell = self._cell(cell_id)
236
+ if cell.source is not None:
237
+ previous = self.store.read_artifact(cell.source)
238
+ if not content.startswith(previous) or content == previous:
239
+ raise ValueError("source extension must strictly append to the prior source")
240
+ if self._actor == "agent" and cell.cell_type == "markdown":
241
+ validate_markdown_document(content.decode("utf-8"))
242
+ previous_length = 0 if cell.source is None else len(previous)
243
+ validate_markdown_checkpoint(content[previous_length:].decode("utf-8"))
244
+ media_type = "text/markdown" if cell.cell_type == "markdown" else "text/x-python"
245
+ reference = self._install(
246
+ content,
247
+ artifact_id=f"{cell_id}-source-v{cell.version + 1}",
248
+ media_type=media_type,
249
+ artifact_schema="mostlyright.cell-source.v1",
250
+ projected=True,
251
+ replacing=cell.source,
252
+ )
253
+ return self._append(
254
+ "cell.source_extended",
255
+ key,
256
+ object_id=cell_id,
257
+ object_version=cell.version + 1,
258
+ previous_object_version=cell.version,
259
+ payload={"source_length": len(source)},
260
+ artifact=reference,
261
+ )
262
+
263
+ def revise_source(self, cell_id: str, source: str, *, key: str) -> VisualRunEvent:
264
+ if not isinstance(source, str):
265
+ raise TypeError("source must be text")
266
+ content = source.encode("utf-8")
267
+ existing = self.store.event_for_key(key)
268
+ if existing is not None:
269
+ reference = self._retry_artifact_reference(
270
+ existing,
271
+ content,
272
+ expected_schema="mostlyright.cell-source.v1",
273
+ )
274
+ return self._append(
275
+ "cell.source_revised",
276
+ key,
277
+ object_id=cell_id,
278
+ object_version=existing.object_version,
279
+ previous_object_version=existing.previous_object_version,
280
+ payload={"source_length": len(source)},
281
+ artifact=reference,
282
+ )
283
+ cell = self._cell(cell_id)
284
+ if self._actor == "agent" and cell.cell_type == "markdown":
285
+ validate_markdown_document(source)
286
+ validate_markdown_checkpoint(source)
287
+ media_type = "text/markdown" if cell.cell_type == "markdown" else "text/x-python"
288
+ reference = self._install(
289
+ content,
290
+ artifact_id=f"{cell_id}-revision-v{cell.version + 1}",
291
+ media_type=media_type,
292
+ artifact_schema="mostlyright.cell-source.v1",
293
+ projected=True,
294
+ replacing=cell.source,
295
+ )
296
+ return self._append(
297
+ "cell.source_revised",
298
+ key,
299
+ object_id=cell_id,
300
+ object_version=cell.version + 1,
301
+ previous_object_version=cell.version,
302
+ payload={"source_length": len(source)},
303
+ artifact=reference,
304
+ )
305
+
306
+ def move_after(self, cell_id: str, after_cell_id: str, *, key: str) -> VisualRunEvent:
307
+ existing = self.store.event_for_key(key)
308
+ if existing is not None:
309
+ return self._append(
310
+ "cell.moved",
311
+ key,
312
+ object_id=cell_id,
313
+ object_version=existing.object_version,
314
+ previous_object_version=existing.previous_object_version,
315
+ payload={"after_cell_id": after_cell_id},
316
+ )
317
+ cell = self._cell(cell_id)
318
+ return self._append(
319
+ "cell.moved",
320
+ key,
321
+ object_id=cell_id,
322
+ object_version=cell.version + 1,
323
+ previous_object_version=cell.version,
324
+ payload={"after_cell_id": after_cell_id},
325
+ )
326
+
327
+ def execution_started(self, cell_id: str, execution_count: int, *, key: str) -> VisualRunEvent:
328
+ existing = self.store.event_for_key(key)
329
+ if existing is not None:
330
+ return self._append(
331
+ "cell.execution_started",
332
+ key,
333
+ object_id=cell_id,
334
+ object_version=existing.object_version,
335
+ previous_object_version=existing.previous_object_version,
336
+ payload={"execution_count": execution_count},
337
+ stage="executing",
338
+ )
339
+ cell = self._cell(cell_id)
340
+ return self._append(
341
+ "cell.execution_started",
342
+ key,
343
+ object_id=cell_id,
344
+ object_version=cell.version + 1,
345
+ previous_object_version=cell.version,
346
+ payload={"execution_count": execution_count},
347
+ stage="executing",
348
+ )
349
+
350
+ def output(
351
+ self,
352
+ cell_id: str,
353
+ output: Mapping[str, Any],
354
+ *,
355
+ output_kind: str,
356
+ key: str,
357
+ index: int | None = None,
358
+ ) -> VisualRunEvent:
359
+ import json
360
+
361
+ content = (
362
+ json.dumps(
363
+ output, ensure_ascii=False, allow_nan=False, sort_keys=True, separators=(",", ":")
364
+ )
365
+ + "\n"
366
+ ).encode()
367
+ existing = self.store.event_for_key(key)
368
+ replace = index is not None
369
+ if existing is not None:
370
+ output_index = index if index is not None else existing.payload.get("output_index")
371
+ reference = self._retry_artifact_reference(
372
+ existing,
373
+ content,
374
+ expected_schema="mostlyright.cell-output.v1",
375
+ )
376
+ return self._append(
377
+ "cell.output_replaced" if replace else "cell.output_appended",
378
+ key,
379
+ object_id=cell_id,
380
+ object_version=existing.object_version,
381
+ previous_object_version=existing.previous_object_version,
382
+ payload={"output_index": output_index, "output_kind": output_kind},
383
+ artifact=reference,
384
+ stage="executing",
385
+ )
386
+ cell = self._cell(cell_id)
387
+ output_index = index if index is not None else len(cell.outputs)
388
+ if replace and not 0 <= output_index < len(cell.outputs):
389
+ raise ValueError("output replacement index is outside the current outputs")
390
+ reference = self._install(
391
+ content,
392
+ artifact_id=f"{cell_id}-output-{output_index}-v{cell.version + 1}",
393
+ media_type="application/json",
394
+ artifact_schema="mostlyright.cell-output.v1",
395
+ projected=True,
396
+ replacing=cell.outputs[output_index] if replace else None,
397
+ )
398
+ return self._append(
399
+ "cell.output_replaced" if replace else "cell.output_appended",
400
+ key,
401
+ object_id=cell_id,
402
+ object_version=cell.version + 1,
403
+ previous_object_version=cell.version,
404
+ payload={"output_index": output_index, "output_kind": output_kind},
405
+ artifact=reference,
406
+ stage="executing",
407
+ )
408
+
409
+ def complete(self, cell_id: str, *, key: str) -> VisualRunEvent:
410
+ existing = self.store.event_for_key(key)
411
+ if existing is not None:
412
+ return self._append(
413
+ "cell.completed",
414
+ key,
415
+ object_id=cell_id,
416
+ object_version=existing.object_version,
417
+ previous_object_version=existing.previous_object_version,
418
+ payload={},
419
+ stage="complete",
420
+ )
421
+ cell = self._cell(cell_id)
422
+ if self._actor == "agent" and cell.cell_type == "markdown" and cell.source is not None:
423
+ validate_markdown_completion(self.store.read_artifact(cell.source).decode("utf-8"))
424
+ return self._append(
425
+ "cell.completed",
426
+ key,
427
+ object_id=cell_id,
428
+ object_version=cell.version + 1,
429
+ previous_object_version=cell.version,
430
+ payload={},
431
+ stage="complete",
432
+ )
433
+
434
+ def fail(self, cell_id: str, error: TerminalError, *, key: str) -> VisualRunEvent:
435
+ existing = self.store.event_for_key(key)
436
+ if existing is not None:
437
+ return self._append(
438
+ "cell.failed",
439
+ key,
440
+ object_id=cell_id,
441
+ object_version=existing.object_version,
442
+ previous_object_version=existing.previous_object_version,
443
+ payload={},
444
+ error=error,
445
+ stage="failed",
446
+ )
447
+ cell = self._cell(cell_id)
448
+ return self._append(
449
+ "cell.failed",
450
+ key,
451
+ object_id=cell_id,
452
+ object_version=cell.version + 1,
453
+ previous_object_version=cell.version,
454
+ payload={},
455
+ error=error,
456
+ stage="failed",
457
+ )
458
+
459
+ def _update_versions(self, object_id: str, key: str) -> tuple[int, int]:
460
+ existing = self.store.event_for_key(key)
461
+ if existing is not None:
462
+ if existing.previous_object_version is None:
463
+ raise VisualRunError(
464
+ "VISUAL_RUN_IDEMPOTENCY_CONFLICT", "update key names a creation command"
465
+ )
466
+ return existing.object_version, existing.previous_object_version
467
+ current = self._state().object_versions.get(object_id)
468
+ if current is None:
469
+ raise KeyError(object_id)
470
+ return current + 1, current
471
+
472
+ @staticmethod
473
+ def _stage_identity(node_id: str) -> tuple[str, str, str]:
474
+ digest = hashlib.sha256(node_id.encode("utf-8")).hexdigest()
475
+ coordinate = node_id if len(node_id) <= 64 else f"{node_id[:47]}-{digest[:16]}"
476
+ return f"stage:{node_id}", digest, coordinate
477
+
478
+ def transformation_started(self, node_id: str, operation: str, *, key: str) -> VisualRunEvent:
479
+ object_id, _, coordinate = self._stage_identity(node_id)
480
+ return self._append(
481
+ "transformation.started",
482
+ key,
483
+ object_id=object_id,
484
+ object_version=1,
485
+ previous_object_version=None,
486
+ payload={"operation": operation},
487
+ phase="building",
488
+ stage=coordinate,
489
+ )
490
+
491
+ @staticmethod
492
+ def _attempt_identity(attempt: str) -> str:
493
+ return f"attempt:{hashlib.sha256(attempt.encode('utf-8')).hexdigest()[:24]}"
494
+
495
+ def attempt_started(self, attempt: str, *, key: str) -> VisualRunEvent:
496
+ return self._append(
497
+ "phase.started",
498
+ key,
499
+ object_id=self._attempt_identity(attempt),
500
+ object_version=1,
501
+ previous_object_version=None,
502
+ payload={},
503
+ phase="building",
504
+ stage="attempt",
505
+ )
506
+
507
+ def attempt_completed(self, attempt: str, *, key: str) -> VisualRunEvent:
508
+ object_id = self._attempt_identity(attempt)
509
+ version, previous = self._update_versions(object_id, key)
510
+ return self._append(
511
+ "phase.completed",
512
+ key,
513
+ object_id=object_id,
514
+ object_version=version,
515
+ previous_object_version=previous,
516
+ payload={},
517
+ phase="building",
518
+ stage="attempt",
519
+ )
520
+
521
+ def attempt_failed(self, attempt: str, error: TerminalError, *, key: str) -> VisualRunEvent:
522
+ object_id = self._attempt_identity(attempt)
523
+ version, previous = self._update_versions(object_id, key)
524
+ return self._append(
525
+ "phase.failed",
526
+ key,
527
+ object_id=object_id,
528
+ object_version=version,
529
+ previous_object_version=previous,
530
+ payload={},
531
+ error=error,
532
+ phase="building",
533
+ stage="attempt",
534
+ )
535
+
536
+ def transformation_completed(
537
+ self, node_id: str, operation: str, row_count: int, *, key: str
538
+ ) -> VisualRunEvent:
539
+ object_id, _, coordinate = self._stage_identity(node_id)
540
+ version, previous = self._update_versions(object_id, key)
541
+ return self._append(
542
+ "transformation.completed",
543
+ key,
544
+ object_id=object_id,
545
+ object_version=version,
546
+ previous_object_version=previous,
547
+ payload={"operation": operation, "row_count": row_count},
548
+ phase="building",
549
+ stage=coordinate,
550
+ )
551
+
552
+ def source_selected(self, source_id: str, source_kind: str, *, key: str) -> VisualRunEvent:
553
+ return self._append(
554
+ "source.selected",
555
+ key,
556
+ object_id=f"source:{source_id}",
557
+ object_version=1,
558
+ previous_object_version=None,
559
+ payload={"source_kind": source_kind},
560
+ phase="researching",
561
+ stage="source-selection",
562
+ )
563
+
564
+ def publish_source_observation(
565
+ self,
566
+ source_id: str,
567
+ observation: Mapping[str, Any],
568
+ *,
569
+ observation_kind: str,
570
+ key: str,
571
+ ) -> VisualRunEvent:
572
+ object_id = f"source:{source_id}"
573
+ version, previous = self._update_versions(object_id, key)
574
+ normalized, redacted = scrub_for_display(dict(observation))
575
+ assert isinstance(normalized, dict)
576
+ content = canonical_json_bytes(normalized) + b"\n"
577
+ digest = hashlib.sha256(source_id.encode("utf-8")).hexdigest()
578
+ reference = self._install(
579
+ content,
580
+ artifact_id=f"source-{digest[:24]}-observation-v{version}",
581
+ media_type="application/json",
582
+ artifact_schema="mostlyright.source-observation.v1",
583
+ display="summary" if redacted else "inline",
584
+ projected=True,
585
+ )
586
+ return self._append(
587
+ "source.observation_ready",
588
+ key,
589
+ object_id=object_id,
590
+ object_version=version,
591
+ previous_object_version=previous,
592
+ payload={"observation_kind": observation_kind},
593
+ artifact=reference,
594
+ phase="researching",
595
+ stage="source-inspection",
596
+ )
597
+
598
+ def publish_quality_observation(
599
+ self,
600
+ checks_passed: int,
601
+ checks_total: int,
602
+ *,
603
+ key: str,
604
+ report: Mapping[str, Any] | None = None,
605
+ ) -> VisualRunEvent:
606
+ observation = (
607
+ dict(report)
608
+ if report is not None
609
+ else {
610
+ "schema_version": "mostlyright.quality-observation.v1",
611
+ "status": "passed" if checks_passed == checks_total else "failed",
612
+ "checks": [],
613
+ }
614
+ )
615
+ observation = {
616
+ "schema_version": "mostlyright.quality-observation.v1",
617
+ "checks_passed": checks_passed,
618
+ "checks_total": checks_total,
619
+ "report": observation,
620
+ }
621
+ normalized, redacted = scrub_for_display(observation)
622
+ assert isinstance(normalized, dict)
623
+ content = canonical_json_bytes(normalized) + b"\n"
624
+ reference = self._install(
625
+ content,
626
+ artifact_id="quality-observation",
627
+ media_type="application/json",
628
+ artifact_schema="mostlyright.quality-observation.v1",
629
+ display="summary" if redacted else "inline",
630
+ )
631
+ return self._append(
632
+ "quality.completed",
633
+ key,
634
+ object_id="quality",
635
+ object_version=1,
636
+ previous_object_version=None,
637
+ payload={"checks_passed": checks_passed, "checks_total": checks_total},
638
+ artifact=reference,
639
+ phase="building",
640
+ stage="quality",
641
+ )
642
+
643
+ def table_written(
644
+ self, table_sha256: str, *, row_count: int, column_count: int, key: str
645
+ ) -> VisualRunEvent:
646
+ if len(table_sha256) != 64 or any(
647
+ character not in "0123456789abcdef" for character in table_sha256
648
+ ):
649
+ raise ValueError("table_sha256 must be lowercase SHA-256")
650
+ content = (
651
+ canonical_json_bytes(
652
+ {
653
+ "schema_version": "mostlyright.table-reference.v1",
654
+ "table_sha256": table_sha256,
655
+ "row_count": row_count,
656
+ "column_count": column_count,
657
+ }
658
+ )
659
+ + b"\n"
660
+ )
661
+ reference = self._install(
662
+ content,
663
+ artifact_id=f"table-{table_sha256[:24]}",
664
+ media_type="application/json",
665
+ artifact_schema="mostlyright.table-reference.v1",
666
+ display="summary",
667
+ )
668
+ return self._append(
669
+ "table_written",
670
+ key,
671
+ object_id="table",
672
+ object_version=1,
673
+ previous_object_version=None,
674
+ payload={"row_count": row_count, "column_count": column_count},
675
+ artifact=reference,
676
+ phase="building",
677
+ stage="table",
678
+ )
679
+
680
+ def publish_table_observation(
681
+ self, table_sha256: str, *, row_count: int, column_count: int, key: str
682
+ ) -> VisualRunEvent:
683
+ return self.table_written(
684
+ table_sha256, row_count=row_count, column_count=column_count, key=key
685
+ )
686
+
687
+ def build_sealed(self, candidate_digest: str, *, key: str) -> VisualRunEvent:
688
+ return self._append(
689
+ "build.sealed",
690
+ key,
691
+ object_id="build",
692
+ object_version=1,
693
+ previous_object_version=None,
694
+ payload={"candidate_digest": candidate_digest},
695
+ phase="building",
696
+ stage="sealing",
697
+ )
698
+
699
+ def build_verified(self, candidate_digest: str, *, key: str) -> VisualRunEvent:
700
+ version, previous = self._update_versions("build", key)
701
+ return self._append(
702
+ "build.verified",
703
+ key,
704
+ object_id="build",
705
+ object_version=version,
706
+ previous_object_version=previous,
707
+ payload={"candidate_digest": candidate_digest},
708
+ phase="building",
709
+ stage="verification",
710
+ )
711
+
712
+ def notebook_materialized(self, kind: str, content: bytes, *, key: str) -> VisualRunEvent:
713
+ if kind not in {"research", "table"}:
714
+ raise ValueError("notebook kind must be research or table")
715
+ digest = hashlib.sha256(content).hexdigest()
716
+ reference = self._install(
717
+ content,
718
+ artifact_id=f"notebook-{kind}-{digest[:24]}",
719
+ media_type="application/x-ipynb+json",
720
+ artifact_schema="mostlyright.notebook.v1",
721
+ display="inline",
722
+ )
723
+ return self._append(
724
+ f"notebook.{kind}_materialized",
725
+ key,
726
+ object_id=f"notebook:{kind}",
727
+ object_version=1,
728
+ previous_object_version=None,
729
+ payload={"notebook_digest": digest},
730
+ artifact=reference,
731
+ phase="building" if kind == "table" else "researching",
732
+ stage="notebook",
733
+ )
734
+
735
+ def complete_run(self, *, key: str) -> VisualRunEvent:
736
+ object_id = self._run_object_id()
737
+ version, previous = self._update_versions(object_id, key)
738
+ return self._append(
739
+ "run.completed",
740
+ key,
741
+ object_id=object_id,
742
+ object_version=version,
743
+ previous_object_version=previous,
744
+ payload={},
745
+ status="completed",
746
+ phase="complete",
747
+ stage="complete",
748
+ )
749
+
750
+ def fail_run(self, error: TerminalError, *, key: str) -> VisualRunEvent:
751
+ object_id = self._run_object_id()
752
+ version, previous = self._update_versions(object_id, key)
753
+ return self._append(
754
+ "run.failed",
755
+ key,
756
+ object_id=object_id,
757
+ object_version=version,
758
+ previous_object_version=previous,
759
+ payload={},
760
+ status="failed",
761
+ error=error,
762
+ phase="failed",
763
+ stage="failed",
764
+ )
765
+
766
+ def publish_stage_observation(
767
+ self,
768
+ observation: Mapping[str, Any],
769
+ *,
770
+ key: str,
771
+ ) -> VisualRunEvent:
772
+ """Persist one deterministic graph-stage observation before candidate sealing."""
773
+
774
+ candidate, redacted = scrub_for_display(dict(observation))
775
+ assert isinstance(candidate, dict)
776
+ if redacted:
777
+ candidate["display"] = "schema_only"
778
+ candidate["sample"] = []
779
+ if "operation_parameters" in candidate:
780
+ candidate["operation_parameters"] = {}
781
+ if "source_coordinates" in candidate:
782
+ candidate["source_coordinates"] = []
783
+ normalized = validate_stage_observation(candidate)
784
+ node_id = str(normalized["node_id"])
785
+ operation = str(normalized["operation"])
786
+ row_count = normalized["output_rows"]
787
+ object_id, node_digest, stage_coordinate = self._stage_identity(node_id)
788
+ if object_id not in self._state().object_versions:
789
+ self.transformation_started(node_id, operation, key=f"{key}:started")
790
+ content = canonical_json_bytes(normalized) + b"\n"
791
+ display = {
792
+ "full": "inline",
793
+ "redacted": "redacted",
794
+ "aggregate_only": "summary",
795
+ "schema_only": "summary",
796
+ }.get(str(normalized["display"]))
797
+ if display is None:
798
+ raise ValueError("stage observation display policy is invalid")
799
+ reference = self._install(
800
+ content,
801
+ artifact_id=f"stage-{node_digest[:24]}-observation",
802
+ media_type="application/json",
803
+ artifact_schema=str(normalized["schema_version"]),
804
+ display=display,
805
+ projected=True,
806
+ )
807
+ version, previous = self._update_versions(object_id, f"{key}:ready")
808
+ return self._append(
809
+ "stage.observation_ready",
810
+ f"{key}:ready",
811
+ object_id=object_id,
812
+ object_version=version,
813
+ previous_object_version=previous,
814
+ payload={"operation": operation, "row_count": row_count},
815
+ artifact=reference,
816
+ phase="building",
817
+ stage=stage_coordinate,
818
+ )
819
+
820
+ def _install(
821
+ self,
822
+ content: bytes,
823
+ *,
824
+ artifact_id: str,
825
+ media_type: str,
826
+ artifact_schema: str,
827
+ display: str = "inline",
828
+ projected: bool = False,
829
+ replacing: ArtifactReference | None = None,
830
+ ) -> ArtifactReference:
831
+ if projected:
832
+ state = self._state()
833
+ current_bytes = sum(
834
+ reference.byte_size
835
+ for cell in state.cells.values()
836
+ for reference in ((cell.source,) if cell.source is not None else ()) + cell.outputs
837
+ ) + sum(reference.byte_size for reference in state.observations)
838
+ prospective_bytes = current_bytes + len(content)
839
+ if replacing is not None:
840
+ prospective_bytes -= replacing.byte_size
841
+ if prospective_bytes > MAX_MATERIALIZED_NOTEBOOK_BYTES:
842
+ raise VisualRunError(
843
+ "VISUAL_RUN_PROJECTION_LIMIT",
844
+ "event would exceed the materialized research notebook limit",
845
+ )
846
+ if len(content) > MAX_MATERIALIZED_NOTEBOOK_BYTES:
847
+ raise VisualRunError(
848
+ "VISUAL_RUN_PROJECTION_LIMIT",
849
+ "artifact exceeds the materialized research notebook limit",
850
+ )
851
+ reference = ArtifactReference.from_mapping(
852
+ {
853
+ "schema_version": "visual-run-artifact-ref.v2",
854
+ "artifact_id": artifact_id,
855
+ "sha256": hashlib.sha256(content).hexdigest(),
856
+ "media_type": media_type,
857
+ "artifact_schema": artifact_schema,
858
+ "byte_size": len(content),
859
+ "truncated": False,
860
+ "original_byte_size": len(content),
861
+ "display": display,
862
+ }
863
+ )
864
+ self.store.install_artifact(reference, content)
865
+ return reference
866
+
867
+ def _retry_artifact_reference(
868
+ self,
869
+ existing: VisualRunEvent,
870
+ content: bytes,
871
+ *,
872
+ expected_schema: str,
873
+ ) -> ArtifactReference:
874
+ """Rebuild retry intent without consulting mutable state or installing an artifact."""
875
+
876
+ reference = existing.artifact
877
+ if reference is None or reference.artifact_schema != expected_schema:
878
+ raise VisualRunError(
879
+ "VISUAL_RUN_IDEMPOTENCY_CONFLICT",
880
+ "idempotency key names another command",
881
+ )
882
+ return ArtifactReference.from_mapping(
883
+ {
884
+ **reference.to_mapping(),
885
+ "sha256": hashlib.sha256(content).hexdigest(),
886
+ "byte_size": len(content),
887
+ "original_byte_size": len(content),
888
+ }
889
+ )
890
+
891
+
892
+ __all__ = ["VisualWork", "attempt_event_key", "next_attempt_coordinate"]