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,898 @@
1
+ """Strict structural evidence for host-owned source discovery.
2
+
3
+ Search and capability receipts are untrusted host evidence. This module does not claim the host
4
+ ran a browser. It deterministically binds every reported lane, query, source comparison, and
5
+ refusal-recovery chain to one question and one proposal set without granting acquisition authority.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from dataclasses import dataclass
12
+ from itertools import pairwise
13
+ from typing import Any
14
+
15
+ from mostlyright.data_harness.canonical import canonical_json_bytes, canonical_sha256
16
+ from mostlyright.data_harness.local_contracts import (
17
+ ContractError,
18
+ SourceProposal,
19
+ source_locator_identity,
20
+ validate_source_locator,
21
+ )
22
+ from mostlyright.data_harness.readers.contracts import ReaderError, identifier, semver
23
+ from mostlyright.data_harness.readers.registry import TOOLBOX
24
+ from mostlyright.data_harness.sources.contracts import FITNESS_DIMENSIONS, MAX_EVIDENCE
25
+
26
+ SOURCE_DISCOVERY_REPORT_SCHEMA_VERSION = "local-source-discovery-report.v2"
27
+ SOURCE_DISCOVERY_POLICY_VERSION = "multi-lane-source-discovery.v2"
28
+
29
+ DISCOVERY_LANES = (
30
+ "registered_adapters",
31
+ "sealed_catalog",
32
+ "host_web_search",
33
+ "official_provider",
34
+ "user_supplied",
35
+ )
36
+ FITNESS_CRITERIA = tuple(sorted(FITNESS_DIMENSIONS))
37
+ DISCOVERY_LANE_STATUSES = ("searched", "unavailable")
38
+ DISCOVERY_CRITERION_RESULTS = ("fail", "pass", "unknown")
39
+ DISCOVERY_FITNESS_DECISIONS = ("eligible", "rejected", "selected")
40
+ DISCOVERY_RECOVERY_OUTCOMES = ("lawful_routes_exhausted", "recovered", "still_rejected")
41
+ DISCOVERY_REJECTION_KINDS = ("fitness_failure", "typed_refusal")
42
+ DISCOVERY_UNAVAILABLE_REASONS = {
43
+ "registered_adapters": ("no_registered_capability",),
44
+ "sealed_catalog": ("catalog_not_configured",),
45
+ "host_web_search": ("host_search_unavailable", "policy_prohibits_network"),
46
+ "official_provider": ("policy_prohibits_network", "provider_search_unavailable"),
47
+ "user_supplied": ("no_user_source_supplied",),
48
+ }
49
+
50
+ _LANE_STATUSES = frozenset(DISCOVERY_LANE_STATUSES)
51
+ _CRITERION_RESULTS = frozenset(DISCOVERY_CRITERION_RESULTS)
52
+ _FITNESS_DECISIONS = frozenset(DISCOVERY_FITNESS_DECISIONS)
53
+ _RECOVERY_OUTCOMES = frozenset(DISCOVERY_RECOVERY_OUTCOMES)
54
+ _REJECTION_KINDS = frozenset(DISCOVERY_REJECTION_KINDS)
55
+ _IDENTIFIER = re.compile(r"^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$")
56
+ _REFUSAL_CODE = re.compile(r"^[A-Z][A-Z0-9_]{1,127}$")
57
+ _SHA256 = re.compile(r"^[0-9a-f]{64}$")
58
+
59
+
60
+ def _object(value: Any, path: str, fields: set[str]) -> dict[str, Any]:
61
+ if not isinstance(value, dict):
62
+ raise ContractError(path, "TYPE", "must be an object")
63
+ if set(value) != fields:
64
+ raise ContractError(path, "FIELDS", f"must contain exactly {sorted(fields)!r}")
65
+ return value
66
+
67
+
68
+ def _array(value: Any, path: str, *, maximum: int, nonempty: bool = False) -> list[Any]:
69
+ if not isinstance(value, list):
70
+ raise ContractError(path, "TYPE", "must be an array")
71
+ minimum = 1 if nonempty else 0
72
+ if not minimum <= len(value) <= maximum:
73
+ raise ContractError(path, "COLLECTION_LIMIT", f"must contain {minimum}..{maximum} items")
74
+ return value
75
+
76
+
77
+ def _text(value: Any, path: str, *, maximum: int = 2_000) -> str:
78
+ if not isinstance(value, str) or not value or len(value) > maximum:
79
+ raise ContractError(path, "TEXT", f"must be non-empty text of at most {maximum} characters")
80
+ if value != value.strip():
81
+ raise ContractError(path, "NONCANONICAL_TEXT", "must not have surrounding whitespace")
82
+ if any(ord(character) < 0x20 and character not in {"\t", "\n", "\r"} for character in value):
83
+ raise ContractError(path, "CONTROL_CHARACTER", "contains a control character")
84
+ return value
85
+
86
+
87
+ def _identifier(value: Any, path: str) -> str:
88
+ text = _text(value, path, maximum=64)
89
+ if _IDENTIFIER.fullmatch(text) is None:
90
+ raise ContractError(path, "IDENTIFIER", "must be a lower-case identifier")
91
+ return text
92
+
93
+
94
+ def _choice(value: Any, choices: frozenset[str], path: str) -> str:
95
+ text = _text(value, path, maximum=64)
96
+ if text not in choices:
97
+ raise ContractError(path, "DISCOVERY_CHOICE", f"must be one of {sorted(choices)!r}")
98
+ return text
99
+
100
+
101
+ def _sha256(value: Any, path: str) -> str:
102
+ text = _text(value, path, maximum=64)
103
+ if _SHA256.fullmatch(text) is None:
104
+ raise ContractError(path, "DIGEST", "must be a lower-case SHA-256 digest")
105
+ return text
106
+
107
+
108
+ def _unique(values: tuple[str, ...], path: str) -> None:
109
+ if len(set(values)) != len(values):
110
+ raise ContractError(path, "DUPLICATE", "items must be unique")
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class DiscoveryQuery:
115
+ """One host query and the immutable host-tool receipt it reports."""
116
+
117
+ query: str
118
+ result_source_ids: tuple[str, ...]
119
+ search_receipt_sha256: str
120
+
121
+ @classmethod
122
+ def from_value(cls, value: Any, path: str) -> DiscoveryQuery:
123
+ data = _object(value, path, {"query", "result_source_ids", "search_receipt_sha256"})
124
+ source_ids = tuple(
125
+ _identifier(item, f"{path}.result_source_ids[{index}]")
126
+ for index, item in enumerate(
127
+ _array(data["result_source_ids"], f"{path}.result_source_ids", maximum=64)
128
+ )
129
+ )
130
+ _unique(source_ids, f"{path}.result_source_ids")
131
+ return cls(
132
+ query=_text(data["query"], f"{path}.query"),
133
+ result_source_ids=source_ids,
134
+ search_receipt_sha256=_sha256(
135
+ data["search_receipt_sha256"], f"{path}.search_receipt_sha256"
136
+ ),
137
+ )
138
+
139
+ def to_dict(self) -> dict[str, Any]:
140
+ return {
141
+ "query": self.query,
142
+ "result_source_ids": list(self.result_source_ids),
143
+ "search_receipt_sha256": self.search_receipt_sha256,
144
+ }
145
+
146
+
147
+ @dataclass(frozen=True)
148
+ class DiscoveryLane:
149
+ """Reported availability and bounded query trace for one mandatory lane."""
150
+
151
+ lane: str
152
+ status: str
153
+ reason_code: str | None
154
+ capability_receipt_sha256: str
155
+ queries: tuple[DiscoveryQuery, ...]
156
+
157
+ @classmethod
158
+ def from_value(cls, value: Any, path: str) -> DiscoveryLane:
159
+ data = _object(
160
+ value,
161
+ path,
162
+ {"lane", "status", "reason_code", "capability_receipt_sha256", "queries"},
163
+ )
164
+ lane = _choice(data["lane"], frozenset(DISCOVERY_LANES), f"{path}.lane")
165
+ status = _choice(data["status"], _LANE_STATUSES, f"{path}.status")
166
+ queries = tuple(
167
+ DiscoveryQuery.from_value(item, f"{path}.queries[{index}]")
168
+ for index, item in enumerate(_array(data["queries"], f"{path}.queries", maximum=32))
169
+ )
170
+ reason_value = data["reason_code"]
171
+ if status == "searched":
172
+ if reason_value is not None:
173
+ raise ContractError(
174
+ f"{path}.reason_code",
175
+ "DISCOVERY_STATE",
176
+ "must be null when the lane was searched",
177
+ )
178
+ if not queries:
179
+ raise ContractError(
180
+ f"{path}.queries",
181
+ "DISCOVERY_STATE",
182
+ "a searched lane must record at least one query",
183
+ )
184
+ reason_code = None
185
+ else:
186
+ reason_code = _choice(
187
+ reason_value,
188
+ frozenset(DISCOVERY_UNAVAILABLE_REASONS[lane]),
189
+ f"{path}.reason_code",
190
+ )
191
+ if queries:
192
+ raise ContractError(
193
+ f"{path}.queries",
194
+ "DISCOVERY_STATE",
195
+ "an unavailable lane cannot claim queries",
196
+ )
197
+ return cls(
198
+ lane=lane,
199
+ status=status,
200
+ reason_code=reason_code,
201
+ capability_receipt_sha256=_sha256(
202
+ data["capability_receipt_sha256"], f"{path}.capability_receipt_sha256"
203
+ ),
204
+ queries=queries,
205
+ )
206
+
207
+ def to_dict(self) -> dict[str, Any]:
208
+ return {
209
+ "lane": self.lane,
210
+ "status": self.status,
211
+ "reason_code": self.reason_code,
212
+ "capability_receipt_sha256": self.capability_receipt_sha256,
213
+ "queries": [item.to_dict() for item in self.queries],
214
+ }
215
+
216
+
217
+ @dataclass(frozen=True)
218
+ class ComparisonDimension:
219
+ """One lossless proposal-stage mapping to canonical source fitness evidence."""
220
+
221
+ dimension: str
222
+ outcome: str
223
+ summary: str
224
+ evidence_digests: tuple[str, ...]
225
+
226
+ @classmethod
227
+ def from_value(cls, value: Any, path: str) -> ComparisonDimension:
228
+ data = _object(value, path, {"dimension", "outcome", "summary", "evidence_digests"})
229
+ evidence_digests = tuple(
230
+ _sha256(item, f"{path}.evidence_digests[{index}]")
231
+ for index, item in enumerate(
232
+ _array(
233
+ data["evidence_digests"],
234
+ f"{path}.evidence_digests",
235
+ maximum=MAX_EVIDENCE,
236
+ nonempty=True,
237
+ )
238
+ )
239
+ )
240
+ _unique(evidence_digests, f"{path}.evidence_digests")
241
+ return cls(
242
+ dimension=_choice(data["dimension"], frozenset(FITNESS_CRITERIA), f"{path}.dimension"),
243
+ outcome=_choice(data["outcome"], _CRITERION_RESULTS, f"{path}.outcome"),
244
+ summary=_text(data["summary"], f"{path}.summary", maximum=1_000),
245
+ evidence_digests=evidence_digests,
246
+ )
247
+
248
+ def to_dict(self) -> dict[str, Any]:
249
+ return {
250
+ "dimension": self.dimension,
251
+ "outcome": self.outcome,
252
+ "summary": self.summary,
253
+ "evidence_digests": list(self.evidence_digests),
254
+ }
255
+
256
+
257
+ @dataclass(frozen=True)
258
+ class DiscoveryReaderPin:
259
+ """A certified Reader identity plus the digest of its complete sealed options."""
260
+
261
+ family_id: str
262
+ family_version: str
263
+ decode_options_digest: str
264
+
265
+ @classmethod
266
+ def from_value(cls, value: Any, path: str) -> DiscoveryReaderPin:
267
+ data = _object(value, path, {"family_id", "family_version", "decode_options_digest"})
268
+ try:
269
+ family_id = identifier(data["family_id"], f"{path}.family_id")
270
+ family_version = semver(data["family_version"], f"{path}.family_version")
271
+ except ReaderError as error:
272
+ raise ContractError(
273
+ path,
274
+ "DISCOVERY_READER_PIN",
275
+ "must contain a canonical certified Reader identity",
276
+ ) from error
277
+ if (family_id, family_version) not in TOOLBOX.families():
278
+ raise ContractError(
279
+ path,
280
+ "DISCOVERY_READER_PIN",
281
+ "must resolve to an exact Reader coordinate in the certified Toolbox",
282
+ )
283
+ return cls(
284
+ family_id=family_id,
285
+ family_version=family_version,
286
+ decode_options_digest=_sha256(
287
+ data["decode_options_digest"], f"{path}.decode_options_digest"
288
+ ),
289
+ )
290
+
291
+ def to_dict(self) -> dict[str, Any]:
292
+ return {
293
+ "family_id": self.family_id,
294
+ "family_version": self.family_version,
295
+ "decode_options_digest": self.decode_options_digest,
296
+ }
297
+
298
+
299
+ @dataclass(frozen=True)
300
+ class SourceComparison:
301
+ """A proposed source evaluated over every canonical fitness dimension."""
302
+
303
+ source_id: str
304
+ decision: str
305
+ rejection_kind: str | None
306
+ promoted_inspection_scope: RecoveryScope | None
307
+ promoted_inspection_receipt_sha256: str | None
308
+ dimensions: tuple[ComparisonDimension, ...]
309
+ rationale: str
310
+ recovery_attempt_ids: tuple[str, ...]
311
+
312
+ @classmethod
313
+ def from_value(cls, value: Any, path: str) -> SourceComparison:
314
+ data = _object(
315
+ value,
316
+ path,
317
+ {
318
+ "source_id",
319
+ "decision",
320
+ "rejection_kind",
321
+ "promoted_inspection_scope",
322
+ "promoted_inspection_receipt_sha256",
323
+ "dimensions",
324
+ "rationale",
325
+ "recovery_attempt_ids",
326
+ },
327
+ )
328
+ dimensions = tuple(
329
+ ComparisonDimension.from_value(item, f"{path}.dimensions[{index}]")
330
+ for index, item in enumerate(
331
+ _array(
332
+ data["dimensions"],
333
+ f"{path}.dimensions",
334
+ maximum=len(FITNESS_CRITERIA),
335
+ nonempty=True,
336
+ )
337
+ )
338
+ )
339
+ dimension_names = tuple(item.dimension for item in dimensions)
340
+ _unique(dimension_names, f"{path}.dimensions")
341
+ if set(dimension_names) != set(FITNESS_CRITERIA):
342
+ raise ContractError(
343
+ f"{path}.dimensions",
344
+ "FITNESS_DIMENSIONS",
345
+ "must contain every canonical source fitness dimension exactly once",
346
+ )
347
+ attempt_ids = tuple(
348
+ _identifier(item, f"{path}.recovery_attempt_ids[{index}]")
349
+ for index, item in enumerate(
350
+ _array(data["recovery_attempt_ids"], f"{path}.recovery_attempt_ids", maximum=64)
351
+ )
352
+ )
353
+ _unique(attempt_ids, f"{path}.recovery_attempt_ids")
354
+ decision = _choice(data["decision"], _FITNESS_DECISIONS, f"{path}.decision")
355
+ rejection_value = data["rejection_kind"]
356
+ if decision == "rejected":
357
+ rejection_kind = _choice(rejection_value, _REJECTION_KINDS, f"{path}.rejection_kind")
358
+ if data["promoted_inspection_scope"] is not None:
359
+ raise ContractError(
360
+ f"{path}.promoted_inspection_scope",
361
+ "DISCOVERY_STATE",
362
+ "must be null when the source is rejected",
363
+ )
364
+ promoted_inspection_scope = None
365
+ if data["promoted_inspection_receipt_sha256"] is not None:
366
+ raise ContractError(
367
+ f"{path}.promoted_inspection_receipt_sha256",
368
+ "DISCOVERY_STATE",
369
+ "must be null when the source is rejected",
370
+ )
371
+ promoted_inspection_receipt_sha256 = None
372
+ else:
373
+ if rejection_value is not None:
374
+ raise ContractError(
375
+ f"{path}.rejection_kind",
376
+ "DISCOVERY_STATE",
377
+ "must be null unless the source is rejected",
378
+ )
379
+ rejection_kind = None
380
+ if data["promoted_inspection_scope"] is None:
381
+ raise ContractError(
382
+ f"{path}.promoted_inspection_scope",
383
+ "DISCOVERY_PROMOTED_SCOPE_REQUIRED",
384
+ "a retained source requires its exact inspected execution coordinate",
385
+ )
386
+ promoted_inspection_scope = RecoveryScope.from_value(
387
+ data["promoted_inspection_scope"], f"{path}.promoted_inspection_scope"
388
+ )
389
+ promoted_inspection_receipt_sha256 = _sha256(
390
+ data["promoted_inspection_receipt_sha256"],
391
+ f"{path}.promoted_inspection_receipt_sha256",
392
+ )
393
+ if rejection_kind == "typed_refusal" and not attempt_ids:
394
+ raise ContractError(
395
+ f"{path}.recovery_attempt_ids",
396
+ "DISCOVERY_REFUSAL_RECOVERY_REQUIRED",
397
+ "a typed refusal requires a changed-scope recovery chain",
398
+ )
399
+ if rejection_kind == "fitness_failure" and attempt_ids:
400
+ raise ContractError(
401
+ f"{path}.recovery_attempt_ids",
402
+ "DISCOVERY_REFUSAL_CLASSIFICATION",
403
+ "a fitness failure cannot carry typed-refusal recovery attempts",
404
+ )
405
+ return cls(
406
+ source_id=_identifier(data["source_id"], f"{path}.source_id"),
407
+ decision=decision,
408
+ rejection_kind=rejection_kind,
409
+ promoted_inspection_scope=promoted_inspection_scope,
410
+ promoted_inspection_receipt_sha256=promoted_inspection_receipt_sha256,
411
+ dimensions=dimensions,
412
+ rationale=_text(data["rationale"], f"{path}.rationale"),
413
+ recovery_attempt_ids=attempt_ids,
414
+ )
415
+
416
+ def to_dict(self) -> dict[str, Any]:
417
+ return {
418
+ "source_id": self.source_id,
419
+ "decision": self.decision,
420
+ "rejection_kind": self.rejection_kind,
421
+ "promoted_inspection_scope": (
422
+ None
423
+ if self.promoted_inspection_scope is None
424
+ else self.promoted_inspection_scope.to_dict()
425
+ ),
426
+ "promoted_inspection_receipt_sha256": self.promoted_inspection_receipt_sha256,
427
+ "dimensions": [item.to_dict() for item in self.dimensions],
428
+ "rationale": self.rationale,
429
+ "recovery_attempt_ids": list(self.recovery_attempt_ids),
430
+ }
431
+
432
+
433
+ @dataclass(frozen=True)
434
+ class RecoveryScope:
435
+ """The exact execution coordinates on one side of a refusal retry."""
436
+
437
+ locator_kind: str
438
+ locator: str
439
+ reader_pin: DiscoveryReaderPin | None
440
+ data_format: str
441
+ host_platform: str
442
+
443
+ @classmethod
444
+ def from_value(cls, value: Any, path: str) -> RecoveryScope:
445
+ data = _object(
446
+ value,
447
+ path,
448
+ {"locator_kind", "locator", "reader_pin", "data_format", "host_platform"},
449
+ )
450
+ reader_value = data["reader_pin"]
451
+ reader_pin = (
452
+ None
453
+ if reader_value is None
454
+ else DiscoveryReaderPin.from_value(reader_value, f"{path}.reader_pin")
455
+ )
456
+ locator_kind = _choice(
457
+ data["locator_kind"],
458
+ frozenset({"relative_path", "https_url", "artifact_reference"}),
459
+ f"{path}.locator_kind",
460
+ )
461
+ return cls(
462
+ locator_kind=locator_kind,
463
+ locator=validate_source_locator(data["locator"], locator_kind, f"{path}.locator"),
464
+ reader_pin=reader_pin,
465
+ data_format=_text(data["data_format"], f"{path}.data_format", maximum=64),
466
+ host_platform=_text(data["host_platform"], f"{path}.host_platform", maximum=256),
467
+ )
468
+
469
+ def to_dict(self) -> dict[str, Any]:
470
+ return {
471
+ "locator_kind": self.locator_kind,
472
+ "locator": self.locator,
473
+ "reader_pin": None if self.reader_pin is None else self.reader_pin.to_dict(),
474
+ "data_format": self.data_format,
475
+ "host_platform": self.host_platform,
476
+ }
477
+
478
+
479
+ @dataclass(frozen=True)
480
+ class RecoveryAttempt:
481
+ """One refusal receipt and one exact replacement inspection receipt."""
482
+
483
+ attempt_id: str
484
+ source_id: str
485
+ refusal_code: str
486
+ refusal_receipt_sha256: str
487
+ refusal_scope: RecoveryScope
488
+ replacement_scope: RecoveryScope
489
+ replacement_receipt_sha256: str
490
+ outcome: str
491
+
492
+ @property
493
+ def changed_fields(self) -> tuple[str, ...]:
494
+ return tuple(
495
+ field
496
+ for field in (
497
+ "locator_kind",
498
+ "locator",
499
+ "reader_pin",
500
+ "data_format",
501
+ "host_platform",
502
+ )
503
+ if getattr(self.refusal_scope, field) != getattr(self.replacement_scope, field)
504
+ )
505
+
506
+ @classmethod
507
+ def from_value(cls, value: Any, path: str) -> RecoveryAttempt:
508
+ data = _object(
509
+ value,
510
+ path,
511
+ {
512
+ "attempt_id",
513
+ "source_id",
514
+ "refusal_code",
515
+ "refusal_receipt_sha256",
516
+ "refusal_scope",
517
+ "replacement_scope",
518
+ "replacement_receipt_sha256",
519
+ "outcome",
520
+ },
521
+ )
522
+ refusal_code = _text(data["refusal_code"], f"{path}.refusal_code", maximum=128)
523
+ if _REFUSAL_CODE.fullmatch(refusal_code) is None:
524
+ raise ContractError(
525
+ f"{path}.refusal_code",
526
+ "DISCOVERY_REFUSAL_CODE",
527
+ "must be a canonical upper-case refusal code",
528
+ )
529
+ attempt = cls(
530
+ attempt_id=_identifier(data["attempt_id"], f"{path}.attempt_id"),
531
+ source_id=_identifier(data["source_id"], f"{path}.source_id"),
532
+ refusal_code=refusal_code,
533
+ refusal_receipt_sha256=_sha256(
534
+ data["refusal_receipt_sha256"], f"{path}.refusal_receipt_sha256"
535
+ ),
536
+ refusal_scope=RecoveryScope.from_value(data["refusal_scope"], f"{path}.refusal_scope"),
537
+ replacement_scope=RecoveryScope.from_value(
538
+ data["replacement_scope"], f"{path}.replacement_scope"
539
+ ),
540
+ replacement_receipt_sha256=_sha256(
541
+ data["replacement_receipt_sha256"], f"{path}.replacement_receipt_sha256"
542
+ ),
543
+ outcome=_choice(data["outcome"], _RECOVERY_OUTCOMES, f"{path}.outcome"),
544
+ )
545
+ if not attempt.changed_fields:
546
+ raise ContractError(
547
+ f"{path}.replacement_scope",
548
+ "DISCOVERY_UNCHANGED_RECOVERY",
549
+ "must change at least one exact execution coordinate",
550
+ )
551
+ return attempt
552
+
553
+ def to_dict(self) -> dict[str, Any]:
554
+ return {
555
+ "attempt_id": self.attempt_id,
556
+ "source_id": self.source_id,
557
+ "refusal_code": self.refusal_code,
558
+ "refusal_receipt_sha256": self.refusal_receipt_sha256,
559
+ "refusal_scope": self.refusal_scope.to_dict(),
560
+ "replacement_scope": self.replacement_scope.to_dict(),
561
+ "replacement_receipt_sha256": self.replacement_receipt_sha256,
562
+ "outcome": self.outcome,
563
+ }
564
+
565
+
566
+ @dataclass(frozen=True)
567
+ class SourceDiscoveryReport:
568
+ """Canonical untrusted evidence that a host reports following the multi-lane policy."""
569
+
570
+ question_id: str
571
+ lanes: tuple[DiscoveryLane, ...]
572
+ comparisons: tuple[SourceComparison, ...]
573
+ recovery_attempts: tuple[RecoveryAttempt, ...]
574
+ schema_version: str = SOURCE_DISCOVERY_REPORT_SCHEMA_VERSION
575
+
576
+ @property
577
+ def canonical_bytes(self) -> bytes:
578
+ return canonical_json_bytes(self.to_dict())
579
+
580
+ @property
581
+ def digest(self) -> str:
582
+ return canonical_sha256(self.to_dict())
583
+
584
+ @classmethod
585
+ def from_value(cls, value: Any) -> SourceDiscoveryReport:
586
+ path = "source_discovery"
587
+ data = _object(
588
+ value,
589
+ path,
590
+ {"schema_version", "question_id", "lanes", "comparisons", "recovery_attempts"},
591
+ )
592
+ version = _text(data["schema_version"], f"{path}.schema_version", maximum=64)
593
+ if version != SOURCE_DISCOVERY_REPORT_SCHEMA_VERSION:
594
+ raise ContractError(
595
+ f"{path}.schema_version",
596
+ "VERSION",
597
+ f"must be {SOURCE_DISCOVERY_REPORT_SCHEMA_VERSION!r}",
598
+ )
599
+ lanes = tuple(
600
+ DiscoveryLane.from_value(item, f"{path}.lanes[{index}]")
601
+ for index, item in enumerate(
602
+ _array(data["lanes"], f"{path}.lanes", maximum=len(DISCOVERY_LANES), nonempty=True)
603
+ )
604
+ )
605
+ lane_names = tuple(item.lane for item in lanes)
606
+ _unique(lane_names, f"{path}.lanes")
607
+ if set(lane_names) != set(DISCOVERY_LANES):
608
+ raise ContractError(
609
+ f"{path}.lanes",
610
+ "DISCOVERY_LANES_INCOMPLETE",
611
+ f"must account for exactly {list(DISCOVERY_LANES)!r}",
612
+ )
613
+ comparisons = tuple(
614
+ SourceComparison.from_value(item, f"{path}.comparisons[{index}]")
615
+ for index, item in enumerate(
616
+ _array(data["comparisons"], f"{path}.comparisons", maximum=64)
617
+ )
618
+ )
619
+ _unique(tuple(item.source_id for item in comparisons), f"{path}.comparisons")
620
+ recovery_attempts = tuple(
621
+ RecoveryAttempt.from_value(item, f"{path}.recovery_attempts[{index}]")
622
+ for index, item in enumerate(
623
+ _array(data["recovery_attempts"], f"{path}.recovery_attempts", maximum=64)
624
+ )
625
+ )
626
+ _unique(tuple(item.attempt_id for item in recovery_attempts), f"{path}.recovery_attempts")
627
+ return cls(
628
+ schema_version=version,
629
+ question_id=_identifier(data["question_id"], f"{path}.question_id"),
630
+ lanes=lanes,
631
+ comparisons=comparisons,
632
+ recovery_attempts=recovery_attempts,
633
+ )
634
+
635
+ def validate_against(
636
+ self,
637
+ *,
638
+ question_id: str,
639
+ proposals: tuple[SourceProposal, ...],
640
+ feasibility: str,
641
+ required_start: str,
642
+ required_end: str,
643
+ ) -> None:
644
+ if self.question_id != question_id:
645
+ raise ContractError(
646
+ "source_discovery.question_id",
647
+ "REFERENCE_MISMATCH",
648
+ "must reference question.question_id",
649
+ )
650
+ proposal_by_source = {item.source_id: item for item in proposals}
651
+ proposal_ids = set(proposal_by_source)
652
+ if {item.source_id for item in self.comparisons} != proposal_ids:
653
+ raise ContractError(
654
+ "source_discovery.comparisons",
655
+ "REFERENCE_MISMATCH",
656
+ "must compare every and only proposed source",
657
+ )
658
+ identities = tuple(
659
+ source_locator_identity(item.locator_kind, item.locator) for item in proposals
660
+ )
661
+ if len(set(identities)) != len(identities):
662
+ raise ContractError(
663
+ "source_proposals.locator",
664
+ "DISCOVERY_DUPLICATE_SOURCE",
665
+ "the same exact source locator cannot be proposed more than once",
666
+ )
667
+
668
+ discovered_ids: set[str] = set()
669
+ query_receipts_by_source: dict[str, set[str]] = {
670
+ source_id: set() for source_id in proposal_ids
671
+ }
672
+ for lane in self.lanes:
673
+ for query in lane.queries:
674
+ for source_id in query.result_source_ids:
675
+ if source_id not in proposal_ids:
676
+ raise ContractError(
677
+ f"source_discovery.lanes.{lane.lane}.queries.result_source_ids",
678
+ "REFERENCE_MISMATCH",
679
+ "must reference a proposed source",
680
+ )
681
+ discovered_ids.add(source_id)
682
+ query_receipts_by_source[source_id].add(query.search_receipt_sha256)
683
+ if discovered_ids != proposal_ids:
684
+ raise ContractError(
685
+ "source_discovery.lanes",
686
+ "DISCOVERY_UNDISCOVERED_PROPOSAL",
687
+ "every proposed source must appear in a searched lane query result",
688
+ )
689
+
690
+ attempt_by_id = {item.attempt_id: item for item in self.recovery_attempts}
691
+ referenced_attempts: set[str] = set()
692
+ for comparison in self.comparisons:
693
+ proposal = proposal_by_source[comparison.source_id]
694
+ if comparison.decision != proposal.fitness_decision:
695
+ raise ContractError(
696
+ f"source_discovery.comparisons.{comparison.source_id}.decision",
697
+ "DECISION_MISMATCH",
698
+ "must match source_proposal.fitness_decision",
699
+ )
700
+ outcomes = {item.dimension: item.outcome for item in comparison.dimensions}
701
+ if comparison.decision == "selected" and set(outcomes.values()) != {"pass"}:
702
+ raise ContractError(
703
+ f"source_discovery.comparisons.{comparison.source_id}.dimensions",
704
+ "UNFIT_SELECTED",
705
+ "a selected source requires every canonical fitness dimension to pass",
706
+ )
707
+ if comparison.rejection_kind == "fitness_failure" and "fail" not in outcomes.values():
708
+ raise ContractError(
709
+ f"source_discovery.comparisons.{comparison.source_id}.dimensions",
710
+ "DECISION_CRITERIA_MISMATCH",
711
+ "a fitness-failure rejection must identify a failed fitness dimension",
712
+ )
713
+ if comparison.rejection_kind == "typed_refusal" and "fail" in outcomes.values():
714
+ raise ContractError(
715
+ f"source_discovery.comparisons.{comparison.source_id}.dimensions",
716
+ "DISCOVERY_REFUSAL_CLASSIFICATION",
717
+ "a typed acquisition refusal cannot be represented as a fitness failure",
718
+ )
719
+ if comparison.decision == "eligible" and "fail" in outcomes.values():
720
+ raise ContractError(
721
+ f"source_discovery.comparisons.{comparison.source_id}.dimensions",
722
+ "DECISION_CRITERIA_MISMATCH",
723
+ "an eligible source cannot have a failed fitness dimension",
724
+ )
725
+ allowed_rights = {
726
+ "approved": {"pass"},
727
+ "conditional": {"pass", "unknown"},
728
+ "unclear": {"unknown", "fail"},
729
+ "prohibited": {"fail"},
730
+ }[proposal.rights_status]
731
+ if outcomes["rights"] not in allowed_rights:
732
+ raise ContractError(
733
+ f"source_discovery.comparisons.{comparison.source_id}.dimensions.rights",
734
+ "DISCOVERY_CRITERIA_EVIDENCE_MISMATCH",
735
+ "must agree with source_proposal.rights_status",
736
+ )
737
+ covers_required_history = (
738
+ proposal.historical_coverage.start_inclusive <= required_start
739
+ and proposal.historical_coverage.end_exclusive >= required_end
740
+ )
741
+ if outcomes["historical_coverage"] == "pass" and not covers_required_history:
742
+ raise ContractError(
743
+ f"source_discovery.comparisons.{comparison.source_id}.dimensions."
744
+ "historical_coverage",
745
+ "DISCOVERY_CRITERIA_EVIDENCE_MISMATCH",
746
+ "cannot pass when source coverage does not contain the required time range",
747
+ )
748
+
749
+ known_evidence = {
750
+ item.content_sha256 for item in proposal.evidence
751
+ } | query_receipts_by_source[comparison.source_id]
752
+ if comparison.promoted_inspection_receipt_sha256 is not None:
753
+ known_evidence.add(comparison.promoted_inspection_receipt_sha256)
754
+ attempts: list[RecoveryAttempt] = []
755
+ for attempt_id in comparison.recovery_attempt_ids:
756
+ attempt = attempt_by_id.get(attempt_id)
757
+ if attempt is None or attempt.source_id != comparison.source_id:
758
+ raise ContractError(
759
+ f"source_discovery.comparisons.{comparison.source_id}.recovery_attempt_ids",
760
+ "REFERENCE_MISMATCH",
761
+ "must reference recovery attempts for this source",
762
+ )
763
+ attempts.append(attempt)
764
+ referenced_attempts.add(attempt_id)
765
+ known_evidence.update(
766
+ {attempt.refusal_receipt_sha256, attempt.replacement_receipt_sha256}
767
+ )
768
+ for dimension in comparison.dimensions:
769
+ if not set(dimension.evidence_digests) <= known_evidence:
770
+ raise ContractError(
771
+ f"source_discovery.comparisons.{comparison.source_id}.dimensions."
772
+ f"{dimension.dimension}.evidence_digests",
773
+ "DISCOVERY_EVIDENCE_MISMATCH",
774
+ "must cite proposal, search, or refusal-recovery evidence for this source",
775
+ )
776
+ self._validate_recovery_chain(comparison, proposal, attempts)
777
+
778
+ if referenced_attempts != set(attempt_by_id):
779
+ raise ContractError(
780
+ "source_discovery.recovery_attempts",
781
+ "DISCOVERY_ORPHAN_RECOVERY",
782
+ "every recovery attempt must be ordered by exactly one source comparison",
783
+ )
784
+ selected = sum(item.fitness_decision == "selected" for item in proposals)
785
+ if feasibility in {"supportable", "supportable_with_limits"} and selected == 0:
786
+ raise ContractError(
787
+ "source_discovery.comparisons",
788
+ "DISCOVERY_FEASIBILITY_MISMATCH",
789
+ "a supportable decision requires at least one selected source",
790
+ )
791
+ if feasibility == "unsupported" and any(
792
+ item.fitness_decision != "rejected" for item in proposals
793
+ ):
794
+ raise ContractError(
795
+ "source_discovery.comparisons",
796
+ "UNSUPPORTED_NOT_EXHAUSTED",
797
+ "unsupported requires every compared source to be rejected",
798
+ )
799
+ if feasibility == "unsupported":
800
+ for comparison in self.comparisons:
801
+ if comparison.rejection_kind != "typed_refusal":
802
+ continue
803
+ final_attempt = attempt_by_id[comparison.recovery_attempt_ids[-1]]
804
+ if final_attempt.outcome != "lawful_routes_exhausted":
805
+ raise ContractError(
806
+ f"source_discovery.recovery_attempts.{final_attempt.attempt_id}.outcome",
807
+ "UNSUPPORTED_NOT_EXHAUSTED",
808
+ "unsupported requires every typed-refusal chain to end "
809
+ "lawful_routes_exhausted",
810
+ )
811
+
812
+ @staticmethod
813
+ def _validate_recovery_chain(
814
+ comparison: SourceComparison,
815
+ proposal: SourceProposal,
816
+ attempts: list[RecoveryAttempt],
817
+ ) -> None:
818
+ retained = comparison.promoted_inspection_scope
819
+ if retained is not None and (
820
+ retained.locator_kind != proposal.locator_kind
821
+ or retained.locator != proposal.locator
822
+ or retained.data_format != proposal.data_format
823
+ ):
824
+ raise ContractError(
825
+ f"source_discovery.comparisons.{comparison.source_id}.promoted_inspection_scope",
826
+ "DISCOVERY_RECOVERY_BINDING",
827
+ "the promoted inspection coordinate must match the proposal locator and format",
828
+ )
829
+ for attempt in attempts[:-1]:
830
+ if attempt.outcome != "still_rejected":
831
+ raise ContractError(
832
+ f"source_discovery.recovery_attempts.{attempt.attempt_id}.outcome",
833
+ "DISCOVERY_RECOVERY_TERMINAL",
834
+ "every non-final refusal retry must end still_rejected",
835
+ )
836
+ for previous, current in pairwise(attempts):
837
+ if previous.replacement_scope != current.refusal_scope:
838
+ raise ContractError(
839
+ f"source_discovery.comparisons.{comparison.source_id}.recovery_attempt_ids",
840
+ "DISCOVERY_RECOVERY_CHAIN",
841
+ "each retry must begin at the exact replacement scope of the prior refusal",
842
+ )
843
+ if not attempts:
844
+ return
845
+ final = attempts[-1]
846
+ if comparison.decision in {"selected", "eligible"}:
847
+ if final.outcome != "recovered":
848
+ raise ContractError(
849
+ f"source_discovery.recovery_attempts.{final.attempt_id}.outcome",
850
+ "DISCOVERY_RECOVERY_OUTCOME",
851
+ "a retained source requires its final refusal retry to recover",
852
+ )
853
+ if final.replacement_scope != comparison.promoted_inspection_scope:
854
+ raise ContractError(
855
+ f"source_discovery.recovery_attempts.{final.attempt_id}.replacement_scope",
856
+ "DISCOVERY_RECOVERY_BINDING",
857
+ "a recovered replacement must match the exact promoted inspection coordinate",
858
+ )
859
+ if final.replacement_receipt_sha256 != comparison.promoted_inspection_receipt_sha256:
860
+ raise ContractError(
861
+ f"source_discovery.recovery_attempts.{final.attempt_id}."
862
+ "replacement_receipt_sha256",
863
+ "DISCOVERY_RECOVERY_BINDING",
864
+ "a recovered replacement receipt must be the promoted inspection receipt",
865
+ )
866
+ else:
867
+ first = attempts[0].refusal_scope
868
+ if (
869
+ first.locator_kind != proposal.locator_kind
870
+ or first.locator != proposal.locator
871
+ or first.data_format != proposal.data_format
872
+ ):
873
+ raise ContractError(
874
+ f"source_discovery.recovery_attempts.{attempts[0].attempt_id}.refusal_scope",
875
+ "DISCOVERY_RECOVERY_BINDING",
876
+ "a rejected refusal chain must start at the proposed locator and format",
877
+ )
878
+ if final.outcome == "recovered":
879
+ raise ContractError(
880
+ f"source_discovery.recovery_attempts.{final.attempt_id}.outcome",
881
+ "DISCOVERY_RECOVERY_OUTCOME",
882
+ "a rejected source cannot end its refusal chain as recovered",
883
+ )
884
+
885
+ def to_dict(self) -> dict[str, Any]:
886
+ return {
887
+ "schema_version": self.schema_version,
888
+ "question_id": self.question_id,
889
+ "lanes": [item.to_dict() for item in self.lanes],
890
+ "comparisons": [item.to_dict() for item in self.comparisons],
891
+ "recovery_attempts": [item.to_dict() for item in self.recovery_attempts],
892
+ }
893
+
894
+
895
+ def parse_source_discovery_report(value: Any) -> SourceDiscoveryReport:
896
+ """Parse untrusted discovery evidence without performing search or acquisition."""
897
+
898
+ return SourceDiscoveryReport.from_value(value)