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,700 @@
1
+ """Fact-gated retrieval over immutable private Build-index entries.
2
+
3
+ The filter grammar in this module is closed data: one AND-only object containing named filters.
4
+ There is no generic operator, expression, regular-expression, SQL, or Python-evaluation seam.
5
+ Every predicate reads only :class:`BuildIndexEntry` facts; missing optional facts fail the filter
6
+ that needs them. Ranking is added separately below so similarity can order an already eligible
7
+ set but can never create membership in it.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from collections.abc import Mapping, Sequence
14
+ from dataclasses import dataclass
15
+ from typing import Any, ClassVar
16
+
17
+ from mostlyright.data_harness.canonical import canonical_json_bytes, canonical_sha256, sha256_bytes
18
+ from mostlyright.data_harness.local_search.contracts import (
19
+ _RIGHTS_STATUSES,
20
+ BuildField,
21
+ BuildIndexEntry,
22
+ BuildTimeCoverage,
23
+ LocalSearchContractError,
24
+ )
25
+
26
+ BUILD_FILTER_VERSION = "harness-private-build-filter.v1"
27
+ MAX_FILTER_ITEMS = 64
28
+ MAX_FILTER_TEXT = 512
29
+ MAX_QUERY_CHARS = 4_096
30
+ MAX_RETRIEVAL_LIMIT = 100
31
+
32
+ _DIGEST = re.compile(r"^[0-9a-f]{64}$")
33
+ _IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$")
34
+ _FILTER_KEYS = frozenset(
35
+ {
36
+ "schema_version",
37
+ "grain",
38
+ "time_overlap",
39
+ "required_fields",
40
+ "sources",
41
+ "rights_statuses",
42
+ "recipe",
43
+ "exact_digest",
44
+ }
45
+ )
46
+ _DIGEST_KINDS = frozenset(
47
+ {"facts", "candidate", "dataset", "requirements", "validation_policy", "lineage"}
48
+ )
49
+ _FILTER_ORDER = (
50
+ "grain",
51
+ "time_overlap",
52
+ "required_fields",
53
+ "sources",
54
+ "rights_statuses",
55
+ "recipe",
56
+ "exact_digest",
57
+ )
58
+
59
+
60
+ class _CanonicalFilter:
61
+ schema_version: ClassVar[str] | str
62
+
63
+ def to_dict(self) -> dict[str, Any]:
64
+ raise NotImplementedError
65
+
66
+ @property
67
+ def canonical_bytes(self) -> bytes:
68
+ return canonical_json_bytes(self.to_dict())
69
+
70
+ @property
71
+ def digest(self) -> str:
72
+ return canonical_sha256(self.to_dict())
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class SourceIdentityFilter(_CanonicalFilter):
77
+ """One exact source identity required to appear in the sealed Build facts."""
78
+
79
+ source_id: str
80
+ content_sha256: str
81
+
82
+ def __post_init__(self) -> None:
83
+ _identifier(self.source_id, "filters.sources.source_id")
84
+ _digest(self.content_sha256, "filters.sources.content_sha256")
85
+
86
+ def to_dict(self) -> dict[str, str]:
87
+ return {"source_id": self.source_id, "content_sha256": self.content_sha256}
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class RecipeIdentityFilter(_CanonicalFilter):
92
+ """The complete immutable recipe identity a reusable Build must carry."""
93
+
94
+ recipe_id: str
95
+ recipe_version: int
96
+ recipe_sha256: str
97
+
98
+ def __post_init__(self) -> None:
99
+ _identifier(self.recipe_id, "filters.recipe.recipe_id")
100
+ _integer(self.recipe_version, "filters.recipe.recipe_version", minimum=1)
101
+ _digest(self.recipe_sha256, "filters.recipe.recipe_sha256")
102
+
103
+ def to_dict(self) -> dict[str, Any]:
104
+ return {
105
+ "recipe_id": self.recipe_id,
106
+ "recipe_version": self.recipe_version,
107
+ "recipe_sha256": self.recipe_sha256,
108
+ }
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class ExactDigestFilter(_CanonicalFilter):
113
+ """One explicitly named immutable Build identity and its exact digest."""
114
+
115
+ kind: str
116
+ sha256: str
117
+
118
+ def __post_init__(self) -> None:
119
+ _choice(self.kind, _DIGEST_KINDS, "filters.exact_digest.kind")
120
+ _digest(self.sha256, "filters.exact_digest.sha256")
121
+
122
+ def to_dict(self) -> dict[str, str]:
123
+ return {"kind": self.kind, "sha256": self.sha256}
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class BuildFilters(_CanonicalFilter):
128
+ """The complete AND-only private Build filter grammar."""
129
+
130
+ grain: tuple[str, ...] | None = None
131
+ time_overlap: BuildTimeCoverage | None = None
132
+ required_fields: tuple[BuildField, ...] = ()
133
+ sources: tuple[SourceIdentityFilter, ...] = ()
134
+ rights_statuses: tuple[str, ...] = ()
135
+ recipe: RecipeIdentityFilter | None = None
136
+ exact_digest: ExactDigestFilter | None = None
137
+ schema_version: str = BUILD_FILTER_VERSION
138
+
139
+ def __post_init__(self) -> None:
140
+ if self.schema_version != BUILD_FILTER_VERSION:
141
+ raise LocalSearchContractError(
142
+ "LOCAL_SEARCH_FILTER_VERSION",
143
+ "filters.schema_version",
144
+ f"must equal {BUILD_FILTER_VERSION!r}",
145
+ )
146
+ if self.grain is not None:
147
+ _text_tuple(self.grain, "filters.grain", nonempty=True, ordered=False)
148
+ _optional_type(self.time_overlap, BuildTimeCoverage, "filters.time_overlap")
149
+ _typed_tuple(self.required_fields, BuildField, "filters.required_fields")
150
+ _typed_tuple(self.sources, SourceIdentityFilter, "filters.sources")
151
+ _text_tuple(self.rights_statuses, "filters.rights_statuses", nonempty=False, ordered=True)
152
+ for index, status in enumerate(self.rights_statuses):
153
+ _choice(status, _RIGHTS_STATUSES, f"filters.rights_statuses[{index}]")
154
+ _optional_type(self.recipe, RecipeIdentityFilter, "filters.recipe")
155
+ _optional_type(self.exact_digest, ExactDigestFilter, "filters.exact_digest")
156
+ _unique(tuple(item.name for item in self.required_fields), "filters.required_fields")
157
+ _unique(tuple(item.source_id for item in self.sources), "filters.sources")
158
+ _sorted_unique(
159
+ tuple((item.source_id, item.content_sha256) for item in self.sources),
160
+ "filters.sources",
161
+ )
162
+
163
+ def to_dict(self) -> dict[str, Any]:
164
+ payload: dict[str, Any] = {"schema_version": self.schema_version}
165
+ if self.grain is not None:
166
+ payload["grain"] = list(self.grain)
167
+ if self.time_overlap is not None:
168
+ payload["time_overlap"] = self.time_overlap.to_dict()
169
+ if self.required_fields:
170
+ payload["required_fields"] = [item.to_dict() for item in self.required_fields]
171
+ if self.sources:
172
+ payload["sources"] = [item.to_dict() for item in self.sources]
173
+ if self.rights_statuses:
174
+ payload["rights_statuses"] = list(self.rights_statuses)
175
+ if self.recipe is not None:
176
+ payload["recipe"] = self.recipe.to_dict()
177
+ if self.exact_digest is not None:
178
+ payload["exact_digest"] = self.exact_digest.to_dict()
179
+ return payload
180
+
181
+
182
+ @dataclass(frozen=True)
183
+ class BuildFilterCheck:
184
+ """One named fact predicate's bounded result."""
185
+
186
+ filter_name: str
187
+ outcome: str
188
+
189
+ def __post_init__(self) -> None:
190
+ _choice(self.filter_name, frozenset(_FILTER_ORDER), "filter_check.filter_name")
191
+ _choice(self.outcome, frozenset({"pass", "fail"}), "filter_check.outcome")
192
+
193
+ def to_dict(self) -> dict[str, str]:
194
+ return {"filter_name": self.filter_name, "outcome": self.outcome}
195
+
196
+
197
+ @dataclass(frozen=True)
198
+ class BuildFilterVerdict:
199
+ """Stable immutable identities plus the complete applied-filter disposition."""
200
+
201
+ facts_sha256: str
202
+ candidate_sha256: str
203
+ disposition: str
204
+ checks: tuple[BuildFilterCheck, ...]
205
+
206
+ def __post_init__(self) -> None:
207
+ _digest(self.facts_sha256, "filter_verdict.facts_sha256")
208
+ _digest(self.candidate_sha256, "filter_verdict.candidate_sha256")
209
+ _choice(
210
+ self.disposition,
211
+ frozenset({"eligible", "ineligible"}),
212
+ "filter_verdict.disposition",
213
+ )
214
+ _typed_tuple(self.checks, BuildFilterCheck, "filter_verdict.checks")
215
+ names = tuple(item.filter_name for item in self.checks)
216
+ expected = tuple(name for name in _FILTER_ORDER if name in names)
217
+ if names != expected or len(set(names)) != len(names):
218
+ raise LocalSearchContractError(
219
+ "LOCAL_SEARCH_FILTER_CHECKS",
220
+ "filter_verdict.checks",
221
+ "must be unique and in the closed filter order",
222
+ )
223
+ expected_disposition = (
224
+ "ineligible" if any(item.outcome == "fail" for item in self.checks) else "eligible"
225
+ )
226
+ if self.disposition != expected_disposition:
227
+ raise LocalSearchContractError(
228
+ "LOCAL_SEARCH_FILTER_DISPOSITION",
229
+ "filter_verdict.disposition",
230
+ "must be derived from the complete filter checks",
231
+ )
232
+
233
+ def to_dict(self) -> dict[str, Any]:
234
+ return {
235
+ "facts_sha256": self.facts_sha256,
236
+ "candidate_sha256": self.candidate_sha256,
237
+ "disposition": self.disposition,
238
+ "checks": [item.to_dict() for item in self.checks],
239
+ }
240
+
241
+
242
+ @dataclass(frozen=True)
243
+ class BuildRetrieval:
244
+ """Score-free result of fact gating followed by deterministic ordering."""
245
+
246
+ question_sha256: str
247
+ filters_sha256: str
248
+ backend_coordinate: str
249
+ filter_verdicts: tuple[BuildFilterVerdict, ...]
250
+ ordered_facts_sha256: tuple[str, ...]
251
+
252
+ def __post_init__(self) -> None:
253
+ _digest(self.question_sha256, "retrieval.question_sha256")
254
+ _digest(self.filters_sha256, "retrieval.filters_sha256")
255
+ _backend_coordinate(self.backend_coordinate)
256
+ _typed_tuple(
257
+ self.filter_verdicts,
258
+ BuildFilterVerdict,
259
+ "retrieval.filter_verdicts",
260
+ maximum=100_000,
261
+ )
262
+ verdict_ids = tuple(item.facts_sha256 for item in self.filter_verdicts)
263
+ if verdict_ids != tuple(sorted(verdict_ids)) or len(set(verdict_ids)) != len(verdict_ids):
264
+ raise LocalSearchContractError(
265
+ "LOCAL_SEARCH_RETRIEVAL_ORDER",
266
+ "retrieval.filter_verdicts",
267
+ "must carry unique facts identities in exact sorted order",
268
+ )
269
+ if not isinstance(self.ordered_facts_sha256, tuple):
270
+ raise LocalSearchContractError(
271
+ "LOCAL_SEARCH_TYPE",
272
+ "retrieval.ordered_facts_sha256",
273
+ "must be an immutable tuple",
274
+ )
275
+ if len(self.ordered_facts_sha256) > MAX_RETRIEVAL_LIMIT:
276
+ raise LocalSearchContractError(
277
+ "LOCAL_SEARCH_RETRIEVAL_LIMIT",
278
+ "retrieval.ordered_facts_sha256",
279
+ f"may contain at most {MAX_RETRIEVAL_LIMIT} identities",
280
+ )
281
+ for index, digest in enumerate(self.ordered_facts_sha256):
282
+ _digest(digest, f"retrieval.ordered_facts_sha256[{index}]")
283
+ if len(set(self.ordered_facts_sha256)) != len(self.ordered_facts_sha256):
284
+ raise LocalSearchContractError(
285
+ "LOCAL_SEARCH_DUPLICATE",
286
+ "retrieval.ordered_facts_sha256",
287
+ "facts identities must be unique",
288
+ )
289
+ eligible = {
290
+ item.facts_sha256 for item in self.filter_verdicts if item.disposition == "eligible"
291
+ }
292
+ if not set(self.ordered_facts_sha256) <= eligible:
293
+ raise LocalSearchContractError(
294
+ "LOCAL_SEARCH_RETRIEVAL_ELIGIBILITY",
295
+ "retrieval.ordered_facts_sha256",
296
+ "may contain only facts-eligible identities",
297
+ )
298
+
299
+ def to_dict(self) -> dict[str, Any]:
300
+ return {
301
+ "question_sha256": self.question_sha256,
302
+ "filters_sha256": self.filters_sha256,
303
+ "backend_coordinate": self.backend_coordinate,
304
+ "filter_verdicts": [item.to_dict() for item in self.filter_verdicts],
305
+ "ordered_facts_sha256": list(self.ordered_facts_sha256),
306
+ }
307
+
308
+
309
+ def parse_build_filters(raw: Any) -> BuildFilters:
310
+ """Parse exactly the versioned closed grammar; all filters combine with AND."""
311
+
312
+ payload = _mapping(raw, "filters")
313
+ unknown = sorted(set(payload) - _FILTER_KEYS)
314
+ if unknown:
315
+ raise LocalSearchContractError(
316
+ "LOCAL_SEARCH_FILTER_FIELDS",
317
+ "filters",
318
+ f"contains unknown fields: {unknown}",
319
+ )
320
+ if "schema_version" not in payload:
321
+ raise LocalSearchContractError(
322
+ "LOCAL_SEARCH_FILTER_FIELDS", "filters", "must name schema_version"
323
+ )
324
+ grain = None
325
+ if "grain" in payload:
326
+ grain = tuple(_array(payload["grain"], "filters.grain"))
327
+ time_overlap = None
328
+ if "time_overlap" in payload:
329
+ value = _mapping(payload["time_overlap"], "filters.time_overlap")
330
+ _exact(value, {"start_inclusive", "end_exclusive"}, "filters.time_overlap")
331
+ time_overlap = BuildTimeCoverage(value["start_inclusive"], value["end_exclusive"])
332
+ fields = tuple(
333
+ _field_filter(item, f"filters.required_fields[{index}]")
334
+ for index, item in enumerate(
335
+ _array(payload.get("required_fields", []), "filters.required_fields")
336
+ )
337
+ )
338
+ sources = tuple(
339
+ _source_filter(item, f"filters.sources[{index}]")
340
+ for index, item in enumerate(_array(payload.get("sources", []), "filters.sources"))
341
+ )
342
+ rights_statuses = tuple(_array(payload.get("rights_statuses", []), "filters.rights_statuses"))
343
+ recipe = None
344
+ if "recipe" in payload:
345
+ value = _mapping(payload["recipe"], "filters.recipe")
346
+ _exact(value, {"recipe_id", "recipe_version", "recipe_sha256"}, "filters.recipe")
347
+ recipe = RecipeIdentityFilter(
348
+ value["recipe_id"], value["recipe_version"], value["recipe_sha256"]
349
+ )
350
+ exact_digest = None
351
+ if "exact_digest" in payload:
352
+ value = _mapping(payload["exact_digest"], "filters.exact_digest")
353
+ _exact(value, {"kind", "sha256"}, "filters.exact_digest")
354
+ exact_digest = ExactDigestFilter(value["kind"], value["sha256"])
355
+ return BuildFilters(
356
+ schema_version=payload["schema_version"],
357
+ grain=grain,
358
+ time_overlap=time_overlap,
359
+ required_fields=fields,
360
+ sources=sources,
361
+ rights_statuses=rights_statuses,
362
+ recipe=recipe,
363
+ exact_digest=exact_digest,
364
+ )
365
+
366
+
367
+ def filter_build_entries(
368
+ entries: tuple[BuildIndexEntry, ...], *, filters: BuildFilters
369
+ ) -> tuple[BuildFilterVerdict, ...]:
370
+ """Evaluate every candidate from sealed facts and return identity-sorted verdicts."""
371
+
372
+ _typed_tuple(entries, BuildIndexEntry, "entries", maximum=100_000)
373
+ if not isinstance(filters, BuildFilters):
374
+ raise LocalSearchContractError(
375
+ "LOCAL_SEARCH_TYPE", "filters", "must be a BuildFilters contract"
376
+ )
377
+ if len({item.facts_sha256 for item in entries}) != len(entries):
378
+ raise LocalSearchContractError(
379
+ "LOCAL_SEARCH_DUPLICATE", "entries", "facts identities must be unique"
380
+ )
381
+ return tuple(_filter_one(entry, filters) for entry in sorted(entries, key=_entry_identity))
382
+
383
+
384
+ def retrieve_build_candidates(
385
+ entries: tuple[BuildIndexEntry, ...],
386
+ *,
387
+ filters: BuildFilters,
388
+ question: str,
389
+ backend: Any,
390
+ limit: int,
391
+ ) -> BuildRetrieval:
392
+ """Gate on sealed facts, then order only the eligible immutable facts identities.
393
+
394
+ The arithmetic remains in this function and dies before return. There is no threshold:
395
+ ``limit`` truncates the already eligible total order and cannot change a filter disposition.
396
+ Candidate text is encoded only after the fact gate passes, which also bounds neural work by
397
+ the number of eligible entries.
398
+ """
399
+
400
+ if not isinstance(question, str) or not 1 <= len(question) <= MAX_QUERY_CHARS:
401
+ raise LocalSearchContractError(
402
+ "LOCAL_SEARCH_QUERY_BOUNDS",
403
+ "question",
404
+ f"must contain 1 to {MAX_QUERY_CHARS} characters",
405
+ )
406
+ if type(limit) is not int or not 1 <= limit <= MAX_RETRIEVAL_LIMIT:
407
+ raise LocalSearchContractError(
408
+ "LOCAL_SEARCH_RETRIEVAL_LIMIT",
409
+ "limit",
410
+ f"must be an integer in [1, {MAX_RETRIEVAL_LIMIT}]",
411
+ )
412
+ descriptor = getattr(backend, "descriptor", None)
413
+ coordinate = getattr(descriptor, "coordinate", None)
414
+ dimensions = getattr(descriptor, "dimensions", None)
415
+ encode = getattr(backend, "encode", None)
416
+ _backend_coordinate(coordinate)
417
+ if type(dimensions) is not int or not 1 <= dimensions <= 4_096:
418
+ raise LocalSearchContractError(
419
+ "LOCAL_SEARCH_BACKEND",
420
+ "backend.descriptor.dimensions",
421
+ "must be an integer in [1, 4096]",
422
+ )
423
+ if not callable(encode):
424
+ raise LocalSearchContractError("LOCAL_SEARCH_BACKEND", "backend.encode", "must be callable")
425
+
426
+ verdicts = filter_build_entries(entries, filters=filters)
427
+ eligible_ids = {item.facts_sha256 for item in verdicts if item.disposition == "eligible"}
428
+ eligible_entries = tuple(
429
+ entry
430
+ for entry in sorted(entries, key=_entry_identity)
431
+ if entry.facts_sha256 in eligible_ids
432
+ )
433
+ ordered: list[tuple[int, str]] = []
434
+ if eligible_entries:
435
+ asked = _vector(encode(question), dimensions, "backend.query_vector")
436
+ for entry in eligible_entries:
437
+ candidate = _vector(
438
+ encode(entry.question),
439
+ dimensions,
440
+ f"backend.candidate_vector[{entry.facts_sha256}]",
441
+ )
442
+ private_similarity = sum(
443
+ left * right for left, right in zip(asked, candidate, strict=True)
444
+ )
445
+ ordered.append((-private_similarity, entry.facts_sha256))
446
+ ordered.sort()
447
+ return BuildRetrieval(
448
+ question_sha256=sha256_bytes(question.encode("utf-8")),
449
+ filters_sha256=filters.digest,
450
+ backend_coordinate=coordinate,
451
+ filter_verdicts=verdicts,
452
+ ordered_facts_sha256=tuple(identity for _private, identity in ordered[:limit]),
453
+ )
454
+
455
+
456
+ def _filter_one(entry: BuildIndexEntry, filters: BuildFilters) -> BuildFilterVerdict:
457
+ checks: list[BuildFilterCheck] = []
458
+ if filters.grain is not None:
459
+ checks.append(BuildFilterCheck("grain", _outcome(entry.grain == filters.grain)))
460
+ if filters.time_overlap is not None:
461
+ coverage = entry.time_coverage
462
+ matches = coverage is not None and (
463
+ coverage.start_inclusive < filters.time_overlap.end_exclusive
464
+ and filters.time_overlap.start_inclusive < coverage.end_exclusive
465
+ )
466
+ checks.append(BuildFilterCheck("time_overlap", _outcome(matches)))
467
+ if filters.required_fields:
468
+ present = {(field.name, field.logical_type) for field in entry.fields}
469
+ required = {(field.name, field.logical_type) for field in filters.required_fields}
470
+ checks.append(BuildFilterCheck("required_fields", _outcome(required <= present)))
471
+ if filters.sources:
472
+ present = {(source.source_id, source.content_sha256) for source in entry.sources}
473
+ required = {(source.source_id, source.content_sha256) for source in filters.sources}
474
+ checks.append(BuildFilterCheck("sources", _outcome(required <= present)))
475
+ if filters.rights_statuses:
476
+ allowed = set(filters.rights_statuses)
477
+ checks.append(
478
+ BuildFilterCheck(
479
+ "rights_statuses",
480
+ _outcome(
481
+ bool(entry.sources) and all(s.rights_status in allowed for s in entry.sources)
482
+ ),
483
+ )
484
+ )
485
+ if filters.recipe is not None:
486
+ lineage = entry.recipe_lineage
487
+ matches = lineage is not None and (
488
+ lineage.recipe_id == filters.recipe.recipe_id
489
+ and lineage.recipe_version == filters.recipe.recipe_version
490
+ and lineage.recipe_sha256 == filters.recipe.recipe_sha256
491
+ )
492
+ checks.append(BuildFilterCheck("recipe", _outcome(matches)))
493
+ if filters.exact_digest is not None:
494
+ observed = _entry_digest(entry, filters.exact_digest.kind)
495
+ checks.append(
496
+ BuildFilterCheck("exact_digest", _outcome(observed == filters.exact_digest.sha256))
497
+ )
498
+ disposition = "ineligible" if any(item.outcome == "fail" for item in checks) else "eligible"
499
+ return BuildFilterVerdict(
500
+ entry.facts_sha256, entry.candidate_sha256, disposition, tuple(checks)
501
+ )
502
+
503
+
504
+ def _entry_digest(entry: BuildIndexEntry, kind: str) -> str:
505
+ return {
506
+ "facts": entry.facts_sha256,
507
+ "candidate": entry.candidate_sha256,
508
+ "dataset": entry.table_sha256,
509
+ "requirements": entry.requirements_sha256,
510
+ "validation_policy": entry.validation_policy_sha256,
511
+ "lineage": entry.lineage_sha256,
512
+ }[kind]
513
+
514
+
515
+ def _entry_identity(entry: BuildIndexEntry) -> tuple[str, str]:
516
+ return entry.facts_sha256, entry.candidate_sha256
517
+
518
+
519
+ def _field_filter(value: Any, path: str) -> BuildField:
520
+ payload = _mapping(value, path)
521
+ _exact(payload, {"name", "logical_type"}, path)
522
+ return BuildField(payload["name"], payload["logical_type"])
523
+
524
+
525
+ def _source_filter(value: Any, path: str) -> SourceIdentityFilter:
526
+ payload = _mapping(value, path)
527
+ _exact(payload, {"source_id", "content_sha256"}, path)
528
+ return SourceIdentityFilter(payload["source_id"], payload["content_sha256"])
529
+
530
+
531
+ def _mapping(value: Any, path: str) -> Mapping[str, Any]:
532
+ if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value):
533
+ raise LocalSearchContractError(
534
+ "LOCAL_SEARCH_FILTER_SHAPE", path, "must be an object with string keys"
535
+ )
536
+ return value
537
+
538
+
539
+ def _array(value: Any, path: str) -> list[Any]:
540
+ if isinstance(value, (str, bytes)) or not isinstance(value, Sequence):
541
+ raise LocalSearchContractError("LOCAL_SEARCH_FILTER_SHAPE", path, "must be an array")
542
+ if len(value) > MAX_FILTER_ITEMS:
543
+ raise LocalSearchContractError(
544
+ "LOCAL_SEARCH_FILTER_BOUNDS",
545
+ path,
546
+ f"may contain at most {MAX_FILTER_ITEMS} items",
547
+ )
548
+ return list(value)
549
+
550
+
551
+ def _exact(value: Mapping[str, Any], expected: set[str], path: str) -> None:
552
+ if set(value) != expected:
553
+ raise LocalSearchContractError(
554
+ "LOCAL_SEARCH_FILTER_FIELDS",
555
+ path,
556
+ f"must contain exactly {sorted(expected)}",
557
+ )
558
+
559
+
560
+ def _identifier(value: Any, path: str) -> None:
561
+ if (
562
+ not isinstance(value, str)
563
+ or len(value) > MAX_FILTER_TEXT
564
+ or _IDENTIFIER.fullmatch(value) is None
565
+ ):
566
+ raise LocalSearchContractError(
567
+ "LOCAL_SEARCH_FILTER_TEXT", path, "must be a bounded identifier"
568
+ )
569
+
570
+
571
+ def _digest(value: Any, path: str) -> None:
572
+ if not isinstance(value, str) or _DIGEST.fullmatch(value) is None:
573
+ raise LocalSearchContractError(
574
+ "LOCAL_SEARCH_FILTER_DIGEST", path, "must be a lowercase SHA-256"
575
+ )
576
+
577
+
578
+ def _integer(value: Any, path: str, *, minimum: int) -> None:
579
+ if type(value) is not int or not minimum <= value <= (1 << 53) - 1:
580
+ raise LocalSearchContractError(
581
+ "LOCAL_SEARCH_FILTER_INTEGER", path, f"must be an integer of at least {minimum}"
582
+ )
583
+
584
+
585
+ def _choice(value: Any, choices: frozenset[str], path: str) -> None:
586
+ if not isinstance(value, str) or value not in choices:
587
+ raise LocalSearchContractError(
588
+ "LOCAL_SEARCH_FILTER_CHOICE", path, f"must be one of {sorted(choices)}"
589
+ )
590
+
591
+
592
+ def _optional_type(value: Any, expected: type[Any], path: str) -> None:
593
+ if value is not None and not isinstance(value, expected):
594
+ raise LocalSearchContractError(
595
+ "LOCAL_SEARCH_TYPE", path, f"must be {expected.__name__} or null"
596
+ )
597
+
598
+
599
+ def _typed_tuple(
600
+ value: Any,
601
+ expected: type[Any],
602
+ path: str,
603
+ maximum: int = MAX_FILTER_ITEMS,
604
+ ) -> None:
605
+ if not isinstance(value, tuple):
606
+ raise LocalSearchContractError("LOCAL_SEARCH_TYPE", path, "must be an immutable tuple")
607
+ if len(value) > maximum:
608
+ raise LocalSearchContractError(
609
+ "LOCAL_SEARCH_FILTER_BOUNDS", path, f"may contain at most {maximum} items"
610
+ )
611
+ if any(not isinstance(item, expected) for item in value):
612
+ raise LocalSearchContractError(
613
+ "LOCAL_SEARCH_TYPE", path, f"items must be {expected.__name__}"
614
+ )
615
+
616
+
617
+ def _text_tuple(value: Any, path: str, *, nonempty: bool, ordered: bool) -> None:
618
+ if not isinstance(value, tuple):
619
+ raise LocalSearchContractError("LOCAL_SEARCH_TYPE", path, "must be an immutable tuple")
620
+ if (nonempty and not value) or len(value) > MAX_FILTER_ITEMS:
621
+ raise LocalSearchContractError(
622
+ "LOCAL_SEARCH_FILTER_BOUNDS", path, f"must contain 1 to {MAX_FILTER_ITEMS} items"
623
+ )
624
+ for index, item in enumerate(value):
625
+ if not isinstance(item, str) or not item or len(item) > MAX_FILTER_TEXT:
626
+ raise LocalSearchContractError(
627
+ "LOCAL_SEARCH_FILTER_TEXT",
628
+ f"{path}[{index}]",
629
+ f"must be non-empty text of at most {MAX_FILTER_TEXT} characters",
630
+ )
631
+ _unique(value, path)
632
+ if ordered and tuple(sorted(value)) != value:
633
+ raise LocalSearchContractError(
634
+ "LOCAL_SEARCH_FILTER_ORDER", path, "must be sorted in exact text order"
635
+ )
636
+
637
+
638
+ def _unique(value: tuple[Any, ...], path: str) -> None:
639
+ if len(set(value)) != len(value):
640
+ raise LocalSearchContractError(
641
+ "LOCAL_SEARCH_FILTER_DUPLICATE", path, "items must be unique"
642
+ )
643
+
644
+
645
+ def _sorted_unique(value: tuple[Any, ...], path: str) -> None:
646
+ _unique(value, path)
647
+ if tuple(sorted(value)) != value:
648
+ raise LocalSearchContractError(
649
+ "LOCAL_SEARCH_FILTER_ORDER", path, "must be sorted in exact identity order"
650
+ )
651
+
652
+
653
+ def _outcome(value: bool) -> str:
654
+ return "pass" if value else "fail"
655
+
656
+
657
+ def _backend_coordinate(value: Any) -> None:
658
+ if (
659
+ not isinstance(value, str)
660
+ or not 1 <= len(value) <= 1_024
661
+ or "@" not in value
662
+ or any(ord(character) < 32 or character in "\x7f\u0085\u2028\u2029" for character in value)
663
+ ):
664
+ raise LocalSearchContractError(
665
+ "LOCAL_SEARCH_BACKEND",
666
+ "backend.descriptor.coordinate",
667
+ "must be the bounded exact one-line backend coordinate",
668
+ )
669
+
670
+
671
+ def _vector(value: Any, dimensions: int, path: str) -> tuple[int, ...]:
672
+ if (
673
+ not isinstance(value, tuple)
674
+ or len(value) != dimensions
675
+ or any(type(item) is not int or not -(1 << 31) <= item <= (1 << 31) - 1 for item in value)
676
+ ):
677
+ raise LocalSearchContractError(
678
+ "LOCAL_SEARCH_VECTOR",
679
+ path,
680
+ "must be an immutable signed-int32 vector matching the backend dimensions",
681
+ )
682
+ return value
683
+
684
+
685
+ __all__ = [
686
+ "BUILD_FILTER_VERSION",
687
+ "MAX_FILTER_ITEMS",
688
+ "MAX_QUERY_CHARS",
689
+ "MAX_RETRIEVAL_LIMIT",
690
+ "BuildFilterCheck",
691
+ "BuildFilterVerdict",
692
+ "BuildFilters",
693
+ "BuildRetrieval",
694
+ "ExactDigestFilter",
695
+ "RecipeIdentityFilter",
696
+ "SourceIdentityFilter",
697
+ "filter_build_entries",
698
+ "parse_build_filters",
699
+ "retrieve_build_candidates",
700
+ ]