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,2152 @@
1
+ """Append-only run events for live status views.
2
+
3
+ The feed is a derived projection, not durable authority, and is excluded from sealed digests. Event
4
+ pointers use candidate-relative paths and SHA-256 digests. The five-field record shape is fixed;
5
+ new event names are added to the closed name sets and fact specification in this module.
6
+
7
+ The vocabulary is extensible by NAME and never by FIELD, so a new fact means a new name. Build
8
+ events cost three things in one commit: an `EVENT_FACTS` entry, a live emit site in `pipeline.py`,
9
+ and a matching branch in `project_sealed_run` that derives the identical record from sealed bytes
10
+ alone. The third is the binding one -- a live BUILD event a projection cannot re-derive is
11
+ forbidden, which is why a time-throttled heartbeat is not in this vocabulary and is not to be
12
+ added to it.
13
+
14
+ The one explicit exception is the bounded post-seal notebook-sidecar sequence. It narrates an
15
+ unsealed convenience write performed by the CLI after the Build exists, carries no path, digest,
16
+ or authority, and cannot be projected from sealed bytes. Its three names are separated below so a
17
+ consumer can never mistake sidecar state for Build evidence. Feed length remains bounded by the
18
+ shape of one Build plus that fixed sequence; per-byte, per-row and per-chunk emission are refused.
19
+
20
+ Live hosted progress is NOT here and is not to be moved here. `progress_events` carries a second,
21
+ avowedly unsealed vocabulary for the hosted timeline; it is disjoint from every name set in this
22
+ module by construction, `project_sealed_run` has no branch for any of its names, and it reads this
23
+ feed only through the `observer` seam below, which adds no name and no record shape. The two
24
+ vocabularies never merge: a progress record proves nothing, and a Build record must always prove
25
+ itself from sealed bytes.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import contextlib
31
+ import datetime
32
+ import errno
33
+ import hashlib
34
+ import heapq
35
+ import json
36
+ import math
37
+ import os
38
+ import re
39
+ import socket
40
+ import stat
41
+ import threading
42
+ import time
43
+ from collections.abc import Callable, Iterator, Mapping, Sequence
44
+ from contextvars import ContextVar
45
+ from pathlib import Path, PurePath
46
+ from typing import Any
47
+
48
+ FEED_SCHEMA_VERSION = "mr-run-events.v2"
49
+ FEED_DIR_NAME = ".mr-events"
50
+
51
+ # Untrusted-input ceilings. A feed may have been produced by another local user or hand-edited, so
52
+ # the reader refuses rather than allocates.
53
+ MAX_FEED_BYTES = 4 * 1024 * 1024
54
+ MAX_FEED_RECORDS = 20_000
55
+ MAX_TEXT_CHARS = 512
56
+ MAX_FACT_KEYS = 24
57
+
58
+ # Bound the aggregate attempt set as well as each file. Selection keeps the newest attempts. If a
59
+ # matching attempt is outside the retained set, callers fall back to sealed receipts.
60
+ MAX_FEED_ATTEMPTS = 32
61
+
62
+
63
+ def _empty_attempt_size(name: str) -> int:
64
+ """Exact byte size of the header-only attempt ``EventWriter`` creates for ``name``."""
65
+
66
+ header = {
67
+ "v": 1,
68
+ "kind": "header",
69
+ "schema_version": FEED_SCHEMA_VERSION,
70
+ "run": Path(name).stem,
71
+ }
72
+ return len((_dumps(header) + "\n").encode("utf-8"))
73
+
74
+
75
+ # Valid event times fit Python's UTC datetime range. The bound also rejects non-finite values and
76
+ # finite numbers that cannot represent supported timestamps.
77
+ _EARLIEST_MOMENT = datetime.datetime.min.replace(tzinfo=datetime.UTC).timestamp()
78
+ _LATEST_MOMENT = datetime.datetime.max.replace(tzinfo=datetime.UTC).timestamp()
79
+
80
+ # Write-side ceilings on the shape of a fact value.
81
+ _MAX_FACT_DEPTH = 6
82
+ _MAX_LIST_ITEMS = 256
83
+ _TRUNCATION_MARKER = "..."
84
+
85
+ _O_NOFOLLOW = getattr(os, "O_NOFOLLOW", 0)
86
+ _O_NONBLOCK = getattr(os, "O_NONBLOCK", 0)
87
+ _O_DIRECTORY = getattr(os, "O_DIRECTORY", 0)
88
+
89
+ # Extend the vocabulary by adding names, not record fields. Local and hosted Build producers share
90
+ # the same record shape.
91
+ BUILD_EVENT_NAMES = frozenset(
92
+ {
93
+ "run_started",
94
+ "rights_checked",
95
+ "sources_read_started",
96
+ "source_read",
97
+ "sources_parsed_started",
98
+ "source_parsed",
99
+ "evidence_matched",
100
+ "cleaning_started",
101
+ "cleaning_applied",
102
+ "sources_profiled",
103
+ "join_completed",
104
+ "rows_selected",
105
+ "checks_started",
106
+ "check_completed",
107
+ "checks_completed",
108
+ "table_written",
109
+ "members_sealed_started",
110
+ "member_sealed",
111
+ "candidate_installed",
112
+ "snapshot_verify_started",
113
+ "member_verified",
114
+ "snapshot_verified",
115
+ "build_sealed",
116
+ "build_failed",
117
+ "run_interrupted",
118
+ }
119
+ )
120
+
121
+ # CLI-only narration of the unsealed ``table.ipynb`` convenience write. These records are
122
+ # intentionally not in ``project_sealed_run``: sealed bytes cannot prove that a sibling file was
123
+ # attempted or written. They carry no location or digest and grant no handoff authority.
124
+ NOTEBOOK_SIDECAR_EVENT_NAMES = frozenset(
125
+ {
126
+ "notebook_sidecar_started",
127
+ "notebook_sidecar_ready",
128
+ "notebook_sidecar_failed",
129
+ }
130
+ )
131
+ NOTEBOOK_SIDECAR_OUTCOME_EVENT_NAMES = NOTEBOOK_SIDECAR_EVENT_NAMES - {"notebook_sidecar_started"}
132
+
133
+ EVENT_NAMES = BUILD_EVENT_NAMES | NOTEBOOK_SIDECAR_EVENT_NAMES
134
+
135
+ # The manifest version a recipe run seals under, spelled here as a LITERAL rather than imported.
136
+ # `project_sealed_run` is pure and imports nothing from `pipeline` -- a module-level import would
137
+ # close a cycle, and a deferred one inside the projector would give the projection a harness
138
+ # dependency it does not otherwise have. `evidence_matched` needs this value to gate on, because a
139
+ # recipe candidate seals `evidence/package.json` and still never matches proposal evidence. A test
140
+ # asserts this equals `pipeline.RECIPE_MANIFEST_VERSION` so the two spellings cannot drift.
141
+ _RECIPE_MANIFEST_VERSION = "candidate-manifest.v9"
142
+
143
+ # Terminal events for attempts that did not seal a Build.
144
+ TERMINAL_EVENT_NAMES = frozenset({"build_failed", "run_interrupted"})
145
+
146
+ # Reserved Courier and Reader event names have bounded but otherwise unenforced fact mappings.
147
+ # These events may
148
+ # carry a scheme-qualified remote locator (`https://`, `s3://`, `gs://`) in a fact value. See
149
+ # `_require_placeless`: that is placeless, because it resolves to the same bytes from every machine.
150
+ # A path on this disk, a `file://` URL and a loopback authority remain refused.
151
+ RESERVED_EVENT_NAMES = frozenset(
152
+ {
153
+ "fetch_started",
154
+ "slice_received",
155
+ "reader_opened",
156
+ "messages_decoded",
157
+ "cycle_locked",
158
+ }
159
+ )
160
+
161
+ # This mapping is the authoritative declaration of what each event asserts. Two producers write
162
+ # Build records -- the live emitter in `pipeline.py` and the post-hoc projector
163
+ # `project_sealed_run` -- and a test asserts their Build prefixes agree. The CLI alone writes the
164
+ # three unsealed sidecar names. No producer may add, drop or rename a fact key; all read this table.
165
+ # A fact set change is a change HERE, and every applicable producer plus its tests moves together.
166
+ # `validate_facts` runs inside `EventWriter.write`, so a producer that drifts raises rather than
167
+ # writing a record that quietly contradicts the declared shape.
168
+ #
169
+ # Three entries need explicit constraints:
170
+ #
171
+ # * `checks_completed` has no `failing` key. `_validate_output` RAISES on the first failed check, so
172
+ # a `checks_completed` record can only ever exist for a run where every check passed; a `failing`
173
+ # key would be a list that is structurally always empty, which reads as a claim the data cannot
174
+ # make. A failed check is narrated by `build_failed`.
175
+ # * `source_read` has no `sha256` key. The live emitter has the raw bytes in hand but must not hash
176
+ # them -- the seal hashes every source already, and a second hash on the read path is work a
177
+ # event feed does not need. The digest is available through the record's
178
+ # `evidence_member` is `raw/<source_id>.csv`, which IS a sealed member, so its `member_sealed`
179
+ # record carries the digest.
180
+ # * `join_completed` requires all seven join keys with no omissions. `_join` always emits every one
181
+ # of them, and a plan always carries exactly one join over at least two sources, so there is no
182
+ # shape where a key is absent. Do not write a "skip if missing" branch.
183
+ # * `check_completed` has no `passed`, `observed` or `expected` key, on the exact precedent
184
+ # `checks_completed` sets immediately above: `_validate_output` raises on the first failed check,
185
+ # so those three would be structurally constant on every record that survives to be read. The
186
+ # record IS emitted before the refusal, so a failing check is still counted; the `build_failed`
187
+ # that follows carries that check's id as its `finding_id`, which is where a failure is narrated.
188
+ # * The six `*_started` markers each carry a `total` and nothing else. The admission test for one is
189
+ # that its denominator is fixed BEFORE its loop starts and is readable out of a sealed member
190
+ # afterwards -- a stage whose size is only knowable once it is finished is not progress, it is a
191
+ # spinner with a number on it. All six anchor `plan.json`, because the plan is what fixes every
192
+ # one of those totals: the source count and the cleaning-step count are literally in it, the check
193
+ # total is `2 + len(grain) + len(quality.not_null)`, and the member set is the static file set for
194
+ # the manifest version plus one `raw/<source_id>.csv` per planned source.
195
+ # * `member_verified` mirrors `member_sealed` exactly, for the same reason: its pointer varies per
196
+ # member and rides in `evidence` rather than in a fact.
197
+ EVENT_FACTS: Mapping[str, tuple[str, ...]] = {
198
+ "run_started": ("sources", "output_intent", "grain", "columns", "evidence_member"),
199
+ "rights_checked": ("sources", "evidence_member"),
200
+ "sources_read_started": ("total", "evidence_member"),
201
+ "source_read": ("source_id", "bytes", "evidence_member"),
202
+ "sources_parsed_started": ("total", "evidence_member"),
203
+ "source_parsed": ("source_id", "rows", "columns", "evidence_member"),
204
+ "evidence_matched": ("matched", "total", "evidence_member"),
205
+ "cleaning_started": ("total", "evidence_member"),
206
+ "cleaning_applied": (
207
+ "step_index",
208
+ "step_total",
209
+ "source_id",
210
+ "operation",
211
+ "evidence_member",
212
+ ),
213
+ "sources_profiled": ("sources", "columns", "evidence_member"),
214
+ "join_completed": (
215
+ "left_rows",
216
+ "right_rows",
217
+ "output_rows",
218
+ "matched_left_rows",
219
+ "unmatched_left_rows",
220
+ "row_multiplier",
221
+ "cardinality",
222
+ "evidence_member",
223
+ ),
224
+ "rows_selected": ("rows", "columns", "evidence_member"),
225
+ "checks_started": ("total", "evidence_member"),
226
+ "check_completed": ("check_id", "index", "total", "evidence_member"),
227
+ "checks_completed": ("passed", "total", "evidence_member"),
228
+ "table_written": ("rows", "columns", "evidence_member"),
229
+ "members_sealed_started": ("total", "evidence_member"),
230
+ "member_sealed": ("bytes",),
231
+ "candidate_installed": ("members", "bytes", "evidence_member"),
232
+ "snapshot_verify_started": ("total", "evidence_member"),
233
+ "member_verified": ("bytes",),
234
+ "snapshot_verified": ("members", "table_sha256", "evidence_member"),
235
+ "build_sealed": ("candidate_digest", "table_sha256", "manifest_version", "row_count"),
236
+ "build_failed": ("finding_id", "severity", "message"),
237
+ "run_interrupted": (),
238
+ "notebook_sidecar_started": (),
239
+ "notebook_sidecar_ready": (),
240
+ "notebook_sidecar_failed": ("code",),
241
+ }
242
+
243
+ # The fixed member each event points at. `member_sealed`, `member_verified` and `build_sealed` are
244
+ # absent BY DESIGN: the first two carry their pointer in `evidence` directly, one per member, and
245
+ # `build_sealed` anchors on the candidate digest, which is not a member of anything. The one event
246
+ # whose pointer varies is `source_read`; `source_member` builds it.
247
+ #
248
+ # `manifest.json` is NOT in `manifest["members"]`, so it can never appear here: `_pointer` would
249
+ # raise on it during projection. Every anchor below is a real member.
250
+ EVIDENCE_MEMBER: Mapping[str, str] = {
251
+ "run_started": "plan.json",
252
+ "rights_checked": "evidence/sources.json",
253
+ "sources_read_started": "plan.json",
254
+ "sources_parsed_started": "plan.json",
255
+ "source_parsed": "evidence/source_profiles.json",
256
+ "evidence_matched": "evidence/package.json",
257
+ "cleaning_started": "plan.json",
258
+ "cleaning_applied": "evidence/lineage.json",
259
+ "sources_profiled": "evidence/source_profiles.json",
260
+ "join_completed": "evidence/join.json",
261
+ # `evidence/profile.json`, not `evidence/lineage.json`: lineage redeems the column count and
262
+ # says nothing about rows, and the profile is the one member carrying BOTH numbers this record
263
+ # states. It is also what the projection actually reads.
264
+ "rows_selected": "evidence/profile.json",
265
+ "checks_started": "plan.json",
266
+ "check_completed": "evidence/quality.json",
267
+ "checks_completed": "evidence/quality.json",
268
+ "table_written": "data/table.parquet",
269
+ "members_sealed_started": "plan.json",
270
+ "candidate_installed": "data/table.parquet",
271
+ "snapshot_verify_started": "plan.json",
272
+ "snapshot_verified": "data/table.parquet",
273
+ }
274
+
275
+ _ATTEMPT_RE = re.compile(r"\A[A-Za-z0-9._-]{1,64}\Z")
276
+ _SOURCE_ID_RE = re.compile(r"\A[A-Za-z0-9._-]{1,64}\Z")
277
+ _MEMBER_PATH_RE = re.compile(r"\A[A-Za-z0-9._/-]{1,128}\Z")
278
+ _SHA256_RE = re.compile(r"\A[0-9a-f]{64}\Z")
279
+ # A Windows drive prefix ANYWHERE in a string. The lookbehind is what keeps `https://` out of it:
280
+ # without it, the `s:/` inside every `https://` locator reads as a drive letter.
281
+ _WINDOWS_DRIVE_RE = re.compile(r"(?<![A-Za-z0-9])[A-Za-z]:[\\/]")
282
+
283
+ # A `/`-rooted path of two or more segments, ANYWHERE in a string. The guard used to test only
284
+ # `text.startswith("/")`, and every `BuildError` message in the tree reads
285
+ # "<what went wrong>: <path>", so this host's absolute paths reached the feed and both UI surfaces
286
+ # untouched. The lookbehind is what keeps a date (`2026/08/07`) and a relative member path
287
+ # (`evidence/sources.json`) out of it: a rooted path's leading slash never follows a word character.
288
+ _EMBEDDED_ABS_PATH_RE = re.compile(r"(?<![A-Za-z0-9_.~-])(?:/[^\s/\\'\"<>|]+){2,}")
289
+
290
+ # A scheme-qualified locator (`https://`, `s3://`, `gs://`, `abfss://`, ...), matched at the start
291
+ # of ONE whitespace-delimited token rather than of the whole string, so a locator quoted inside
292
+ # prose ("downloading from https://noaa.gov/...") is read as the locator it is.
293
+ _URL_SCHEME_RE = re.compile(r"\A([A-Za-z][A-Za-z0-9+.-]*)://")
294
+
295
+ # Punctuation a locator gets wrapped in when a sentence carries it.
296
+ _TOKEN_TRIM = "()[]{}<>\"'`,;"
297
+ # The schemes that mean "this machine" however they are dressed.
298
+ _LOCAL_URL_SCHEMES = frozenset({"file", "localhost", "unix", "fd"})
299
+ _LOCAL_URL_HOSTS = frozenset({"localhost", "127.0.0.1", "0.0.0.0", "::1", "0:0:0:0:0:0:0:1"})
300
+
301
+ # Computed once at import. A hostname shorter than four characters is skipped: a two-letter host
302
+ # name would match inside half the English language, and a guard with absurd false positives is a
303
+ # guard someone turns off.
304
+ try:
305
+ _HOSTNAME = socket.gethostname()
306
+ except OSError: # pragma: no cover - gethostname does not fail on a supported platform
307
+ _HOSTNAME = ""
308
+ _HOSTNAME_MATCH = _HOSTNAME.lower() if len(_HOSTNAME) >= 4 else ""
309
+
310
+ _RECORD_KEYS = frozenset({"seq", "at", "event", "facts", "evidence"})
311
+ _EVIDENCE_KEYS = frozenset({"member", "sha256"})
312
+
313
+ _ALL_EVENT_NAMES = EVENT_NAMES | RESERVED_EVENT_NAMES
314
+
315
+
316
+ def source_member(source_id: str) -> str:
317
+ """Return the candidate-relative member path carrying the raw bytes of ``source_id``.
318
+
319
+ The source id is matched against the member-path character class before it is interpolated, so
320
+ a source id can never widen the pointer vocabulary or introduce a traversal segment.
321
+ """
322
+
323
+ if not isinstance(source_id, str) or not _SOURCE_ID_RE.match(source_id):
324
+ raise ValueError(f"source id is not a member-path token: {source_id!r}")
325
+ return f"raw/{source_id}.csv"
326
+
327
+
328
+ def validate_facts(event: str, facts: Mapping[str, Any]) -> None:
329
+ """Enforce the normative fact set for ``event``; raise ``ValueError`` naming the difference.
330
+
331
+ The error names missing and extra keys. Reserved event names without an ``EVENT_FACTS`` entry
332
+ accept any mapping within ``MAX_FACT_KEYS``.
333
+ """
334
+
335
+ if event not in _ALL_EVENT_NAMES:
336
+ raise ValueError(f"event name is outside the closed vocabulary: {event!r}")
337
+ if not isinstance(facts, Mapping):
338
+ raise ValueError(f"facts for {event!r} is not a mapping")
339
+ if any(not isinstance(key, str) for key in facts):
340
+ raise ValueError(f"facts for {event!r} carries a non-string key")
341
+ if len(facts) > MAX_FACT_KEYS:
342
+ raise ValueError(f"facts for {event!r} carries {len(facts)} keys, over {MAX_FACT_KEYS}")
343
+
344
+ declared = EVENT_FACTS.get(event)
345
+ if declared is None:
346
+ return None
347
+
348
+ present = set(facts)
349
+ expected = set(declared)
350
+ if present == expected:
351
+ return None
352
+ missing = sorted(expected - present)
353
+ extra = sorted(present - expected)
354
+ raise ValueError(
355
+ f"facts for {event!r} do not match the declared set: "
356
+ f"missing={missing} extra={extra} declared={list(declared)}"
357
+ )
358
+
359
+
360
+ def run_feed_dir(run_dir: str | os.PathLike[str]) -> Path:
361
+ """Return the feed directory for ONE run: ``<runs parent>/.mr-events/<run name>/``.
362
+
363
+ The feed lives in the runs PARENT and never inside the run directory, for two reasons that both
364
+ matter:
365
+
366
+ * A failed build has no run directory at all. `build_candidate` stages into a hidden temporary
367
+ directory and renames only at the very end, and pre-creating the output raises
368
+ ``OUTPUT_EXISTS``. A feed inside the run directory could therefore never show a failure --
369
+ the exact case a watcher most wants to see.
370
+ * A file inside the run directory would be a new file inside a closed candidate member set
371
+ (G1). The feed must never be a member of anything.
372
+
373
+ It is scoped by the RUN'S OWN NAME, which is what makes a feed belong to a run. ``mr-data
374
+ build`` and ``mr-data recipe-run`` routinely put many runs in one parent; a flat feed directory
375
+ made "newest file wins" the only available rule, and a reader following it narrated one run's
376
+ build under another run's name and then raised a tamper refusal on the first receipt link,
377
+ because the digests it cited belonged to the other run. A directory per run removes the
378
+ ambiguity rather than resolving it: a foreign feed is not in the directory at all.
379
+ """
380
+
381
+ resolved = Path(run_dir).absolute()
382
+ return resolved.parent / FEED_DIR_NAME / _require_run_name(resolved.name)
383
+
384
+
385
+ def feed_path(output_dir: str | os.PathLike[str], producer_attempt: str) -> Path:
386
+ """Return the feed file for one build attempt: ``<run feed dir>/<attempt>.jsonl``.
387
+
388
+ ``producer_attempt`` is matched against ``[A-Za-z0-9._-]{1,64}`` before it reaches a path, so an
389
+ attempt id can never traverse. Each attempt has its own file and timestamp origin.
390
+ """
391
+
392
+ return run_feed_dir(output_dir) / f"{_require_attempt_id(producer_attempt)}.jsonl"
393
+
394
+
395
+ def _require_run_name(name: str) -> str:
396
+ """Refuse a run name that cannot be one path component. Never repairs, only refuses."""
397
+
398
+ if not name or name in {".", ".."} or len(name) > 128:
399
+ raise ValueError(f"run name is not a safe path component: {name!r}")
400
+ if "/" in name or "\\" in name or "\x00" in name:
401
+ raise ValueError(f"run name is not a safe path component: {name!r}")
402
+ return name
403
+
404
+
405
+ def _require_attempt_id(producer_attempt: str) -> str:
406
+ if not isinstance(producer_attempt, str) or not _ATTEMPT_RE.match(producer_attempt):
407
+ raise ValueError(f"producer attempt id is not a safe token: {producer_attempt!r}")
408
+ if producer_attempt in {".", ".."}:
409
+ raise ValueError(f"producer attempt id is not a safe token: {producer_attempt!r}")
410
+ return producer_attempt
411
+
412
+
413
+ def _require_member_path(value: Any, key: str) -> None:
414
+ if not isinstance(value, str) or not _MEMBER_PATH_RE.match(value):
415
+ raise ValueError(f"{key} is not a candidate-relative member path: {value!r}")
416
+ if value.startswith("/") or ".." in value.split("/"):
417
+ raise ValueError(f"{key} is not a candidate-relative member path: {value!r}")
418
+
419
+
420
+ def _iter_strings(value: Any, key: str) -> Any:
421
+ """Yield every ``(key, string)`` pair reachable in a fact or evidence value."""
422
+
423
+ if isinstance(value, str):
424
+ yield key, value
425
+ elif isinstance(value, Mapping):
426
+ for inner_key, inner in value.items():
427
+ yield from _iter_strings(inner, f"{key}.{inner_key}")
428
+ elif isinstance(value, (list, tuple)):
429
+ for index, item in enumerate(value):
430
+ yield from _iter_strings(item, f"{key}[{index}]")
431
+
432
+
433
+ class PlaceNamedError(ValueError):
434
+ """A fact string named a local place instead of portable evidence.
435
+
436
+ A distinct type because a distinct RECOVERY applies. A producer whose fact KEYS drifted from
437
+ ``EVENT_FACTS`` is a code defect and disarms the sink deliberately, so the drift shows up as a
438
+ truncated feed rather than as a feed full of wrong records. A fact whose VALUE happened to
439
+ carry a path is not that: the record is still true, only unportable, so the sink redacts the
440
+ offending strings and continues writing the feed.
441
+ """
442
+
443
+
444
+ class UnwritableTextError(ValueError):
445
+ """A fact string carried text no surface can encode as UTF-8 -- a lone surrogate.
446
+
447
+ A distinct type for the same reason ``PlaceNamedError`` is one: a distinct RECOVERY applies.
448
+ The record is still true, only unwritable, so the sink repairs the offending strings through
449
+ ``writable_text`` and keeps narrating rather than disarming. See ``writable_text`` for what a
450
+ lone surrogate costs every surface that has to encode it.
451
+ """
452
+
453
+
454
+ def _require_location_blind(record: Mapping[str, Any]) -> None:
455
+ """Refuse any record that names a local place instead of portable evidence.
456
+
457
+ Local and hosted workers must emit identical records. Absolute paths, home directories,
458
+ ``file://`` URLs, UNC prefixes, and host names are therefore refused. Pointers use a
459
+ candidate-relative member path and SHA-256 digest.
460
+ """
461
+
462
+ evidence = record.get("evidence")
463
+ if evidence is not None:
464
+ if not isinstance(evidence, Mapping) or set(evidence) != _EVIDENCE_KEYS:
465
+ raise ValueError("evidence must be exactly {'member', 'sha256'} or None")
466
+ _require_member_path(evidence["member"], "evidence.member")
467
+ if not isinstance(evidence["sha256"], str) or not _SHA256_RE.match(evidence["sha256"]):
468
+ raise ValueError(f"evidence.sha256 is not a sha256 digest: {evidence['sha256']!r}")
469
+
470
+ facts = record.get("facts") or {}
471
+ if "evidence_member" in facts:
472
+ _require_member_path(facts["evidence_member"], "facts.evidence_member")
473
+
474
+ for scope, container in (("facts", facts), ("evidence", evidence)):
475
+ if container is None:
476
+ continue
477
+ for key, text in _iter_strings(container, scope):
478
+ _require_placeless(key, text)
479
+
480
+
481
+ def _locator_tokens(text: str) -> Iterator[str]:
482
+ """Yield the whitespace-delimited tokens of ``text``, unwrapped from sentence punctuation.
483
+
484
+ The guard reads a fact string token by token rather than as one blob because local paths and
485
+ remote locators can both live inside prose. ``output already exists: /private/tmp/run`` carries
486
+ a place in its last token; ``downloading from https://noaa.gov/t00z.grib2`` carries a placeless
487
+ locator in its last token. A rule anchored at position 0 gets both wrong in opposite directions.
488
+ """
489
+
490
+ for raw in text.split():
491
+ token = raw.strip(_TOKEN_TRIM)
492
+ if token:
493
+ yield token
494
+
495
+
496
+ def _require_placeless(key: str, text: str) -> None:
497
+ """Refuse a string that names THIS machine. A remote locator is not such a string.
498
+
499
+ The invariant is portability, not punctuation: the record a Cloud Run job emits for a Build
500
+ must be the record this laptop emits for it. ``/Users/rob/runs/out.csv`` fails that test;
501
+ ``https://noaa.gov/hrrr/t00z.grib2`` passes it, because the URL resolves to the same bytes from
502
+ either machine. Both tests apply WHEREVER the string in question sits:
503
+
504
+ * a place is refused anywhere -- ``output already exists: /private/tmp/run`` is the shape every
505
+ ``BuildError`` message in this tree has, and a guard anchored at position 0 admitted all of
506
+ them, so a real ``build_failed`` record carried this disk's paths onto the feed and onto both
507
+ UI surfaces;
508
+ * a scheme-qualified remote locator is admitted anywhere -- the guard used to refuse every
509
+ string containing ``//``, which refused exactly the fact the Courier and Reader lanes carry.
510
+
511
+ Still refused, however the string is dressed: ``file://`` and any other local scheme, a rooted
512
+ absolute path, a Windows drive, a UNC share, ``~``, a loopback or ``.localhost`` authority
513
+ (this machine wearing a URL), and this host's own name.
514
+
515
+ Raises :class:`PlaceNamedError` so a caller can tell "this record is unportable" (redactable)
516
+ apart from "this record is malformed" (a producer defect).
517
+ """
518
+
519
+ lowered = text.lower()
520
+ if "file://" in lowered:
521
+ raise PlaceNamedError(f"{key} names a file URL: {text!r}")
522
+ # A UNC share, checked literally rather than through `os.sep` so the rule is the same on every
523
+ # platform (on POSIX `os.sep + os.sep` is just `//`, which the per-token branch below catches).
524
+ if "\\\\" in text:
525
+ raise PlaceNamedError(f"{key} names a network share: {text!r}")
526
+ if _HOSTNAME_MATCH and _HOSTNAME_MATCH in lowered:
527
+ raise PlaceNamedError(f"{key} names this machine")
528
+
529
+ for token in _locator_tokens(text):
530
+ scheme_match = _URL_SCHEME_RE.match(token)
531
+ if scheme_match is not None:
532
+ scheme = scheme_match.group(1).lower()
533
+ if scheme in _LOCAL_URL_SCHEMES:
534
+ raise PlaceNamedError(f"{key} names a location on this machine: {token!r}")
535
+ if _authority_is_local(token.lower()[scheme_match.end() :]):
536
+ raise PlaceNamedError(f"{key} names this machine: {token!r}")
537
+ # A remote locator means the same thing from every machine, so it is placeless.
538
+ continue
539
+
540
+ if token.startswith("/") or _EMBEDDED_ABS_PATH_RE.search(token):
541
+ raise PlaceNamedError(f"{key} names an absolute path: {token!r}")
542
+ if _WINDOWS_DRIVE_RE.search(token):
543
+ raise PlaceNamedError(f"{key} names an absolute path: {token!r}")
544
+ if token.startswith("~") or "~/" in token:
545
+ raise PlaceNamedError(f"{key} names a home directory: {token!r}")
546
+ if "//" in token:
547
+ raise PlaceNamedError(f"{key} names a network or URL location: {token!r}")
548
+
549
+
550
+ def _authority_is_local(remainder: str) -> bool:
551
+ """True when a URL's authority is a loopback name this machine alone resolves."""
552
+
553
+ authority = remainder.split("/", 1)[0].split("?", 1)[0].split("#", 1)[0]
554
+ authority = authority.rsplit("@", 1)[-1]
555
+ if authority.startswith("["):
556
+ host = authority.split("]", 1)[0].lstrip("[")
557
+ else:
558
+ host = authority.split(":", 1)[0]
559
+ return host in _LOCAL_URL_HOSTS or host.endswith(".localhost")
560
+
561
+
562
+ def _normalize_json(value: Any, *, key: str, depth: int = 0) -> Any:
563
+ """Return a JSON-safe copy of ``value`` or raise ``ValueError``.
564
+
565
+ Fact values are JSON scalars, lists, and flat mappings of scalars -- `rights_checked` reports
566
+ one mapping per source, and a tuple on a dataclass has to arrive here as a list because JSON has
567
+ no tuple. Depth and width are bounded so a fact value can never be a document.
568
+ """
569
+
570
+ if depth > _MAX_FACT_DEPTH:
571
+ raise ValueError(f"fact {key!r} nests deeper than {_MAX_FACT_DEPTH}")
572
+ if value is None or isinstance(value, bool):
573
+ return value
574
+ if isinstance(value, str):
575
+ if writable_text(value) != value:
576
+ # BESIDE THE NON-FINITE FLOAT REFUSAL, and for the identical reason: a fact this writer
577
+ # accepts must be one every surface downstream can encode. `_dumps` uses
578
+ # `ensure_ascii=True`, so a lone surrogate was written out as a pure-ASCII `\ud800`
579
+ # escape and read back as a lone surrogate -- the writer's boundary and the reader's
580
+ # boundary were both open on the same value class. Refused here rather than repaired,
581
+ # so a producer that builds a fact out of undecodable bytes learns it; `event_sink`
582
+ # turns the refusal into a repaired record rather than a lost one, which is exactly
583
+ # what it already does for a fact that named a place.
584
+ raise UnwritableTextError(f"fact {key!r} carries text no surface can encode")
585
+ return value
586
+ if isinstance(value, int):
587
+ return value
588
+ if isinstance(value, float):
589
+ if not math.isfinite(value):
590
+ raise ValueError(f"fact {key!r} is not a finite number")
591
+ return value
592
+ if isinstance(value, Mapping):
593
+ if len(value) > MAX_FACT_KEYS:
594
+ raise ValueError(f"fact {key!r} carries more than {MAX_FACT_KEYS} keys")
595
+ normalized: dict[str, Any] = {}
596
+ for inner_key, inner in value.items():
597
+ if not isinstance(inner_key, str):
598
+ raise ValueError(f"fact {key!r} carries a non-string key")
599
+ if writable_text(inner_key) != inner_key:
600
+ raise UnwritableTextError(f"fact {key!r} carries a key no surface can encode")
601
+ normalized[inner_key] = _normalize_json(
602
+ inner, key=f"{key}.{inner_key}", depth=depth + 1
603
+ )
604
+ return normalized
605
+ if isinstance(value, Sequence):
606
+ items = list(value)
607
+ if len(items) > _MAX_LIST_ITEMS:
608
+ raise ValueError(f"fact {key!r} carries more than {_MAX_LIST_ITEMS} items")
609
+ return [_normalize_json(item, key=f"{key}[]", depth=depth + 1) for item in items]
610
+ raise ValueError(f"fact {key!r} is not a JSON value: {type(value).__name__}")
611
+
612
+
613
+ def _normalize_facts(event: str, facts: Mapping[str, Any]) -> dict[str, Any]:
614
+ return {key: _normalize_json(value, key=key) for key, value in facts.items()}
615
+
616
+
617
+ def open_regular_file(
618
+ name: str | os.PathLike[str],
619
+ *,
620
+ dir_fd: int | None = None,
621
+ flags: int = os.O_RDONLY,
622
+ mode: int = 0o600,
623
+ ) -> int:
624
+ """Open ``name`` as a nonblocking regular file without following links.
625
+
626
+ ``fstat`` validates the opened descriptor. With ``dir_fd``, each path component is opened using
627
+ ``O_DIRECTORY | O_NOFOLLOW`` beneath the caller's trusted root. Without ``dir_fd``, only the
628
+ leaf is validated.
629
+ """
630
+
631
+ parent_fd, leaf, borrowed = _descend_to_parent(name, dir_fd)
632
+ try:
633
+ handle_fd = os.open(leaf, flags | _O_NOFOLLOW | _O_NONBLOCK, mode, dir_fd=parent_fd)
634
+ finally:
635
+ if not borrowed and parent_fd is not None:
636
+ os.close(parent_fd)
637
+ try:
638
+ opened = os.fstat(handle_fd)
639
+ if not stat.S_ISREG(opened.st_mode) or opened.st_nlink != 1:
640
+ raise OSError("not a regular file where a regular file should be")
641
+ except BaseException:
642
+ os.close(handle_fd)
643
+ raise
644
+ return handle_fd
645
+
646
+
647
+ def open_directory(name: str | os.PathLike[str], *, dir_fd: int | None = None) -> int:
648
+ """Open ``name`` as a directory, refusing a symlink at every component ``name`` names.
649
+
650
+ The directory half of ``open_regular_file``, with the same trust rule: with ``dir_fd`` every
651
+ component of ``name`` is proved, without it the directory part of ``name`` is the caller's own
652
+ and the leaf is proved. Callers hold the returned descriptor and read relative to it, so the
653
+ directory measured and the directory read are the same object however the name is rebound
654
+ afterwards.
655
+ """
656
+
657
+ parent_fd, leaf, borrowed = _descend_to_parent(name, dir_fd)
658
+ try:
659
+ return _open_component_directory(leaf, parent_fd)
660
+ finally:
661
+ if not borrowed and parent_fd is not None:
662
+ os.close(parent_fd)
663
+
664
+
665
+ def duplicate_directory(directory_fd: int) -> int:
666
+ """Return an independently owned reference to an already proved directory descriptor."""
667
+
668
+ duplicate = os.dup(directory_fd)
669
+ try:
670
+ if not stat.S_ISDIR(os.fstat(duplicate).st_mode):
671
+ raise OSError(errno.ENOTDIR, "not a directory where a directory should be")
672
+ except BaseException:
673
+ os.close(duplicate)
674
+ raise
675
+ return duplicate
676
+
677
+
678
+ def _open_component_directory(name: str, dir_fd: int | None) -> int:
679
+ """Open ONE path component as a directory. Never follows a symlink, never blocks."""
680
+
681
+ fd = os.open(name, os.O_RDONLY | _O_DIRECTORY | _O_NOFOLLOW | _O_NONBLOCK, dir_fd=dir_fd)
682
+ try:
683
+ if not stat.S_ISDIR(os.fstat(fd).st_mode):
684
+ # Reachable only where `O_DIRECTORY` does not exist, in which case the flag is 0 and
685
+ # the open accepted a regular file. Asked of the DESCRIPTOR, as everywhere else here.
686
+ raise OSError(errno.ENOTDIR, "not a directory where a directory should be")
687
+ except BaseException:
688
+ os.close(fd)
689
+ raise
690
+ return fd
691
+
692
+
693
+ def _descend_to_parent(
694
+ name: str | os.PathLike[str], dir_fd: int | None
695
+ ) -> tuple[int | None, str, bool]:
696
+ """Return ``(parent_fd, leaf, borrowed)`` for ``name``. ``borrowed`` fds are the caller's.
697
+
698
+ Below a ``dir_fd`` every component is opened on its own with ``O_NOFOLLOW``, so a symlinked
699
+ directory component is refused instead of followed. ``.`` and ``..`` are refused outright: a
700
+ descent that honoured them would walk back out of the directory the caller proved, which is
701
+ the whole thing the descent exists to prevent.
702
+ """
703
+
704
+ parts = PurePath(os.fspath(name)).parts
705
+ if not parts:
706
+ raise OSError(errno.ENOENT, "an empty name opens nothing")
707
+ if dir_fd is None:
708
+ if len(parts) == 1:
709
+ return None, parts[0], True
710
+ prefix = os.path.dirname(os.fspath(name))
711
+ return os.open(prefix, os.O_RDONLY | _O_DIRECTORY), parts[-1], False
712
+ if os.path.isabs(os.fspath(name)):
713
+ raise OSError(errno.EINVAL, "an absolute name is not relative to a directory")
714
+
715
+ parent_fd, borrowed = dir_fd, True
716
+ try:
717
+ for part in parts[:-1]:
718
+ if part in (os.curdir, os.pardir):
719
+ raise OSError(errno.EINVAL, f"a member path may not contain {part!r}")
720
+ child = _open_component_directory(part, parent_fd)
721
+ if not borrowed:
722
+ os.close(parent_fd)
723
+ parent_fd, borrowed = child, False
724
+ except BaseException:
725
+ if not borrowed:
726
+ os.close(parent_fd)
727
+ raise
728
+ return parent_fd, parts[-1], borrowed
729
+
730
+
731
+ def _regular_file_identity(info: os.stat_result) -> tuple[int, ...]:
732
+ """Identity fields that must stay fixed while a mutable plain file is read."""
733
+
734
+ return (
735
+ info.st_dev,
736
+ info.st_ino,
737
+ info.st_mode,
738
+ info.st_nlink,
739
+ info.st_uid,
740
+ info.st_gid,
741
+ info.st_size,
742
+ info.st_mtime_ns,
743
+ info.st_ctime_ns,
744
+ )
745
+
746
+
747
+ def read_regular_bytes(
748
+ path: str | os.PathLike[str], *, dir_fd: int | None = None, max_bytes: int
749
+ ) -> bytes:
750
+ """Read one regular file whole through ``open_regular_file``. Raises ``OSError``, never blocks.
751
+
752
+ The descriptor-based read rejects symlinks and non-regular files without a separate path lookup.
753
+ Nonblocking open prevents a FIFO from stalling the caller.
754
+
755
+ ``max_bytes`` refuses rather than allocates, and the size it tests comes off the DESCRIPTOR, not
756
+ off a second look at the path. The read is bounded one byte past the ceiling so a file that grew
757
+ between the two is refused as oversized rather than read without a limit.
758
+
759
+ ``dir_fd`` reads ``path`` relative to a directory descriptor the caller already holds, which is
760
+ how ``rederive_feed`` reads a candidate the verify has pinned open rather than re-walking the
761
+ run directory by name. It is the same argument ``watch`` passes for the same reason.
762
+ """
763
+
764
+ handle_fd = open_regular_file(path, dir_fd=dir_fd)
765
+ try:
766
+ before = os.fstat(handle_fd)
767
+ if before.st_size > max_bytes:
768
+ raise OSError("larger than this reader's ceiling")
769
+ chunks: list[bytes] = []
770
+ remaining = max_bytes + 1
771
+ while remaining > 0:
772
+ chunk = os.read(handle_fd, min(remaining, 1 << 16))
773
+ if not chunk:
774
+ break
775
+ chunks.append(chunk)
776
+ remaining -= len(chunk)
777
+ payload = b"".join(chunks)
778
+ after = os.fstat(handle_fd)
779
+ finally:
780
+ os.close(handle_fd)
781
+ if len(payload) > max_bytes:
782
+ raise OSError("larger than this reader's ceiling")
783
+ if _regular_file_identity(before) != _regular_file_identity(after):
784
+ raise OSError("regular file changed while it was read")
785
+ named = os.stat(path, dir_fd=dir_fd, follow_symlinks=False)
786
+ if not stat.S_ISREG(named.st_mode) or named.st_nlink != 1:
787
+ raise OSError("not a regular file where a regular file should be")
788
+ if _regular_file_identity(before) != _regular_file_identity(named):
789
+ raise OSError("regular file name changed while it was read")
790
+ return payload
791
+
792
+
793
+ def snapshot_candidate_namespace(
794
+ candidate_fd: int,
795
+ members: Sequence[str],
796
+ *,
797
+ manifest_name: str = "manifest.json",
798
+ ) -> tuple[tuple[tuple[str, ...], tuple[int, ...], tuple[str, ...]], ...]:
799
+ """Return the exact descriptor-relative directory namespace for candidate members.
800
+
801
+ Every directory is opened through :func:`open_directory`, and its identity is checked before
802
+ and after listing. The result is suitable as one component of a retained proof: an added,
803
+ removed, replaced, or permission-changed directory invalidates the proof even when all named
804
+ member files are otherwise unchanged.
805
+ """
806
+
807
+ expected: dict[tuple[str, ...], set[str]] = {(): {manifest_name}}
808
+ for member in members:
809
+ _require_member_path(member, "manifest member path")
810
+ parts = tuple(member.split("/"))
811
+ parent: tuple[str, ...] = ()
812
+ for component in parts[:-1]:
813
+ expected.setdefault(parent, set()).add(component)
814
+ parent = (*parent, component)
815
+ expected.setdefault(parent, set())
816
+ expected.setdefault(parent, set()).add(parts[-1])
817
+
818
+ observed: list[tuple[tuple[str, ...], tuple[int, ...], tuple[str, ...]]] = []
819
+ for parts, expected_children in sorted(expected.items()):
820
+ directory_fd = (
821
+ os.dup(candidate_fd)
822
+ if not parts
823
+ else open_directory("/".join(parts), dir_fd=candidate_fd)
824
+ )
825
+ try:
826
+ before = os.fstat(directory_fd)
827
+ children = tuple(sorted(os.listdir(directory_fd)))
828
+ after = os.fstat(directory_fd)
829
+ finally:
830
+ os.close(directory_fd)
831
+ before_identity = _directory_identity(before)
832
+ if before_identity != _directory_identity(after):
833
+ raise OSError("candidate directory changed while its namespace was listed")
834
+ if set(children) != expected_children:
835
+ raise OSError("candidate namespace differs from its manifest")
836
+ observed.append((parts, before_identity, children))
837
+ return tuple(observed)
838
+
839
+
840
+ def _directory_identity(info: os.stat_result) -> tuple[int, ...]:
841
+ return (
842
+ info.st_dev,
843
+ info.st_ino,
844
+ info.st_mode,
845
+ info.st_nlink,
846
+ info.st_uid,
847
+ info.st_gid,
848
+ info.st_mtime_ns,
849
+ info.st_ctime_ns,
850
+ )
851
+
852
+
853
+ def _create_feed_directory(feed_dir: str | os.PathLike[str]) -> int:
854
+ """Create a private feed directory and return its validated descriptor.
855
+
856
+ Each component is created relative to its parent descriptor, opened with
857
+ ``O_DIRECTORY | O_NOFOLLOW``, checked for ownership, and tightened to mode 0700. Directories
858
+ owned by another principal are refused.
859
+ """
860
+
861
+ feed_dir = Path(feed_dir)
862
+ feed_dir.parent.parent.mkdir(parents=True, exist_ok=True)
863
+ root_fd = os.open(feed_dir.parent.parent, os.O_RDONLY | _O_DIRECTORY)
864
+ try:
865
+ events_fd = _create_private_directory(feed_dir.parent.name, root_fd)
866
+ finally:
867
+ os.close(root_fd)
868
+ try:
869
+ return _create_private_directory(feed_dir.name, events_fd)
870
+ finally:
871
+ os.close(events_fd)
872
+
873
+
874
+ def _create_private_directory(name: str, dir_fd: int) -> int:
875
+ """Create one component ``0o700`` under ``dir_fd`` and return a descriptor that proves it."""
876
+
877
+ try:
878
+ os.mkdir(name, 0o700, dir_fd=dir_fd)
879
+ except FileExistsError:
880
+ pass
881
+ fd = _open_component_directory(name, dir_fd)
882
+ try:
883
+ info = os.fstat(fd)
884
+ getuid = getattr(os, "getuid", None)
885
+ if getuid is not None and info.st_uid != getuid():
886
+ raise OSError(errno.EPERM, f"feed directory {name!r} belongs to another user")
887
+ if stat.S_IMODE(info.st_mode) != 0o700:
888
+ # Ours, so it is tightened rather than refused -- through the descriptor, so the
889
+ # object whose mode changes is the object that was proved.
890
+ os.fchmod(fd, 0o700)
891
+ except BaseException:
892
+ os.close(fd)
893
+ raise
894
+ return fd
895
+
896
+
897
+ class EventWriter:
898
+ """An append-only writer over one feed file.
899
+
900
+ One descriptor per feed, opened ``O_APPEND`` and written one whole line per ``os.write`` call,
901
+ so a reader tailing the file never sees a torn record and two writers on the same feed cannot
902
+ interleave a line. ``O_NOFOLLOW`` because another local user may have swapped a symlink in.
903
+ """
904
+
905
+ def __init__(self, path: str | os.PathLike[str], *, run: str | None = None) -> None:
906
+ self._path = Path(path)
907
+ self._run = _require_attempt_id(run if run is not None else self._path.stem)
908
+ self._lock = threading.Lock()
909
+
910
+ existing = read_feed(self._path)
911
+ self._seq = int(existing[-1]["seq"]) if existing else 0
912
+ self._last_at = float(existing[-1]["at"]) if existing else 0.0
913
+ torn_tail = _ends_mid_record(self._path)
914
+
915
+ feed_dir_fd = _create_feed_directory(self._path.parent)
916
+ try:
917
+ # Through the same descriptor-checked open the readers use, and RELATIVE to the
918
+ # directory descriptor just proved. A FIFO where the feed should be blocks a WRITE open
919
+ # too -- until some process opens it for reading, which may be never -- so `mr-data
920
+ # run` hung while arming its feed, before the build had done anything at all.
921
+ self._fd: int | None = open_regular_file(
922
+ self._path.name,
923
+ dir_fd=feed_dir_fd,
924
+ flags=os.O_WRONLY | os.O_CREAT | os.O_APPEND,
925
+ mode=0o600,
926
+ )
927
+ finally:
928
+ os.close(feed_dir_fd)
929
+ if os.fstat(self._fd).st_size == 0:
930
+ header = {
931
+ "v": 1,
932
+ "kind": "header",
933
+ "schema_version": FEED_SCHEMA_VERSION,
934
+ "run": self._run,
935
+ }
936
+ os.write(self._fd, (_dumps(header) + "\n").encode("utf-8"))
937
+ elif torn_tail:
938
+ # The feed ends mid-record: a previous writer died, or the file was truncated. Close the
939
+ # torn line off with a newline before appending, so the next record starts on a line of
940
+ # its own instead of being glued onto the wreckage. The torn line stays unreadable --
941
+ # that is correct, it was never complete -- but every record after it is readable again.
942
+ os.write(self._fd, b"\n")
943
+
944
+ @property
945
+ def path(self) -> Path:
946
+ return self._path
947
+
948
+ def write(
949
+ self,
950
+ event: str,
951
+ facts: Mapping[str, Any],
952
+ evidence: Mapping[str, Any] | None = None,
953
+ ) -> None:
954
+ """Append one record. Raises ``ValueError`` on anything the vocabulary does not allow."""
955
+
956
+ if event not in _ALL_EVENT_NAMES:
957
+ raise ValueError(f"event name is outside the closed vocabulary: {event!r}")
958
+ validate_facts(event, facts)
959
+ normalized_facts = _normalize_facts(event, facts)
960
+ normalized_evidence = None if evidence is None else dict(evidence)
961
+
962
+ with self._lock:
963
+ if self._fd is None:
964
+ raise ValueError("event writer is closed")
965
+ now = time.time()
966
+ if now < self._last_at:
967
+ # A feed's `at` is strictly non-decreasing: a reader draws elapsed time from it and
968
+ # a clock that stepped backwards must not read as a stage that took negative time.
969
+ now = self._last_at
970
+ record = {
971
+ "seq": self._seq + 1,
972
+ "at": now,
973
+ "event": event,
974
+ "facts": normalized_facts,
975
+ "evidence": normalized_evidence,
976
+ }
977
+ _require_location_blind(record)
978
+ line = (_dumps(record) + "\n").encode("utf-8")
979
+ os.write(self._fd, line)
980
+ self._seq += 1
981
+ self._last_at = now
982
+
983
+ def close(self) -> None:
984
+ with self._lock:
985
+ if self._fd is not None:
986
+ os.close(self._fd)
987
+ self._fd = None
988
+
989
+ def __enter__(self) -> EventWriter:
990
+ return self
991
+
992
+ def __exit__(self, *exc_info: object) -> None:
993
+ self.close()
994
+
995
+
996
+ def _ends_mid_record(path: Path) -> bool:
997
+ """True when the feed exists, is non-empty, and its last byte is not a newline.
998
+
999
+ The size comes off the same descriptor the seek uses, rather than off a ``stat`` of the name
1000
+ followed by an open of the name: the second look is the one another local user gets to answer.
1001
+ """
1002
+
1003
+ try:
1004
+ with os.fdopen(open_regular_file(path), "rb") as handle:
1005
+ size = os.fstat(handle.fileno()).st_size
1006
+ if size == 0:
1007
+ return False
1008
+ handle.seek(size - 1)
1009
+ return handle.read(1) != b"\n"
1010
+ except OSError:
1011
+ return False
1012
+
1013
+
1014
+ def _dumps(payload: Mapping[str, Any]) -> str:
1015
+ return json.dumps(
1016
+ payload, ensure_ascii=True, allow_nan=False, sort_keys=True, separators=(",", ":")
1017
+ )
1018
+
1019
+
1020
+ def read_feed(path: str | os.PathLike[str]) -> list[dict[str, Any]]:
1021
+ """Read a feed. This is the untrusted-input boundary: it never raises for a malformed feed.
1022
+
1023
+ A missing file, a directory, an oversized file, an absent or foreign header -- each reads back
1024
+ as no feed at all. A feed is ignored WHOLE or understood; it is never half-understood. A
1025
+ trailing line with no newline is the torn record a concurrent writer is mid-way through, and it
1026
+ is dropped rather than parsed.
1027
+
1028
+ ``read_feed`` deliberately does NOT call ``validate_facts``. A reader that refused a record
1029
+ because its keys drifted would turn a producer bug into a blank page; the reader's job is to
1030
+ show whatever the feed safely said, and the fact spec is enforced where the record is written.
1031
+
1032
+ An empty result is TWO states, and a reader that must tell them apart asks ``feed_is_readable``.
1033
+ """
1034
+
1035
+ return _read_feed_parts(path)[1]
1036
+
1037
+
1038
+ def feed_is_readable(path: str | os.PathLike[str]) -> bool:
1039
+ """Whether these bytes read back AS one of our feeds, however little the feed says.
1040
+
1041
+ ``read_feed`` returns ``[]`` for two states a reader must never confuse.
1042
+
1043
+ BYTES THAT ARE NOT A FEED read as nothing: a missing or oversized file, a foreign or absent
1044
+ header, a first line still being written. A reader should disbelieve them and keep whatever it
1045
+ last understood.
1046
+
1047
+ AN ATTEMPT THAT NARRATED NOTHING is a perfectly good feed that has no records yet, or will never
1048
+ have any. It is the routine product of an idempotent re-run -- ``mr-data run`` on an
1049
+ already-built workspace returns the existing result without rebuilding, so it arms a feed and
1050
+ writes only the header -- and it is also every armed feed for the instant before its first
1051
+ record. Treating it as unreadable bytes made a completed, verifying run render as though nothing
1052
+ had happened: a header-only file never changes size, so a watcher that memoised it as corrupt
1053
+ blocked the receipts fallback -- the one that makes deleting a feed cost nothing but the show --
1054
+ for the rest of the server's life.
1055
+ """
1056
+
1057
+ return _read_feed_parts(path)[0]
1058
+
1059
+
1060
+ def _read_feed_parts(path: str | os.PathLike[str]) -> tuple[bool, list[dict[str, Any]]]:
1061
+ """``(these bytes are one of our feeds, the records they carry)``. Never raises."""
1062
+
1063
+ # ONE open, through the descriptor-checked helper. This used to be `stat`, then
1064
+ # `os.path.isfile`, then `Path.read_bytes` -- three separate looks at one name, the last of them
1065
+ # a blocking open. A local user swapping a FIFO in between the check and the read parked the
1066
+ # watcher's poll thread forever, four times a second's worth of chances to win the race, while
1067
+ # the server kept serving a snapshot that could never change again.
1068
+ try:
1069
+ payload = read_regular_bytes(Path(path), max_bytes=MAX_FEED_BYTES)
1070
+ except OSError:
1071
+ return False, []
1072
+
1073
+ text = payload.decode("utf-8", "replace")
1074
+ if not text:
1075
+ return False, []
1076
+ if text.endswith("\n"):
1077
+ lines = text[:-1].split("\n")
1078
+ else:
1079
+ # The final line has no newline: it is torn, and the whole recovery rule is to drop it.
1080
+ lines = text.split("\n")[:-1]
1081
+ if not lines:
1082
+ return False, []
1083
+
1084
+ if not _header_is_ours(lines[0]):
1085
+ return False, []
1086
+
1087
+ records: list[dict[str, Any]] = []
1088
+ for line in lines[1:]:
1089
+ if len(records) >= MAX_FEED_RECORDS:
1090
+ break
1091
+ record = _read_record(line)
1092
+ if record is not None:
1093
+ records.append(record)
1094
+ return True, records
1095
+
1096
+
1097
+ def select_narrated_feed(
1098
+ feed_dir: str | os.PathLike[str],
1099
+ *,
1100
+ sealed_candidate_digest: str | None = None,
1101
+ ) -> tuple[Path | None, int, list[dict[str, Any]]]:
1102
+ """Select one attempt as ``(path, size, records)``.
1103
+
1104
+ A matching sealed digest wins. Otherwise, finished attempts that did not produce the sealed
1105
+ Build are ignored, and the newest nonempty active attempt is selected. At most
1106
+ ``MAX_FEED_ATTEMPTS`` nonempty files are considered. If only an unreadable or empty file exists,
1107
+ the newest path is returned with no records so the caller can report it.
1108
+ """
1109
+
1110
+ found: list[tuple[float, str, int]] = []
1111
+ newest_empty: tuple[float, str, int] | None = None
1112
+ try:
1113
+ with os.scandir(feed_dir) as entries:
1114
+ for entry in entries:
1115
+ if not entry.name.endswith(".jsonl"):
1116
+ continue
1117
+ try:
1118
+ # follow_symlinks=False: another local user may have pointed a name here.
1119
+ info = entry.stat(follow_symlinks=False)
1120
+ except OSError:
1121
+ continue
1122
+ if not stat.S_ISREG(info.st_mode):
1123
+ # THIS IS A SORT, NOT A GUARD. It picks which names are worth reading and
1124
+ # gathers the mtime they are ordered by; it says nothing about what the name
1125
+ # will mean when `read_feed` below opens it. Reading it as protection is how
1126
+ # the blocking open under it went unnoticed -- the open itself is what has to
1127
+ # be safe, and `open_regular_file` is what makes it so.
1128
+ continue
1129
+ candidate = (info.st_mtime, entry.name, info.st_size)
1130
+ if info.st_size == _empty_attempt_size(entry.name):
1131
+ # Empty no-op invocations do not use nonempty-attempt slots, so idempotent
1132
+ # re-runs cannot evict the attempt that sealed the candidate.
1133
+ if newest_empty is None or candidate > newest_empty:
1134
+ newest_empty = candidate
1135
+ continue
1136
+ # Bound the set while scanning. A min-heap of ``MAX_FEED_ATTEMPTS`` keeps the newest
1137
+ # files regardless of the order the directory
1138
+ # yields its names in, so neither the list nor the reads below it grow with the
1139
+ # number of files somebody put here. Appending first and truncating after would
1140
+ # have bounded the reads and not the scan.
1141
+ if len(found) < MAX_FEED_ATTEMPTS:
1142
+ heapq.heappush(found, candidate)
1143
+ else:
1144
+ heapq.heappushpop(found, candidate)
1145
+ except OSError:
1146
+ return None, 0, []
1147
+ if not found and newest_empty is None:
1148
+ return None, 0, []
1149
+
1150
+ found.sort(reverse=True)
1151
+ base = Path(feed_dir)
1152
+ narrated: tuple[Path, int, list[dict[str, Any]]] | None = None
1153
+ narrating: tuple[Path, int, list[dict[str, Any]]] | None = None
1154
+ matching: tuple[Path, int, list[dict[str, Any]]] | None = None
1155
+ narrating_precedes_match = False
1156
+ for _mtime, name, size in found:
1157
+ records = read_feed(base / name)
1158
+ if not records:
1159
+ continue
1160
+ candidate = (base / name, size, records)
1161
+ if (
1162
+ matching is None
1163
+ and sealed_candidate_digest is not None
1164
+ and _sealed(records) == sealed_candidate_digest
1165
+ ):
1166
+ matching = candidate
1167
+ narrating_precedes_match = narrating is not None
1168
+ if narrated is None:
1169
+ narrated = candidate
1170
+ if narrating is None and not _finished(records):
1171
+ narrating = candidate
1172
+ if sealed_candidate_digest is not None:
1173
+ # A newer unfinished attempt is a retry narrating work after the retained matching seal.
1174
+ # Return it so the consumer can validate its own ``run_started`` and enforce monotonic
1175
+ # attempt replacement. Otherwise the matching seal remains the candidate's narration.
1176
+ if narrating is not None and (matching is None or narrating_precedes_match):
1177
+ return narrating
1178
+ return matching if matching is not None else (None, 0, [])
1179
+ if narrated is not None:
1180
+ return narrated
1181
+ if found:
1182
+ _mtime, name, size = found[0]
1183
+ else:
1184
+ assert newest_empty is not None
1185
+ _mtime, name, size = newest_empty
1186
+ return base / name, size, []
1187
+
1188
+
1189
+ def _finished(records: Sequence[Mapping[str, Any]]) -> bool:
1190
+ """Return whether this attempt sealed, stopped, or closed a sidecar-only invocation."""
1191
+
1192
+ if _sealed(records) is not None:
1193
+ return True
1194
+ return records[-1].get("event") in (TERMINAL_EVENT_NAMES | NOTEBOOK_SIDECAR_OUTCOME_EVENT_NAMES)
1195
+
1196
+
1197
+ def _sealed(records: Sequence[Mapping[str, Any]]) -> str | None:
1198
+ """The candidate digest this attempt sealed, or ``None`` if it sealed nothing."""
1199
+
1200
+ for record in reversed(records):
1201
+ if record.get("event") != "build_sealed":
1202
+ continue
1203
+ facts = record.get("facts")
1204
+ digest = facts.get("candidate_digest") if isinstance(facts, Mapping) else None
1205
+ return digest if isinstance(digest, str) else None
1206
+ return None
1207
+
1208
+
1209
+ def _header_is_ours(line: str) -> bool:
1210
+ try:
1211
+ header = json.loads(line)
1212
+ except (ValueError, RecursionError):
1213
+ return False
1214
+ return (
1215
+ isinstance(header, dict)
1216
+ and header.get("kind") == "header"
1217
+ and header.get("schema_version") == FEED_SCHEMA_VERSION
1218
+ )
1219
+
1220
+
1221
+ def _read_record(line: str) -> dict[str, Any] | None:
1222
+ if not line:
1223
+ return None
1224
+ try:
1225
+ parsed = json.loads(line)
1226
+ except (ValueError, RecursionError):
1227
+ return None
1228
+ if not isinstance(parsed, dict) or set(parsed) != set(_RECORD_KEYS):
1229
+ return None
1230
+
1231
+ seq = parsed["seq"]
1232
+ at = parsed["at"]
1233
+ event = parsed["event"]
1234
+ facts = parsed["facts"]
1235
+ evidence = parsed["evidence"]
1236
+ if isinstance(seq, bool) or not isinstance(seq, int):
1237
+ return None
1238
+ if isinstance(at, bool) or not isinstance(at, (int, float)):
1239
+ return None
1240
+ try:
1241
+ moment = float(at)
1242
+ except (OverflowError, ValueError):
1243
+ # Convert once and drop values that cannot be represented as floats.
1244
+ return None
1245
+ if not _EARLIEST_MOMENT <= moment <= _LATEST_MOMENT:
1246
+ # A MOMENT ON A CLOCK, not merely a finite float, and the difference between those two
1247
+ # questions is a permanent stall. `json.loads` accepts the bare tokens `Infinity`,
1248
+ # `-Infinity` and `NaN`, and it accepts `1.7e308` -- which is finite, is genuinely a float,
1249
+ # and is not a time. Every reader of this record subtracts one moment from another and
1250
+ # converts the difference to an integer: `watch._render_header` does it against the wall
1251
+ # clock, and `watch._render_log` does it against the FIRST record's moment, which is the
1252
+ # one that matters here. Two admitted records at opposite ends of the float range make
1253
+ # that subtraction overflow to `inf`, and `int(inf)` raises. A finiteness test cannot see
1254
+ # it, because both operands are finite; only a bound on the value can.
1255
+ #
1256
+ # However it arrives -- a token that is not a number, an integer no float can hold, or a
1257
+ # float no clock can name -- the cost used to be the whole view for the life of the
1258
+ # server: the watcher counted a failed poll, spliced the stall note onto the last good page
1259
+ # and never rendered another record, and no `except` downstream could give the page back
1260
+ # because the poisoned record was still in the feed on every subsequent read. `mr-data
1261
+ # fleet` had it worse: `scan_fleet` reads every child's feed through this same boundary and
1262
+ # catches only `OSError`, so one poisoned line in one run's feed ended the whole scan with a
1263
+ # traceback and no rows at all, for every other run in the directory.
1264
+ #
1265
+ # This is the boundary that exists to stop that. A record whose `at` is not a moment this
1266
+ # reader can carry is dropped here, the same way a record whose `at` is a string is dropped,
1267
+ # and the rest of the feed is read normally -- one bad line costs that line and never the
1268
+ # view. A guard in the renderers would have been a second place that has to know this rule,
1269
+ # and each renderer would have had to know it about PAIRS of records rather than about one.
1270
+ return None
1271
+ if not isinstance(event, str) or not isinstance(facts, dict):
1272
+ return None
1273
+ if evidence is not None:
1274
+ if not isinstance(evidence, dict) or set(evidence) != set(_EVIDENCE_KEYS):
1275
+ return None
1276
+ if not isinstance(evidence["member"], str) or not isinstance(evidence["sha256"], str):
1277
+ return None
1278
+
1279
+ clamped_facts = {
1280
+ # The KEY crosses the same boundary as the value. `json.loads` puts a lone surrogate in a
1281
+ # key as readily as in a string, and every surface that re-encodes the record encodes both.
1282
+ _clamp_text(key): _clamp(value)
1283
+ for key, value in list(facts.items())[:MAX_FACT_KEYS]
1284
+ if isinstance(key, str)
1285
+ }
1286
+ clamped_evidence = None if evidence is None else {k: _clamp(v) for k, v in evidence.items()}
1287
+ return {
1288
+ "seq": seq,
1289
+ "at": moment,
1290
+ "event": _clamp_text(event),
1291
+ "facts": clamped_facts,
1292
+ "evidence": clamped_evidence,
1293
+ }
1294
+
1295
+
1296
+ def writable_text(value: str) -> str:
1297
+ """Return UTF-8-encodable text, escaping lone surrogates as ASCII byte sequences.
1298
+
1299
+ Escaping occurs before length clamping so the result remains within the text ceiling.
1300
+ """
1301
+
1302
+ try:
1303
+ value.encode("utf-8")
1304
+ except UnicodeEncodeError:
1305
+ return value.encode("utf-8", "backslashreplace").decode("ascii")
1306
+ return value
1307
+
1308
+
1309
+ def _clamp_text(value: str) -> str:
1310
+ value = writable_text(value)
1311
+ if len(value) <= MAX_TEXT_CHARS:
1312
+ return value
1313
+ return value[: MAX_TEXT_CHARS - len(_TRUNCATION_MARKER)] + _TRUNCATION_MARKER
1314
+
1315
+
1316
+ def _clamp(value: Any, depth: int = 0) -> Any:
1317
+ if isinstance(value, str):
1318
+ return _clamp_text(value)
1319
+ if depth >= _MAX_FACT_DEPTH:
1320
+ return None
1321
+ if isinstance(value, dict):
1322
+ return {
1323
+ _clamp_text(key): _clamp(inner, depth + 1)
1324
+ for key, inner in list(value.items())[:MAX_FACT_KEYS]
1325
+ if isinstance(key, str)
1326
+ }
1327
+ if isinstance(value, list):
1328
+ return [_clamp(item, depth + 1) for item in value[:_MAX_LIST_ITEMS]]
1329
+ if isinstance(value, float) and not math.isfinite(value):
1330
+ # A FACT VALUE THAT CANNOT BE WRITTEN DOWN AGAIN. `json.loads` accepts the bare tokens
1331
+ # `Infinity`, `-Infinity` and `NaN` anywhere a number may appear, and every surface that
1332
+ # hands a record on re-serializes it with `allow_nan=False`: `_dumps` here, and
1333
+ # `cli._emit`. `mr-data status --events` therefore died with an uncaught
1334
+ # `ValueError: Out of range float values are not JSON compliant: inf` -- `_main`'s except
1335
+ # tuple lists `json.JSONDecodeError` and not bare `ValueError`, so it left a traceback,
1336
+ # no output, and no records for ANY event. The record stays in the feed, so every later
1337
+ # invocation died the same way: one appended line cost that surface for good.
1338
+ #
1339
+ # The bound belongs here for the same reason the bound on `at` does. This is the one
1340
+ # boundary that decides what a record may carry, the writer already refuses these values
1341
+ # at the matching boundary (`_normalize_json` raises on a non-finite fact), and a
1342
+ # `try/except` at the CLI would have been a second place that has to know the rule --
1343
+ # one per surface, each of them a chance to miss it, as `fleet` and `render_page`
1344
+ # surviving this same line while `status --events` did not already showed.
1345
+ #
1346
+ # Nulled rather than dropping the whole record, because that is what every other bound in
1347
+ # this function does to a value it cannot carry: text over `MAX_TEXT_CHARS` is truncated,
1348
+ # keys past `MAX_FACT_KEYS` are left out, and a value nested past `_MAX_FACT_DEPTH`
1349
+ # already becomes exactly this `None`. `at` is structural -- every consumer subtracts it,
1350
+ # so a record without a moment is not a record -- while a fact is payload, and the seq,
1351
+ # moment and event name beside it are still true.
1352
+ return None
1353
+ return value
1354
+
1355
+
1356
+ # --------------------------------------------------------------------------------------------
1357
+ # The byte-neutral instrumentation sink
1358
+ # --------------------------------------------------------------------------------------------
1359
+
1360
+ EventSink = Callable[[str, Mapping[str, Any], Mapping[str, Any] | None], None]
1361
+
1362
+ # Why a ContextVar and not a parameter: threading a sink through `build_candidate` ->
1363
+ # `_replay_candidate_derivations` -> `_create_and_seal_candidate` -> `verify_candidate` ->
1364
+ # `_read_sealed_candidate_tree` would change five signatures on the most security-sensitive path in
1365
+ # the tree, for a feature that emits no bytes into anything those functions produce. A context also
1366
+ # gives the isolation the hosted case needs: a thread starts from an empty or explicitly copied
1367
+ # context, so one worker's build can never write another worker's feed.
1368
+ _EVENTS: ContextVar[EventSink | None] = ContextVar("mostlyright_run_events", default=None)
1369
+
1370
+
1371
+ def emit(
1372
+ event: str,
1373
+ facts: Mapping[str, Any],
1374
+ evidence: Mapping[str, Any] | None = None,
1375
+ ) -> None:
1376
+ """Emit one derived event. Reads nothing, hashes nothing, and returns nothing.
1377
+
1378
+ With no sink armed this is a ContextVar read and a return -- the build path pays nothing. The
1379
+ result of an ``emit`` reaches no member, no manifest and no digest (G1); a call site is a
1380
+ statement, never an expression whose value is used.
1381
+
1382
+ On ANY exception from the sink the sink is DISARMED for the rest of the arming context rather
1383
+ than retried. A failed event sink must not affect the Build.
1384
+
1385
+ A consequence worth knowing before debugging one: a producer whose facts drift from
1386
+ ``EVENT_FACTS`` raises inside ``EventWriter.write``, which this function catches, which
1387
+ therefore disarms the feed for the rest of the run. That is deliberate -- a feed missing its
1388
+ tail is a loud failure, while a feed carrying a wrong fact set would be a quiet one -- but it
1389
+ means a drifted producer shows up as a TRUNCATED feed. Look for a ``validate_facts``
1390
+ ``ValueError`` first.
1391
+ """
1392
+
1393
+ sink = _EVENTS.get()
1394
+ if sink is None:
1395
+ return None
1396
+ try:
1397
+ sink(event, facts, evidence)
1398
+ except Exception:
1399
+ _EVENTS.set(None)
1400
+ return None
1401
+
1402
+
1403
+ @contextlib.contextmanager
1404
+ def sink_disabled() -> Iterator[None]:
1405
+ """Suppress events inside the block and restore the previous sink on exit.
1406
+
1407
+ This exists for the candidate verify path: it re-derives every member from the sealed bytes,
1408
+ and a re-derivation is not the build. Emitting it would double every parse, clean, join and
1409
+ check event in the feed -- the viewer would show the run happening twice.
1410
+ """
1411
+
1412
+ token = _EVENTS.set(None)
1413
+ try:
1414
+ yield
1415
+ finally:
1416
+ _EVENTS.reset(token)
1417
+
1418
+
1419
+ @contextlib.contextmanager
1420
+ def observer(watcher: EventSink) -> Iterator[None]:
1421
+ """Arm ``watcher`` alongside whatever sink is already armed, for the duration of the block.
1422
+
1423
+ This is a SEAM, not a vocabulary change. It adds no name, no fact key and no record shape, and
1424
+ it cannot make an unsealed fact reach the feed: a watcher is only ever CALLED with records the
1425
+ build already emitted, and its return value is discarded. The seam exists so a second, avowedly
1426
+ unsealed consumer -- `progress_events`, whose module docstring states the boundary -- can watch
1427
+ a build in flight without either vocabulary learning about the other.
1428
+
1429
+ Composition, not replacement. The previous sink stays armed and is called FIRST, so a watcher
1430
+ can never cost the feed a record; a watcher that raises is caught here and disarms nothing,
1431
+ because ``emit`` disarms the whole sink on an exception and one broken watcher must not silence
1432
+ the feed. The pairing with ``sink_disabled`` is the one that matters on the verify path: a
1433
+ re-derivation suppresses the sink, so a watcher installed through this seam sees the build once,
1434
+ exactly like the feed does.
1435
+ """
1436
+
1437
+ previous = _EVENTS.get()
1438
+
1439
+ def _both(
1440
+ event: str, facts: Mapping[str, Any], evidence: Mapping[str, Any] | None = None
1441
+ ) -> None:
1442
+ if previous is not None:
1443
+ previous(event, facts, evidence)
1444
+ try:
1445
+ watcher(event, facts, evidence)
1446
+ except Exception:
1447
+ return None
1448
+ return None
1449
+
1450
+ token = _EVENTS.set(_both)
1451
+ try:
1452
+ yield
1453
+ finally:
1454
+ _EVENTS.reset(token)
1455
+
1456
+
1457
+ @contextlib.contextmanager
1458
+ def event_sink(path: str | os.PathLike[str], *, producer_attempt: str) -> Iterator[EventWriter]:
1459
+ """Arm the feed for the duration of one build attempt and close it with a terminal record.
1460
+
1461
+ Terminal-record rules -- this is the answer to "watching a failed build shows nothing":
1462
+
1463
+ * a clean exit writes nothing extra, because the build already emitted ``build_sealed``;
1464
+ * ``KeyboardInterrupt`` or ``SystemExit`` writes one ``run_interrupted``;
1465
+ * any other exception writes one ``build_failed`` carrying the exception's ``finding_id`` and
1466
+ ``severity`` when it has them.
1467
+
1468
+ Those key sets are the ``EVENT_FACTS`` entries for the two terminal events, so the exit path is
1469
+ validated by the same spec as every other record. The context manager NEVER suppresses the
1470
+ exception, and every write on the exit path is wrapped so a failing feed cannot mask the real
1471
+ error.
1472
+ """
1473
+
1474
+ writer = EventWriter(path, run=producer_attempt)
1475
+
1476
+ def _sink(
1477
+ event: str, facts: Mapping[str, Any], evidence: Mapping[str, Any] | None = None
1478
+ ) -> None:
1479
+ """Write one record; a record that named a place costs that record and nothing more.
1480
+
1481
+ ``emit`` disarms the sink on any exception, which is right for a producer whose fact keys
1482
+ drifted and wrong for a fact that merely carried a path: one such string must not drop
1483
+ later records or the terminal record. The
1484
+ location refusal is handled HERE, before it can reach ``emit``: the record is rewritten
1485
+ with its offending tokens redacted, and if even that is refused the record alone is
1486
+ dropped. The sink stays armed either way.
1487
+
1488
+ Unwritable text takes the same route for the same reason. A filename carrying invalid
1489
+ UTF-8 reaches a fact through ``surrogateescape`` with no attacker anywhere -- the producer
1490
+ writes it itself -- so the value is repaired through ``writable_text`` and the record is
1491
+ kept. Only the string changes; the fact it states is unchanged.
1492
+ """
1493
+
1494
+ try:
1495
+ writer.write(event, facts, evidence)
1496
+ except (PlaceNamedError, UnwritableTextError):
1497
+ # ONE retry carrying BOTH repairs, in the order that composes: text is made writable
1498
+ # first, then places are redacted out of it. Two separate retries would have dropped a
1499
+ # record that needed both -- and a message built from an undecodable filename is
1500
+ # exactly a message that also carries a path.
1501
+ try:
1502
+ writer.write(event, _redact_places(_repair_text(facts)), evidence)
1503
+ except ValueError:
1504
+ return None
1505
+ return None
1506
+
1507
+ token = _EVENTS.set(_sink)
1508
+ try:
1509
+ yield writer
1510
+ except (KeyboardInterrupt, SystemExit):
1511
+ _write_terminal(writer, "run_interrupted", {})
1512
+ raise
1513
+ except BaseException as exc:
1514
+ finding_id = getattr(exc, "finding_id", None)
1515
+ _write_terminal(
1516
+ writer,
1517
+ "build_failed",
1518
+ {
1519
+ "finding_id": None if finding_id is None else _clamp_text(str(finding_id)),
1520
+ "severity": _clamp_text(str(getattr(exc, "severity", "blocker"))),
1521
+ "message": _clamp_text(str(exc)),
1522
+ },
1523
+ )
1524
+ raise
1525
+ finally:
1526
+ _EVENTS.reset(token)
1527
+ try:
1528
+ writer.close()
1529
+ except Exception: # pragma: no cover - closing a descriptor twice is the only shape here
1530
+ pass
1531
+
1532
+
1533
+ _WITHHELD = "withheld: it named a location"
1534
+ # What one refused token becomes. A whole message replaced by `_WITHHELD` reads as a hole; the
1535
+ # sentence around the path is the half a reader needs -- "output already exists: (a location on
1536
+ # this machine)" says what went wrong, and says it identically from every machine.
1537
+ _PLACE_PLACEHOLDER = "(a location on this machine)"
1538
+
1539
+
1540
+ def _redact_place_text(key: str, text: str) -> str:
1541
+ """Return ``text`` with every token the location guard refuses replaced, or ``_WITHHELD``.
1542
+
1543
+ Token-wise, so the prose survives and only the place is lost. If what remains still names a
1544
+ place -- this host's name spelled inside a word, for instance -- the whole string is withheld
1545
+ rather than half-cleaned: a partial redaction that still leaks is worse than a blank.
1546
+ """
1547
+
1548
+ rebuilt: list[str] = []
1549
+ for chunk in re.split(r"(\s+)", text):
1550
+ if not chunk or chunk.isspace():
1551
+ rebuilt.append(chunk)
1552
+ continue
1553
+ try:
1554
+ _require_placeless(key, chunk)
1555
+ except PlaceNamedError:
1556
+ rebuilt.append(_PLACE_PLACEHOLDER)
1557
+ else:
1558
+ rebuilt.append(chunk)
1559
+ result = "".join(rebuilt)
1560
+ try:
1561
+ _require_placeless(key, result)
1562
+ except PlaceNamedError:
1563
+ return _WITHHELD
1564
+ return _clamp_text(result)
1565
+
1566
+
1567
+ def _repair_text(value: Any) -> Any:
1568
+ """Recursively make every string reachable in a fact value one a surface can encode.
1569
+
1570
+ The mirror of ``_redact_places``, over the other refusal the writer owns. Keys are repaired
1571
+ too: a fact KEY carrying a lone surrogate poisons ``json.dumps`` exactly as a value does.
1572
+ """
1573
+
1574
+ if isinstance(value, str):
1575
+ return writable_text(value)
1576
+ if isinstance(value, Mapping):
1577
+ return {
1578
+ (writable_text(k) if isinstance(k, str) else k): _repair_text(inner)
1579
+ for k, inner in value.items()
1580
+ }
1581
+ if isinstance(value, (list, tuple)):
1582
+ return [_repair_text(item) for item in value]
1583
+ return value
1584
+
1585
+
1586
+ def _redact_places(value: Any, *, key: str = "facts") -> Any:
1587
+ """Recursively redact every place-naming string reachable in a fact value."""
1588
+
1589
+ if isinstance(value, str):
1590
+ return _redact_place_text(key, value)
1591
+ if isinstance(value, Mapping):
1592
+ return {
1593
+ inner_key: _redact_places(inner, key=f"{key}.{inner_key}")
1594
+ for inner_key, inner in value.items()
1595
+ }
1596
+ if isinstance(value, (list, tuple)):
1597
+ return [_redact_places(item, key=f"{key}[]") for item in value]
1598
+ return value
1599
+
1600
+
1601
+ def _write_terminal(writer: EventWriter, event: str, facts: Mapping[str, Any]) -> None:
1602
+ """Write the one terminal record, redacting rather than losing it if the guard refuses.
1603
+
1604
+ An exception message routinely carries a path, and a build that failed is exactly the run a
1605
+ watcher most wants to see end. So the first attempt writes the message as it stands; if the
1606
+ location-blind guard refuses it, the second attempt writes the same record with the offending
1607
+ tokens replaced. Either way the feed ends with exactly one terminal record, and neither attempt
1608
+ can raise into the real exception on its way out.
1609
+ """
1610
+
1611
+ attempts: tuple[Mapping[str, Any], ...] = (facts, _redact_places(facts))
1612
+ for attempt in attempts:
1613
+ try:
1614
+ writer.write(event, attempt)
1615
+ return
1616
+ except ValueError:
1617
+ continue
1618
+ except Exception:
1619
+ return
1620
+
1621
+
1622
+ # --------------------------------------------------------------------------------------------
1623
+ # The post-hoc projection over a sealed run
1624
+ # --------------------------------------------------------------------------------------------
1625
+ #
1626
+ # `emit` writes records during a build. `project_sealed_run` derives the same ordered records from
1627
+ # sealed bytes. Both use `EVENT_FACTS` and `validate_facts`; derived records may differ only in
1628
+ # `at`.
1629
+ # A manifest and its evidence members are sufficient when no event feed exists.
1630
+
1631
+
1632
+ def project_sealed_run(
1633
+ manifest: Mapping[str, Any],
1634
+ member_bytes: Mapping[str, bytes],
1635
+ ) -> list[dict[str, Any]]:
1636
+ """Derive the ordered event feed for one finished run from its sealed bytes alone.
1637
+
1638
+ PURE. It takes the parsed ``manifest.json`` mapping and a mapping of candidate-relative member
1639
+ path to that member's bytes, both already in memory, and it imports nothing from ``pipeline``.
1640
+ A caller that has already done the verified read must be able to project without the candidate
1641
+ being read a second time, and a projector that opened files itself would become exactly the
1642
+ second reader this design refuses to grow.
1643
+
1644
+ Every evidence pointer resolves through ``manifest["members"]``, so a projected event physically
1645
+ cannot cite a member the manifest does not carry. Every record passes
1646
+ ``validate_facts`` and the location-blind guard before it is returned.
1647
+
1648
+ ``at`` is ``0.0`` on every record. A projection has no clock, and a clock it invented would be a
1649
+ timestamp claiming to be an observation.
1650
+
1651
+ ``build_failed`` and ``run_interrupted`` are NEVER projected, and no branch here should ever add
1652
+ them: a sealed run did not fail, and a run that was interrupted never sealed a manifest to
1653
+ project from. Those two events exist only on the live path.
1654
+
1655
+ Raises ``ValueError`` when the manifest's member list is malformed, when a member the projection
1656
+ needs was not supplied, or when a supplied member is not the JSON this projector was written
1657
+ against. It refuses rather than returning a partial feed: a half-projection reads to a viewer
1658
+ exactly like a run that stopped half way.
1659
+ """
1660
+
1661
+ members_by_path = _manifest_members_by_path(manifest)
1662
+ records: list[dict[str, Any]] = []
1663
+
1664
+ def add(
1665
+ event: str,
1666
+ facts: Mapping[str, Any],
1667
+ evidence: Mapping[str, Any] | None,
1668
+ ) -> None:
1669
+ records.append(_projected_record(len(records) + 1, event, facts, evidence))
1670
+
1671
+ def pointer(member: str) -> dict[str, Any]:
1672
+ return _pointer(members_by_path, member)
1673
+
1674
+ plan = _member_json(member_bytes, "plan.json")
1675
+ source_ids = [source["id"] for source in plan["sources"]]
1676
+
1677
+ add(
1678
+ "run_started",
1679
+ {
1680
+ "sources": list(source_ids),
1681
+ "output_intent": plan["output_intent"],
1682
+ "grain": list(plan["grain"]),
1683
+ "columns": list(plan["select"]),
1684
+ "evidence_member": EVIDENCE_MEMBER["run_started"],
1685
+ },
1686
+ pointer(EVIDENCE_MEMBER["run_started"]),
1687
+ )
1688
+
1689
+ source_records = _member_json(member_bytes, EVIDENCE_MEMBER["rights_checked"])
1690
+ add(
1691
+ "rights_checked",
1692
+ {
1693
+ "sources": [
1694
+ {
1695
+ "source_id": record["source_id"],
1696
+ "status": record["rights"]["status"],
1697
+ "permissions": list(record["rights"]["permissions"]),
1698
+ }
1699
+ for record in source_records
1700
+ ],
1701
+ "evidence_member": EVIDENCE_MEMBER["rights_checked"],
1702
+ },
1703
+ pointer(EVIDENCE_MEMBER["rights_checked"]),
1704
+ )
1705
+
1706
+ add(
1707
+ "sources_read_started",
1708
+ {"total": len(plan["sources"]), "evidence_member": EVIDENCE_MEMBER["sources_read_started"]},
1709
+ pointer(EVIDENCE_MEMBER["sources_read_started"]),
1710
+ )
1711
+
1712
+ for source_id in source_ids:
1713
+ member = source_member(source_id)
1714
+ add(
1715
+ "source_read",
1716
+ {
1717
+ "source_id": source_id,
1718
+ # From the MANIFEST, not from the bytes in hand: the manifest entry's `bytes` is
1719
+ # `len(member_bytes[...])` by construction at seal time, so this is the identical
1720
+ # integer the live emitter reports, and taking it from the receipt keeps this
1721
+ # function honest even when a caller supplied bytes it derived some other way.
1722
+ "bytes": _manifest_entry(members_by_path, member)["bytes"],
1723
+ "evidence_member": member,
1724
+ },
1725
+ pointer(member),
1726
+ )
1727
+
1728
+ add(
1729
+ "sources_parsed_started",
1730
+ {
1731
+ "total": len(plan["sources"]),
1732
+ "evidence_member": EVIDENCE_MEMBER["sources_parsed_started"],
1733
+ },
1734
+ pointer(EVIDENCE_MEMBER["sources_parsed_started"]),
1735
+ )
1736
+
1737
+ profiles = _member_json(member_bytes, EVIDENCE_MEMBER["source_parsed"])
1738
+ for source_id in source_ids:
1739
+ raw = profiles[source_id]["raw"]
1740
+ add(
1741
+ "source_parsed",
1742
+ {
1743
+ "source_id": source_id,
1744
+ "rows": raw["row_count"],
1745
+ "columns": len(raw["columns"]),
1746
+ "evidence_member": EVIDENCE_MEMBER["source_parsed"],
1747
+ },
1748
+ pointer(EVIDENCE_MEMBER["source_parsed"]),
1749
+ )
1750
+
1751
+ # THE ONE CONDITIONAL RECORD. Live it fires only when
1752
+ # `package_context is not None and not allow_versioned_acquisition` -- offline runs, never
1753
+ # recipe runs and never plain builds. The sealed equivalent of that condition is BOTH halves
1754
+ # below: a recipe candidate seals `evidence/package.json` too, so the member's presence alone
1755
+ # would make the projection narrate a match the live feed never claimed, and the agreement test
1756
+ # would fail on precisely this branch.
1757
+ #
1758
+ # Both sides count over the PLAN's sources rather than over the proposal list, so a package
1759
+ # whose proposals are a superset of the plan cannot make the two disagree. The sealed key is
1760
+ # `evidence_sha256s`; `content_sha256` is the in-memory `SourceProposal` spelling of the same
1761
+ # values -- the same situation as `source`/`source_id` on `cleaning_applied`.
1762
+ matched_member = EVIDENCE_MEMBER["evidence_matched"]
1763
+ if (
1764
+ matched_member in members_by_path
1765
+ and manifest.get("manifest_version") != _RECIPE_MANIFEST_VERSION
1766
+ ):
1767
+ package = _member_json(member_bytes, matched_member)
1768
+ cited_by_source = {
1769
+ proposal["source_id"]: set(proposal["evidence_sha256s"])
1770
+ for proposal in package["source_proposals"]
1771
+ }
1772
+ matched = sum(
1773
+ 1
1774
+ for source_id in source_ids
1775
+ if _manifest_entry(members_by_path, source_member(source_id))["sha256"]
1776
+ in cited_by_source.get(source_id, frozenset())
1777
+ )
1778
+ add(
1779
+ "evidence_matched",
1780
+ {
1781
+ "matched": matched,
1782
+ "total": len(plan["sources"]),
1783
+ "evidence_member": matched_member,
1784
+ },
1785
+ pointer(matched_member),
1786
+ )
1787
+
1788
+ cleaning = plan["cleaning"]
1789
+ add(
1790
+ "cleaning_started",
1791
+ {"total": len(cleaning), "evidence_member": EVIDENCE_MEMBER["cleaning_started"]},
1792
+ pointer(EVIDENCE_MEMBER["cleaning_started"]),
1793
+ )
1794
+ for index, step in enumerate(cleaning, start=1):
1795
+ add(
1796
+ "cleaning_applied",
1797
+ {
1798
+ "step_index": index,
1799
+ "step_total": len(cleaning),
1800
+ # The sealed key is `source`; the parsed dataclass attribute the live emitter reads
1801
+ # is `source_id`. Same value, two spellings, and this is the sealed side.
1802
+ "source_id": step["source"],
1803
+ "operation": step["operation"],
1804
+ "evidence_member": EVIDENCE_MEMBER["cleaning_applied"],
1805
+ },
1806
+ pointer(EVIDENCE_MEMBER["cleaning_applied"]),
1807
+ )
1808
+
1809
+ # The `cleaned` stage of the same member `source_parsed` read the `raw` stage of. Cleaning may
1810
+ # add or drop columns, so this is the count AFTER the steps above, not before them.
1811
+ add(
1812
+ "sources_profiled",
1813
+ {
1814
+ "sources": len(plan["sources"]),
1815
+ "columns": sum(
1816
+ len(profiles[source_id]["cleaned"]["columns"]) for source_id in source_ids
1817
+ ),
1818
+ "evidence_member": EVIDENCE_MEMBER["sources_profiled"],
1819
+ },
1820
+ pointer(EVIDENCE_MEMBER["sources_profiled"]),
1821
+ )
1822
+
1823
+ join = _member_json(member_bytes, EVIDENCE_MEMBER["join_completed"])
1824
+ join_facts = {
1825
+ key: join[key] for key in EVENT_FACTS["join_completed"] if key != "evidence_member"
1826
+ }
1827
+ join_facts["evidence_member"] = EVIDENCE_MEMBER["join_completed"]
1828
+ add("join_completed", join_facts, pointer(EVIDENCE_MEMBER["join_completed"]))
1829
+
1830
+ profile = _member_json(member_bytes, EVIDENCE_MEMBER["rows_selected"])
1831
+ add(
1832
+ "rows_selected",
1833
+ {
1834
+ "rows": profile["row_count"],
1835
+ "columns": len(plan["select"]),
1836
+ "evidence_member": EVIDENCE_MEMBER["rows_selected"],
1837
+ },
1838
+ pointer(EVIDENCE_MEMBER["rows_selected"]),
1839
+ )
1840
+
1841
+ quality = _member_json(member_bytes, EVIDENCE_MEMBER["checks_completed"])
1842
+ checks = quality["checks"]
1843
+ # The live emitter cannot count this member -- it does not exist yet -- so it computes the same
1844
+ # total from the plan: `2 + len(grain) + len(quality.not_null)`. The identity of those two
1845
+ # numbers is pinned by its own test, because a drift there fails the agreement test with a
1846
+ # message about a count rather than about the formula that produced it.
1847
+ add(
1848
+ "checks_started",
1849
+ {"total": len(checks), "evidence_member": EVIDENCE_MEMBER["checks_started"]},
1850
+ pointer(EVIDENCE_MEMBER["checks_started"]),
1851
+ )
1852
+ for index, check in enumerate(checks, start=1):
1853
+ add(
1854
+ "check_completed",
1855
+ {
1856
+ "check_id": check["check_id"],
1857
+ "index": index,
1858
+ "total": len(checks),
1859
+ "evidence_member": EVIDENCE_MEMBER["check_completed"],
1860
+ },
1861
+ pointer(EVIDENCE_MEMBER["check_completed"]),
1862
+ )
1863
+ add(
1864
+ "checks_completed",
1865
+ {
1866
+ # `passed` equals `total` on every sealed run, because `_validate_output` raises on the
1867
+ # first failed check. Counting rather than asserting is deliberate: the count is what
1868
+ # the member says, and a projector that hardcoded the equality would be asserting a
1869
+ # pipeline invariant instead of reading evidence.
1870
+ "passed": sum(1 for check in checks if check["passed"] is True),
1871
+ "total": len(checks),
1872
+ "evidence_member": EVIDENCE_MEMBER["checks_completed"],
1873
+ },
1874
+ pointer(EVIDENCE_MEMBER["checks_completed"]),
1875
+ )
1876
+
1877
+ # `profile` was read above for `rows_selected`; both records state numbers out of the one member
1878
+ # that carries them.
1879
+ add(
1880
+ "table_written",
1881
+ {
1882
+ "rows": profile["row_count"],
1883
+ "columns": len(profile["columns"]),
1884
+ "evidence_member": EVIDENCE_MEMBER["table_written"],
1885
+ },
1886
+ pointer(EVIDENCE_MEMBER["table_written"]),
1887
+ )
1888
+
1889
+ add(
1890
+ "members_sealed_started",
1891
+ {
1892
+ "total": len(members_by_path),
1893
+ "evidence_member": EVIDENCE_MEMBER["members_sealed_started"],
1894
+ },
1895
+ pointer(EVIDENCE_MEMBER["members_sealed_started"]),
1896
+ )
1897
+ for member in sorted(members_by_path):
1898
+ entry = members_by_path[member]
1899
+ add(
1900
+ "member_sealed",
1901
+ {"bytes": entry["bytes"]},
1902
+ {"member": member, "sha256": entry["sha256"]},
1903
+ )
1904
+
1905
+ # A pure sum over the member table. Nothing here is hashed and nothing is opened: a fact is a
1906
+ # read of a value the seal already computed, which is what keeps the feed digest-neutral (G1).
1907
+ add(
1908
+ "candidate_installed",
1909
+ {
1910
+ "members": len(members_by_path),
1911
+ "bytes": sum(entry["bytes"] for entry in members_by_path.values()),
1912
+ "evidence_member": EVIDENCE_MEMBER["candidate_installed"],
1913
+ },
1914
+ pointer(EVIDENCE_MEMBER["candidate_installed"]),
1915
+ )
1916
+
1917
+ add(
1918
+ "snapshot_verify_started",
1919
+ {
1920
+ "total": len(members_by_path),
1921
+ "evidence_member": EVIDENCE_MEMBER["snapshot_verify_started"],
1922
+ },
1923
+ pointer(EVIDENCE_MEMBER["snapshot_verify_started"]),
1924
+ )
1925
+ # Sorted by member path, exactly as `member_sealed` is, so the two halves of the receipt read in
1926
+ # the same order on both producers.
1927
+ for member in sorted(members_by_path):
1928
+ entry = members_by_path[member]
1929
+ add(
1930
+ "member_verified",
1931
+ {"bytes": entry["bytes"]},
1932
+ {"member": member, "sha256": entry["sha256"]},
1933
+ )
1934
+ add(
1935
+ "snapshot_verified",
1936
+ {
1937
+ "members": len(members_by_path),
1938
+ "table_sha256": manifest["table_sha256"],
1939
+ "evidence_member": EVIDENCE_MEMBER["snapshot_verified"],
1940
+ },
1941
+ pointer(EVIDENCE_MEMBER["snapshot_verified"]),
1942
+ )
1943
+
1944
+ add(
1945
+ "build_sealed",
1946
+ {
1947
+ "candidate_digest": manifest["candidate_digest"],
1948
+ "table_sha256": manifest["table_sha256"],
1949
+ "manifest_version": manifest["manifest_version"],
1950
+ "row_count": profile["row_count"],
1951
+ },
1952
+ # The one record with no pointer: a candidate digest is not a member of anything.
1953
+ None,
1954
+ )
1955
+
1956
+ return records
1957
+
1958
+
1959
+ def rederive_feed(
1960
+ run_dir: str | os.PathLike[str],
1961
+ *,
1962
+ pinned_candidate_fd: int | None = None,
1963
+ ) -> list[dict[str, Any]]:
1964
+ """Return a feed projected from a retained, verified sealed-run snapshot.
1965
+
1966
+ Three controls bind the projection to verified bytes:
1967
+
1968
+ * ``verify_candidate`` is called with ``_retained_leases``, so the descriptors it proved the
1969
+ run through stay OPEN across this whole function instead of closing at its return. Every
1970
+ member below is read relative to that retained candidate descriptor -- ``run_dir`` is never
1971
+ walked again, so the candidate directory cannot be replaced underneath the read.
1972
+ * The manifest is NOT re-read. ``VerifiedCandidate.manifest`` is the mapping the verify proved,
1973
+ so the byte counts and digests every member is checked against come from the proof rather
1974
+ than from a file an attacker may have rewritten since.
1975
+ * Each member's bytes are hashed and compared to that proved digest before anything is
1976
+ projected. A descriptor-relative open still resolves a NAME inside the pinned directory, so
1977
+ retention alone proves where the read went, not what it found; the digest is what proves the
1978
+ content. A single mismatch refuses the complete projection.
1979
+
1980
+ ``lease.validate()`` then re-checks the retained roots at the linearization point the pipeline
1981
+ designed it for, and the lease is closed on every path out.
1982
+
1983
+ Retention pins the read but not the caller's expected subject. It starts at the
1984
+ ``candidate`` name this call resolves, so the answer is about whichever directory sat at that
1985
+ name at that instant -- internally consistent, and not necessarily the run a long-lived reader
1986
+ thinks it is watching. A watcher that holds its own descriptor on the candidate and measures
1987
+ anything against it is therefore asking about one directory and being answered about another,
1988
+ ``pinned_candidate_fd`` binds that subject: pass the expected descriptor, and verification of
1989
+ any other directory raises. Identity is
1990
+ compared through ``fstat`` on two OPEN descriptors, so neither can be renamed out from under
1991
+ the comparison, and an inode cannot be reused while a descriptor holds it -- equal ``(st_dev,
1992
+ st_ino)`` is the same directory, not a directory with the same name.
1993
+
1994
+ Raises whatever ``verify_candidate`` raises for a run that is missing, malformed or tampered,
1995
+ ``ValueError`` for a member whose bytes are not the ones the manifest records or for a
1996
+ candidate that is not the pinned one, and ``OSError`` for a member that is absent or is not a
1997
+ regular file. It never returns an empty list for a run it could not read: a viewer renders an
1998
+ empty feed as "nothing happened", and "I could not verify this" is not "nothing happened".
1999
+ """
2000
+
2001
+ # Deferred on purpose. `pipeline` imports this module for the emission seam, so a module-level
2002
+ # import here would close a cycle; it also keeps `events` loadable with zero harness
2003
+ # dependencies for the writer and reader paths, which is asserted by a test.
2004
+ from mostlyright.data_harness.pipeline import verify_candidate
2005
+
2006
+ run = Path(run_dir)
2007
+ leases: list[Any] = []
2008
+ verified = verify_candidate(run, _retained_leases=leases)
2009
+ if not leases: # pragma: no cover - developer invariant
2010
+ raise ValueError("the verify returned no retained candidate snapshot to project from")
2011
+ lease = leases[0]
2012
+ try:
2013
+ if pinned_candidate_fd is not None:
2014
+ _require_pinned_candidate(pinned_candidate_fd, lease.candidate_fd)
2015
+ manifest = verified.manifest
2016
+ member_bytes = _read_proved_members(lease.candidate_fd, manifest)
2017
+ lease.validate()
2018
+ finally:
2019
+ lease.close()
2020
+ return project_sealed_run(manifest, member_bytes)
2021
+
2022
+
2023
+ def _require_pinned_candidate(pinned_fd: int, proved_fd: int) -> None:
2024
+ """Refuse a projection that is about a directory other than the caller's pinned candidate.
2025
+
2026
+ Checked BEFORE a single member is read, so a decoy's bytes are never loaded, let alone
2027
+ projected. Both arguments are open descriptors: ``fstat`` on an open descriptor answers about
2028
+ the object itself rather than about a name, so nothing here can be raced by a rename.
2029
+ """
2030
+
2031
+ pinned = os.fstat(pinned_fd)
2032
+ proved = os.fstat(proved_fd)
2033
+ if (pinned.st_dev, pinned.st_ino) != (proved.st_dev, proved.st_ino):
2034
+ raise ValueError(
2035
+ "the candidate this run verified is not the candidate directory this reader pinned"
2036
+ )
2037
+
2038
+
2039
+ def _read_proved_members(candidate_fd: int, manifest: Mapping[str, Any]) -> dict[str, bytes]:
2040
+ """Read every member under the verify's own candidate descriptor, pinned to its proved digest.
2041
+
2042
+ ``candidate_fd`` is the descriptor ``verify_candidate`` retained, so nothing here resolves a
2043
+ path from the run directory. The byte count and digest come from the manifest the verify
2044
+ proved. Any member that does not hash to it raises, and the caller has nothing to project.
2045
+ """
2046
+
2047
+ members_by_path = _manifest_members_by_path(manifest)
2048
+ member_bytes: dict[str, bytes] = {}
2049
+ for member, entry in members_by_path.items():
2050
+ # The member paths were just verified, but they are still strings out of a file: resolving
2051
+ # one into a name without re-checking its shape is how a traversal gets a second chance.
2052
+ _require_member_path(member, "manifest member path")
2053
+ recorded_bytes = entry["bytes"]
2054
+ recorded_digest = entry["sha256"]
2055
+ if type(recorded_bytes) is not int or recorded_bytes < 0:
2056
+ raise ValueError(f"manifest member {member!r} records no byte count")
2057
+ if not isinstance(recorded_digest, str) or not _SHA256_RE.match(recorded_digest):
2058
+ raise ValueError(f"manifest member {member!r} records no sha256 digest")
2059
+ raw = read_regular_bytes(member, dir_fd=candidate_fd, max_bytes=recorded_bytes)
2060
+ if len(raw) != recorded_bytes or hashlib.sha256(raw).hexdigest() != recorded_digest:
2061
+ raise ValueError(f"member {member!r} is not the bytes this run's manifest records")
2062
+ member_bytes[member] = raw
2063
+ return member_bytes
2064
+
2065
+
2066
+ def _manifest_members_by_path(manifest: Mapping[str, Any]) -> dict[str, dict[str, Any]]:
2067
+ """Return the manifest's member table keyed by path, or raise ``ValueError``."""
2068
+
2069
+ if not isinstance(manifest, Mapping):
2070
+ raise ValueError("manifest is not a mapping")
2071
+ members = manifest.get("members")
2072
+ if not isinstance(members, list):
2073
+ raise ValueError("manifest carries no members list")
2074
+ by_path: dict[str, dict[str, Any]] = {}
2075
+ for entry in members:
2076
+ if not isinstance(entry, Mapping):
2077
+ raise ValueError(f"manifest member entry is not a mapping: {type(entry).__name__}")
2078
+ missing = [key for key in ("path", "bytes", "sha256") if key not in entry]
2079
+ if missing:
2080
+ raise ValueError(f"manifest member entry is missing {missing}")
2081
+ path = entry["path"]
2082
+ if not isinstance(path, str):
2083
+ raise ValueError(f"manifest member path is not a string: {path!r}")
2084
+ by_path[path] = dict(entry)
2085
+ if not by_path:
2086
+ raise ValueError("manifest carries no members")
2087
+ return by_path
2088
+
2089
+
2090
+ def _manifest_entry(
2091
+ members_by_path: Mapping[str, Mapping[str, Any]], member: str
2092
+ ) -> Mapping[str, Any]:
2093
+ entry = members_by_path.get(member)
2094
+ if entry is None:
2095
+ raise ValueError(f"the manifest carries no member {member!r} for this event's evidence")
2096
+ return entry
2097
+
2098
+
2099
+ def _pointer(members_by_path: Mapping[str, Mapping[str, Any]], member: str) -> dict[str, Any]:
2100
+ """Resolve one evidence pointer through the manifest, or raise.
2101
+
2102
+ The projection discharges its evidence-binding and portability invariants by construction: a
2103
+ projected event cannot name a member the manifest does not carry, and the digest it reports is
2104
+ the one the seal recorded, never one this function computed.
2105
+ """
2106
+
2107
+ return {"member": member, "sha256": _manifest_entry(members_by_path, member)["sha256"]}
2108
+
2109
+
2110
+ def _projected_record(
2111
+ seq: int,
2112
+ event: str,
2113
+ facts: Mapping[str, Any],
2114
+ evidence: Mapping[str, Any] | None,
2115
+ ) -> dict[str, Any]:
2116
+ """Build one record through the same two gates the writer applies. One helper, one gate.
2117
+
2118
+ Nothing in ``project_sealed_run`` hand-checks a key set. A projector that checked its own keys
2119
+ inline would be a second opinion about the fact spec, and the point of `EVENT_FACTS` is that
2120
+ there is exactly one.
2121
+ """
2122
+
2123
+ validate_facts(event, facts)
2124
+ record = {
2125
+ "seq": seq,
2126
+ "at": 0.0,
2127
+ "event": event,
2128
+ "facts": dict(facts),
2129
+ "evidence": None if evidence is None else dict(evidence),
2130
+ }
2131
+ _require_location_blind(record)
2132
+ return record
2133
+
2134
+
2135
+ def _member_json(member_bytes: Mapping[str, bytes], member: str) -> Any:
2136
+ """Return one sealed member parsed as JSON, or raise ``ValueError`` naming the member."""
2137
+
2138
+ if not isinstance(member_bytes, Mapping):
2139
+ raise ValueError("member bytes is not a mapping")
2140
+ raw = member_bytes.get(member)
2141
+ if raw is None:
2142
+ raise ValueError(f"the projection needs member {member!r}, which was not supplied")
2143
+ if not isinstance(raw, (bytes, bytearray)):
2144
+ raise ValueError(f"member {member!r} was not supplied as bytes")
2145
+ return _decode_json(bytes(raw), member)
2146
+
2147
+
2148
+ def _decode_json(raw: bytes, member: str) -> Any:
2149
+ try:
2150
+ return json.loads(raw.decode("utf-8"))
2151
+ except (ValueError, RecursionError) as exc:
2152
+ raise ValueError(f"member {member!r} is not the JSON this projection expects") from exc