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,671 @@
1
+ """The plain first line: what a person reads instead of the raise-site message.
2
+
3
+ A typed error is raised deep inside the harness, in the engineering wording the module around it
4
+ uses -- ``candidate run ancestor chain cannot be opened safely``. That wording is right where it is
5
+ written: it is precise, it is the same word the function names use, and it is what an engineer wants
6
+ in a bug report. It is wrong the moment it is the first line a person reads out of a terminal.
7
+
8
+ The one-direction rule resolves that by translating at the boundary rather than by renaming the
9
+ insides. This module is that translation. It answers one question -- *does this message use an
10
+ internal word?* -- and, when it does, hands back a plain sentence for the same failure instead.
11
+
12
+ Three things keep the translation honest:
13
+
14
+ * **Nothing is hidden.** The machine-readable object under ``--json`` still carries the raise-site
15
+ message verbatim, the typed code is still printed, and the plain block says where the exact
16
+ wording is. A person who wants the engineering sentence is one flag away from it.
17
+ * **A clean message is left alone.** Most refusals already name a column, a field path, or a check
18
+ and read perfectly well. Substituting those would lose detail and buy nothing, so a message with
19
+ no internal word comes through untouched.
20
+ * **The detail survives.** Where a message ends in a name -- a path, a member, a field -- that tail
21
+ is carried onto the plain sentence, because *which* file changed is the actionable half.
22
+
23
+ The term list here is the same list ``scripts/vocabulary_sweep.py`` reads out of
24
+ ``scripts/vocabulary-sweep.d/_terms.json``; ``tests/test_ux_headline.py`` asserts the two are equal,
25
+ so the boundary enforces exactly the words the gate bans and no others.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import errno
31
+ import os
32
+ import re
33
+ from typing import NamedTuple
34
+
35
+ from mostlyright.data_harness.canonical import CanonicalJSONError
36
+ from mostlyright.data_harness.ux.path_kind import A_FOLDER, A_LINK
37
+ from mostlyright.data_harness.ux.remediation import COMPOUND_PREFIX_FAMILY, PREFIX_FAMILY
38
+
39
+ # --------------------------------------------------------------------------------------------
40
+ # The banned words
41
+ # --------------------------------------------------------------------------------------------
42
+ #
43
+ # Written one word per literal rather than as phrases. Every literal below has no whitespace in it,
44
+ # which is exactly the shape the vocabulary sweep treats as a name rather than as wording -- so the
45
+ # list that enforces the rule is not itself reported as a breach of it. That is a consequence of the
46
+ # sweep's stated rule, not an exclusion argued for this file.
47
+
48
+ INTERNAL_TERMS: tuple[tuple[str, ...], ...] = (
49
+ ("candidate",),
50
+ ("sealed", "candidate"),
51
+ ("frozen", "recipe"),
52
+ ("crawler",),
53
+ ("trusted", "retriever"),
54
+ ("sandbox",),
55
+ ("normalizer",),
56
+ ("normalizer", "child"),
57
+ ("format", "family"),
58
+ ("parser",),
59
+ ("reader", "registry"),
60
+ ("promotion", "gate"),
61
+ ("golden", "fixture"),
62
+ ("decode", "fingerprint"),
63
+ ("acquisition", "receipt"),
64
+ ("producer",),
65
+ ("verifier",),
66
+ ("byte-range", "acquisition"),
67
+ ("strict", "206"),
68
+ ("Content-Range", "validation"),
69
+ ("watermark",),
70
+ ("cycle", "cursor"),
71
+ ("two-phase", "cycle", "pinning"),
72
+ ("immutable", "version", "store"),
73
+ ("latest", "pointer"),
74
+ ("drift", "finding"),
75
+ ("repair", "task"),
76
+ ("workspace", "plan"),
77
+ )
78
+
79
+
80
+ def _compile(words: tuple[str, ...]) -> re.Pattern[str]:
81
+ """Word-boundary anchored, case-insensitive, tolerant of a trailing plural.
82
+
83
+ The same construction ``scripts/vocabulary_sweep.py`` uses, so a word the gate would report on
84
+ a page is the same word this boundary refuses to print.
85
+ """
86
+
87
+ body = r"\s+".join(re.escape(word) for word in words)
88
+ return re.compile(rf"\b{body}s?\b", re.IGNORECASE)
89
+
90
+
91
+ _PATTERNS: tuple[re.Pattern[str], ...] = tuple(_compile(words) for words in INTERNAL_TERMS)
92
+
93
+ # Characters that cannot appear in an ordinary English word. A token carrying one is a name -- a
94
+ # path, a flag, a field path, a placeholder -- and a name is never wording, so `candidate` inside
95
+ # `.work/run/candidate/manifest.json` is not a word anyone chose to write in a sentence.
96
+ _NAME_CHARACTERS = frozenset("\\_=$[]{}<>|@")
97
+ _DOTTED_NAME = re.compile(r"[A-Za-z0-9]\.[A-Za-z0-9]")
98
+ # A slash on its own is not enough. `candidate/run child namespace` and `assignment/candidate` are
99
+ # two words with a slash between them -- wording, and exactly the wording this boundary exists to
100
+ # replace. A slash means "path" only when the token also looks like one: rooted, or carrying a
101
+ # suffix or another name character.
102
+ _ROOTED_PATH = re.compile(r"^(?:[.~]{0,2}/|[A-Za-z]:[\\/])")
103
+
104
+ # What a removed name is replaced with before matching. It has to be something no term pattern can
105
+ # join across, or blanking `sealed <name> candidate` would manufacture the phrase `sealed candidate`
106
+ # that nobody wrote.
107
+ _NAME_PLACEHOLDER = "..."
108
+
109
+
110
+ def _is_name(token: str) -> bool:
111
+ """Whether one whitespace-delimited token is a name rather than a word."""
112
+
113
+ if _NAME_CHARACTERS & set(token) or _DOTTED_NAME.search(token) is not None:
114
+ return True
115
+ return "/" in token and _ROOTED_PATH.match(token) is not None
116
+
117
+
118
+ def carries_internal_wording(text: str) -> bool:
119
+ """Whether the prose in ``text`` uses one of the internal words, ignoring names inside it."""
120
+
121
+ prose = " ".join(_NAME_PLACEHOLDER if _is_name(token) else token for token in text.split())
122
+ return any(pattern.search(prose) for pattern in _PATTERNS)
123
+
124
+
125
+ # --------------------------------------------------------------------------------------------
126
+ # The plain sentences
127
+ # --------------------------------------------------------------------------------------------
128
+ #
129
+ # Every entry is a sentence body with no closing full stop, because the renderer adds one when the
130
+ # failure had no detail to name and a colon when it did. Written for the reader of a terminal: what
131
+ # stopped, in the words the rest of the product uses.
132
+
133
+ _LAST_RESORT = "That command stopped before it finished"
134
+
135
+ # One per family in `remediation.FAMILIES`. The safety net: a code nobody has curated still reads
136
+ # plainly, because its prefix has a family and every family has a sentence.
137
+ FAMILY_HEADLINES: dict[str, str] = {
138
+ "ingestion": "A resumable source copy could not safely advance",
139
+ "hosted lane": (
140
+ "That work on the Mostly Right backend could not be started, watched, or fetched"
141
+ ),
142
+ "sources": "A source this Recipe names could not be used",
143
+ "rights": "That source may not be used on these terms",
144
+ "connectors": "The connector that reaches that source could not be used",
145
+ "cadence": "The record of how often that source publishes could not be added to",
146
+ "capture": "A recording of a live source could not be sealed or read back",
147
+ "reading": "That file could not be opened",
148
+ "cleaning": "A cleaning step could not be carried out",
149
+ "joins": "The join between two sources did not hold",
150
+ "quality": "A check this Build has to pass did not pass",
151
+ "recipes": "A Recipe, its approval, or its run settings is not valid",
152
+ "json": "That file is not JSON the harness can read",
153
+ "authoring": "That YAML file could not be read as a Recipe",
154
+ "workbench": "This Workbench folder is not in a state this command can use",
155
+ "workbench record": "The sealed control tree of this Workbench folder could not be read",
156
+ "review": "The sealed review of this Build could not be made or read",
157
+ "output": "That path could not be written to",
158
+ "sealing": "The sealed Build could not be read or written safely",
159
+ "network": "Fetching those bytes did not succeed",
160
+ "platform": "This machine is missing filesystem features this step needs",
161
+ "handoff": "Packaging this Build for Studio, or reading a job back, did not succeed",
162
+ "hosted authoring": "One hosted recipe proposal could not be authored from its bundle",
163
+ "hosted resources": "This hosted worker does not have enough bounded memory for graph work",
164
+ "dataset handoff": "Bringing the released Build back here did not finish",
165
+ "source catalog": "That entry in the Source Catalog could not be written",
166
+ "catalog fill": "Filling the Source Catalog from that provider did not succeed",
167
+ "catalog authoring": "The entries of that public catalog could not be written",
168
+ "catalog generation": "That generation of the public catalog could not be published",
169
+ "catalog update": "That published catalog release could not be installed on this computer",
170
+ "local search": "That search over what is already on this machine could not be answered",
171
+ "search model": "The search model on this machine could not be used",
172
+ "login": "The credential on this machine could not be used",
173
+ "viewer": "The local notebook viewer could not be started or used",
174
+ }
175
+
176
+ # One per code whose raise sites use an internal word today. Ordered by prefix, and within a prefix
177
+ # by the code itself, so the list reads as a table rather than as a history.
178
+ HEADLINES: dict[str, str] = {
179
+ "GRAPH_PACKAGE_UNSUPPORTED": "This graph Recipe cannot use packaged inputs yet",
180
+ # Opening a Build folder and everything under it.
181
+ # Neither headline says "folder" because it is printed unchanged with no path observation,
182
+ # and both codes are reached
183
+ # at paths that are not folders: `CANDIDATE_ABSENT` where nothing is at the path at all, and
184
+ # `CANDIDATE_ANCESTOR_INVALID` on the `ENOTDIR` that a plain file, a pipe, a bound socket and a
185
+ # character device all give. "There is no Build in that folder" and "The Build folder could not
186
+ # be opened" were the first line a person read in every one of those cases.
187
+ "CANDIDATE_ABSENT": "Nothing here can be read as a Build",
188
+ "CANDIDATE_ANCESTOR_INVALID": "The Build could not be opened",
189
+ "CANDIDATE_ANCESTOR_RACE": "The folders leading to the Build changed while they were "
190
+ "being opened",
191
+ "CANDIDATE_ANCESTOR_SYMLINK": "A folder on the way to the Build is a link",
192
+ "CANDIDATE_CONSTRUCTION_INVALID": "The Build could not be written out safely",
193
+ "CANDIDATE_CONSTRUCTION_UNSUPPORTED": "This machine cannot write a Build safely",
194
+ "CANDIDATE_DIGEST_MISMATCH": "The Build fingerprint does not match",
195
+ "CANDIDATE_ENVELOPE_INVALID": "The packaged Build is not valid",
196
+ "CANDIDATE_HASH_MISMATCH": "A file inside the Build changed",
197
+ "CANDIDATE_INPUT_REQUIRED": "This job has nothing to build from",
198
+ "CANDIDATE_INSTALL_INVALID": "The Build that was put in place is not the one that was made",
199
+ "CANDIDATE_INVALID": "This Build is missing something it has to have",
200
+ "CANDIDATE_LIMIT_INVALID": "A size limit this Build is held to is not one the harness can use",
201
+ "PROFILE_INVALID": "The Build's column summary could not be made into the service's own",
202
+ "CANDIDATE_MEMBER_INVALID": "A file inside the Build could not be read safely",
203
+ "CANDIDATE_MEMBER_MISMATCH": "The files in the Build are not the files its Receipt lists",
204
+ "CANDIDATE_MISSING": "The record points at a Build that is not there",
205
+ "CANDIDATE_NAMESPACE_RACE": "The Build folder changed while it was being checked",
206
+ "CANDIDATE_NOT_COMMITTED": "This Workbench folder has no finished Build yet",
207
+ "CANDIDATE_PLATFORM_UNSUPPORTED": "This machine is missing filesystem features this step needs",
208
+ "CANDIDATE_PRODUCER_MISMATCH": "The Build already there was not made by this job",
209
+ "CANDIDATE_RACE": "The Build changed while it was being read",
210
+ "CANDIDATE_REPLAY_MISMATCH": "Replaying the Build did not reproduce its files",
211
+ "CANDIDATE_SCOPE_MISMATCH": "That Build does not belong to the job that asked for it",
212
+ "CANDIDATE_SNAPSHOT_INVALID": "The Build moved while it was being read",
213
+ "CANDIDATE_STATE_MISMATCH": "The Build does not match this Workbench folder's record",
214
+ "CANDIDATE_TOO_LARGE": "This Build is larger than the harness will handle",
215
+ # The Courier, and what it brought back.
216
+ "CRAWLER_ADAPTER_UNAVAILABLE": "The Courier has no approved way to reach that source",
217
+ "CRAWLER_DOWNLOAD_FAILED": "The Courier could not fetch those bytes",
218
+ "CRAWLER_DOWNLOAD_INVALID": "What the Courier fetched is not what was asked for",
219
+ "CRAWLER_ENVIRONMENT_FORBIDDEN": "The Courier was handed a setting it is not allowed to carry",
220
+ "CRAWLER_NORMALIZATION_INPUT_INVALID": "What the Courier brought back could not be turned "
221
+ "into exact bytes",
222
+ "CRAWLER_POLICY_MISMATCH": "The rules the Courier was given for what it may fetch are not "
223
+ "the ones in force",
224
+ "CRAWLER_POLICY_REQUIRED": "The rules for what the Courier may fetch are not available",
225
+ "CRAWLER_POLL_TIMEOUT": "The Courier did not answer in time",
226
+ "CRAWLER_PROTOCOL_BINDING": "The Courier's answer is not tied to the request that was sent",
227
+ "CRAWLER_PROTOCOL_FAILURE": "The Courier returned a failure this harness cannot safely carry",
228
+ "CRAWLER_PROTOCOL_LIMIT": "That message to or from the Courier is larger than the harness "
229
+ "will carry",
230
+ "CRAWLER_PROTOCOL_SCHEMA": "The Courier's failure message uses a version this harness does "
231
+ "not know",
232
+ "ACQUIRE_HOSTED_COLLECTION_UNSUPPORTED": "This bounded collection needs the local attested "
233
+ "clean room",
234
+ "HOSTED_COLLECTION_UNSUPPORTED": "This bounded collection needs the local attested clean room",
235
+ "CRAWLER_PROTOCOL_TYPE": "The Courier's answer is not the shape an answer has to be",
236
+ "CRAWLER_PROTOCOL_VERSION": "The Courier is speaking a version this harness does not know",
237
+ "CRAWLER_REMOTE_FAILED": "The Courier reported a failure",
238
+ "CRAWLER_REMOTE_REQUIRED": "This fetch has to go through the Courier",
239
+ "CRAWLER_RESULT_INVALID": "The Courier's answer is not valid",
240
+ "CRAWLER_QUERY_INVALID": "The Courier was given a source request it cannot safely run",
241
+ "CRAWLER_SESSION_INVALID": "The Courier's session is not valid",
242
+ "CRAWLER_SESSION_SCOPE_MISMATCH": "The Courier's session belongs to a different job",
243
+ "SOURCE_CADENCE_CONTEXT_INVALID": "The source update evidence does not match this job",
244
+ "SOURCE_ADAPTER_UNREGISTERED": "No approved hosted adapter can refresh that source",
245
+ "CRAWLER_SOURCE_SCOPE_MISMATCH": "The Courier fetched a source this job did not name",
246
+ # Reading a source's bytes for one question in a research session.
247
+ "PROBE_SOURCE_ACQUISITION_MISBOUND": "The fetched source does not belong to the question that "
248
+ "asked for it",
249
+ "PROBE_SOURCE_RESULT_INVALID": "What came back for that source does not match what was fetched",
250
+ # Packaging for Studio, and the jobs it hands back.
251
+ "DEPLOY_REQUEST_INVALID": "This Recipe cannot be sent to Studio in its current form",
252
+ "DEPLOY_WORKER_POLICY_MISMATCH": "This Recipe would be checked under different rules after "
253
+ "it is sent",
254
+ "ARTIFACT_DOWNLOAD_BUDGET_EXCEEDED": "That download is larger than this job allows",
255
+ "ARTIFACT_SCOPE_MISMATCH": "That file belongs to a different job",
256
+ "ARTIFACT_SPLIT_VIEW": "That file read back differently the second time",
257
+ "BOOTSTRAP_CONFIG_INVALID": "The start-up settings are not valid",
258
+ "JOB_INVALID": "The job description is not valid",
259
+ "MANIFEST_INVALID": "The Receipt is not valid",
260
+ "PLAN_SCOPE_MISMATCH": "That plan belongs to a different job",
261
+ "PREDECESSOR_SCOPE_MISMATCH": "The Build this one follows belongs to a different job",
262
+ "PRODUCER_MISMATCH": "The Builder that ran is not the Builder this job enrolled",
263
+ "SESSION_LEASE_EXPIRY_UNREASONABLE": "This session has more time than this worker may "
264
+ "safely use",
265
+ "SIGNED_SESSION_SCOPE_MISMATCH": "That signed session belongs to a different job",
266
+ "SIGNING_KEY_STALE": "That signing key is no longer the enrolled one",
267
+ "VERIFIER_SCOPE_INVALID": "The Checker's assignment is not valid",
268
+ "VERIFIER_SCOPE_STALE": "The Checker's assignment is out of date",
269
+ # Reaching a source through its connector, and what the Clean room hands back.
270
+ "ACQUIRED_SNAPSHOT": "The saved copy of that source does not match its Receipt",
271
+ "ACQUISITION_SNAPSHOT": "The saved copy of that source could not be read safely",
272
+ "AUTHENTICATED_SOURCE_REQUIRES_TRUSTED_GATEWAY": "That source needs a sign-in, and the "
273
+ "Courier is never given one",
274
+ "REJECTED_NOT_RECEIPT": "Content that was refused cannot be given a Receipt saying it was "
275
+ "admitted",
276
+ "SANDBOX_ATTESTATION_MISMATCH": "The Clean room ran under different rules from the ones "
277
+ "pinned for this job",
278
+ "SANDBOX_ATTESTATION_REQUIRED": "This fetch needs the Clean room rules pinned for this job, "
279
+ "and none were given",
280
+ "SANDBOX_RESULT": "What came out of the Clean room is not tied to the exact bytes it was given",
281
+ "SNAPSHOT_CONTENT": "The saved copy of that source does not match its Receipt",
282
+ "STREAM_CONTENT_BINDING": "What came out of the Clean room is not tied to the exact stream "
283
+ "bytes",
284
+ "STREAM_QUERY_BINDING": "What came back from that stream does not match what was asked for",
285
+ "STREAM_STATE": "The record of where that stream had got to is not valid",
286
+ "STREAM_STATE_LIMIT": "The record of what that stream has already been shown is larger than "
287
+ "the harness will hold",
288
+ # Opening the bytes of a source file.
289
+ "PARSE_EXTERNAL_REFERENCE": "That file points at something outside itself, and the Reader "
290
+ "will not follow it",
291
+ "PARSE_INPUT": "The Reader was not given exact bytes to read",
292
+ "PARSE_INPUT_LIMIT": "That file is empty, or larger than the Reader will open",
293
+ "PARSE_MEDIA_MISMATCH": "The declared file type does not match the Reader that was chosen",
294
+ "PARSE_SUFFIX_MISMATCH": "The filename ending does not match the Reader that was chosen",
295
+ "READER_CONTRACT": "That file does not meet the Reader's required shape",
296
+ "READER_OPTIONS": "Those Reader settings are not valid for this file",
297
+ # A Recipe, its pinned inputs, and the evidence a run leaves behind.
298
+ "CONTEXT_REQUIRED": "This Recipe run is missing part of the package it has to be given",
299
+ "TABLE_PREVIEW_INVALID": "The preview of this table is not valid",
300
+ "ENGINE_NOT_ALLOWLISTED": "That engine is not one this harness will build with",
301
+ "ENGINE_RUNTIME_MISMATCH": "The engine that ran is not the engine recorded",
302
+ "RECIPE_CANDIDATE": "That Receipt was not written by a Recipe run",
303
+ "RECIPE_MATERIALIZATION": "What was written out does not match the Recipe",
304
+ "RECIPE_PREDECESSOR": "This run does not follow on from the last good one",
305
+ "RECIPE_TEMPLATE": "This Recipe does not match the approved template",
306
+ "RECIPE_RECEIPT_FIELDS": "That Receipt names a version it does not have the fields for",
307
+ "RECIPE_RESULT_MISMATCH": "The sealed Recipe evidence does not match what was built",
308
+ "RECIPE_SOURCE_CLASSIFICATION": "A source's exact bytes were refused by this Recipe's rules",
309
+ "RECIPE_SOURCE_STATE_INVALID": "The record of which sources were used is not exact",
310
+ "RECIPE_SOURCE_STATE_MISMATCH": "The sources used do not match the ones replayed independently",
311
+ "RECIPE_WATERMARK": "The Bookmark for a source is not where this run needs it to be",
312
+ "RECOMMENDATION_PARTITION": "The suggested source list is not split into valid groups",
313
+ "REQUIREMENT_BINDING": "The suggested sources do not match this question's requirements",
314
+ "UNSUPPORTED": "This question was answered as one the harness cannot support, so nothing "
315
+ "can be built from it",
316
+ "URL_HOST": "That source address does not name a valid host",
317
+ # The sealed review of a Build: who was enrolled to review it, what they signed, and what was
318
+ # sealed from it. Only the refusals whose own wording uses an internal word are here; the rest
319
+ # of `review.py` reads plainly already and comes through untouched.
320
+ "ASSIGNMENT_ATTEMPTS_NOT_DISTINCT": "One attempt at this job is enrolled in two roles of its "
321
+ "review",
322
+ "ASSIGNMENT_CANDIDATE_MISMATCH": "This review was assigned for a different Build",
323
+ "ASSIGNMENT_CYCLE_COUNT_INVALID": "This review assignment records a repair round, and this "
324
+ "version reviews first builds only",
325
+ "ASSIGNMENT_KEYS_NOT_DISTINCT": "The reviewers and the Checker have to sign with different "
326
+ "keys",
327
+ "ASSIGNMENT_PRINCIPALS_NOT_DISTINCT": "One person cannot hold two roles in one review",
328
+ "ASSIGNMENT_PRODUCER_ATTEMPT_MISMATCH": "This review was assigned for a different build "
329
+ "attempt",
330
+ "ASSIGNMENT_PRODUCER_MISMATCH": "This review was assigned for a build by somebody else",
331
+ "ASSIGNMENT_VERIFIER_KEY_MISMATCH": "That is not the signing key this review enrolled for the "
332
+ "Checker",
333
+ "ASSIGNMENT_VERIFIER_NOT_ENROLLED": "The Checker is not the one this review enrolled",
334
+ "DECISION_SNAPSHOT_MISSING": "The Build was not held still while it was checked",
335
+ "REPORT_CYCLE_COUNT_INVALID": "That review report records a repair round, and this version "
336
+ "reviews first builds only",
337
+ "REPORT_FIELD_MISMATCH": "That review report is about a different Build or a different "
338
+ "assignment",
339
+ # The Workbench folder's own durable record.
340
+ "RESUME_NOT_STARTED": "This Workbench folder has not been run yet, so there is nothing to "
341
+ "pick up",
342
+ "REVIEW_CANDIDATE_MISMATCH": "The review points at a different Build",
343
+ "RUN_STATE_INVALID": "This Workbench folder's record is not valid",
344
+ # Searching the Source Catalog and the private index over the Builds on this machine, and the
345
+ # record each search keeps of what it looked at. Only the refusals whose own wording uses an
346
+ # internal word are here; the rest of that area reads plainly and comes through untouched.
347
+ "CATALOG_TRACE_CANDIDATES": "The record of what this search looked at is not exact",
348
+ "CATALOG_TRACE_PARTITION": "The groups this search sorted its results into are not the "
349
+ "results it had",
350
+ "LOCAL_SEARCH_SERVICE_BINDING": "The Build this search chose is not the one that was "
351
+ "re-checked",
352
+ "LOCAL_SEARCH_TRACE_BINDING": "The record of this search does not match the Builds it read",
353
+ "NEURAL_PIN_MISMATCH": "The search model on this machine is not the one it is pinned to",
354
+ # Filling the Source Catalog and writing its entries. Both of these say "the payload", "the
355
+ # facts and citation" or "the parser" in their own wording, which is a sentence written for
356
+ # somebody reading the code rather than for somebody who ran a command.
357
+ "CATALOG_DELTA_RECORD_BINDING": "That catalog entry says something the provider's record does "
358
+ "not",
359
+ "HARVEST_XML_ENCODING": "That provider's answer is not written in the text encoding it says "
360
+ "it is",
361
+ }
362
+
363
+ # For the refusals raised without a typed code at all, keyed by the class that carries them.
364
+ # `naming` resolves a class through its hierarchy, so a subclass answers through its parent's
365
+ # sentence unless it is written here itself.
366
+ EXCEPTION_HEADLINES: dict[str, str] = {
367
+ "ReviewError": "This review was not accepted",
368
+ "SignerProviderError": "That signing key could not be used",
369
+ # An `OSError` the harness raised itself, rather than one the operating system handed it. The
370
+ # ones the system raises say things like "No such file or directory" and are already plain, so
371
+ # they never reach this map; the ones written here are safety refusals from putting a Build in
372
+ # place or starting a Clean room, and they read like the inside of the machine.
373
+ "OSError": "This step could not be completed safely on this machine",
374
+ }
375
+
376
+
377
+ class SystemRefusal(NamedTuple):
378
+ """What a person reads for one errno, and the key its remediation is written under.
379
+
380
+ One record rather than two tables. The sentence lived here and the remediation key lived in
381
+ `cli.py`, in a second `if` chain over the same errnos, and nothing held the two together: an
382
+ errno with a sentence and no key printed advice-free, an errno with a key and no sentence
383
+ printed as a number. They are the same decision and they are made once.
384
+
385
+ ``observed`` is what the errno itself establishes, in the nouns :mod:`ux.path_kind` publishes,
386
+ and ``None`` where it establishes nothing. It is declared here rather than borrowed from
387
+ ``code`` because one code serves two errnos that found different things: ``EISDIR`` is the
388
+ kernel saying it is a directory, ``ENOTDIR`` is the kernel saying only that a component is not
389
+ one, and both answer through ``PATH_NOT_A_FILE``. ``scripts/path_kind_gate.py`` reads this
390
+ field and refuses a headline that names any other kind.
391
+ """
392
+
393
+ headline: str
394
+ code: str
395
+ observed: str | None = None
396
+
397
+
398
+ # Refusals a filesystem step was stopped by, keyed by errno. An errno message is English already --
399
+ # that is why it never reaches the term map above -- but `str(OSError)` is not that message: it is
400
+ # `[Errno 62] Too many levels of symbolic links: '/some/path'`, which opens with a number in
401
+ # brackets and quotes the path like a value in a log. `OSError.filename` carries that path as a
402
+ # value, so the line a person reads can be a sentence with the path at the end of it.
403
+ #
404
+ # Every entry is one a workbench command can surface, including self-referential links and names
405
+ # longer than the filesystem accepts.
406
+ SYSTEM_REFUSALS: dict[int, SystemRefusal] = {
407
+ errno.EACCES: SystemRefusal("This machine did not allow that", "PERMISSION_DENIED"),
408
+ # `EISDIR` is the kernel saying the target is a directory, so the noun is the refusal itself.
409
+ errno.EISDIR: SystemRefusal(
410
+ "That path is a folder, and this command needs a file", "PATH_NOT_A_FILE", observed=A_FOLDER
411
+ ),
412
+ # `ELOOP` is the kernel saying it followed links until it gave up, so the noun is the refusal.
413
+ errno.ELOOP: SystemRefusal(
414
+ "That path is a link that leads back to itself", "PATH_LINK_LOOP", observed=A_LINK
415
+ ),
416
+ errno.ENAMETOOLONG: SystemRefusal(
417
+ "That name is longer than this machine allows", "PATH_NAME_TOO_LONG"
418
+ ),
419
+ # `ENOENT` does not establish that a file or folder is absent: the kernel says the *name did not
420
+ # resolve*, and a link that leads nowhere is a name that does not resolve while the entry it
421
+ # names sits right there in `ls -l`. So the sentence says what the errno knows -- the path led
422
+ # nowhere -- and the three commands whose own reads meet a link now type their own code before
423
+ # they ever reach here.
424
+ errno.ENOENT: SystemRefusal("That path did not lead to anything", "PATH_ABSENT"),
425
+ # `ENOTDIR` establishes only that a component used as a directory is not one; the kernel says
426
+ # a component was used as a
427
+ # directory and is not one, and a pipe, a socket and a device all return it. The sentence
428
+ # reaches `show`, `diff`, `peek`, `author` and `recipe-validate`, so `--output /dev/null/x`
429
+ # and every read through a device called that device a file. It now says what the errno says
430
+ # and nothing more, and `observed` is `None` because nothing was observed.
431
+ errno.ENOTDIR: SystemRefusal("A step of that path is not a folder", "PATH_NOT_A_FILE"),
432
+ errno.EPERM: SystemRefusal("This machine did not allow that", "PERMISSION_DENIED"),
433
+ }
434
+
435
+ # The safety net, and the reason the table above no longer has to be complete to be safe. It used
436
+ # to say that only errnos a command can put in front of someone were listed and that anything else
437
+ # "keeps the operating system's own wording" -- a premise about reachability that was asserted
438
+ # rather than measured, and false both ways round: `ELOOP` and `ENAMETOOLONG` are two commands and
439
+ # one typed path away, and what an unlisted errno kept was not the system's wording but Python's
440
+ # assembly of it, opening with a bracketed number.
441
+ #
442
+ # So an errno with no entry now reads as plainly as one with an entry, and says less rather than
443
+ # saying it in engineering. The exact sentence is one `--json` away, which is the same trade every
444
+ # other translation in this module makes. `FAMILY_HEADLINES` is the same net one layer up: a code
445
+ # nobody has curated still reads plainly because its prefix has a family.
446
+ ANY_SYSTEM_REFUSAL = SystemRefusal("This machine refused that", "SYSTEM_REFUSED")
447
+
448
+
449
+ # The strict-JSON refusals are the one class whose first line is unreadable for a reason that has
450
+ # nothing to do with internal words. A `CanonicalJSONError` assembles itself as
451
+ # `<pointer>: <detail> [<code>]`, and the pointer for a whole document is `$` -- so the first thing
452
+ # a person reads out of a terminal is a dollar sign and a colon, which is a shell prompt, not a
453
+ # sentence. The three facts the error carries are right; only the assembly is wrong, so these are
454
+ # rebuilt from those facts rather than substituted for.
455
+ _JSON_SUBJECT = "That file is not JSON the harness can read"
456
+
457
+ # The JSON pointer for the whole document. Any other pointer names a place inside the file, and that
458
+ # is the half of the message worth keeping.
459
+ _WHOLE_DOCUMENT = "$"
460
+
461
+
462
+ def json_headline(error: BaseException) -> str | None:
463
+ """The plain first line for a strict-JSON refusal, or ``None`` when it is not one.
464
+
465
+ ``None`` for everything else, so the caller can fall through to the other translations. The
466
+ exact assembled string is untouched on the machine-readable object, exactly as it is for every
467
+ other rewriting at this boundary.
468
+ """
469
+
470
+ if not isinstance(error, CanonicalJSONError):
471
+ return None
472
+ pointer = error.path
473
+ if pointer and pointer != _WHOLE_DOCUMENT:
474
+ return f"{_JSON_SUBJECT}: {error.detail}, at {pointer}."
475
+ return f"{_JSON_SUBJECT}: {error.detail}."
476
+
477
+
478
+ def refused_path(error: BaseException | None) -> str | None:
479
+ """The path an ``OSError`` names, when it names one that can be printed as a path.
480
+
481
+ ``filename`` is ``None`` for a refusal raised on a descriptor and may be an ``int`` file
482
+ descriptor or ``bytes``; neither is a path a person can act on, so both are declined rather
483
+ than rendered. One rule, used by the sentence below and by the command line when it decides
484
+ which path a refusal is about, because two answers to "which path is this" is how a block came
485
+ to name one file and its advice another.
486
+ """
487
+
488
+ if not isinstance(error, OSError):
489
+ return None
490
+ named = error.filename
491
+ if isinstance(named, str):
492
+ return named
493
+ if isinstance(named, os.PathLike):
494
+ return str(named)
495
+ return None
496
+
497
+
498
+ def system_refusal(error: BaseException) -> SystemRefusal | None:
499
+ """What to say and where its fix is written, for a refusal carrying an errno.
500
+
501
+ ``None`` for anything with no errno, which is every `OSError` the harness assembles out of its
502
+ own wording without one. It is **not** ``None`` for the few the harness constructs *with* an
503
+ errno -- ``review.py`` and ``pipeline.py`` raise a handful for install-time safety -- and this
504
+ function cannot tell those from the system's own. It therefore treats this table as a
505
+ best-effort translation for both sources.
506
+ """
507
+
508
+ if not isinstance(error, OSError) or error.errno is None:
509
+ return None
510
+ return SYSTEM_REFUSALS.get(error.errno, ANY_SYSTEM_REFUSAL)
511
+
512
+
513
+ def system_headline(error: BaseException) -> str | None:
514
+ """The plain first line for a refusal carrying an errno, or ``None``.
515
+
516
+ Total over the errnos: every one gets a sentence, a curated one where there is one and the net
517
+ otherwise, so no line a person reads is ever a bracketed error number.
518
+ """
519
+
520
+ refusal = system_refusal(error)
521
+ if refusal is None:
522
+ return None
523
+ named = refused_path(error)
524
+ return f"{refusal.headline}: {named}" if named else f"{refusal.headline}."
525
+
526
+
527
+ def system_lookup_code(error: BaseException) -> str | None:
528
+ """The remediation key for a refusal that carries no typed code of its own.
529
+
530
+ Never printed as a code and never reaches the machine-readable object, because nothing raises
531
+ it: it exists so an untyped refusal a person can hit still gets plain sentences. It comes off
532
+ the same record as the sentence above, so a refusal that reads plainly always has a fix under
533
+ it and neither half can be added without the other.
534
+ """
535
+
536
+ refusal = system_refusal(error)
537
+ return None if refusal is None else refusal.code
538
+
539
+
540
+ # What the plain block says about the sentence it replaced, so nothing looks hidden.
541
+ EXACT_WORDING_HINT = "Exact wording: add --json to the same command."
542
+
543
+ # What separates "what went wrong" from "which thing it was about". A colon followed by a space, so
544
+ # a clock time or a Windows drive letter inside a path is never mistaken for that break.
545
+ _SEPARATOR = re.compile(r":\s")
546
+
547
+ # The typed classes that append their own code assemble as `<subject>: <detail> [CODE]`. The plain
548
+ # block already prints the code on a line of its own, so carrying it inside the sentence as well is
549
+ # a duplicate -- and it is a duplicate spelled out of exactly the engineering vocabulary the
550
+ # sentence is being translated out of, so it can put an internal word back on a line the boundary
551
+ # just cleaned.
552
+ _TRAILING_CODE = re.compile(r"\s*\[[A-Z0-9_]+\]\s*$")
553
+
554
+
555
+ def without_repeated_code(message: str, code: str | None) -> str:
556
+ """``message`` with its trailing ``[CODE]`` dropped when the block prints that code anyway.
557
+
558
+ Several typed classes assemble themselves as ``<subject>: <detail> [CODE]``. The plain block
559
+ prints ``Code: <CODE>`` on a line of its own two lines below, so the bracketed copy is the same
560
+ fact twice -- and it is the copy spelled in the engineering vocabulary, which is how an internal
561
+ word gets back onto a line that carries none in its wording.
562
+
563
+ Only an exact match for the code being printed is removed, so nothing can be lost here that is
564
+ not already on screen. Removing it is not a translation and is not flagged as one: no wording
565
+ changes, and the machine-readable object is untouched as always.
566
+ """
567
+
568
+ if not code:
569
+ return message
570
+ return re.sub(rf"\s*\[{re.escape(code)}\]\s*$", "", message)
571
+
572
+
573
+ def _subject_carries_internal_wording(message: str) -> bool:
574
+ """Whether the thing a message opens with is named with an internal word.
575
+
576
+ A refusal is written ``<subject>: <what went wrong>``, and the subject is usually a field path
577
+ inside a contract record. The rule above deliberately treats a path as a name rather than as
578
+ wording, because `candidate` buried inside `.work/run/candidate/manifest.json` is nobody's
579
+ sentence. That exemption is right in the middle of a line and wrong at the start of one:
580
+ ``sandbox.parsed: ...`` puts an internal word in front of a person as the subject of what they
581
+ are about to read. So a subject that carries one is grounds for substituting, even though the
582
+ same token further along the line would not be -- and the exact string is still one flag away.
583
+ """
584
+
585
+ head = _SEPARATOR.split(message, maxsplit=1)
586
+ if len(head) < 2:
587
+ return False
588
+ return any(pattern.search(head[0]) for pattern in _PATTERNS)
589
+
590
+
591
+ def naming(error: BaseException) -> str:
592
+ """The class name this refusal's plain sentence is written against.
593
+
594
+ The class hierarchy is followed, so the eight `OSError` subclasses answer through `OSError`:
595
+ what a person is being told is that a filesystem step was refused, not which class carried the
596
+ refusal.
597
+ """
598
+
599
+ for cls in type(error).__mro__:
600
+ if cls.__name__ in EXCEPTION_HEADLINES:
601
+ return cls.__name__
602
+ return type(error).__name__
603
+
604
+
605
+ def plain_headline(code: str | None, message: str, *, raised_as: str | None = None) -> str | None:
606
+ """The plain first line for one failure, or ``None`` when the message is already plain.
607
+
608
+ ``None`` rather than the message itself, because the caller has to know whether it substituted:
609
+ a replaced sentence has to say where the exact one went, and an untouched one must not.
610
+
611
+ ``code`` is the typed code the error carries; ``raised_as`` is the name of the class that raised
612
+ it, used only for the refusals that carry no code.
613
+ """
614
+
615
+ if not carries_internal_wording(message) and not _subject_carries_internal_wording(message):
616
+ return None
617
+ plain = HEADLINES.get(code or "")
618
+ if plain is None and code:
619
+ # The compound table corrects a prefix the way remediation's family_for does, so the
620
+ # headline and the remediation paragraph under it always answer as the same family.
621
+ compound = "_".join(code.split("_", 2)[:2])
622
+ family = COMPOUND_PREFIX_FAMILY.get(compound) or PREFIX_FAMILY.get(
623
+ code.split("_", 1)[0], ""
624
+ )
625
+ plain = FAMILY_HEADLINES.get(family)
626
+ if plain is None:
627
+ plain = EXCEPTION_HEADLINES.get(raised_as or "", _LAST_RESORT)
628
+ tail = _plain_tail(message)
629
+ return f"{plain}: {tail}" if tail else f"{plain}."
630
+
631
+
632
+ def _plain_tail(message: str) -> str | None:
633
+ """The longest trailing part of ``message``, after a colon, that names something plainly.
634
+
635
+ Most refusals are written ``what went wrong: which thing``. The left half is the part that uses
636
+ the engineering words; the right half is a path, a member name, or a field path, and it is the
637
+ half that says which of a thousand files this is about. Taking the longest clean suffix keeps
638
+ that detail without carrying any of the wording the boundary is here to replace.
639
+
640
+ The class's own trailing ``[CODE]`` is dropped first: the plain block prints the code on its own
641
+ line, so keeping it here would both repeat it and re-introduce an internal word spelled in
642
+ upper case.
643
+ """
644
+
645
+ trimmed = _TRAILING_CODE.sub("", message)
646
+ for separator in _SEPARATOR.finditer(trimmed):
647
+ tail = trimmed[separator.end() :].strip()
648
+ if tail and not carries_internal_wording(tail) and _TRAILING_CODE.search(tail) is None:
649
+ return tail
650
+ return None
651
+
652
+
653
+ __all__ = [
654
+ "ANY_SYSTEM_REFUSAL",
655
+ "EXACT_WORDING_HINT",
656
+ "EXCEPTION_HEADLINES",
657
+ "FAMILY_HEADLINES",
658
+ "HEADLINES",
659
+ "INTERNAL_TERMS",
660
+ "SYSTEM_REFUSALS",
661
+ "SystemRefusal",
662
+ "carries_internal_wording",
663
+ "json_headline",
664
+ "naming",
665
+ "plain_headline",
666
+ "refused_path",
667
+ "system_headline",
668
+ "system_lookup_code",
669
+ "system_refusal",
670
+ "without_repeated_code",
671
+ ]