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,1087 @@
1
+ """Build the private local index from bounded, exact-verified sealed Builds.
2
+
3
+ Discovery and verification are deliberately separate operations. Discovery retains the supplied
4
+ root descriptor, follows no names, and returns only root-relative candidate coordinates plus
5
+ sanitized diagnostics. It never reads a dataset member. Publication later opens each coordinate
6
+ through the existing exact verifier and writes only beneath the explicit index root.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ctypes
12
+ import errno
13
+ import heapq
14
+ import json
15
+ import os
16
+ import stat
17
+ import sys
18
+ import time
19
+ import uuid
20
+ from collections.abc import Callable, Mapping
21
+ from dataclasses import dataclass
22
+ from pathlib import Path, PurePosixPath
23
+ from typing import Any
24
+
25
+ from mostlyright.data_harness import pipeline
26
+ from mostlyright.data_harness.canonical import canonical_json_bytes, canonical_sha256, sha256_bytes
27
+ from mostlyright.data_harness.local_search.contracts import (
28
+ BuildDiagnosticDocument,
29
+ BuildField,
30
+ BuildIndexDiagnostic,
31
+ BuildIndexEntry,
32
+ BuildIndexManifest,
33
+ BuildLocator,
34
+ BuildLocatorDocument,
35
+ BuildRecipeLineage,
36
+ BuildSourceFacts,
37
+ BuildTimeCoverage,
38
+ LocalSearchContractError,
39
+ SealedProfileFactsV1,
40
+ SnapshotMember,
41
+ manifest_aggregate_sha256,
42
+ )
43
+ from mostlyright.data_harness.local_search.sealed import capture_build_index_snapshot
44
+
45
+ _OPEN_DIRECTORY = (
46
+ os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
47
+ )
48
+ _RENAME_ARGTYPES = [
49
+ ctypes.c_int,
50
+ ctypes.c_char_p,
51
+ ctypes.c_int,
52
+ ctypes.c_char_p,
53
+ ctypes.c_uint,
54
+ ]
55
+
56
+
57
+ class LocalSearchIndexError(RuntimeError):
58
+ """A stable private-index refusal without raw filesystem error text."""
59
+
60
+ def __init__(self, code: str, path: str, detail: str) -> None:
61
+ self.code = code
62
+ self.path = path
63
+ self.detail = detail
64
+ super().__init__(f"{path}: {detail} [{code}]")
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class BuildDiscoveryLimits:
69
+ """Aggregate ceilings for one discovery and indexing operation."""
70
+
71
+ max_depth: int = 6
72
+ max_entries: int = 100_000
73
+ max_builds: int = 10_000
74
+ max_receipt_bytes: int = 2 * 1024 * 1024
75
+ max_member_bytes: int = pipeline.MAX_CANDIDATE_MEMBER_BYTES
76
+ max_aggregate_bytes: int = pipeline.MAX_CANDIDATE_BYTES
77
+ max_elapsed_seconds: float = 60.0
78
+ max_diagnostics: int = 100_000
79
+
80
+ def __post_init__(self) -> None:
81
+ integer_bounds = (
82
+ ("max_depth", self.max_depth, 32),
83
+ ("max_entries", self.max_entries, 1_000_000),
84
+ ("max_builds", self.max_builds, 100_000),
85
+ ("max_receipt_bytes", self.max_receipt_bytes, 64 * 1024 * 1024),
86
+ # Bounded by the candidate ceilings in ``pipeline``: a member may be one
87
+ # year-scale normalized source and the aggregate may hold the sources a
88
+ # graph execution retains, so the search index admits what a build seals.
89
+ ("max_member_bytes", self.max_member_bytes, pipeline.MAX_CANDIDATE_MEMBER_BYTES),
90
+ ("max_aggregate_bytes", self.max_aggregate_bytes, pipeline.MAX_CANDIDATE_BYTES),
91
+ ("max_diagnostics", self.max_diagnostics, 100_000),
92
+ )
93
+ for name, value, maximum in integer_bounds:
94
+ if type(value) is not int or not 1 <= value <= maximum:
95
+ raise LocalSearchIndexError(
96
+ "BUILD_LIMIT", f"limits.{name}", f"must be an integer in [1, {maximum}]"
97
+ )
98
+ if (
99
+ isinstance(self.max_elapsed_seconds, bool)
100
+ or not isinstance(self.max_elapsed_seconds, (int, float))
101
+ or not 0 < self.max_elapsed_seconds <= 3600
102
+ ):
103
+ raise LocalSearchIndexError(
104
+ "BUILD_LIMIT",
105
+ "limits.max_elapsed_seconds",
106
+ "must be a number in (0, 3600]",
107
+ )
108
+ if self.max_receipt_bytes > self.max_member_bytes:
109
+ raise LocalSearchIndexError(
110
+ "BUILD_LIMIT",
111
+ "limits.max_receipt_bytes",
112
+ "cannot exceed max_member_bytes",
113
+ )
114
+ if self.max_member_bytes > self.max_aggregate_bytes:
115
+ raise LocalSearchIndexError(
116
+ "BUILD_LIMIT",
117
+ "limits.max_member_bytes",
118
+ "cannot exceed max_aggregate_bytes",
119
+ )
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class BuildDiscovery:
124
+ """Deterministic candidate coordinates and bounded diagnostics from one root."""
125
+
126
+ candidates: tuple[str, ...]
127
+ diagnostics: tuple[BuildIndexDiagnostic, ...]
128
+ entries_seen: int
129
+ builds_seen: int
130
+
131
+
132
+ @dataclass(frozen=True)
133
+ class PublishedBuildIndex:
134
+ """One completely published private index successor."""
135
+
136
+ index_root: Path
137
+ manifest: BuildIndexManifest
138
+ manifest_sha256: str
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class _RootHandle:
143
+ descriptor: int
144
+ identity: tuple[int, int]
145
+
146
+
147
+ @dataclass
148
+ class _WalkState:
149
+ entries_seen: int = 0
150
+ builds_seen: int = 0
151
+ stopped: bool = False
152
+
153
+
154
+ @dataclass(frozen=True)
155
+ class _DescendingName:
156
+ """Heap item whose smallest value is the lexicographically greatest retained name."""
157
+
158
+ name: str
159
+
160
+ def __lt__(self, other: _DescendingName) -> bool:
161
+ return self.name > other.name
162
+
163
+
164
+ def discover_build_candidates(
165
+ root: Path,
166
+ *,
167
+ limits: BuildDiscoveryLimits | None = None,
168
+ ) -> BuildDiscovery:
169
+ """Find possible sealed Builds below one retained, no-follow directory root.
170
+
171
+ A candidate is only a coordinate whose ``candidate/manifest.json`` entry is a bounded,
172
+ single-link regular file. No receipt bytes or dataset rows are read here. Exact admission is
173
+ exclusively the job of :func:`pipeline.open_verified_snapshot` during publication.
174
+ """
175
+
176
+ if not isinstance(root, Path):
177
+ raise LocalSearchIndexError("BUILD_INVALID", "root", "must be a pathlib.Path")
178
+ selected = limits or BuildDiscoveryLimits()
179
+ if not isinstance(selected, BuildDiscoveryLimits):
180
+ raise LocalSearchIndexError("BUILD_LIMIT", "limits", "must be BuildDiscoveryLimits")
181
+
182
+ opened = _open_root(root)
183
+ try:
184
+ discovered = _discover_from_open_root(opened, selected, started=time.monotonic())
185
+ _validate_root_binding(root, opened)
186
+ return discovered
187
+ finally:
188
+ os.close(opened.descriptor)
189
+
190
+
191
+ def _discover_from_open_root(
192
+ opened: _RootHandle,
193
+ limits: BuildDiscoveryLimits,
194
+ *,
195
+ started: float,
196
+ ) -> BuildDiscovery:
197
+ state = _WalkState()
198
+ candidates: list[str] = []
199
+ diagnostics: list[BuildIndexDiagnostic] = []
200
+ pending: list[tuple[tuple[str, ...], int]] = [((), 0)]
201
+ while pending and not state.stopped:
202
+ relative, depth = heapq.heappop(pending)
203
+ if time.monotonic() - started > limits.max_elapsed_seconds:
204
+ _diagnose(
205
+ diagnostics,
206
+ limits,
207
+ "BUILD_LIMIT",
208
+ _diagnostic_path(relative),
209
+ )
210
+ break
211
+ try:
212
+ directory = _open_directory_beneath(opened.descriptor, relative)
213
+ except OSError:
214
+ _diagnose(
215
+ diagnostics,
216
+ limits,
217
+ "BUILD_UNREADABLE",
218
+ _diagnostic_path(relative),
219
+ )
220
+ continue
221
+ try:
222
+ candidate = _candidate_disposition(directory, relative, limits)
223
+ if candidate is not None:
224
+ disposition, candidate_entries = candidate
225
+ if state.entries_seen + candidate_entries > limits.max_entries:
226
+ state.stopped = True
227
+ _diagnose(
228
+ diagnostics,
229
+ limits,
230
+ "BUILD_LIMIT",
231
+ _diagnostic_path(relative),
232
+ )
233
+ continue
234
+ state.entries_seen += candidate_entries
235
+ if disposition != "BUILD_CANDIDATE":
236
+ _diagnose(
237
+ diagnostics,
238
+ limits,
239
+ disposition,
240
+ _diagnostic_path(relative),
241
+ )
242
+ continue
243
+ if state.builds_seen >= limits.max_builds:
244
+ _diagnose(
245
+ diagnostics,
246
+ limits,
247
+ "BUILD_LIMIT",
248
+ _diagnostic_path(relative),
249
+ )
250
+ state.stopped = True
251
+ continue
252
+ state.builds_seen += 1
253
+ candidates.append(_relative_path(relative))
254
+ continue
255
+
256
+ remaining_entries = limits.max_entries - state.entries_seen
257
+ children, incomplete, first_excluded = _directory_children(
258
+ directory, maximum=remaining_entries
259
+ )
260
+ for name, kind in children:
261
+ _consume_entry(state, limits)
262
+ if kind != "directory":
263
+ if kind != "regular":
264
+ _diagnose(diagnostics, limits, kind, _join(relative, name))
265
+ continue
266
+ child_relative = (*relative, name)
267
+ if depth >= limits.max_depth:
268
+ _diagnose(
269
+ diagnostics,
270
+ limits,
271
+ "BUILD_LIMIT",
272
+ _relative_path(child_relative),
273
+ )
274
+ else:
275
+ heapq.heappush(pending, (child_relative, depth + 1))
276
+ if first_excluded is not None:
277
+ state.stopped = True
278
+ _diagnose(
279
+ diagnostics,
280
+ limits,
281
+ "BUILD_LIMIT",
282
+ _join(relative, first_excluded),
283
+ )
284
+ if incomplete:
285
+ _diagnose(
286
+ diagnostics,
287
+ limits,
288
+ "BUILD_UNREADABLE",
289
+ _diagnostic_path(relative),
290
+ )
291
+ finally:
292
+ os.close(directory)
293
+ return BuildDiscovery(
294
+ candidates=tuple(sorted(candidates)),
295
+ diagnostics=tuple(
296
+ sorted(set(diagnostics), key=lambda item: (item.relative_path, item.code))
297
+ ),
298
+ entries_seen=state.entries_seen,
299
+ builds_seen=state.builds_seen,
300
+ )
301
+
302
+
303
+ def _validate_root_binding(root: Path, opened: _RootHandle) -> None:
304
+ after = _open_root(root)
305
+ try:
306
+ if after.identity != opened.identity:
307
+ raise LocalSearchIndexError(
308
+ "BUILD_RACE", "root", "changed while Build discovery was in progress"
309
+ )
310
+ finally:
311
+ os.close(after.descriptor)
312
+
313
+
314
+ def _open_root(root: Path) -> _RootHandle:
315
+ try:
316
+ named = os.lstat(root)
317
+ except OSError as error:
318
+ raise LocalSearchIndexError(
319
+ "BUILD_UNREADABLE", "root", "cannot inspect the discovery root"
320
+ ) from error
321
+ if stat.S_ISLNK(named.st_mode):
322
+ raise LocalSearchIndexError(
323
+ "BUILD_SYMLINK", "root", "the discovery root must not be a link"
324
+ )
325
+ try:
326
+ descriptor = os.open(root, _OPEN_DIRECTORY)
327
+ except OSError as error:
328
+ raise LocalSearchIndexError(
329
+ "BUILD_UNREADABLE", "root", "cannot open a retained no-follow directory"
330
+ ) from error
331
+ try:
332
+ opened = os.fstat(descriptor)
333
+ if not stat.S_ISDIR(named.st_mode) or not stat.S_ISDIR(opened.st_mode):
334
+ raise LocalSearchIndexError(
335
+ "BUILD_SPECIAL", "root", "the discovery root must be a directory"
336
+ )
337
+ identity = (opened.st_dev, opened.st_ino)
338
+ if identity != (named.st_dev, named.st_ino):
339
+ raise LocalSearchIndexError(
340
+ "BUILD_RACE", "root", "named and opened discovery roots differ"
341
+ )
342
+ return _RootHandle(descriptor, identity)
343
+ except BaseException:
344
+ os.close(descriptor)
345
+ raise
346
+
347
+
348
+ def _open_directory_beneath(root_fd: int, relative: tuple[str, ...]) -> int:
349
+ descriptor = os.dup(root_fd)
350
+ try:
351
+ for name in relative:
352
+ named = os.stat(name, dir_fd=descriptor, follow_symlinks=False)
353
+ child = os.open(name, _OPEN_DIRECTORY, dir_fd=descriptor)
354
+ opened = os.fstat(child)
355
+ if (
356
+ not stat.S_ISDIR(named.st_mode)
357
+ or not stat.S_ISDIR(opened.st_mode)
358
+ or (named.st_dev, named.st_ino) != (opened.st_dev, opened.st_ino)
359
+ ):
360
+ os.close(child)
361
+ raise OSError(errno.ESTALE, "descendant directory changed")
362
+ os.close(descriptor)
363
+ descriptor = child
364
+ return descriptor
365
+ except BaseException:
366
+ os.close(descriptor)
367
+ raise
368
+
369
+
370
+ def _candidate_disposition(
371
+ directory_fd: int,
372
+ relative: tuple[str, ...],
373
+ limits: BuildDiscoveryLimits,
374
+ ) -> tuple[str, int] | None:
375
+ try:
376
+ candidate = os.stat("candidate", dir_fd=directory_fd, follow_symlinks=False)
377
+ except FileNotFoundError:
378
+ return None
379
+ except OSError:
380
+ return "BUILD_UNREADABLE", 1
381
+ if stat.S_ISLNK(candidate.st_mode):
382
+ return "BUILD_SYMLINK", 1
383
+ if not stat.S_ISDIR(candidate.st_mode):
384
+ return "BUILD_SPECIAL", 1
385
+
386
+ candidate_fd = -1
387
+ try:
388
+ candidate_fd = os.open("candidate", _OPEN_DIRECTORY, dir_fd=directory_fd)
389
+ opened = os.fstat(candidate_fd)
390
+ if (candidate.st_dev, candidate.st_ino) != (opened.st_dev, opened.st_ino):
391
+ return "BUILD_RACE", 1
392
+ try:
393
+ receipt = os.stat("manifest.json", dir_fd=candidate_fd, follow_symlinks=False)
394
+ except FileNotFoundError:
395
+ return "BUILD_INVALID", 1
396
+ except OSError:
397
+ return "BUILD_UNREADABLE", 2
398
+ if stat.S_ISLNK(receipt.st_mode):
399
+ return "BUILD_SYMLINK", 2
400
+ if not stat.S_ISREG(receipt.st_mode):
401
+ return "BUILD_SPECIAL", 2
402
+ if receipt.st_nlink != 1:
403
+ return "BUILD_HARDLINK", 2
404
+ if receipt.st_size > limits.max_receipt_bytes:
405
+ return "BUILD_LIMIT", 2
406
+ if not relative:
407
+ return "BUILD_INVALID", 2
408
+ return "BUILD_CANDIDATE", 2
409
+ except OSError:
410
+ return "BUILD_RACE", 1
411
+ finally:
412
+ if candidate_fd >= 0:
413
+ os.close(candidate_fd)
414
+
415
+
416
+ def _directory_children(
417
+ directory_fd: int, *, maximum: int
418
+ ) -> tuple[list[tuple[str, str]], bool, str | None]:
419
+ """Return only the lexically first bounded child set from an incremental enumeration."""
420
+
421
+ retained: list[_DescendingName] = []
422
+ incomplete = False
423
+ try:
424
+ with os.scandir(directory_fd) as entries:
425
+ iterator = iter(entries)
426
+ while True:
427
+ try:
428
+ entry = next(iterator)
429
+ except StopIteration:
430
+ break
431
+ except OSError:
432
+ incomplete = True
433
+ break
434
+ if entry.name == "candidate":
435
+ continue
436
+ item = _DescendingName(entry.name)
437
+ if len(retained) < maximum + 1:
438
+ heapq.heappush(retained, item)
439
+ elif entry.name < retained[0].name:
440
+ heapq.heapreplace(retained, item)
441
+ except OSError:
442
+ incomplete = True
443
+
444
+ selected_names = sorted(item.name for item in retained)
445
+ first_excluded = selected_names[maximum] if len(selected_names) > maximum else None
446
+ children: list[tuple[str, str]] = []
447
+ for name in selected_names[:maximum]:
448
+ try:
449
+ observed = os.stat(name, dir_fd=directory_fd, follow_symlinks=False)
450
+ except OSError:
451
+ children.append((name, "BUILD_UNREADABLE"))
452
+ continue
453
+ if stat.S_ISLNK(observed.st_mode):
454
+ children.append((name, "BUILD_SYMLINK"))
455
+ elif stat.S_ISDIR(observed.st_mode):
456
+ children.append((name, "directory"))
457
+ elif not stat.S_ISREG(observed.st_mode):
458
+ children.append((name, "BUILD_SPECIAL"))
459
+ else:
460
+ children.append((name, "regular"))
461
+ return children, incomplete, first_excluded
462
+
463
+
464
+ def _consume_entry(state: _WalkState, limits: BuildDiscoveryLimits) -> bool:
465
+ if state.entries_seen >= limits.max_entries:
466
+ state.stopped = True
467
+ return False
468
+ state.entries_seen += 1
469
+ return True
470
+
471
+
472
+ def _diagnose(
473
+ diagnostics: list[BuildIndexDiagnostic],
474
+ limits: BuildDiscoveryLimits,
475
+ code: str,
476
+ relative_path: str,
477
+ ) -> None:
478
+ if len(diagnostics) >= limits.max_diagnostics:
479
+ return
480
+ diagnostics.append(BuildIndexDiagnostic(code, relative_path))
481
+
482
+
483
+ def _diagnostic_path(relative: tuple[str, ...]) -> str:
484
+ return _relative_path(relative) if relative else "root"
485
+
486
+
487
+ def _join(relative: tuple[str, ...], name: str) -> str:
488
+ return _relative_path((*relative, name))
489
+
490
+
491
+ def _relative_path(relative: tuple[str, ...]) -> str:
492
+ return PurePosixPath(*relative).as_posix()
493
+
494
+
495
+ def build_private_index(
496
+ build_root: Path,
497
+ index_root: Path,
498
+ *,
499
+ limits: BuildDiscoveryLimits | None = None,
500
+ fault_hook: Callable[[str], None] | None = None,
501
+ ) -> PublishedBuildIndex:
502
+ """Verify local Builds once and atomically publish a complete private index successor."""
503
+
504
+ if not isinstance(build_root, Path) or not isinstance(index_root, Path):
505
+ raise LocalSearchIndexError(
506
+ "BUILD_INVALID", "root", "Build and index roots must be pathlib.Path values"
507
+ )
508
+ selected = limits or BuildDiscoveryLimits()
509
+ if not isinstance(selected, BuildDiscoveryLimits):
510
+ raise LocalSearchIndexError("BUILD_LIMIT", "limits", "must be BuildDiscoveryLimits")
511
+ if fault_hook is not None and not callable(fault_hook):
512
+ raise LocalSearchIndexError("BUILD_INVALID", "fault_hook", "must be callable")
513
+
514
+ build_root = Path(os.path.abspath(os.fspath(build_root)))
515
+ index_root = Path(os.path.abspath(os.fspath(index_root)))
516
+ try:
517
+ common = Path(os.path.commonpath((build_root, index_root)))
518
+ except ValueError as error:
519
+ raise LocalSearchIndexError(
520
+ "BUILD_INVALID", "index_root", "must share a local filesystem coordinate"
521
+ ) from error
522
+ if common == build_root:
523
+ raise LocalSearchIndexError(
524
+ "BUILD_INVALID", "index_root", "must be outside the Build discovery root"
525
+ )
526
+
527
+ opened = _open_root(build_root)
528
+ started = time.monotonic()
529
+ try:
530
+ discovered = _discover_from_open_root(opened, selected, started=started)
531
+ diagnostics = list(discovered.diagnostics)
532
+ entries_by_digest: dict[str, BuildIndexEntry] = {}
533
+ locators: list[BuildLocator] = []
534
+ aggregate_bytes = 0
535
+
536
+ for relative_path in discovered.candidates:
537
+ if time.monotonic() - started > selected.max_elapsed_seconds:
538
+ _diagnose(diagnostics, selected, "BUILD_LIMIT", relative_path)
539
+ break
540
+ relative = tuple(PurePosixPath(relative_path).parts)
541
+ try:
542
+ run_fd = _open_directory_beneath(opened.descriptor, relative)
543
+ except OSError:
544
+ _diagnose(diagnostics, selected, "BUILD_RACE", relative_path)
545
+ continue
546
+ try:
547
+ remaining_aggregate_bytes = selected.max_aggregate_bytes - aggregate_bytes
548
+ if remaining_aggregate_bytes < 1:
549
+ _diagnose(diagnostics, selected, "BUILD_LIMIT", relative_path)
550
+ continue
551
+ with pipeline.open_verified_snapshot(
552
+ build_root.joinpath(*relative),
553
+ _run_fd=run_fd,
554
+ _ancestor_validator=lambda: _validate_retained_root(build_root, opened),
555
+ _max_manifest_bytes=min(selected.max_receipt_bytes, remaining_aggregate_bytes),
556
+ _max_member_bytes=min(selected.max_member_bytes, remaining_aggregate_bytes),
557
+ _max_aggregate_bytes=remaining_aggregate_bytes,
558
+ ) as snapshot:
559
+ verified = snapshot.verified
560
+ manifest = verified.manifest
561
+ member_sizes = _verified_member_sizes(manifest)
562
+ if any(size > selected.max_member_bytes for size in member_sizes):
563
+ _diagnose(diagnostics, selected, "BUILD_LIMIT", relative_path)
564
+ continue
565
+ build_bytes = sum(member_sizes) + len(
566
+ pipeline.canonical_json_line_bytes(manifest)
567
+ )
568
+ if aggregate_bytes + build_bytes > selected.max_aggregate_bytes:
569
+ _diagnose(diagnostics, selected, "BUILD_LIMIT", relative_path)
570
+ continue
571
+ entry = _entry_from_verified_snapshot(snapshot)
572
+ if time.monotonic() - started > selected.max_elapsed_seconds:
573
+ _diagnose(diagnostics, selected, "BUILD_LIMIT", relative_path)
574
+ continue
575
+ before_digest = verified.candidate_digest
576
+ snapshot.validate()
577
+ after_digest = snapshot.verified.candidate_digest
578
+ if after_digest != before_digest or entry.candidate_sha256 != before_digest:
579
+ _diagnose(diagnostics, selected, "BUILD_RACE", relative_path)
580
+ continue
581
+ aggregate_bytes += build_bytes
582
+ entries_by_digest.setdefault(entry.facts_sha256, entry)
583
+ locators.append(BuildLocator(entry.facts_sha256, relative_path))
584
+ except pipeline.BuildError as error:
585
+ _diagnose(
586
+ diagnostics,
587
+ selected,
588
+ _diagnostic_for_build_error(error),
589
+ relative_path,
590
+ )
591
+ except LocalSearchContractError:
592
+ _diagnose(diagnostics, selected, "BUILD_INVALID", relative_path)
593
+ finally:
594
+ os.close(run_fd)
595
+
596
+ _validate_retained_root(build_root, opened)
597
+ finally:
598
+ os.close(opened.descriptor)
599
+
600
+ ordered_entries = tuple(entries_by_digest[key] for key in sorted(entries_by_digest))
601
+ locator_document = BuildLocatorDocument(
602
+ tuple(sorted(set(locators), key=lambda item: (item.facts_sha256, item.relative_path)))
603
+ )
604
+ diagnostic_document = BuildDiagnosticDocument(
605
+ tuple(sorted(set(diagnostics), key=lambda item: (item.relative_path, item.code)))
606
+ )
607
+ return _publish_index(
608
+ index_root,
609
+ ordered_entries,
610
+ locator_document,
611
+ diagnostic_document,
612
+ fault_hook=fault_hook,
613
+ )
614
+
615
+
616
+ def _validate_retained_root(root: Path, opened: _RootHandle) -> None:
617
+ observed = os.fstat(opened.descriptor)
618
+ if (observed.st_dev, observed.st_ino) != opened.identity:
619
+ raise LocalSearchIndexError("BUILD_RACE", "root", "retained Build root changed")
620
+ _validate_root_binding(root, opened)
621
+
622
+
623
+ def _verified_member_sizes(manifest: Mapping[str, Any]) -> tuple[int, ...]:
624
+ members = manifest.get("members")
625
+ if not isinstance(members, list):
626
+ raise pipeline.BuildError("MANIFEST_INVALID", "candidate members are invalid")
627
+ sizes: list[int] = []
628
+ for member in members:
629
+ if not isinstance(member, Mapping) or type(member.get("bytes")) is not int:
630
+ raise pipeline.BuildError("MANIFEST_INVALID", "candidate member size is invalid")
631
+ sizes.append(member["bytes"])
632
+ return tuple(sizes)
633
+
634
+
635
+ def _entry_from_verified_snapshot(snapshot: pipeline.VerifiedSnapshot) -> BuildIndexEntry:
636
+ """Derive every index field from the one retained verified observation."""
637
+
638
+ verified = snapshot.verified
639
+ inspection = snapshot.inspection()
640
+ profile_raw = snapshot.member_bytes("evidence/profile.json")
641
+ profile = _json_object(profile_raw, "evidence/profile.json")
642
+ lineage = _json_object(snapshot.member_bytes("evidence/lineage.json"), "evidence/lineage.json")
643
+ sources_value = _json_value(
644
+ snapshot.member_bytes("evidence/sources.json"), "evidence/sources.json"
645
+ )
646
+ if not isinstance(sources_value, list):
647
+ raise pipeline.BuildError("CANDIDATE_EVIDENCE_SCHEMA", "sources evidence is invalid")
648
+
649
+ columns = profile.get("columns")
650
+ types = profile.get("types")
651
+ null_counts = profile.get("null_counts")
652
+ row_count = profile.get("row_count")
653
+ if (
654
+ profile.get("schema_version") != pipeline.PROFILE_EVIDENCE_VERSION
655
+ or not isinstance(columns, list)
656
+ or not isinstance(types, Mapping)
657
+ or not isinstance(null_counts, Mapping)
658
+ or type(row_count) is not int
659
+ or set(types) != set(columns)
660
+ or set(null_counts) != set(columns)
661
+ ):
662
+ raise pipeline.BuildError("CANDIDATE_EVIDENCE_SCHEMA", "profile evidence is invalid")
663
+
664
+ requirements_raw = _optional_snapshot_member(snapshot, "requirements.json")
665
+ requirements = (
666
+ _json_object(requirements_raw, "requirements.json")
667
+ if requirements_raw is not None
668
+ else None
669
+ )
670
+ manifest = verified.manifest
671
+ requirements_sha256 = (
672
+ sha256_bytes(requirements_raw)
673
+ if requirements_raw is not None
674
+ else _manifest_digest(manifest, "plan_sha256")
675
+ )
676
+ time_coverage = _time_coverage(requirements)
677
+
678
+ source_facts: list[BuildSourceFacts] = []
679
+ for source in sources_value:
680
+ if not isinstance(source, Mapping):
681
+ raise pipeline.BuildError("CANDIDATE_EVIDENCE_SCHEMA", "source evidence is invalid")
682
+ rights = source.get("rights")
683
+ if not isinstance(rights, Mapping):
684
+ raise pipeline.BuildError("CANDIDATE_EVIDENCE_SCHEMA", "source rights are invalid")
685
+ source_facts.append(
686
+ BuildSourceFacts(
687
+ source_id=source.get("source_id"),
688
+ content_sha256=source.get("sha256"),
689
+ rights_status=rights.get("status"),
690
+ rights_evidence_sha256=sha256_bytes(canonical_json_bytes(dict(rights))),
691
+ )
692
+ )
693
+
694
+ recipe_lineage = _recipe_lineage(snapshot)
695
+ return BuildIndexEntry(
696
+ question=inspection.question,
697
+ question_sha256=sha256_bytes(inspection.question.encode("utf-8")),
698
+ requirements_sha256=requirements_sha256,
699
+ grain=inspection.grain,
700
+ time_coverage=time_coverage,
701
+ fields=tuple(BuildField(name, str(types[name])) for name in columns),
702
+ sources=tuple(sorted(source_facts, key=lambda item: item.source_id)),
703
+ profile=SealedProfileFactsV1(
704
+ evidence_sha256=sha256_bytes(profile_raw),
705
+ row_count=row_count,
706
+ columns=tuple(columns),
707
+ logical_types=tuple(str(types[name]) for name in columns),
708
+ null_counts=tuple(null_counts[name] for name in columns),
709
+ ),
710
+ recipe_lineage=recipe_lineage,
711
+ candidate_sha256=verified.candidate_digest,
712
+ table_sha256=verified.table_sha256,
713
+ validation_policy_sha256=verified.validation_policy_digest,
714
+ lineage_sha256=sha256_bytes(canonical_json_bytes(lineage)),
715
+ )
716
+
717
+
718
+ def _optional_snapshot_member(snapshot: pipeline.VerifiedSnapshot, path: str) -> bytes | None:
719
+ member_paths = {
720
+ item.get("path")
721
+ for item in snapshot.verified.manifest.get("members", ())
722
+ if isinstance(item, Mapping)
723
+ }
724
+ if path not in member_paths:
725
+ return None
726
+ return snapshot.member_bytes(path)
727
+
728
+
729
+ def _recipe_lineage(snapshot: pipeline.VerifiedSnapshot) -> BuildRecipeLineage | None:
730
+ recipe_raw = _optional_snapshot_member(snapshot, "recipe.json")
731
+ if recipe_raw is None:
732
+ return None
733
+ execution_raw = _optional_snapshot_member(snapshot, "recipe-execution.json")
734
+ if execution_raw is None:
735
+ raise pipeline.BuildError("CANDIDATE_MEMBER_MISMATCH", "recipe execution is absent")
736
+ recipe = _json_object(recipe_raw, "recipe.json")
737
+ execution = _json_object(execution_raw, "recipe-execution.json")
738
+ recipe_sha256 = canonical_sha256(recipe)
739
+ if execution.get("recipe_digest") != recipe_sha256:
740
+ raise pipeline.BuildError("CANDIDATE_MEMBER_INVALID", "recipe lineage is invalid")
741
+ return BuildRecipeLineage(
742
+ recipe_id=recipe.get("recipe_id"),
743
+ recipe_version=recipe.get("recipe_version"),
744
+ recipe_sha256=recipe_sha256,
745
+ predecessor_candidate_sha256=execution.get("predecessor_candidate_digest"),
746
+ )
747
+
748
+
749
+ def _time_coverage(requirements: Mapping[str, Any] | None) -> BuildTimeCoverage | None:
750
+ if requirements is None:
751
+ return None
752
+ value = requirements.get("time_range")
753
+ if not isinstance(value, Mapping):
754
+ raise pipeline.BuildError("CANDIDATE_MEMBER_INVALID", "requirements time range is invalid")
755
+ start = value.get("start_inclusive")
756
+ end = value.get("end_exclusive")
757
+ if not isinstance(start, str) or not isinstance(end, str):
758
+ raise pipeline.BuildError("CANDIDATE_MEMBER_INVALID", "requirements time range is invalid")
759
+ return BuildTimeCoverage(start[:10], end[:10])
760
+
761
+
762
+ def _manifest_digest(manifest: Mapping[str, Any], name: str) -> str:
763
+ value = manifest.get(name)
764
+ if not isinstance(value, str):
765
+ raise pipeline.BuildError("MANIFEST_INVALID", f"candidate {name} is invalid")
766
+ return value
767
+
768
+
769
+ def _json_value(raw: bytes, path: str) -> Any:
770
+ try:
771
+ return json.loads(raw)
772
+ except (UnicodeError, json.JSONDecodeError) as error:
773
+ raise pipeline.BuildError("CANDIDATE_MEMBER_INVALID", f"{path} is invalid") from error
774
+
775
+
776
+ def _json_object(raw: bytes, path: str) -> Mapping[str, Any]:
777
+ value = _json_value(raw, path)
778
+ if not isinstance(value, Mapping):
779
+ raise pipeline.BuildError("CANDIDATE_MEMBER_INVALID", f"{path} is not an object")
780
+ return value
781
+
782
+
783
+ def _diagnostic_for_build_error(error: pipeline.BuildError) -> str:
784
+ finding = error.finding_id
785
+ if "RACE" in finding:
786
+ return "BUILD_RACE"
787
+ if "TOO_LARGE" in finding or "LIMIT" in finding:
788
+ return "BUILD_LIMIT"
789
+ if "SYMLINK" in finding:
790
+ return "BUILD_SYMLINK"
791
+ if "HARDLINK" in finding:
792
+ return "BUILD_HARDLINK"
793
+ return "BUILD_INVALID"
794
+
795
+
796
+ def _publish_index(
797
+ index_root: Path,
798
+ entries: tuple[BuildIndexEntry, ...],
799
+ locators: BuildLocatorDocument,
800
+ diagnostics: BuildDiagnosticDocument,
801
+ *,
802
+ fault_hook: Callable[[str], None] | None,
803
+ ) -> PublishedBuildIndex:
804
+ parent = index_root.parent
805
+ parent_fd = _open_publish_parent(parent)
806
+ staging_name = f".{index_root.name}.tmp-{uuid.uuid4().hex}"
807
+ staging = parent / staging_name
808
+ published = False
809
+ staging_exists = False
810
+ staging_fd = -1
811
+ cleanup_fd = -1
812
+ cleanup_identity: tuple[int, int] | None = None
813
+ try:
814
+ try:
815
+ os.mkdir(staging_name, mode=0o700, dir_fd=parent_fd)
816
+ except FileExistsError as error: # pragma: no cover - UUID collision
817
+ raise LocalSearchIndexError(
818
+ "INDEX_DURABILITY", "index_root", "staging coordinate already exists"
819
+ ) from error
820
+ staging_exists = True
821
+ staging_fd = os.open(staging_name, _OPEN_DIRECTORY, dir_fd=parent_fd)
822
+ staging_named = os.stat(staging_name, dir_fd=parent_fd, follow_symlinks=False)
823
+ staging_opened = os.fstat(staging_fd)
824
+ cleanup_identity = _directory_identity(staging_opened)
825
+ if (
826
+ not stat.S_ISDIR(staging_named.st_mode)
827
+ or _directory_identity(staging_named) != cleanup_identity
828
+ ):
829
+ raise LocalSearchIndexError(
830
+ "INDEX_DURABILITY", "index_root", "staging root identity changed"
831
+ )
832
+ cleanup_fd = staging_fd
833
+ manifest = _write_index_snapshot(staging_fd, entries, locators, diagnostics)
834
+ manifest_bytes = manifest.canonical_bytes
835
+ manifest_sha256 = sha256_bytes(manifest_bytes)
836
+ os.fsync(staging_fd)
837
+
838
+ capture_build_index_snapshot(staging, expected_manifest_sha256=manifest_sha256)
839
+ if fault_hook is not None:
840
+ fault_hook("after_staging")
841
+
842
+ existing = _entry_stat(parent_fd, index_root.name)
843
+ if existing is None:
844
+ os.rename(staging_name, index_root.name, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
845
+ staging_exists = False
846
+ elif stat.S_ISDIR(existing.st_mode):
847
+ previous_fd = os.open(index_root.name, _OPEN_DIRECTORY, dir_fd=parent_fd)
848
+ previous_opened = os.fstat(previous_fd)
849
+ if _directory_identity(existing) != _directory_identity(previous_opened):
850
+ os.close(previous_fd)
851
+ raise LocalSearchIndexError(
852
+ "INDEX_DURABILITY", "index_root", "existing index root identity changed"
853
+ )
854
+ try:
855
+ _rename_exchange(parent_fd, staging_name, index_root.name)
856
+ except BaseException:
857
+ os.close(previous_fd)
858
+ raise
859
+ cleanup_fd = previous_fd
860
+ cleanup_identity = _directory_identity(previous_opened)
861
+ else:
862
+ raise LocalSearchIndexError(
863
+ "INDEX_DURABILITY", "index_root", "existing index root is not a directory"
864
+ )
865
+ published = True
866
+ os.fsync(parent_fd)
867
+ if fault_hook is not None:
868
+ fault_hook("after_publish")
869
+ captured = capture_build_index_snapshot(
870
+ index_root, expected_manifest_sha256=manifest_sha256
871
+ )
872
+ if captured.manifest != manifest:
873
+ raise LocalSearchIndexError(
874
+ "INDEX_DURABILITY", "index_root", "published successor failed exact readback"
875
+ )
876
+ return PublishedBuildIndex(index_root, manifest, manifest_sha256)
877
+ finally:
878
+ try:
879
+ if staging_exists and cleanup_fd >= 0 and cleanup_identity is not None:
880
+ _remove_open_tree_at(
881
+ parent_fd,
882
+ staging_name,
883
+ cleanup_fd,
884
+ expected_identity=cleanup_identity,
885
+ ignore_errors=published,
886
+ )
887
+ finally:
888
+ if cleanup_fd >= 0 and cleanup_fd != staging_fd:
889
+ os.close(cleanup_fd)
890
+ if staging_fd >= 0:
891
+ os.close(staging_fd)
892
+ os.close(parent_fd)
893
+
894
+
895
+ def _open_publish_parent(parent: Path) -> int:
896
+ try:
897
+ named = os.lstat(parent)
898
+ descriptor = os.open(parent, _OPEN_DIRECTORY)
899
+ opened = os.fstat(descriptor)
900
+ except OSError as error:
901
+ raise LocalSearchIndexError(
902
+ "INDEX_DURABILITY", "index_root", "cannot open the retained publication parent"
903
+ ) from error
904
+ if not stat.S_ISDIR(named.st_mode) or (named.st_dev, named.st_ino) != (
905
+ opened.st_dev,
906
+ opened.st_ino,
907
+ ):
908
+ os.close(descriptor)
909
+ raise LocalSearchIndexError(
910
+ "INDEX_DURABILITY", "index_root", "publication parent identity changed"
911
+ )
912
+ return descriptor
913
+
914
+
915
+ def _write_index_snapshot(
916
+ root_fd: int,
917
+ entries: tuple[BuildIndexEntry, ...],
918
+ locators: BuildLocatorDocument,
919
+ diagnostics: BuildDiagnosticDocument,
920
+ ) -> BuildIndexManifest:
921
+ os.mkdir("entries", mode=0o700, dir_fd=root_fd)
922
+ entries_fd = os.open("entries", _OPEN_DIRECTORY, dir_fd=root_fd)
923
+ entry_members: list[SnapshotMember] = []
924
+ try:
925
+ for entry in sorted(entries, key=lambda item: item.facts_sha256):
926
+ raw = entry.canonical_bytes
927
+ name = f"{entry.facts_sha256}.json"
928
+ _create_file_at(entries_fd, name, raw, mode=0o444)
929
+ entry_members.append(SnapshotMember(f"entries/{name}", len(raw), sha256_bytes(raw)))
930
+ os.fsync(entries_fd)
931
+ finally:
932
+ os.close(entries_fd)
933
+
934
+ locator_raw = locators.canonical_bytes
935
+ diagnostic_raw = diagnostics.canonical_bytes
936
+ _create_file_at(root_fd, "locators.json", locator_raw, mode=0o600)
937
+ _create_file_at(root_fd, "diagnostics.json", diagnostic_raw, mode=0o600)
938
+ locator_member = SnapshotMember("locators.json", len(locator_raw), sha256_bytes(locator_raw))
939
+ diagnostic_member = SnapshotMember(
940
+ "diagnostics.json", len(diagnostic_raw), sha256_bytes(diagnostic_raw)
941
+ )
942
+ members = (*entry_members, locator_member, diagnostic_member)
943
+ manifest = BuildIndexManifest(
944
+ entries=tuple(entry_members),
945
+ locators=locator_member,
946
+ diagnostics=diagnostic_member,
947
+ entry_count=len(entry_members),
948
+ locator_count=len(locators.locators),
949
+ diagnostic_count=len(diagnostics.diagnostics),
950
+ total_bytes=sum(item.bytes for item in members),
951
+ aggregate_sha256=manifest_aggregate_sha256(members),
952
+ )
953
+ _create_file_at(root_fd, "manifest.json", manifest.canonical_bytes, mode=0o600)
954
+ return manifest
955
+
956
+
957
+ def _create_file_at(parent_fd: int, name: str, raw: bytes, *, mode: int) -> None:
958
+ flags = (
959
+ os.O_RDWR
960
+ | os.O_CREAT
961
+ | os.O_EXCL
962
+ | getattr(os, "O_CLOEXEC", 0)
963
+ | getattr(os, "O_NOFOLLOW", 0)
964
+ )
965
+ descriptor = os.open(name, flags, 0o600, dir_fd=parent_fd)
966
+ try:
967
+ view = memoryview(raw)
968
+ while view:
969
+ written = os.write(descriptor, view)
970
+ if written < 1:
971
+ raise LocalSearchIndexError(
972
+ "INDEX_DURABILITY", name, "member write stopped making progress"
973
+ )
974
+ view = view[written:]
975
+ os.fsync(descriptor)
976
+ observed = os.pread(descriptor, len(raw) + 1, 0)
977
+ if observed != raw:
978
+ raise LocalSearchIndexError(
979
+ "INDEX_DURABILITY", name, "member readback differs from committed bytes"
980
+ )
981
+ os.fchmod(descriptor, mode)
982
+ os.fsync(descriptor)
983
+ finally:
984
+ os.close(descriptor)
985
+
986
+
987
+ def _entry_stat(parent_fd: int, name: str) -> os.stat_result | None:
988
+ try:
989
+ return os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
990
+ except FileNotFoundError:
991
+ return None
992
+
993
+
994
+ def _directory_identity(observed: os.stat_result) -> tuple[int, int]:
995
+ return observed.st_dev, observed.st_ino
996
+
997
+
998
+ def _remove_open_tree_at(
999
+ parent_fd: int,
1000
+ name: str,
1001
+ descriptor: int,
1002
+ *,
1003
+ expected_identity: tuple[int, int],
1004
+ ignore_errors: bool,
1005
+ ) -> None:
1006
+ """Remove only the still-bound descriptor-rooted staging tree."""
1007
+
1008
+ try:
1009
+ named = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
1010
+ opened = os.fstat(descriptor)
1011
+ if (
1012
+ not stat.S_ISDIR(named.st_mode)
1013
+ or not stat.S_ISDIR(opened.st_mode)
1014
+ or _directory_identity(named) != expected_identity
1015
+ or _directory_identity(opened) != expected_identity
1016
+ ):
1017
+ return
1018
+ _clear_open_directory(descriptor)
1019
+ after = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
1020
+ if _directory_identity(after) != expected_identity:
1021
+ return
1022
+ os.rmdir(name, dir_fd=parent_fd)
1023
+ except FileNotFoundError:
1024
+ return
1025
+ except OSError as error:
1026
+ if not ignore_errors:
1027
+ raise LocalSearchIndexError(
1028
+ "INDEX_DURABILITY", "index_root", "staging cleanup failed safely"
1029
+ ) from error
1030
+
1031
+
1032
+ def _clear_open_directory(directory_fd: int) -> None:
1033
+ with os.scandir(directory_fd) as iterator:
1034
+ names = sorted(entry.name for entry in iterator)
1035
+ for name in names:
1036
+ named = os.stat(name, dir_fd=directory_fd, follow_symlinks=False)
1037
+ if stat.S_ISDIR(named.st_mode):
1038
+ child_fd = os.open(name, _OPEN_DIRECTORY, dir_fd=directory_fd)
1039
+ try:
1040
+ opened = os.fstat(child_fd)
1041
+ identity = _directory_identity(opened)
1042
+ if _directory_identity(named) != identity:
1043
+ raise OSError(errno.ESTALE, "cleanup child identity changed")
1044
+ _clear_open_directory(child_fd)
1045
+ after = os.stat(name, dir_fd=directory_fd, follow_symlinks=False)
1046
+ if _directory_identity(after) != identity:
1047
+ raise OSError(errno.ESTALE, "cleanup child identity changed")
1048
+ os.rmdir(name, dir_fd=directory_fd)
1049
+ finally:
1050
+ os.close(child_fd)
1051
+ else:
1052
+ os.unlink(name, dir_fd=directory_fd)
1053
+
1054
+
1055
+ def _rename_exchange(parent_fd: int, left: str, right: str) -> None:
1056
+ libc = ctypes.CDLL(None, use_errno=True)
1057
+ left_raw = os.fsencode(left)
1058
+ right_raw = os.fsencode(right)
1059
+ if sys.platform == "darwin" and hasattr(libc, "renameatx_np"):
1060
+ rename = libc.renameatx_np
1061
+ rename.argtypes = _RENAME_ARGTYPES
1062
+ rename.restype = ctypes.c_int
1063
+ result = rename(parent_fd, left_raw, parent_fd, right_raw, 0x00000002)
1064
+ elif hasattr(libc, "renameat2"):
1065
+ rename = libc.renameat2
1066
+ rename.argtypes = _RENAME_ARGTYPES
1067
+ rename.restype = ctypes.c_int
1068
+ result = rename(parent_fd, left_raw, parent_fd, right_raw, 0x00000002)
1069
+ else: # pragma: no cover - supported product platforms provide one primitive
1070
+ raise LocalSearchIndexError(
1071
+ "INDEX_DURABILITY", "index_root", "atomic directory exchange is unavailable"
1072
+ )
1073
+ if result != 0:
1074
+ error_number = ctypes.get_errno()
1075
+ raise LocalSearchIndexError(
1076
+ "INDEX_DURABILITY", "index_root", "atomic directory exchange failed"
1077
+ ) from OSError(error_number, os.strerror(error_number))
1078
+
1079
+
1080
+ __all__ = [
1081
+ "BuildDiscovery",
1082
+ "BuildDiscoveryLimits",
1083
+ "LocalSearchIndexError",
1084
+ "PublishedBuildIndex",
1085
+ "build_private_index",
1086
+ "discover_build_candidates",
1087
+ ]