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,2019 @@
1
+ """Lazy, strict boundary between Harness-private contracts and Studio wire models.
2
+
3
+ Studio's generated Python package is deliberately optional. This module imports no Studio
4
+ module at import time, so local/replay execution remains installable without the hosted client.
5
+ Every hosted mapping is explicit, carries tenant/fence context, and returns the generated model
6
+ plus deterministic mapping evidence.
7
+
8
+ Terms used throughout. The worker protocol that issues them is docs/HOSTED-WORKERS.md.
9
+
10
+ - Generation and fence: Studio-owned positive integers that order reissues and retries of one
11
+ attempt. The Harness never derives or increments them. It carries the exact values Studio
12
+ issued and rejects any server value that differs, so a stale worker cannot write over newer
13
+ state. producer_generation, producer_fence, verification_generation, verifier_generation
14
+ and verifier_fence are all of this kind. object_generation is not: it is the storage
15
+ object's own generation, a decimal string on signed-session wire values.
16
+ - Epoch: a Studio-owned positive integer naming which candidate selection (selection_epoch) or
17
+ which recipe activation (activation_epoch) a scope belongs to. Also carried, never derived.
18
+ - HostedFence: the tenant and attempt coordinates a caller must supply before this module will
19
+ parse a Studio-owned value. Parsing fails closed when a required coordinate is absent.
20
+ - Model-free V3: the contract set pinned by PINNED_CONTRACT_SET, in which verification is
21
+ deterministic and no model sits in the loop. The surfaces that once carried model-mediated
22
+ semantic review still exist here and always raise, so a caller on that path fails loudly
23
+ instead of sending evidence Studio no longer accepts.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import hashlib
29
+ import importlib
30
+ import json
31
+ import re
32
+ from collections.abc import Mapping, Sequence
33
+ from dataclasses import dataclass
34
+ from datetime import UTC, datetime
35
+ from functools import lru_cache
36
+ from types import ModuleType
37
+ from typing import Any
38
+ from uuid import UUID
39
+
40
+ from mostlyright.data_harness.agent_runtime import AgentCallEvidence
41
+ from mostlyright.data_harness.local_contracts import (
42
+ LocalQuestion,
43
+ LocalRequirements,
44
+ SourceProposal,
45
+ )
46
+ from mostlyright.data_harness.local_contracts import (
47
+ TablePlan as LocalTablePlan,
48
+ )
49
+ from mostlyright.data_harness.recipe import BACKFILL_POLICY_SCHEMA_VERSION, BackfillPolicy
50
+
51
+ PINNED_CONTRACT_SET = "mostlyright-studio-v3"
52
+ PINNED_CONTRACT_VERSION = "3.0.0"
53
+ PINNED_GENERATOR = "openapi-python-client"
54
+ PINNED_GENERATOR_VERSION = "0.27.1"
55
+ PINNED_OPENAPI_SOURCE_SHA256 = "d63d5e36527c7a13b5c27c73589b583604b832b11231efc73267d63ed490e694"
56
+
57
+ _DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
58
+ _IMAGE_DIGEST = re.compile(r"^[a-z0-9][a-z0-9._/-]{0,254}@sha256:[0-9a-f]{64}$")
59
+ _SEMVER = re.compile(r"^[1-9][0-9]*\.[0-9]+\.[0-9]+$")
60
+ _UTC = re.compile(
61
+ r"^(?:[0-9]{4})-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])"
62
+ r"T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\.[0-9]{1,9})?Z$"
63
+ )
64
+ _UUID_FIELDS = frozenset(
65
+ {
66
+ "admission_id",
67
+ "artifact_id",
68
+ "actor_principal_id",
69
+ "budget_id",
70
+ "call_id",
71
+ "closure_review_id",
72
+ "connector_id",
73
+ "event_id",
74
+ "finding_id",
75
+ "gate_lineage_id",
76
+ "table_plan_id",
77
+ "predecessor_table_plan_id",
78
+ "producer_attempt_id",
79
+ "dataset_id",
80
+ "question_id",
81
+ "requirements_id",
82
+ "review_id",
83
+ "rights_decision_id",
84
+ "run_id",
85
+ "source_id",
86
+ "submission_id",
87
+ "superseded_by_admission_id",
88
+ "verifier_principal_id",
89
+ "workspace_id",
90
+ "attempt_id",
91
+ }
92
+ )
93
+ _TIMESTAMP_FIELDS = frozenset(
94
+ {
95
+ "occurred_at",
96
+ "created_at",
97
+ "sealed_at",
98
+ "retention_expires_at",
99
+ "admitted_at",
100
+ "selected_at",
101
+ "revoked_at",
102
+ "opened_at",
103
+ "closed_at",
104
+ "started_at",
105
+ "completed_at",
106
+ "start_inclusive",
107
+ "end_exclusive",
108
+ "result_observed_at",
109
+ }
110
+ )
111
+ _BARE_DIGEST_FIELDS = frozenset(
112
+ {
113
+ "artifact_manifest_digest",
114
+ "bootstrap_table_digest",
115
+ "bootstrap_verification_digest",
116
+ "execution_scope_digest",
117
+ "lineage_digest",
118
+ "predecessor_recipe_digest",
119
+ "recipe_approval_digest",
120
+ "recipe_digest",
121
+ "resulting_source_states_digest",
122
+ "source_authority_bindings_digest",
123
+ "source_authority_digest",
124
+ "state_digest",
125
+ }
126
+ )
127
+
128
+
129
+ class StudioBoundaryError(RuntimeError):
130
+ """Base error for hosted-boundary loading, compatibility, and mapping failures."""
131
+
132
+
133
+ class StudioClientUnavailableError(StudioBoundaryError, ImportError):
134
+ """The optional generated Studio client is not installed."""
135
+
136
+
137
+ class StudioClientIncompatibleError(StudioBoundaryError):
138
+ """The installed generated client does not match the pinned contract identity."""
139
+
140
+
141
+ class StudioBoundaryValidationError(StudioBoundaryError, ValueError):
142
+ """A local value or generated wire value cannot cross the boundary losslessly."""
143
+
144
+
145
+ @dataclass(frozen=True)
146
+ class StudioContractIdentity:
147
+ contract_set: str
148
+ contract_version: str
149
+ generator: str
150
+ generator_version: str
151
+ openapi_source_sha256: str
152
+
153
+ def to_dict(self) -> dict[str, str]:
154
+ return {
155
+ "contract_set": self.contract_set,
156
+ "contract_version": self.contract_version,
157
+ "generator": self.generator,
158
+ "generator_version": self.generator_version,
159
+ "openapi_source_sha256": self.openapi_source_sha256,
160
+ }
161
+
162
+
163
+ @dataclass(frozen=True)
164
+ class BoundaryMappingEvidence:
165
+ contract: StudioContractIdentity
166
+ model_name: str
167
+ local_schema_version: str
168
+ local_digest: str
169
+ workspace_id: UUID
170
+ run_id: UUID | None = None
171
+ attempt_id: UUID | None = None
172
+ producer_generation: int | None = None
173
+ producer_fence: int | None = None
174
+
175
+ def to_dict(self) -> dict[str, Any]:
176
+ result: dict[str, Any] = {
177
+ "contract": self.contract.to_dict(),
178
+ "model_name": self.model_name,
179
+ "local_schema_version": self.local_schema_version,
180
+ "local_digest": _wire_digest(
181
+ self.local_digest,
182
+ "local_digest",
183
+ allow_raw=True,
184
+ ),
185
+ "workspace_id": str(self.workspace_id),
186
+ }
187
+ for name in ("run_id", "attempt_id"):
188
+ value = getattr(self, name)
189
+ if value is not None:
190
+ result[name] = str(value)
191
+ for name in ("producer_generation", "producer_fence"):
192
+ value = getattr(self, name)
193
+ if value is not None:
194
+ result[name] = value
195
+ return result
196
+
197
+
198
+ @dataclass(frozen=True)
199
+ class MappedStudioValue:
200
+ """A generated Studio model paired with the exact contract/mapping identity."""
201
+
202
+ model: Any
203
+ evidence: BoundaryMappingEvidence
204
+
205
+ def to_wire_dict(self) -> dict[str, Any]:
206
+ return studio_wire_dict(self.model)
207
+
208
+
209
+ @dataclass(frozen=True)
210
+ class QuestionIdentity:
211
+ workspace_id: UUID
212
+ dataset_id: UUID
213
+ question_id: UUID
214
+ created_by_principal_id: UUID
215
+ status: str = "draft"
216
+
217
+ def __post_init__(self) -> None:
218
+ _uuids(self)
219
+ _nonempty(self.status, "status", maximum=64)
220
+
221
+
222
+ @dataclass(frozen=True)
223
+ class RequirementsIdentity:
224
+ workspace_id: UUID
225
+ requirements_id: UUID
226
+ question_id: UUID
227
+ prediction_cutoff: str | None = None
228
+
229
+ def __post_init__(self) -> None:
230
+ _uuids(self)
231
+ if self.prediction_cutoff is not None:
232
+ _parse_utc(self.prediction_cutoff, "prediction_cutoff")
233
+
234
+
235
+ @dataclass(frozen=True)
236
+ class OpaqueSecretBinding:
237
+ name: str
238
+ handle_id: str
239
+ workspace_id: UUID
240
+ connector_id: UUID
241
+ attempt_id: UUID
242
+ purpose: str
243
+
244
+ def __post_init__(self) -> None:
245
+ _uuids(self)
246
+ if not re.fullmatch(r"[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*", self.name):
247
+ raise StudioBoundaryValidationError("secret.name must be a canonical identifier")
248
+ if not re.fullmatch(r"shr_[A-Za-z0-9_-]{24,120}", self.handle_id):
249
+ raise StudioBoundaryValidationError(
250
+ "secret.handle_id must be a server-issued opaque handle"
251
+ )
252
+ if self.purpose not in {"provider_auth", "source_acquisition", "web_extraction"}:
253
+ raise StudioBoundaryValidationError("secret.purpose is unsupported")
254
+
255
+
256
+ @dataclass(frozen=True)
257
+ class SourceIdentity:
258
+ workspace_id: UUID
259
+ dataset_id: UUID
260
+ source_id: UUID
261
+ data_classification: str
262
+ raw_retention_days: int
263
+ derived_retention_days: int
264
+ tombstone_required: bool
265
+ status: str
266
+ rights_evidence_digest: str | None
267
+ rights_claim_digest: str
268
+ rights_policy_digest: str
269
+ rights_adjudication: str
270
+ credential_reference_ids: tuple[UUID, ...] = ()
271
+ connector_contract_version: str | None = None
272
+ rights_decision_id: UUID | None = None
273
+
274
+ def __post_init__(self) -> None:
275
+ _uuids(self)
276
+ _nonempty(self.data_classification, "data_classification", maximum=64)
277
+ _bounded_int(self.raw_retention_days, "raw_retention_days", 0, 3650)
278
+ _bounded_int(self.derived_retention_days, "derived_retention_days", 0, 3650)
279
+ if type(self.tombstone_required) is not bool:
280
+ raise StudioBoundaryValidationError("tombstone_required must be a boolean")
281
+ _nonempty(self.status, "status", maximum=64)
282
+ if self.rights_evidence_digest is not None:
283
+ _wire_digest(self.rights_evidence_digest, "rights_evidence_digest")
284
+ _wire_digest(self.rights_claim_digest, "rights_claim_digest")
285
+ _wire_digest(self.rights_policy_digest, "rights_policy_digest")
286
+ if self.rights_adjudication not in {"server_derived", "human_reviewed"}:
287
+ raise StudioBoundaryValidationError("rights_adjudication is unsupported")
288
+ if self.rights_adjudication == "human_reviewed":
289
+ if self.rights_decision_id is None or self.rights_evidence_digest is None:
290
+ raise StudioBoundaryValidationError(
291
+ "human_reviewed rights require decision ID and evidence digest"
292
+ )
293
+ elif self.rights_decision_id is not None:
294
+ raise StudioBoundaryValidationError(
295
+ "server_derived rights cannot carry a human decision ID"
296
+ )
297
+ if len(self.credential_reference_ids) > 16:
298
+ raise StudioBoundaryValidationError("credential_reference_ids exceeds 16 entries")
299
+ if not all(isinstance(item, UUID) for item in self.credential_reference_ids):
300
+ raise StudioBoundaryValidationError("credential_reference_ids must contain UUIDs")
301
+ if len(self.credential_reference_ids) != len(set(self.credential_reference_ids)):
302
+ raise StudioBoundaryValidationError("credential_reference_ids contains duplicates")
303
+ if self.connector_contract_version is not None:
304
+ _semver(self.connector_contract_version, "connector_contract_version")
305
+
306
+
307
+ @dataclass(frozen=True)
308
+ class PlanIdentity:
309
+ workspace_id: UUID
310
+ dataset_id: UUID
311
+ table_plan_id: UUID
312
+ question_id: UUID
313
+ requirements_id: UUID
314
+ source_ids: tuple[tuple[str, UUID], ...]
315
+ revision: int
316
+ transformation_contract_version: str
317
+ validation_policy_digest: str
318
+ backfill_policy: BackfillPolicy
319
+ status: str
320
+ created_at: str
321
+ predecessor_table_plan_id: UUID | None = None
322
+ # map_plan, the only consumer, rejects any non-None value on the model-free V3 plan boundary.
323
+ semantic_approval_key_digest: str | None = None
324
+
325
+ def __post_init__(self) -> None:
326
+ _uuids(self)
327
+ _bounded_int(self.revision, "revision", 1, (1 << 63) - 1)
328
+ _semver(self.transformation_contract_version, "transformation_contract_version")
329
+ _wire_digest(self.validation_policy_digest, "validation_policy_digest")
330
+ if not isinstance(self.backfill_policy, BackfillPolicy):
331
+ raise StudioBoundaryValidationError("backfill_policy must be a concrete Harness policy")
332
+ _nonempty(self.status, "status", maximum=64)
333
+ _parse_utc(self.created_at, "created_at")
334
+ if self.semantic_approval_key_digest is not None:
335
+ _wire_digest(self.semantic_approval_key_digest, "semantic_approval_key_digest")
336
+ local_names = [item[0] for item in self.source_ids]
337
+ if len(local_names) != len(set(local_names)):
338
+ raise StudioBoundaryValidationError("source_ids contains duplicate local identifiers")
339
+ hosted_ids = [item[1] for item in self.source_ids]
340
+ if len(hosted_ids) != len(set(hosted_ids)):
341
+ raise StudioBoundaryValidationError("source_ids contains duplicate hosted identifiers")
342
+ if not all(isinstance(item, UUID) for item in hosted_ids):
343
+ raise StudioBoundaryValidationError("source_ids must map to UUID values")
344
+
345
+ @property
346
+ def backfill_policy_digest(self) -> str:
347
+ """Studio wire digest of the exact immutable Harness policy bytes."""
348
+
349
+ return f"sha256:{self.backfill_policy.digest}"
350
+
351
+
352
+ @dataclass(frozen=True)
353
+ class AgentCallIdentity:
354
+ workspace_id: UUID
355
+ run_id: UUID
356
+ attempt_id: UUID
357
+ call_id: UUID
358
+ budget_id: UUID
359
+ evidence_digest: str
360
+ started_at: str
361
+ completed_at: str
362
+ currency: str
363
+
364
+ def __post_init__(self) -> None:
365
+ _uuids(self)
366
+ if not re.fullmatch(r"[0-9a-f]{64}", self.evidence_digest):
367
+ raise StudioBoundaryValidationError("evidence_digest must be a raw SHA-256")
368
+ started = _parse_utc(self.started_at, "started_at")
369
+ completed = _parse_utc(self.completed_at, "completed_at")
370
+ if completed < started:
371
+ raise StudioBoundaryValidationError("completed_at precedes started_at")
372
+ if self.currency != "USD":
373
+ raise StudioBoundaryValidationError("agent-call micro-cost currency must be USD")
374
+
375
+
376
+ @dataclass(frozen=True)
377
+ class HostedFence:
378
+ """Expected tenant, run, and attempt coordinates for parsing Studio-owned values.
379
+
380
+ Optional fields are supplied only when the caller holds them. Each parser states which of
381
+ them it requires and fails closed when a required one is None.
382
+ """
383
+
384
+ workspace_id: UUID
385
+ run_id: UUID
386
+ attempt_id: UUID | None = None
387
+ gate_lineage_id: UUID | None = None
388
+ producer_generation: int | None = None
389
+ producer_fence: int | None = None
390
+
391
+ def __post_init__(self) -> None:
392
+ _uuids(self)
393
+ for name in ("producer_generation", "producer_fence"):
394
+ value = getattr(self, name)
395
+ if value is not None:
396
+ _bounded_int(value, name, 1, (1 << 63) - 1)
397
+
398
+
399
+ @dataclass(frozen=True)
400
+ class ProducerEvidenceScope:
401
+ """Server-issued producer attempt coordinates required on evidence ingress."""
402
+
403
+ workspace_id: UUID
404
+ run_id: UUID
405
+ attempt_id: UUID
406
+ producer_generation: int
407
+ producer_fence: int
408
+
409
+ def __post_init__(self) -> None:
410
+ _uuids(self)
411
+ _bounded_int(
412
+ self.producer_generation,
413
+ "producer_generation",
414
+ 1,
415
+ (1 << 63) - 1,
416
+ )
417
+ _bounded_int(self.producer_fence, "producer_fence", 1, (1 << 63) - 1)
418
+
419
+
420
+ @dataclass(frozen=True)
421
+ class VerifierReviewScope:
422
+ """Complete server-issued coordinates for verifier reads and report submission."""
423
+
424
+ workspace_id: UUID
425
+ run_id: UUID
426
+ gate_lineage_id: UUID
427
+ attempt_id: UUID
428
+ verification_generation: int
429
+ verifier_generation: int
430
+ verifier_fence: int
431
+ admission_id: UUID
432
+ selection_epoch: int
433
+ candidate_digest: str
434
+ candidate_contract_version: str
435
+ dataset_id: UUID
436
+ table_id: UUID
437
+ table_recipe_id: str
438
+ recipe_version: int
439
+ recipe_digest: str
440
+ activation_epoch: int
441
+ execution_scope_digest: str
442
+ validation_policy_digest: str
443
+ release_policy_digest: str
444
+ artifact_scope_digest: str
445
+ verifier_principal_id: UUID
446
+ verifier_image_digest: str
447
+ required_check_ids: tuple[str, ...]
448
+ attestation_scheme: str
449
+ attestation_key_id: str
450
+ attestation_key_version: int
451
+ resulting_source_states: tuple[Mapping[str, Any], ...] | None = None
452
+ resulting_source_states_digest: str | None = None
453
+
454
+ def __post_init__(self) -> None:
455
+ _uuids(self)
456
+ for name in (
457
+ "verification_generation",
458
+ "verifier_generation",
459
+ "verifier_fence",
460
+ "selection_epoch",
461
+ "recipe_version",
462
+ "activation_epoch",
463
+ ):
464
+ _bounded_int(getattr(self, name), name, 1, (1 << 63) - 1)
465
+ for name in (
466
+ "candidate_digest",
467
+ "validation_policy_digest",
468
+ "release_policy_digest",
469
+ "artifact_scope_digest",
470
+ ):
471
+ _wire_digest(getattr(self, name), name)
472
+ _bare_digest(self.recipe_digest, "recipe_digest")
473
+ _bare_digest(self.execution_scope_digest, "execution_scope_digest")
474
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}", self.table_recipe_id):
475
+ raise StudioBoundaryValidationError("table_recipe_id is not canonical")
476
+ _semver(self.candidate_contract_version, "candidate_contract_version")
477
+ if not _IMAGE_DIGEST.fullmatch(self.verifier_image_digest):
478
+ raise StudioBoundaryValidationError("verifier_image_digest is not canonical")
479
+ if (
480
+ type(self.required_check_ids) is not tuple
481
+ or not self.required_check_ids
482
+ or len(self.required_check_ids) != len(set(self.required_check_ids))
483
+ ):
484
+ raise StudioBoundaryValidationError(
485
+ "required_check_ids must be a nonempty unique tuple"
486
+ )
487
+ for check_id in self.required_check_ids:
488
+ if not re.fullmatch(r"[A-Z][A-Z0-9_]{2,127}", check_id):
489
+ raise StudioBoundaryValidationError("required_check_ids contains an invalid ID")
490
+ if (self.resulting_source_states is None) != (self.resulting_source_states_digest is None):
491
+ raise StudioBoundaryValidationError(
492
+ "resulting source states and digest must both be present or absent"
493
+ )
494
+ if self.resulting_source_states is not None:
495
+ if type(self.resulting_source_states) is not tuple or not self.resulting_source_states:
496
+ raise StudioBoundaryValidationError(
497
+ "resulting_source_states must be a nonempty tuple"
498
+ )
499
+ canonical_states = [dict(item) for item in self.resulting_source_states]
500
+ if canonical_states != sorted(
501
+ canonical_states,
502
+ key=lambda item: item.get("source_id", ""),
503
+ ):
504
+ raise StudioBoundaryValidationError(
505
+ "resulting_source_states must be ordered by source_id"
506
+ )
507
+ expected_digest = hashlib.sha256(
508
+ json.dumps(
509
+ canonical_states,
510
+ sort_keys=True,
511
+ separators=(",", ":"),
512
+ ensure_ascii=False,
513
+ ).encode("utf-8")
514
+ ).hexdigest()
515
+ _match(
516
+ self.resulting_source_states_digest,
517
+ expected_digest,
518
+ "resulting_source_states_digest",
519
+ )
520
+ if self.attestation_scheme != "ed25519-v1":
521
+ raise StudioBoundaryValidationError("attestation_scheme is unsupported")
522
+ _nonempty(self.attestation_key_id, "attestation_key_id", maximum=512)
523
+ _bounded_int(
524
+ self.attestation_key_version,
525
+ "attestation_key_version",
526
+ 1,
527
+ (1 << 63) - 1,
528
+ )
529
+
530
+
531
+ @dataclass(frozen=True)
532
+ class _StudioAPI:
533
+ models: ModuleType
534
+ unset: Any
535
+ identity: StudioContractIdentity
536
+
537
+
538
+ def client_contract_identity() -> StudioContractIdentity:
539
+ """Load and verify the optional generated client, returning its pinned identity."""
540
+
541
+ return _studio_api().identity
542
+
543
+
544
+ def ensure_studio_client() -> None:
545
+ """Fail closed if the installed hosted extra is absent or incompatible."""
546
+
547
+ _studio_api()
548
+
549
+
550
+ @lru_cache(maxsize=1)
551
+ def _studio_api() -> _StudioAPI:
552
+ try:
553
+ contract_info = importlib.import_module("mostlyright_studio.contract_info")
554
+ models = importlib.import_module("mostlyright_studio.models")
555
+ studio_types = importlib.import_module("mostlyright_studio.types")
556
+ except ModuleNotFoundError as exc:
557
+ raise StudioClientUnavailableError(
558
+ "Hosted Studio functionality requires the exact generated Studio V3 client. "
559
+ "Install the supported 'mostlyright-data[hosted]' distribution; the "
560
+ "base worker/crawler distribution intentionally excludes Studio client code."
561
+ ) from exc
562
+
563
+ actual = StudioContractIdentity(
564
+ contract_set=getattr(contract_info, "CONTRACT_SET", ""),
565
+ contract_version=getattr(contract_info, "CONTRACT_VERSION", ""),
566
+ generator=getattr(contract_info, "GENERATOR", ""),
567
+ generator_version=getattr(contract_info, "GENERATOR_VERSION", ""),
568
+ openapi_source_sha256=getattr(contract_info, "OPENAPI_SOURCE_SHA256", ""),
569
+ )
570
+ expected = StudioContractIdentity(
571
+ contract_set=PINNED_CONTRACT_SET,
572
+ contract_version=PINNED_CONTRACT_VERSION,
573
+ generator=PINNED_GENERATOR,
574
+ generator_version=PINNED_GENERATOR_VERSION,
575
+ openapi_source_sha256=PINNED_OPENAPI_SOURCE_SHA256,
576
+ )
577
+ if actual != expected:
578
+ raise StudioClientIncompatibleError(
579
+ f"generated Studio client identity mismatch: expected {expected.to_dict()}, "
580
+ f"got {actual.to_dict()}"
581
+ )
582
+ # Import-time probe, not the complete dependency list. Other generated symbols this module
583
+ # resolves are absent here on purpose: SourceHealthObservation, SourceFitnessReport,
584
+ # ContractHalfOpenTimeRange, and every generated enum. A client missing one of those fails
585
+ # at first use instead, as StudioClientIncompatibleError from _strict_generated_parse or
586
+ # _require_generated_model, or as AttributeError from a direct api.models access.
587
+ required_models = {
588
+ "CandidateAdmissionAndStudioSelection",
589
+ "ContractProducerAgentCallCommand",
590
+ "ContractProducerSignedSession",
591
+ "ContractProducerSourceFitnessCommand",
592
+ "ContractProducerSourceObservationCommand",
593
+ "ContractAffectedArtifact",
594
+ "ContractApprovedBackfillPolicy",
595
+ "ContractCategory",
596
+ "ContractFindingProposal",
597
+ "ContractRightsDecisionCommand",
598
+ "ContractRightsReview",
599
+ "ContractSubmissionCommand",
600
+ "ContractVerifierDownloadCommand",
601
+ "ContractVerifierFindingCommand",
602
+ "ContractVerifierRuntimeJob",
603
+ "ContractVerifierSignedSession",
604
+ "TablePlanProposalPrecursor",
605
+ "TableQuestion",
606
+ "TableRequirements",
607
+ "DurableRunEvent",
608
+ "ImmutableArtifactMetadata",
609
+ "LosslessAgentCallEvidence",
610
+ "RecipeBoundDeterministicFinding",
611
+ "SourceRegistryEntry",
612
+ }
613
+ missing = sorted(required_models - set(vars(models)))
614
+ if missing:
615
+ raise StudioClientIncompatibleError(
616
+ f"generated Studio client is missing required models: {missing}"
617
+ )
618
+ return _StudioAPI(models=models, unset=studio_types.UNSET, identity=actual)
619
+
620
+
621
+ def map_question(question: LocalQuestion, identity: QuestionIdentity) -> MappedStudioValue:
622
+ api = _studio_api()
623
+ if len(question.text) < 10:
624
+ raise StudioBoundaryValidationError("question.text is shorter than Studio's minimum")
625
+ question_preimage = {
626
+ "workspace_id": str(identity.workspace_id),
627
+ "dataset_id": str(identity.dataset_id),
628
+ "question_id": str(identity.question_id),
629
+ "text": question.text,
630
+ }
631
+ question_digest = (
632
+ "sha256:"
633
+ + hashlib.sha256(
634
+ json.dumps(
635
+ question_preimage,
636
+ ensure_ascii=False,
637
+ allow_nan=False,
638
+ separators=(",", ":"),
639
+ sort_keys=True,
640
+ ).encode("utf-8")
641
+ ).hexdigest()
642
+ )
643
+ model = api.models.TableQuestion(
644
+ schema_version=PINNED_CONTRACT_VERSION,
645
+ question_id=identity.question_id,
646
+ workspace_id=identity.workspace_id,
647
+ dataset_id=identity.dataset_id,
648
+ text=question.text,
649
+ question_digest=question_digest,
650
+ status=_generated_enum(
651
+ api.models.TableQuestionStatus,
652
+ identity.status,
653
+ "question.status",
654
+ ),
655
+ created_at=_parse_utc(question.created_at, "question.created_at"),
656
+ created_by_principal_id=identity.created_by_principal_id,
657
+ )
658
+ return _mapped(model, question, identity.workspace_id)
659
+
660
+
661
+ def map_requirements(
662
+ requirements: LocalRequirements,
663
+ identity: RequirementsIdentity,
664
+ ) -> MappedStudioValue:
665
+ api = _studio_api()
666
+ prediction_cutoff = (
667
+ api.unset
668
+ if identity.prediction_cutoff is None
669
+ else _parse_utc(identity.prediction_cutoff, "prediction_cutoff")
670
+ )
671
+ model = api.models.TableRequirements(
672
+ schema_version=PINNED_CONTRACT_VERSION,
673
+ requirements_id=identity.requirements_id,
674
+ workspace_id=identity.workspace_id,
675
+ question_id=identity.question_id,
676
+ population=requirements.population,
677
+ time_range=api.models.ContractHalfOpenTimeRange(
678
+ start_inclusive=_parse_utc(
679
+ requirements.time_range.start_inclusive,
680
+ "requirements.time_range.start_inclusive",
681
+ ),
682
+ end_exclusive=_parse_utc(
683
+ requirements.time_range.end_exclusive,
684
+ "requirements.time_range.end_exclusive",
685
+ ),
686
+ ),
687
+ output_grain=list(requirements.output_grain.columns),
688
+ required_fields=list(requirements.required_fields),
689
+ target_policy=_generated_enum(
690
+ api.models.TableRequirementsTargetPolicy,
691
+ requirements.target_policy,
692
+ "requirements.target_policy",
693
+ ),
694
+ success_criteria=list(requirements.success_criteria),
695
+ feasibility=api.models.TableRequirementsFeasibility(
696
+ decision=_generated_enum(
697
+ api.models.TableRequirementsFeasibilityDecision,
698
+ requirements.feasibility.decision,
699
+ "requirements.feasibility.decision",
700
+ ),
701
+ reason_codes=[
702
+ _generated_enum(
703
+ api.models.TableRequirementsFeasibilityReasonCodesItem,
704
+ code.upper(),
705
+ f"requirements.feasibility.reason_codes[{index}]",
706
+ )
707
+ for index, code in enumerate(requirements.feasibility.reason_codes)
708
+ ],
709
+ narrative=requirements.feasibility.narrative,
710
+ ),
711
+ requirements_digest=_wire_digest(
712
+ requirements.digest,
713
+ "requirements.digest",
714
+ allow_raw=True,
715
+ ),
716
+ created_at=_parse_utc(requirements.created_at, "requirements.created_at"),
717
+ prediction_cutoff=prediction_cutoff,
718
+ )
719
+ return _mapped(model, requirements, identity.workspace_id)
720
+
721
+
722
+ def map_source_proposal(
723
+ proposal: SourceProposal,
724
+ identity: SourceIdentity,
725
+ ) -> MappedStudioValue:
726
+ api = _studio_api()
727
+ locator_kind = {
728
+ "artifact_reference": "artifact",
729
+ "https_url": "https_url",
730
+ }.get(proposal.locator_kind)
731
+ if locator_kind is None:
732
+ raise StudioBoundaryValidationError(
733
+ f"source locator kind {proposal.locator_kind!r} has no Studio wire representation"
734
+ )
735
+ if proposal.rights_status in {"approved", "conditional"} and (
736
+ identity.rights_evidence_digest is None
737
+ ):
738
+ raise StudioBoundaryValidationError(
739
+ "approved/conditional source mapping requires rights_evidence_digest"
740
+ )
741
+ connector_version: Any = (
742
+ api.unset
743
+ if identity.connector_contract_version is None
744
+ else identity.connector_contract_version
745
+ )
746
+ rights_digest: Any = (
747
+ api.unset if identity.rights_evidence_digest is None else identity.rights_evidence_digest
748
+ )
749
+ rights_decision_id: Any = (
750
+ api.unset if identity.rights_decision_id is None else identity.rights_decision_id
751
+ )
752
+ model = api.models.SourceRegistryEntry(
753
+ schema_version=PINNED_CONTRACT_VERSION,
754
+ source_id=identity.source_id,
755
+ workspace_id=identity.workspace_id,
756
+ dataset_id=identity.dataset_id,
757
+ name=proposal.display_name,
758
+ source_class=_generated_enum(
759
+ api.models.SourceRegistryEntrySourceClass,
760
+ proposal.source_class,
761
+ "source.source_class",
762
+ ),
763
+ locator=api.models.SourceRegistryEntryLocator(
764
+ kind=_generated_enum(
765
+ api.models.SourceRegistryEntryLocatorKind,
766
+ locator_kind,
767
+ "source.locator.kind",
768
+ ),
769
+ display_locator=proposal.locator,
770
+ connector_contract_version=connector_version,
771
+ ),
772
+ credential_reference_ids=list(identity.credential_reference_ids),
773
+ data_classification=_generated_enum(
774
+ api.models.SourceRegistryEntryDataClassification,
775
+ identity.data_classification,
776
+ "source.data_classification",
777
+ ),
778
+ rights_status=_generated_enum(
779
+ api.models.SourceRegistryEntryRightsStatus,
780
+ proposal.rights_status,
781
+ "source.rights_status",
782
+ ),
783
+ rights_claim_digest=identity.rights_claim_digest,
784
+ rights_policy_digest=identity.rights_policy_digest,
785
+ rights_adjudication=_generated_enum(
786
+ api.models.SourceRegistryEntryRightsAdjudication,
787
+ identity.rights_adjudication,
788
+ "source.rights_adjudication",
789
+ ),
790
+ retention_policy=api.models.SourceRegistryEntryRetentionPolicy(
791
+ raw_days=identity.raw_retention_days,
792
+ derived_days=identity.derived_retention_days,
793
+ tombstone_required=identity.tombstone_required,
794
+ ),
795
+ status=_generated_enum(
796
+ api.models.SourceRegistryEntryStatus,
797
+ identity.status,
798
+ "source.status",
799
+ ),
800
+ created_at=_parse_utc(proposal.observed_at, "proposal.observed_at"),
801
+ rights_evidence_digest=rights_digest,
802
+ rights_decision_id=rights_decision_id,
803
+ )
804
+ return _mapped(model, proposal, identity.workspace_id)
805
+
806
+
807
+ def map_plan(plan: LocalTablePlan, identity: PlanIdentity) -> MappedStudioValue:
808
+ api = _studio_api()
809
+ mapping = dict(identity.source_ids)
810
+ local_sources = {source.source_id for source in plan.sources}
811
+ if set(mapping) != local_sources:
812
+ raise StudioBoundaryValidationError(
813
+ "plan source identity mapping must exactly match local plan sources"
814
+ )
815
+ predecessor: Any = (
816
+ api.unset
817
+ if identity.predecessor_table_plan_id is None
818
+ else identity.predecessor_table_plan_id
819
+ )
820
+ if identity.semantic_approval_key_digest is not None:
821
+ raise StudioBoundaryValidationError(
822
+ "semantic_approval_key_digest is retired from the model-free V3 plan boundary"
823
+ )
824
+ approved_backfill_policy = _strict_generated_parse(
825
+ "ContractApprovedBackfillPolicy",
826
+ identity.backfill_policy.to_dict(),
827
+ expected_schema_version=BACKFILL_POLICY_SCHEMA_VERSION,
828
+ )
829
+ model = api.models.TablePlanProposalPrecursor(
830
+ schema_version=PINNED_CONTRACT_VERSION,
831
+ table_plan_id=identity.table_plan_id,
832
+ workspace_id=identity.workspace_id,
833
+ dataset_id=identity.dataset_id,
834
+ question_id=identity.question_id,
835
+ requirements_id=identity.requirements_id,
836
+ revision=identity.revision,
837
+ source_ids=[mapping[source.source_id] for source in plan.sources],
838
+ output_grain=list(plan.grain),
839
+ transformation_contract_version=identity.transformation_contract_version,
840
+ validation_policy_digest=identity.validation_policy_digest,
841
+ approved_backfill_policy=approved_backfill_policy,
842
+ backfill_policy_digest=identity.backfill_policy_digest,
843
+ plan_digest=_wire_digest(plan.digest, "plan.digest", allow_raw=True),
844
+ status=_generated_enum(
845
+ api.models.TablePlanProposalPrecursorStatus,
846
+ identity.status,
847
+ "plan.status",
848
+ ),
849
+ created_at=_parse_utc(identity.created_at, "created_at"),
850
+ predecessor_table_plan_id=predecessor,
851
+ )
852
+ return _mapped(model, plan, identity.workspace_id)
853
+
854
+
855
+ def map_agent_call_evidence(
856
+ evidence: AgentCallEvidence,
857
+ identity: AgentCallIdentity,
858
+ ) -> MappedStudioValue:
859
+ api = _studio_api()
860
+ if identity.evidence_digest != evidence.evidence_sha256:
861
+ raise StudioBoundaryValidationError("agent-call identity is not bound to this evidence")
862
+ wall_elapsed = _parse_utc(identity.completed_at, "completed_at") - _parse_utc(
863
+ identity.started_at, "started_at"
864
+ )
865
+ if int(wall_elapsed.total_seconds() * 1_000_000) != evidence.timing.elapsed_micros:
866
+ raise StudioBoundaryValidationError(
867
+ "hosted wall-clock interval does not match deterministic timing evidence"
868
+ )
869
+ secret_references: list[dict[str, Any]] = []
870
+ for index, reference in enumerate(evidence.secret_references):
871
+ path = f"secret_references[{index}]"
872
+ try:
873
+ workspace_id = UUID(reference.workspace_id)
874
+ connector_id = UUID(reference.connector_id)
875
+ attempt_id = UUID(reference.attempt_id)
876
+ except ValueError as exc:
877
+ raise StudioBoundaryValidationError(
878
+ f"{path} scope identifiers must be canonical UUIDs"
879
+ ) from exc
880
+ _match(workspace_id, identity.workspace_id, f"{path}.workspace_id")
881
+ _match(attempt_id, identity.attempt_id, f"{path}.attempt_id")
882
+ secret_references.append(
883
+ {
884
+ "name": reference.name,
885
+ "handle_id": reference.handle_id,
886
+ "workspace_id": str(workspace_id),
887
+ "connector_id": str(connector_id),
888
+ "attempt_id": str(attempt_id),
889
+ "purpose": reference.purpose,
890
+ }
891
+ )
892
+ tool_names = {
893
+ "search": "sanctioned_search",
894
+ "browse": "sanctioned_browse",
895
+ }
896
+ tool_evidence: list[dict[str, Any]] = []
897
+ for index, item in enumerate(evidence.tool_evidence):
898
+ tool_name = tool_names.get(item.name)
899
+ if tool_name is None:
900
+ raise StudioBoundaryValidationError(
901
+ f"tool_evidence[{index}].name has no Studio wire representation"
902
+ )
903
+ tool_evidence.append(
904
+ {
905
+ "sequence": item.sequence,
906
+ "name": tool_name,
907
+ "arguments_digest": _wire_digest(
908
+ item.arguments_sha256,
909
+ f"tool_evidence[{index}].arguments_sha256",
910
+ allow_raw=True,
911
+ ),
912
+ "invocation_digest": _wire_digest(
913
+ item.invocation_sha256,
914
+ f"tool_evidence[{index}].invocation_sha256",
915
+ allow_raw=True,
916
+ ),
917
+ }
918
+ )
919
+ wire: dict[str, Any] = {
920
+ "schema_version": PINNED_CONTRACT_VERSION,
921
+ "call_id": str(identity.call_id),
922
+ "workspace_id": str(identity.workspace_id),
923
+ "run_id": str(identity.run_id),
924
+ "attempt_id": str(identity.attempt_id),
925
+ "budget_id": str(identity.budget_id),
926
+ "runtime_version": evidence.runtime_version,
927
+ "request_contract_version": evidence.request_contract_version,
928
+ "response_contract_version": evidence.response_contract_version,
929
+ "provider": evidence.provider,
930
+ "model_identifier": evidence.model_identifier,
931
+ "provider_configuration_digest": _wire_digest(
932
+ evidence.provider_configuration_sha256,
933
+ "provider_configuration_sha256",
934
+ allow_raw=True,
935
+ ),
936
+ "role": evidence.role,
937
+ "prompt_template_version": evidence.prompt_template_version,
938
+ "prompt_digest": _wire_digest(
939
+ evidence.prompt_sha256,
940
+ "prompt_sha256",
941
+ allow_raw=True,
942
+ ),
943
+ "tool_policy_version": evidence.tool_policy_version,
944
+ "request_digest": _wire_digest(
945
+ evidence.request_sha256,
946
+ "request_sha256",
947
+ allow_raw=True,
948
+ ),
949
+ "secret_references": secret_references,
950
+ "tool_evidence": tool_evidence,
951
+ "usage": {
952
+ "accounting_status": evidence.usage.accounting_status,
953
+ "input_tokens": evidence.usage.input_tokens,
954
+ "output_tokens": evidence.usage.output_tokens,
955
+ "total_tokens": evidence.usage.total_tokens,
956
+ "cached_input_tokens": evidence.usage.cached_input_tokens,
957
+ "reasoning_tokens": evidence.usage.reasoning_tokens,
958
+ "cost_usd_micros": evidence.usage.cost_usd_micros,
959
+ },
960
+ "timing": {
961
+ **evidence.timing.to_dict(),
962
+ "deadline_micros": evidence.deadline_micros,
963
+ },
964
+ "outcome": evidence.outcome,
965
+ "started_at": identity.started_at,
966
+ "completed_at": identity.completed_at,
967
+ }
968
+ if evidence.replay_fixture_version is not None:
969
+ wire["replay_fixture_version"] = evidence.replay_fixture_version
970
+ if evidence.response_sha256 is not None:
971
+ wire["response_digest"] = _wire_digest(
972
+ evidence.response_sha256,
973
+ "response_sha256",
974
+ allow_raw=True,
975
+ )
976
+ if evidence.raw_response_sha256 is not None:
977
+ wire["raw_response_digest"] = _wire_digest(
978
+ evidence.raw_response_sha256,
979
+ "raw_response_sha256",
980
+ allow_raw=True,
981
+ )
982
+ if evidence.failure_code is not None:
983
+ wire["failure_code"] = evidence.failure_code
984
+ model = _strict_generated_parse("LosslessAgentCallEvidence", wire)
985
+ return MappedStudioValue(
986
+ model=model,
987
+ evidence=BoundaryMappingEvidence(
988
+ contract=api.identity,
989
+ model_name=type(model).__name__,
990
+ local_schema_version=evidence.schema_version,
991
+ local_digest=evidence.evidence_sha256,
992
+ workspace_id=identity.workspace_id,
993
+ run_id=identity.run_id,
994
+ attempt_id=identity.attempt_id,
995
+ ),
996
+ )
997
+
998
+
999
+ def map_producer_agent_call_submission(
1000
+ evidence: AgentCallEvidence,
1001
+ identity: AgentCallIdentity,
1002
+ scope: ProducerEvidenceScope,
1003
+ ) -> Any:
1004
+ """Map local lossless agent evidence into the typed fenced producer command."""
1005
+
1006
+ _match(identity.workspace_id, scope.workspace_id, "workspace_id")
1007
+ _match(identity.run_id, scope.run_id, "run_id")
1008
+ _match(identity.attempt_id, scope.attempt_id, "attempt_id")
1009
+ mapped = map_agent_call_evidence(evidence, identity)
1010
+ api = _studio_api()
1011
+ return api.models.ContractProducerAgentCallCommand(
1012
+ schema_version=PINNED_CONTRACT_VERSION,
1013
+ workspace_id=scope.workspace_id,
1014
+ run_id=scope.run_id,
1015
+ attempt_id=scope.attempt_id,
1016
+ producer_generation=scope.producer_generation,
1017
+ producer_fence=scope.producer_fence,
1018
+ authority="producer_attempt_evidence",
1019
+ payload_digest=_evidence_payload_digest(
1020
+ "submitProducerAgentCallEvidence",
1021
+ mapped.to_wire_dict(),
1022
+ ),
1023
+ agent_call=mapped.model,
1024
+ )
1025
+
1026
+
1027
+ def map_producer_source_observation_submission(
1028
+ observation: Any,
1029
+ scope: ProducerEvidenceScope,
1030
+ ) -> Any:
1031
+ """Wrap an exact generated source observation in producer-only ingress authority."""
1032
+
1033
+ _require_generated_model(observation, "SourceHealthObservation")
1034
+ _match(observation.workspace_id, scope.workspace_id, "source_observation.workspace_id")
1035
+ api = _studio_api()
1036
+ return api.models.ContractProducerSourceObservationCommand(
1037
+ schema_version=PINNED_CONTRACT_VERSION,
1038
+ workspace_id=scope.workspace_id,
1039
+ run_id=scope.run_id,
1040
+ attempt_id=scope.attempt_id,
1041
+ producer_generation=scope.producer_generation,
1042
+ producer_fence=scope.producer_fence,
1043
+ authority="producer_attempt_evidence",
1044
+ payload_digest=_evidence_payload_digest(
1045
+ "submitProducerSourceObservation",
1046
+ studio_wire_dict(observation),
1047
+ ),
1048
+ source_observation=observation,
1049
+ )
1050
+
1051
+
1052
+ def map_producer_source_fitness_submission(
1053
+ report: Any,
1054
+ scope: ProducerEvidenceScope,
1055
+ ) -> Any:
1056
+ """Wrap an exact generated source-fitness report in producer-only authority."""
1057
+
1058
+ _require_generated_model(report, "SourceFitnessReport")
1059
+ _match(report.workspace_id, scope.workspace_id, "source_fitness_report.workspace_id")
1060
+ api = _studio_api()
1061
+ return api.models.ContractProducerSourceFitnessCommand(
1062
+ schema_version=PINNED_CONTRACT_VERSION,
1063
+ workspace_id=scope.workspace_id,
1064
+ run_id=scope.run_id,
1065
+ attempt_id=scope.attempt_id,
1066
+ producer_generation=scope.producer_generation,
1067
+ producer_fence=scope.producer_fence,
1068
+ authority="producer_attempt_evidence",
1069
+ payload_digest=_evidence_payload_digest(
1070
+ "submitProducerSourceFitness",
1071
+ studio_wire_dict(report),
1072
+ ),
1073
+ source_fitness_report=report,
1074
+ )
1075
+
1076
+
1077
+ def map_verifier_semantic_review_submission(
1078
+ scope: VerifierReviewScope,
1079
+ *,
1080
+ decision: str,
1081
+ review_digest: str,
1082
+ ) -> Any:
1083
+ """Reject the retired semantic-review ingress under the model-free V3 boundary."""
1084
+
1085
+ del scope, decision, review_digest
1086
+ raise StudioBoundaryValidationError(
1087
+ "semantic-review submission is retired from the model-free V3 verifier boundary"
1088
+ )
1089
+
1090
+
1091
+ def map_verifier_finding_submission(
1092
+ scope: VerifierReviewScope,
1093
+ *,
1094
+ check_id: str,
1095
+ code: str,
1096
+ severity: str,
1097
+ summary: str,
1098
+ evidence_digest: str,
1099
+ finding_source: str,
1100
+ evaluator_finding_id: str | None = None,
1101
+ category: str | None = None,
1102
+ affected_artifact: Mapping[str, Any] | None = None,
1103
+ acceptance_conditions: Sequence[str] | None = None,
1104
+ ) -> Any:
1105
+ """Submit finding content while Studio derives status, scope, identity, and closure."""
1106
+
1107
+ if check_id not in scope.required_check_ids:
1108
+ raise StudioBoundaryValidationError(
1109
+ "finding check_id is outside the verifier required-check inventory"
1110
+ )
1111
+ expected_code = f"REQUIRED_CHECK_FAILED_{check_id}"[:128]
1112
+ if code != expected_code:
1113
+ raise StudioBoundaryValidationError("finding code does not bind its check_id")
1114
+ if not re.fullmatch(r"[A-Z][A-Z0-9_]{2,127}", code):
1115
+ raise StudioBoundaryValidationError("finding code is not canonical")
1116
+ _nonempty(summary, "summary", maximum=2000)
1117
+ api = _studio_api()
1118
+ if finding_source != "deterministic_check":
1119
+ raise StudioBoundaryValidationError(
1120
+ "finding_source must be deterministic_check in model-free V3"
1121
+ )
1122
+ if evaluator_finding_id is not None:
1123
+ raise StudioBoundaryValidationError(
1124
+ "evaluator_finding_id is retired from the model-free V3 finding boundary"
1125
+ )
1126
+ proposal_fields: dict[str, Any] = {
1127
+ "schema_version": PINNED_CONTRACT_VERSION,
1128
+ "finding_source": "deterministic_check",
1129
+ "check_id": check_id,
1130
+ "code": code,
1131
+ "severity": _generated_enum(
1132
+ api.models.ContractSeverity,
1133
+ severity,
1134
+ "severity",
1135
+ ),
1136
+ "summary": summary,
1137
+ "evidence_digest": _wire_digest(evidence_digest, "evidence_digest"),
1138
+ }
1139
+ if category is not None:
1140
+ proposal_fields["category"] = _generated_enum(
1141
+ api.models.ContractCategory,
1142
+ category,
1143
+ "category",
1144
+ )
1145
+ if affected_artifact is not None:
1146
+ if not isinstance(affected_artifact, Mapping) or set(affected_artifact) != {
1147
+ "artifact_id",
1148
+ "kind",
1149
+ "content_digest",
1150
+ }:
1151
+ raise StudioBoundaryValidationError("affected_artifact fields are not exact")
1152
+ try:
1153
+ artifact_id = UUID(str(affected_artifact["artifact_id"]))
1154
+ except (TypeError, ValueError) as error:
1155
+ raise StudioBoundaryValidationError(
1156
+ "affected_artifact.artifact_id is not a UUID"
1157
+ ) from error
1158
+ kind = affected_artifact["kind"]
1159
+ _nonempty(
1160
+ kind,
1161
+ "affected_artifact.kind",
1162
+ maximum=64,
1163
+ )
1164
+ if not re.fullmatch(r"[a-z][a-z0-9_]{1,63}", kind):
1165
+ raise StudioBoundaryValidationError("affected_artifact.kind is not canonical")
1166
+ proposal_fields["affected_artifact"] = api.models.ContractAffectedArtifact(
1167
+ artifact_id=artifact_id,
1168
+ kind=kind,
1169
+ content_digest=_wire_digest(
1170
+ affected_artifact["content_digest"],
1171
+ "affected_artifact.content_digest",
1172
+ ),
1173
+ )
1174
+ if acceptance_conditions is not None:
1175
+ conditions = list(acceptance_conditions)
1176
+ if not conditions or len(conditions) > 32 or len(conditions) != len(set(conditions)):
1177
+ raise StudioBoundaryValidationError(
1178
+ "acceptance_conditions must be a nonempty unique bounded list"
1179
+ )
1180
+ for index, condition in enumerate(conditions):
1181
+ _nonempty(condition, f"acceptance_conditions[{index}]", maximum=1000)
1182
+ proposal_fields["acceptance_conditions"] = conditions
1183
+ proposal = api.models.ContractFindingProposal(**proposal_fields)
1184
+ return api.models.ContractVerifierFindingCommand(
1185
+ schema_version=PINNED_CONTRACT_VERSION,
1186
+ workspace_id=scope.workspace_id,
1187
+ run_id=scope.run_id,
1188
+ attempt_id=scope.attempt_id,
1189
+ verification_generation=scope.verification_generation,
1190
+ verifier_generation=scope.verifier_generation,
1191
+ verifier_fence=scope.verifier_fence,
1192
+ authority="independent_verifier_review",
1193
+ payload_digest=_evidence_payload_digest(
1194
+ "submitVerifierFinding",
1195
+ studio_wire_dict(proposal),
1196
+ ),
1197
+ finding_proposal=proposal,
1198
+ )
1199
+
1200
+
1201
+ def parse_verifier_job_scope(value: Mapping[str, Any]) -> VerifierReviewScope:
1202
+ """Discover the complete immutable report scope from the canonical verifier job."""
1203
+
1204
+ model = _strict_generated_parse("ContractVerifierRuntimeJob", value)
1205
+ wire = studio_wire_dict(model)
1206
+ return VerifierReviewScope(
1207
+ workspace_id=model.workspace_id,
1208
+ run_id=model.run_id,
1209
+ gate_lineage_id=model.gate_lineage_id,
1210
+ attempt_id=model.attempt_id,
1211
+ verification_generation=model.verification_generation,
1212
+ verifier_generation=model.verifier_generation,
1213
+ verifier_fence=model.verifier_fence,
1214
+ admission_id=model.admission_id,
1215
+ selection_epoch=model.selection_epoch,
1216
+ candidate_digest=model.candidate_digest,
1217
+ candidate_contract_version=model.candidate_contract_version,
1218
+ dataset_id=model.dataset_id,
1219
+ table_id=model.table_id,
1220
+ table_recipe_id=model.table_recipe_id,
1221
+ recipe_version=model.recipe_version,
1222
+ recipe_digest=model.recipe_digest,
1223
+ activation_epoch=model.activation_epoch,
1224
+ execution_scope_digest=model.execution_scope_digest,
1225
+ validation_policy_digest=model.validation_policy_digest,
1226
+ release_policy_digest=model.release_policy_digest,
1227
+ artifact_scope_digest=model.artifact_scope_digest,
1228
+ verifier_principal_id=model.verifier_principal_id,
1229
+ verifier_image_digest=model.verifier_image_digest,
1230
+ required_check_ids=tuple(model.required_check_ids),
1231
+ attestation_scheme=wire["attestation_scheme"],
1232
+ attestation_key_id=model.attestation_key_id,
1233
+ attestation_key_version=model.attestation_key_version,
1234
+ )
1235
+
1236
+
1237
+ def map_verifier_download_command(
1238
+ scope: VerifierReviewScope,
1239
+ *,
1240
+ artifact_id: UUID,
1241
+ expected_content_digest: str,
1242
+ ) -> Any:
1243
+ """Request a download; Studio resolves and stamps immutable object generation."""
1244
+
1245
+ if not isinstance(artifact_id, UUID):
1246
+ raise StudioBoundaryValidationError("artifact_id must be a UUID")
1247
+ api = _studio_api()
1248
+ return api.models.ContractVerifierDownloadCommand(
1249
+ schema_version=PINNED_CONTRACT_VERSION,
1250
+ workspace_id=scope.workspace_id,
1251
+ run_id=scope.run_id,
1252
+ attempt_id=scope.attempt_id,
1253
+ verification_generation=scope.verification_generation,
1254
+ verifier_generation=scope.verifier_generation,
1255
+ fence=scope.verifier_fence,
1256
+ artifact_id=artifact_id,
1257
+ expected_content_digest=_wire_digest(
1258
+ expected_content_digest,
1259
+ "expected_content_digest",
1260
+ ),
1261
+ )
1262
+
1263
+
1264
+ def parse_verifier_signed_session(
1265
+ value: Mapping[str, Any],
1266
+ scope: VerifierReviewScope,
1267
+ *,
1268
+ artifact_id: UUID,
1269
+ expected_content_digest: str,
1270
+ expected_kind: str,
1271
+ expected_media_type: str,
1272
+ expected_contract_version: str,
1273
+ expected_classification: str,
1274
+ expected_size_bytes: int,
1275
+ ) -> Any:
1276
+ """Parse a signed session and reject any stale verification or retry scope."""
1277
+
1278
+ model = _strict_generated_parse("ContractVerifierSignedSession", value)
1279
+ _match_verifier_scope(
1280
+ model,
1281
+ scope,
1282
+ fence_name="fence",
1283
+ attempt_name="attempt_id",
1284
+ )
1285
+ wire = studio_wire_dict(model)
1286
+ if (
1287
+ wire["direction"] != "download"
1288
+ or wire["method"] != "GET"
1289
+ or wire["purpose"] != "verifier_selected_artifact_download"
1290
+ or wire["route_authority"] != "verifier_attempt"
1291
+ ):
1292
+ raise StudioBoundaryValidationError("verifier signed session is not download-only")
1293
+ if not re.fullmatch(r"[1-9][0-9]{0,31}", wire["object_generation"]):
1294
+ raise StudioBoundaryValidationError(
1295
+ "verifier signed session object_generation is not canonical"
1296
+ )
1297
+ expected = {
1298
+ "artifact_id": str(artifact_id),
1299
+ "expected_content_digest": expected_content_digest,
1300
+ "kind": expected_kind,
1301
+ "media_type": expected_media_type,
1302
+ "contract_version": expected_contract_version,
1303
+ "classification": expected_classification,
1304
+ "expected_size_bytes": expected_size_bytes,
1305
+ }
1306
+ for name, expected_value in expected.items():
1307
+ _match(wire[name], expected_value, f"verifier_signed_session.{name}")
1308
+ return model
1309
+
1310
+
1311
+ def parse_producer_input_signed_session(
1312
+ value: Mapping[str, Any],
1313
+ *,
1314
+ workspace_id: UUID,
1315
+ run_id: UUID,
1316
+ attempt_id: UUID,
1317
+ producer_generation: int,
1318
+ producer_fence: int,
1319
+ artifact_id: UUID,
1320
+ media_type: str,
1321
+ expected_size_bytes: int,
1322
+ expected_content_digest: str,
1323
+ ) -> Any:
1324
+ """Parse one Studio-issued immutable candidate-input download session."""
1325
+
1326
+ required = {
1327
+ "schema_version",
1328
+ "session_id",
1329
+ "reservation_id",
1330
+ "reservation_digest",
1331
+ "workspace_id",
1332
+ "run_id",
1333
+ "artifact_id",
1334
+ "kind",
1335
+ "purpose",
1336
+ "classification",
1337
+ "media_type",
1338
+ "contract_version",
1339
+ "object_generation",
1340
+ "object_key_digest",
1341
+ "direction",
1342
+ "route_authority",
1343
+ "method",
1344
+ "signed_url",
1345
+ "required_headers",
1346
+ "expected_content_digest",
1347
+ "expected_size_bytes",
1348
+ "issued_at",
1349
+ "expires_at",
1350
+ "single_use",
1351
+ "attempt_id",
1352
+ "producer_generation",
1353
+ "fence",
1354
+ }
1355
+ if not isinstance(value, Mapping) or set(value) != required:
1356
+ raise StudioBoundaryValidationError(
1357
+ "producer candidate-input signed session fields are not exact"
1358
+ )
1359
+ model = _strict_generated_parse("ContractProducerSignedSession", value)
1360
+ wire = studio_wire_dict(model)
1361
+ expected = {
1362
+ "workspace_id": str(workspace_id),
1363
+ "run_id": str(run_id),
1364
+ "attempt_id": str(attempt_id),
1365
+ "producer_generation": producer_generation,
1366
+ "fence": producer_fence,
1367
+ "artifact_id": str(artifact_id),
1368
+ "kind": "candidate_input_bundle",
1369
+ "purpose": "producer_candidate_input_download",
1370
+ "classification": "internal",
1371
+ "media_type": media_type,
1372
+ "expected_size_bytes": expected_size_bytes,
1373
+ "expected_content_digest": expected_content_digest,
1374
+ "contract_version": PINNED_CONTRACT_VERSION,
1375
+ "direction": "download",
1376
+ "route_authority": "producer_attempt",
1377
+ "method": "GET",
1378
+ "single_use": False,
1379
+ }
1380
+ mismatched = [name for name, item in expected.items() if wire.get(name) != item]
1381
+ if mismatched:
1382
+ raise StudioBoundaryValidationError(
1383
+ "producer candidate-input signed session differs at " + ",".join(mismatched)
1384
+ )
1385
+ if not re.fullmatch(r"[1-9][0-9]{0,31}", wire["object_generation"]):
1386
+ raise StudioBoundaryValidationError(
1387
+ "producer candidate-input object_generation is not canonical"
1388
+ )
1389
+ for name in (
1390
+ "reservation_digest",
1391
+ "object_key_digest",
1392
+ "expected_content_digest",
1393
+ ):
1394
+ _wire_digest(wire[name], f"producer_candidate_input.{name}")
1395
+ _bounded_int(
1396
+ wire["expected_size_bytes"],
1397
+ "producer_candidate_input.expected_size_bytes",
1398
+ 1,
1399
+ 2_147_483_648,
1400
+ )
1401
+ return model
1402
+
1403
+
1404
+ def parse_verifier_submission(
1405
+ value: Mapping[str, Any],
1406
+ scope: VerifierReviewScope,
1407
+ ) -> Any:
1408
+ """Parse a report command and reject a stale stable generation or retry lease."""
1409
+
1410
+ model = _strict_generated_parse("ContractSubmissionCommand", value)
1411
+ _match_verifier_scope(
1412
+ model,
1413
+ scope,
1414
+ fence_name="verifier_fence",
1415
+ attempt_name="verifier_attempt_id",
1416
+ )
1417
+ _match_verifier_report_scope(model, scope)
1418
+ return model
1419
+
1420
+
1421
+ def parse_verifier_semantic_evidence(
1422
+ value: Mapping[str, Any],
1423
+ scope: VerifierReviewScope,
1424
+ *,
1425
+ artifact_references: Sequence[Mapping[str, Any]],
1426
+ ) -> Any:
1427
+ """Reject the retired model-mediated semantic evidence surface."""
1428
+
1429
+ del value, scope, artifact_references
1430
+ raise StudioBoundaryValidationError(
1431
+ "semantic evaluation evidence is retired from the model-free V3 verifier boundary"
1432
+ )
1433
+
1434
+
1435
+ def parse_source_rights_review(
1436
+ value: Mapping[str, Any],
1437
+ *,
1438
+ workspace_id: UUID,
1439
+ source_id: UUID,
1440
+ ) -> Any:
1441
+ """Parse a rights-review body and bind it to one workspace and source.
1442
+
1443
+ The parsed review carries the source version and the rights, evidence, policy, and review
1444
+ coordinate digests required by a decision. This module validates only the body.
1445
+ """
1446
+
1447
+ model = _strict_generated_parse("ContractRightsReview", value)
1448
+ _match(model.workspace_id, workspace_id, "workspace_id")
1449
+ _match(model.source_id, source_id, "source_id")
1450
+ return model
1451
+
1452
+
1453
+ def map_source_rights_decision(
1454
+ review: Any,
1455
+ *,
1456
+ step_up_challenge_id: UUID,
1457
+ decision: str,
1458
+ conditions_digest: str | None = None,
1459
+ reason: str | None = None,
1460
+ ) -> Any:
1461
+ """Carry every canonical rights-review coordinate into a human decision."""
1462
+
1463
+ _require_generated_model(review, "ContractRightsReview")
1464
+ if not isinstance(step_up_challenge_id, UUID):
1465
+ raise StudioBoundaryValidationError("step_up_challenge_id must be a UUID")
1466
+ api = _studio_api()
1467
+ if conditions_digest is not None:
1468
+ _wire_digest(conditions_digest, "conditions_digest")
1469
+ if reason is not None:
1470
+ _nonempty(reason, "reason", maximum=4000)
1471
+ return api.models.ContractRightsDecisionCommand(
1472
+ schema_version=PINNED_CONTRACT_VERSION,
1473
+ workspace_id=review.workspace_id,
1474
+ source_id=review.source_id,
1475
+ rights_claim_digest=review.rights_claim_digest,
1476
+ rights_evidence_digest=review.rights_evidence_digest,
1477
+ rights_policy_digest=review.rights_policy_digest,
1478
+ rights_review_coordinate_digest=review.rights_review_coordinate_digest,
1479
+ decision=_generated_enum(
1480
+ api.models.ContractRightsDecisionCommandDecision,
1481
+ decision,
1482
+ "decision",
1483
+ ),
1484
+ expected_source_version=review.source_version,
1485
+ step_up_challenge_id=step_up_challenge_id,
1486
+ conditions_digest=(api.unset if conditions_digest is None else conditions_digest),
1487
+ reason=api.unset if reason is None else reason,
1488
+ )
1489
+
1490
+
1491
+ def validate_source_rights_decision_scope(command: Any, current_review: Any) -> None:
1492
+ """Fail closed when a decision no longer matches the current rights review.
1493
+
1494
+ Compares the command's workspace, source, rights claim/evidence/policy digests, review
1495
+ coordinate digest, and expected_source_version against a freshly parsed review. One
1496
+ difference means the review moved after the decision was built, so the decision is stale.
1497
+ """
1498
+
1499
+ _require_generated_model(command, "ContractRightsDecisionCommand")
1500
+ _require_generated_model(current_review, "ContractRightsReview")
1501
+ bindings = (
1502
+ ("workspace_id", "workspace_id"),
1503
+ ("source_id", "source_id"),
1504
+ ("rights_claim_digest", "rights_claim_digest"),
1505
+ ("rights_evidence_digest", "rights_evidence_digest"),
1506
+ ("rights_policy_digest", "rights_policy_digest"),
1507
+ ("rights_review_coordinate_digest", "rights_review_coordinate_digest"),
1508
+ ("expected_source_version", "source_version"),
1509
+ )
1510
+ for command_name, review_name in bindings:
1511
+ _match(
1512
+ getattr(command, command_name),
1513
+ getattr(current_review, review_name),
1514
+ command_name,
1515
+ )
1516
+
1517
+
1518
+ def parse_run_event(value: Mapping[str, Any], fence: HostedFence) -> Any:
1519
+ model = _strict_generated_parse("DurableRunEvent", value)
1520
+ _match(model.workspace_id, fence.workspace_id, "workspace_id")
1521
+ _match(model.run_id, fence.run_id, "run_id")
1522
+ _match_optional_attempt(model, fence)
1523
+ _bounded_int(model.sequence, "sequence", 1, (1 << 63) - 1)
1524
+ if not re.fullmatch(r"[A-Za-z0-9_-]+", model.cursor):
1525
+ raise StudioBoundaryValidationError("cursor is not a canonical opaque cursor")
1526
+ return model
1527
+
1528
+
1529
+ def parse_artifact(value: Mapping[str, Any], fence: HostedFence) -> Any:
1530
+ if fence.attempt_id is None:
1531
+ raise StudioBoundaryValidationError("artifact parsing requires an attempt fence")
1532
+ model = _strict_generated_parse("ImmutableArtifactMetadata", value)
1533
+ _match(model.workspace_id, fence.workspace_id, "workspace_id")
1534
+ _match(model.run_id, fence.run_id, "run_id")
1535
+ _match(model.attempt_id, fence.attempt_id, "attempt_id")
1536
+ _bounded_int(model.size_bytes, "size_bytes", 0, (1 << 63) - 1)
1537
+ wire = studio_wire_dict(model)
1538
+ _semver(wire["contract_version"], "contract_version")
1539
+ if not re.fullmatch(r"[a-z0-9.+-]+/[a-z0-9.+-]+", wire["media_type"]):
1540
+ raise StudioBoundaryValidationError("media_type is not canonical")
1541
+ if wire["storage_state"] == "sealed" and "sealed_at" not in wire:
1542
+ raise StudioBoundaryValidationError("sealed artifact lacks sealed_at")
1543
+ return model
1544
+
1545
+
1546
+ def parse_candidate_admission(value: Mapping[str, Any], fence: HostedFence) -> Any:
1547
+ required = {
1548
+ "attempt_id": fence.attempt_id,
1549
+ "gate_lineage_id": fence.gate_lineage_id,
1550
+ "producer_generation": fence.producer_generation,
1551
+ "producer_fence": fence.producer_fence,
1552
+ }
1553
+ missing = sorted(name for name, item in required.items() if item is None)
1554
+ if missing:
1555
+ raise StudioBoundaryValidationError(
1556
+ f"candidate admission parsing requires explicit fences: {missing}"
1557
+ )
1558
+ model = _strict_generated_parse("CandidateAdmissionAndStudioSelection", value)
1559
+ _match(model.workspace_id, fence.workspace_id, "workspace_id")
1560
+ _match(model.run_id, fence.run_id, "run_id")
1561
+ _match(model.gate_lineage_id, fence.gate_lineage_id, "gate_lineage_id")
1562
+ _match(model.producer_attempt_id, fence.attempt_id, "producer_attempt_id")
1563
+ _match(model.producer_generation, fence.producer_generation, "producer_generation")
1564
+ _match(model.producer_fence, fence.producer_fence, "producer_fence")
1565
+ wire = studio_wire_dict(model)
1566
+ _semver(wire["candidate_contract_version"], "candidate_contract_version")
1567
+ if "selection_epoch" in wire:
1568
+ _bounded_int(wire["selection_epoch"], "selection_epoch", 1, (1 << 63) - 1)
1569
+ status_requirements = {
1570
+ "selected": {"selection_epoch", "selected_at", "selection_actor"},
1571
+ "superseded": {"selection_epoch", "superseded_by_admission_id"},
1572
+ "revoked": {"selection_epoch", "revoked_at", "revocation_reason"},
1573
+ }
1574
+ absent = sorted(status_requirements.get(wire["status"], set()) - set(wire))
1575
+ if absent:
1576
+ raise StudioBoundaryValidationError(
1577
+ f"{wire['status']} admission lacks conditional fields: {absent}"
1578
+ )
1579
+ if not wire["artifact_references"]:
1580
+ raise StudioBoundaryValidationError("candidate admission requires artifact references")
1581
+ references = [
1582
+ (
1583
+ item["workspace_id"],
1584
+ item["artifact_id"],
1585
+ item["contract_version"],
1586
+ item["content_digest"],
1587
+ )
1588
+ for item in wire["artifact_references"]
1589
+ ]
1590
+ if len(references) != len(set(references)):
1591
+ raise StudioBoundaryValidationError("candidate admission has duplicate artifact references")
1592
+ for index, item in enumerate(wire["artifact_references"]):
1593
+ if item["workspace_id"] != str(fence.workspace_id):
1594
+ raise StudioBoundaryValidationError(
1595
+ f"artifact_references[{index}].workspace_id fence mismatch"
1596
+ )
1597
+ _semver(item["contract_version"], f"artifact_references[{index}].contract_version")
1598
+ return model
1599
+
1600
+
1601
+ def parse_finding(value: Mapping[str, Any], fence: HostedFence) -> Any:
1602
+ if fence.gate_lineage_id is None:
1603
+ raise StudioBoundaryValidationError("finding parsing requires gate_lineage_id")
1604
+ model = _strict_generated_parse("RecipeBoundDeterministicFinding", value)
1605
+ _match(model.workspace_id, fence.workspace_id, "workspace_id")
1606
+ _match(model.run_id, fence.run_id, "run_id")
1607
+ _match(model.gate_lineage_id, fence.gate_lineage_id, "gate_lineage_id")
1608
+ wire = studio_wire_dict(model)
1609
+ if not re.fullmatch(r"[A-Z][A-Z0-9_]{2,127}", wire["code"]):
1610
+ raise StudioBoundaryValidationError("finding code is not canonical")
1611
+ if type(wire["blocks_release"]) is not bool:
1612
+ raise StudioBoundaryValidationError("blocks_release must be a boolean")
1613
+ if wire["status"] == "closed":
1614
+ required = {"closed_at", "closure_review_id", "closure_evidence_digest"}
1615
+ absent = sorted(required - set(wire))
1616
+ if absent or wire["blocks_release"]:
1617
+ raise StudioBoundaryValidationError("closed finding violates closure invariants")
1618
+ if wire["severity"] in {"blocker", "critical", "high"} and wire["status"] in {
1619
+ "open",
1620
+ "repair_in_progress",
1621
+ "closure_review_pending",
1622
+ }:
1623
+ if not wire["blocks_release"]:
1624
+ raise StudioBoundaryValidationError(
1625
+ "unresolved high-severity finding must block release"
1626
+ )
1627
+ return model
1628
+
1629
+
1630
+ def studio_wire_dict(model: Any) -> dict[str, Any]:
1631
+ """Serialize only a generated model and normalize its UTC lexical wire form."""
1632
+
1633
+ to_dict = getattr(model, "to_dict", None)
1634
+ if not callable(to_dict) or not type(model).__module__.startswith("mostlyright_studio.models."):
1635
+ raise StudioBoundaryValidationError("value is not a generated Studio model")
1636
+ value = _wire_value(to_dict(), key=None)
1637
+ if not isinstance(value, dict):
1638
+ raise StudioBoundaryValidationError("generated Studio model did not serialize to an object")
1639
+ # The root path is the only marker _assert_wire_scalars has for the two crawl-session
1640
+ # exceptions: a session's request_digest is a bare hex digest, and its source_id names a
1641
+ # source inside the frozen recipe rather than a Studio row.
1642
+ model_name = type(model).__name__
1643
+ path = (
1644
+ "$.public_crawl"
1645
+ if model_name in {"PublicCrawlSession", "HostedPublicHTTPSAcquisitionSession"}
1646
+ else (
1647
+ "$.source_cadence"
1648
+ if model_name in {"ContractProducerAppendContext", "ContractInferenceRecord"}
1649
+ else "$"
1650
+ )
1651
+ )
1652
+ _assert_wire_scalars(value, path)
1653
+ return value
1654
+
1655
+
1656
+ def _evidence_payload_digest(operation: str, payload: Mapping[str, Any]) -> str:
1657
+ """Derive the Studio evidence digest; callers cannot supply an outer digest."""
1658
+
1659
+ operations = {
1660
+ "submitProducerAgentCallEvidence",
1661
+ "submitProducerSourceObservation",
1662
+ "submitProducerSourceFitness",
1663
+ "submitVerifierSemanticReview",
1664
+ "submitVerifierFinding",
1665
+ }
1666
+ if operation not in operations:
1667
+ raise StudioBoundaryValidationError(f"unsupported evidence-ingress operation {operation!r}")
1668
+ _assert_wire_scalars(payload, "$.payload")
1669
+ preimage = {
1670
+ "domain": "mostlyright.studio.evidence-ingress",
1671
+ "version": "1",
1672
+ "type": operation,
1673
+ "payload": dict(payload),
1674
+ }
1675
+ encoded = json.dumps(
1676
+ preimage,
1677
+ ensure_ascii=False,
1678
+ allow_nan=False,
1679
+ separators=(",", ":"),
1680
+ sort_keys=True,
1681
+ ).encode("utf-8")
1682
+ return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
1683
+
1684
+
1685
+ def _strict_generated_parse(
1686
+ model_name: str,
1687
+ value: Mapping[str, Any],
1688
+ *,
1689
+ expected_schema_version: str = PINNED_CONTRACT_VERSION,
1690
+ ) -> Any:
1691
+ if not isinstance(value, Mapping) or not all(isinstance(key, str) for key in value):
1692
+ raise StudioBoundaryValidationError(
1693
+ f"{model_name} wire value must be a string-keyed object"
1694
+ )
1695
+ api = _studio_api()
1696
+ model_type = getattr(api.models, model_name, None)
1697
+ if model_type is None:
1698
+ raise StudioClientIncompatibleError(f"generated model {model_name!r} is unavailable")
1699
+ original = dict(value)
1700
+ try:
1701
+ model = model_type.from_dict(original)
1702
+ serialized = studio_wire_dict(model)
1703
+ except (KeyError, TypeError, ValueError, AttributeError) as exc:
1704
+ raise StudioBoundaryValidationError(
1705
+ f"{model_name} failed generated-model parsing: {exc}"
1706
+ ) from exc
1707
+ if serialized != original:
1708
+ raise StudioBoundaryValidationError(
1709
+ f"{model_name} contains unknown, missing, lossy, or noncanonical wire values"
1710
+ )
1711
+ if serialized.get("schema_version") != expected_schema_version:
1712
+ raise StudioBoundaryValidationError(f"{model_name} has an unsupported schema_version")
1713
+ return model
1714
+
1715
+
1716
+ def _require_generated_model(value: Any, model_name: str) -> None:
1717
+ api = _studio_api()
1718
+ model_type = getattr(api.models, model_name, None)
1719
+ if model_type is None:
1720
+ raise StudioClientIncompatibleError(f"generated model {model_name!r} is unavailable")
1721
+ if type(value) is not model_type:
1722
+ raise StudioBoundaryValidationError(f"value must be the exact generated {model_name} model")
1723
+ studio_wire_dict(value)
1724
+
1725
+
1726
+ def _match_verifier_scope(
1727
+ model: Any,
1728
+ scope: VerifierReviewScope,
1729
+ *,
1730
+ fence_name: str,
1731
+ attempt_name: str,
1732
+ ) -> None:
1733
+ _match(model.workspace_id, scope.workspace_id, "workspace_id")
1734
+ _match(model.run_id, scope.run_id, "run_id")
1735
+ _match(getattr(model, attempt_name), scope.attempt_id, attempt_name)
1736
+ _match(
1737
+ model.verification_generation,
1738
+ scope.verification_generation,
1739
+ "verification_generation",
1740
+ )
1741
+ _match(model.verifier_generation, scope.verifier_generation, "verifier_generation")
1742
+ _match(getattr(model, fence_name), scope.verifier_fence, fence_name)
1743
+
1744
+
1745
+ def _match_verifier_report_scope(model: Any, scope: VerifierReviewScope) -> None:
1746
+ """Reject a report that differs from any server-issued immutable coordinate."""
1747
+
1748
+ wire = studio_wire_dict(model)
1749
+ for name in (
1750
+ "gate_lineage_id",
1751
+ "admission_id",
1752
+ "selection_epoch",
1753
+ "candidate_digest",
1754
+ "candidate_contract_version",
1755
+ "dataset_id",
1756
+ "table_id",
1757
+ "table_recipe_id",
1758
+ "recipe_version",
1759
+ "recipe_digest",
1760
+ "activation_epoch",
1761
+ "execution_scope_digest",
1762
+ "validation_policy_digest",
1763
+ "release_policy_digest",
1764
+ "artifact_scope_digest",
1765
+ "verifier_principal_id",
1766
+ "verifier_image_digest",
1767
+ ):
1768
+ _match(getattr(model, name), getattr(scope, name), name)
1769
+ _match(
1770
+ tuple(model.required_check_ids),
1771
+ scope.required_check_ids,
1772
+ "required_check_ids",
1773
+ )
1774
+ if scope.resulting_source_states is None:
1775
+ raise StudioBoundaryValidationError(
1776
+ "report parsing requires candidate-bound resulting source states"
1777
+ )
1778
+ _match(
1779
+ tuple(wire["resulting_source_states"]),
1780
+ scope.resulting_source_states,
1781
+ "resulting_source_states",
1782
+ )
1783
+ _match(
1784
+ model.resulting_source_states_digest,
1785
+ scope.resulting_source_states_digest,
1786
+ "resulting_source_states_digest",
1787
+ )
1788
+ attestation = wire["attestation"]
1789
+ _match(attestation["scheme"], scope.attestation_scheme, "attestation.scheme")
1790
+ _match(attestation["key_id"], scope.attestation_key_id, "attestation.key_id")
1791
+ _match(
1792
+ attestation["key_version"],
1793
+ scope.attestation_key_version,
1794
+ "attestation.key_version",
1795
+ )
1796
+
1797
+
1798
+ def _mapped(model: Any, local: Any, workspace_id: UUID) -> MappedStudioValue:
1799
+ api = _studio_api()
1800
+ return MappedStudioValue(
1801
+ model=model,
1802
+ evidence=BoundaryMappingEvidence(
1803
+ contract=api.identity,
1804
+ model_name=type(model).__name__,
1805
+ local_schema_version=local.schema_version,
1806
+ local_digest=local.digest,
1807
+ workspace_id=workspace_id,
1808
+ ),
1809
+ )
1810
+
1811
+
1812
+ def _wire_value(value: Any, *, key: str | None) -> Any:
1813
+ if isinstance(value, dict):
1814
+ return {item_key: _wire_value(item, key=item_key) for item_key, item in value.items()}
1815
+ if isinstance(value, list):
1816
+ return [_wire_value(item, key=key) for item in value]
1817
+ if isinstance(value, str) and key in _TIMESTAMP_FIELDS and value.endswith("+00:00"):
1818
+ return f"{value[:-6]}Z"
1819
+ return value
1820
+
1821
+
1822
+ def _assert_wire_scalars(value: Any, path: str = "$") -> None:
1823
+ if isinstance(value, dict):
1824
+ for key, item in value.items():
1825
+ item_path = f"{path}.{key}"
1826
+ if item is None:
1827
+ continue
1828
+ if key in _TIMESTAMP_FIELDS:
1829
+ _parse_utc(item, item_path)
1830
+ if key in {"image_digest", "verifier_image_digest"}:
1831
+ if not _IMAGE_DIGEST.fullmatch(item):
1832
+ raise StudioBoundaryValidationError(f"{item_path} is not canonical")
1833
+ elif (
1834
+ key in _BARE_DIGEST_FIELDS
1835
+ or (key == "request_digest" and "public_crawl" in path)
1836
+ or (key == "cadence_probe_receipt_digest" and "public_crawl" in path)
1837
+ or (key.endswith("digest") and "cadence_authority" in path)
1838
+ or (key.endswith("digest") and "source_cadence" in path)
1839
+ # Everything under reviewed_build is the Harness's own frozen-recipe
1840
+ # evidence; dataset-recipe.schema.json pins those digests as bare
1841
+ # lowercase hex "intentionally unprefixed to match the Harness contract".
1842
+ or (key.endswith("digest") and "reviewed_build" in path)
1843
+ ):
1844
+ _bare_digest(item, item_path)
1845
+ elif key.endswith("digest"):
1846
+ _wire_digest(item, item_path)
1847
+ # Under these subtrees source_id is a frozen-recipe source name, not a Studio
1848
+ # source row, so the canonical-UUID check below must not apply to it.
1849
+ harness_source_id = key == "source_id" and any(
1850
+ marker in path
1851
+ for marker in (
1852
+ "committed_source_states",
1853
+ "resulting_source_states",
1854
+ "source_authority_bindings",
1855
+ "public_crawl",
1856
+ "source_cadence",
1857
+ "reviewed_build",
1858
+ )
1859
+ )
1860
+ if key in _UUID_FIELDS and not harness_source_id:
1861
+ try:
1862
+ parsed = UUID(item)
1863
+ except (TypeError, ValueError, AttributeError) as exc:
1864
+ raise StudioBoundaryValidationError(
1865
+ f"{item_path} must be a canonical UUID"
1866
+ ) from exc
1867
+ if str(parsed) != item:
1868
+ raise StudioBoundaryValidationError(
1869
+ f"{item_path} must be a canonical lowercase UUID"
1870
+ )
1871
+ _assert_wire_scalars(item, item_path)
1872
+ elif isinstance(value, list):
1873
+ for index, item in enumerate(value):
1874
+ _assert_wire_scalars(item, f"{path}[{index}]")
1875
+ elif value is None or type(value) in {str, int, bool}:
1876
+ return
1877
+ else:
1878
+ raise StudioBoundaryValidationError(f"{path} is not a strict JSON value")
1879
+
1880
+
1881
+ def _wire_digest(value: str, path: str, *, allow_raw: bool = False) -> str:
1882
+ if allow_raw and re.fullmatch(r"[0-9a-f]{64}", value):
1883
+ value = f"sha256:{value}"
1884
+ if not isinstance(value, str) or not _DIGEST.fullmatch(value):
1885
+ raise StudioBoundaryValidationError(f"{path} must be a sha256:<lowercase hex> digest")
1886
+ return value
1887
+
1888
+
1889
+ def _bare_digest(value: str, path: str) -> str:
1890
+ if not isinstance(value, str) or not re.fullmatch(r"[0-9a-f]{64}", value):
1891
+ raise StudioBoundaryValidationError(f"{path} must be a bare sha256 lowercase hex digest")
1892
+ return value
1893
+
1894
+
1895
+ def _parse_utc(value: str, path: str) -> datetime:
1896
+ if not isinstance(value, str) or not _UTC.fullmatch(value):
1897
+ raise StudioBoundaryValidationError(f"{path} must be a canonical UTC timestamp ending in Z")
1898
+ try:
1899
+ parsed = datetime.fromisoformat(value[:-1] + "+00:00")
1900
+ except ValueError as exc:
1901
+ raise StudioBoundaryValidationError(f"{path} is not a calendar-valid timestamp") from exc
1902
+ if parsed.tzinfo != UTC:
1903
+ raise StudioBoundaryValidationError(f"{path} must be UTC")
1904
+ return parsed
1905
+
1906
+
1907
+ def _semver(value: str, path: str) -> str:
1908
+ if not isinstance(value, str) or not _SEMVER.fullmatch(value):
1909
+ raise StudioBoundaryValidationError(f"{path} must be a nonzero-major semantic version")
1910
+ return value
1911
+
1912
+
1913
+ def _generated_enum(enum_type: Any, value: str, path: str) -> Any:
1914
+ try:
1915
+ return enum_type(value)
1916
+ except (TypeError, ValueError) as exc:
1917
+ raise StudioBoundaryValidationError(
1918
+ f"{path} is not supported by the pinned generated Studio client: {value!r}"
1919
+ ) from exc
1920
+
1921
+
1922
+ def _nonempty(value: str, path: str, *, maximum: int) -> None:
1923
+ if not isinstance(value, str) or not value or len(value) > maximum:
1924
+ raise StudioBoundaryValidationError(f"{path} must be 1..{maximum} characters")
1925
+
1926
+
1927
+ def _bounded_int(value: int, path: str, minimum: int, maximum: int) -> None:
1928
+ if type(value) is not int or not minimum <= value <= maximum:
1929
+ raise StudioBoundaryValidationError(
1930
+ f"{path} must be an integer from {minimum} through {maximum}"
1931
+ )
1932
+
1933
+
1934
+ def _uuids(value: Any) -> None:
1935
+ """Require UUID instances for the attributes named in _UUID_FIELDS.
1936
+
1937
+ Attributes outside that allowlist are not inspected, so a UUID-annotated field whose name is
1938
+ absent from _UUID_FIELDS goes unchecked here.
1939
+ """
1940
+
1941
+ for name, item in vars(value).items():
1942
+ if name in _UUID_FIELDS and item is not None and not isinstance(item, UUID):
1943
+ raise StudioBoundaryValidationError(f"{name} must be a UUID")
1944
+
1945
+
1946
+ def _match(actual: Any, expected: Any, path: str) -> None:
1947
+ """Require an exact match against the server-issued value named by path.
1948
+
1949
+ Callers use it for fences, digests, identities, and policy coordinates alike. The raised
1950
+ message says fence mismatch in every case, so path is what names the value that differed.
1951
+ """
1952
+
1953
+ if actual != expected:
1954
+ raise StudioBoundaryValidationError(
1955
+ f"{path} fence mismatch: expected {expected!s}, got {actual!s}"
1956
+ )
1957
+
1958
+
1959
+ def _match_optional_attempt(model: Any, fence: HostedFence) -> None:
1960
+ api = _studio_api()
1961
+ actual = model.attempt_id
1962
+ if isinstance(actual, type(api.unset)):
1963
+ if fence.attempt_id is not None:
1964
+ raise StudioBoundaryValidationError("attempt_id fence expected but event omitted it")
1965
+ elif fence.attempt_id is None:
1966
+ raise StudioBoundaryValidationError("event carries attempt_id without an explicit fence")
1967
+ else:
1968
+ _match(actual, fence.attempt_id, "attempt_id")
1969
+
1970
+
1971
+ __all__ = [
1972
+ "PINNED_CONTRACT_SET",
1973
+ "PINNED_CONTRACT_VERSION",
1974
+ "PINNED_GENERATOR",
1975
+ "PINNED_GENERATOR_VERSION",
1976
+ "PINNED_OPENAPI_SOURCE_SHA256",
1977
+ "AgentCallIdentity",
1978
+ "BoundaryMappingEvidence",
1979
+ "HostedFence",
1980
+ "MappedStudioValue",
1981
+ "OpaqueSecretBinding",
1982
+ "PlanIdentity",
1983
+ "ProducerEvidenceScope",
1984
+ "QuestionIdentity",
1985
+ "RequirementsIdentity",
1986
+ "SourceIdentity",
1987
+ "StudioBoundaryError",
1988
+ "StudioBoundaryValidationError",
1989
+ "StudioClientIncompatibleError",
1990
+ "StudioClientUnavailableError",
1991
+ "StudioContractIdentity",
1992
+ "VerifierReviewScope",
1993
+ "client_contract_identity",
1994
+ "ensure_studio_client",
1995
+ "map_agent_call_evidence",
1996
+ "map_plan",
1997
+ "map_producer_agent_call_submission",
1998
+ "map_producer_source_fitness_submission",
1999
+ "map_producer_source_observation_submission",
2000
+ "map_question",
2001
+ "map_requirements",
2002
+ "map_source_proposal",
2003
+ "map_source_rights_decision",
2004
+ "map_verifier_download_command",
2005
+ "map_verifier_finding_submission",
2006
+ "map_verifier_semantic_review_submission",
2007
+ "parse_artifact",
2008
+ "parse_candidate_admission",
2009
+ "parse_finding",
2010
+ "parse_producer_input_signed_session",
2011
+ "parse_run_event",
2012
+ "parse_source_rights_review",
2013
+ "parse_verifier_job_scope",
2014
+ "parse_verifier_semantic_evidence",
2015
+ "parse_verifier_signed_session",
2016
+ "parse_verifier_submission",
2017
+ "studio_wire_dict",
2018
+ "validate_source_rights_decision_scope",
2019
+ ]