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,2195 @@
1
+ """Errors name their fix: the two-layer remediation map.
2
+
3
+ A typed error code is what an engineer quotes in a bug report. On its own it is not an answer, and
4
+ for the commonest failure of all -- pointing a command at a folder that is not there -- the honest
5
+ engineering wording reads like a security incident. This module turns a code into the two or three
6
+ plain sentences that say what happened and what to do next.
7
+
8
+ There are two layers, because 1714 typed codes across 208 prefixes can reach a person at a
9
+ workbench, and a hand-written sentence for each one would be neither affordable nor honest:
10
+
11
+ * **Families.** Every code prefix is assigned to a family. A family is one paragraph: what this
12
+ class of failure is about, where to look, and what to do. Every prefix has one, so no code can
13
+ fail to say something useful.
14
+ * **Curated codes.** The failures a person actually meets get their own exact sentences, and where
15
+ one command is the whole answer, the command itself.
16
+
17
+ Those two counts are not a note somebody remembered to update; `tests/test_ux_remediation.py`
18
+ recomputes them off the live source and fails when this paragraph drifts from it.
19
+
20
+ Nothing here prints, and nothing here reads the filesystem. Where a sentence turns on what is on
21
+ disk -- whether the path is there, whether it holds a Build, what kind of thing is in the way -- the
22
+ fact is passed in by the caller that looked, and an entry with no fact says something kind-neutral
23
+ rather than guessing one. :mod:`ux.path_kind` is imported for the one noun that decides which of
24
+ those wordings applies; nothing here calls it. The text is layered on top of the typed error at the
25
+ boundary where it is rendered; it never replaces it. The machine-readable error object is unchanged
26
+ by anything in this file.
27
+
28
+ `hosted_worker.py` is deliberately not excluded from the completeness gate: its codes are reachable
29
+ through `mr-data export-hosted-candidate`, so they resolve through the `handoff` family.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import inspect
35
+ from dataclasses import dataclass
36
+
37
+ from mostlyright.data_harness.ux.path_kind import (
38
+ A_DEVICE,
39
+ A_FILE,
40
+ A_FOLDER,
41
+ A_LINK,
42
+ A_PIPE,
43
+ A_SOCKET,
44
+ NOTHING_THERE,
45
+ UNKNOWN_KIND,
46
+ )
47
+
48
+ # What a sentence says when the command had no single subject path to name.
49
+ _PATH_FALLBACK = "that path"
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class Family:
54
+ """One plain paragraph covering a whole class of failure.
55
+
56
+ ``kinds_established`` is the same declaration :class:`Remediation` carries, for a paragraph
57
+ reached by a whole prefix rather than by one code. It is empty for almost every family, because
58
+ almost no family knows what is at a path.
59
+ """
60
+
61
+ lines: tuple[str, ...]
62
+ kinds_established: tuple[str, ...] = ()
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class Remediation:
67
+ """The exact answer for one code.
68
+
69
+ ``lines`` may contain ``{path}``; it is filled with the subject path when the command had one.
70
+ ``command`` is used when a single command is the whole answer, and is rendered last. It belongs
71
+ to ``lines``: an entry that answers differently because of what the caller found on disk names
72
+ no command, because the command was written for the finding the default wording describes.
73
+ ``when_path_exists`` is the alternative wording for the entries whose plain reading would
74
+ otherwise be a lie when the path is in fact there. ``when_nothing_built_yet`` sits between the
75
+ two: the folder is there, but it holds no Build, which is a different failure from a Build that
76
+ cannot be opened and names a completely different fix.
77
+
78
+ ``when_not_a_file`` is the fourth, and it is claimed the other way round from the three above:
79
+ the default is what an entry says about a *plain file*, so this wording is used unless the
80
+ caller looked and found one. It may contain ``{kind}`` as well as ``{path}``, filled with the
81
+ plain noun for what was actually there -- and with a kind-neutral word when the look failed or
82
+ raced, because an entry that has been given no observation may not assert one.
83
+
84
+ ``when_not_a_folder`` is the fifth, and it is the same shape as ``when_not_a_file`` about the
85
+ other noun. An entry whose default is written about a folder keeps that default only where the
86
+ caller observed one. This prevents a file, pipe, socket, or device from receiving folder-only
87
+ instructions.
88
+
89
+ ``kinds_established`` is the sixth field and it is not a wording: it is what the *raise site*
90
+ for this code had already found by the time it raised. Some codes are themselves the
91
+ observation -- ``OUTPUT_SYMLINK`` is raised only after a link was met on the way down, and
92
+ ``PEEK_TARGET_NOT_A_FILE`` only after a descriptor reported something that is not a regular
93
+ file -- and a sentence for one of those may name the kind its own refusal established without
94
+ the caller having to look again. It may name nothing else.
95
+ :data:`ux.path_kind.NOTHING_THERE` is one of the kinds it can declare, because saying that
96
+ nothing is at a path is a claim about what is at that path exactly as much as naming a noun is.
97
+ The default is empty, which is what makes an entry written tomorrow red rather than quietly
98
+ wrong: ``scripts/path_kind_gate.py`` renders every entry under every observation a caller can
99
+ make and refuses any noun that neither the observation nor this field supports.
100
+
101
+ ``command_needs_a_build`` is the seventh, and it is the same declaration for an instruction
102
+ rather than a noun. ``mr-data show`` opens a Build folder and reads its Receipt, so naming it at
103
+ the subject path asserts a Build is there; at a path that is merely taken it exits 1 and points
104
+ back at the refusal it was offered as the fix for. An entry that declares this keeps its command
105
+ only where the caller looked and found a Build.
106
+ """
107
+
108
+ lines: tuple[str, ...]
109
+ command: str | None = None
110
+ when_path_exists: tuple[str, ...] = ()
111
+ when_nothing_built_yet: tuple[str, ...] = ()
112
+ when_not_a_file: tuple[str, ...] = ()
113
+ when_not_a_folder: tuple[str, ...] = ()
114
+ kinds_established: tuple[str, ...] = ()
115
+ command_needs_a_build: bool = False
116
+
117
+
118
+ # ------------------------------------------------------------------------------------------------
119
+ # Layer 1: families
120
+ # ------------------------------------------------------------------------------------------------
121
+
122
+ FAMILIES: dict[str, Family] = {
123
+ "sources": Family(
124
+ (
125
+ "This is about finding and reading the files your Recipe names.",
126
+ "Check that every source the message names exists inside the input folder, is a plain "
127
+ "file rather than a link, and can be read.",
128
+ "Nothing was built, so nothing has to be undone.",
129
+ )
130
+ ),
131
+ "reading": Family(
132
+ (
133
+ "This is about opening the bytes of a file: its format, its text encoding, and the "
134
+ "size limits the harness applies to anything it opens.",
135
+ "The Reader that opened the file names what it could not accept.",
136
+ "Open the file yourself to confirm it is the format you think it is, and check its "
137
+ "size against the limits in docs/SOURCES-AND-ACQUISITION.md.",
138
+ ),
139
+ # Every code in this family is raised by a Reader that had already opened the bytes, which
140
+ # `ux.plain_file` licenses only on `S_ISREG`. Telling the reader to open it themselves is
141
+ # therefore an instruction that returns, which is the whole of what this declaration says.
142
+ kinds_established=(A_FILE,),
143
+ ),
144
+ "rights": Family(
145
+ (
146
+ "This is about whether a source may be used at all, and on what terms.",
147
+ "Every source carries a licence, a classification, and any obligations that come with "
148
+ "it. One that does not clear all three is refused before a byte of it reaches a Build.",
149
+ "The message names the source and the term it did not clear. Either supply the "
150
+ "evidence that clears it, or choose a different source.",
151
+ )
152
+ ),
153
+ "connectors": Family(
154
+ (
155
+ "This is about the connector that reaches a source: the packaged code that opens it, "
156
+ "the query it is given, and the Clean room it runs in.",
157
+ "A connector is pinned by version and fingerprint, is given no sign-in of its own, and "
158
+ "has to hand back the exact bytes it was asked for.",
159
+ "The message names the connector and the part of that it did not keep. Nothing was "
160
+ "built, so nothing has to be undone.",
161
+ )
162
+ ),
163
+ "cadence": Family(
164
+ (
165
+ "This is about the record of how often a source actually publishes: one folder of "
166
+ "probes kept on this computer, one entry per probe, never rewritten.",
167
+ "Each entry names the exact source, the settings it was probed under, and the entry "
168
+ "before it. A folder is one such set of settings, so a probe taken under different "
169
+ "ones belongs in a different folder rather than at the end of this one.",
170
+ "The message names what did not line up. Nothing was built and no earlier entry was "
171
+ "touched, so nothing has to be undone.",
172
+ )
173
+ ),
174
+ "capture": Family(
175
+ (
176
+ "This is about a recording of a live source, and the batches it was sealed into on "
177
+ "this computer.",
178
+ "The message names the file and the part of it that did not hold: a recorded message "
179
+ "that could not be read, a connection that left a hole, or a sealed batch that no "
180
+ "longer matches what was written down about it.",
181
+ "Nothing was built, so nothing has to be undone. Record again from the messages you "
182
+ "kept, into a folder that holds nothing else.",
183
+ )
184
+ ),
185
+ "ingestion": Family(
186
+ (
187
+ "This is about the durable, resumable copy of a source: the exact source version, "
188
+ "its ordered chunks, and the checkpoint that joins them.",
189
+ "Each chunk is checked against its recorded SHA-256 before the checkpoint advances, "
190
+ "and a resumed run must name the same source version and predecessor it saw before.",
191
+ "The message names the limit, file, digest, or fence that did not hold. Keep the "
192
+ "checkpoint and verified chunks, repair the named storage or source-version problem, "
193
+ "then resume; completed chunks do not need to be downloaded again.",
194
+ )
195
+ ),
196
+ "cleaning": Family(
197
+ (
198
+ "This is about one cleaning step: a rename, a type change, or a column the step needs.",
199
+ "The message names the column and the step that stopped.",
200
+ "Check the column exists under exactly that name, and that the type you asked for can "
201
+ "hold every value already in it.",
202
+ )
203
+ ),
204
+ "joins": Family(
205
+ (
206
+ "This is about joining two sources together.",
207
+ "Check the join keys exist on both sides, hold no empty values, and match the row "
208
+ "relationship the Recipe declares.",
209
+ "The join evidence in the Build folder lists the rows that did not match.",
210
+ )
211
+ ),
212
+ "quality": Family(
213
+ (
214
+ "This is about the checks a Build has to pass before it can be sealed.",
215
+ "The message names the check that failed.",
216
+ "Either fix the data the check is unhappy with, or change the checks the Recipe "
217
+ "requires and say in the Recipe why.",
218
+ )
219
+ ),
220
+ "recipes": Family(
221
+ (
222
+ "This is about a Recipe, its approval, or the settings that pin how it runs.",
223
+ "The message names the exact field path inside the file.",
224
+ "Open that file, correct that one field, and run the command again.",
225
+ ),
226
+ # An uncurated code with this prefix is a field inside a document that was already read
227
+ # whole, and `ux.plain_file` reads a document only off a descriptor that answered `S_ISREG`.
228
+ # The one code in this family raised before anything is read -- `RECIPE_OUTPUT_EXISTS`, on
229
+ # a path that is taken -- is curated and never reaches this paragraph.
230
+ kinds_established=(A_FILE,),
231
+ ),
232
+ "json": Family(
233
+ (
234
+ "This is about the exact text of a JSON file the harness was told to read.",
235
+ "JSON is read strictly here: UTF-8 with no byte-order mark, no key written twice "
236
+ "inside one object, whole numbers only, and no not-a-number values. A file other "
237
+ "tools accept can still be refused.",
238
+ "The line above says what was wrong, and where inside the file it is when it is not "
239
+ "the whole of it.",
240
+ )
241
+ ),
242
+ "authoring": Family(
243
+ (
244
+ "This is about the YAML file you are writing a Recipe in.",
245
+ "The line above says what stopped the reading, and where.",
246
+ "author reads names with values under them, lists, and plain values: text, whole "
247
+ "numbers, decimals, true, false, and null. Anything else is refused by name rather "
248
+ "than guessed at, because whatever comes out of that file is what you go on to "
249
+ "approve.",
250
+ )
251
+ ),
252
+ "workbench": Family(
253
+ (
254
+ "This is about where a Workbench folder got to: the step it is on, who ran it, and "
255
+ "what it recorded about the last one.",
256
+ "Run mr-data status on the folder to see where it stopped, then mr-data resume to "
257
+ "carry on from that point.",
258
+ )
259
+ ),
260
+ "viewer": Family(
261
+ (
262
+ "This is about the local notebook viewer and the folders it watches.",
263
+ "Workspace mode needs one explicit research folder and either a future Workbench path "
264
+ "or an existing Harness Workbench. Run-directory mode accepts a future path or an "
265
+ "existing verified Harness Build.",
266
+ "Use a real existing folder with --research-dir, and replace any invalid existing "
267
+ "research.ipynb before starting the viewer again.",
268
+ )
269
+ ),
270
+ # A damaged control tree needs a fresh Workbench; status and resume read the same damaged tree.
271
+ "workbench record": Family(
272
+ (
273
+ "This is about the sealed control tree of a Workbench folder: the four files the job "
274
+ "was described in, and the durable record that pins them.",
275
+ "Those files are sealed when the folder is made and are never edited in place, so a "
276
+ "folder whose control tree no longer matches its record is not carried on from. Every "
277
+ "command that opens this folder will say the same thing, including status and resume.",
278
+ "Make a fresh Workbench folder with mr-data init, at a path that does not exist yet, "
279
+ "from the same four files. A Build already sealed in the damaged folder is unaffected "
280
+ "and can still be read: point mr-data show at the result folder inside it.",
281
+ )
282
+ ),
283
+ # The sealed two-reviewer review, which had no paragraph of its own until every refusal in
284
+ # `review.py` was given a code. `REVIEW` already belongs to `workbench` and stays there: those
285
+ # codes are about where a Workbench folder got to, which `mr-data status` reads and answers.
286
+ # These are about the documents a review is made of and the envelope they are sealed into, and
287
+ # "run status, then resume" is no answer to a signature that does not verify.
288
+ "review": Family(
289
+ (
290
+ "This is about the sealed review of a Build: the assignment that says who reviews it, "
291
+ "the two signed reports, and the decision sealed from them.",
292
+ "A review is signed by the people the assignment enrolls and is never edited "
293
+ "afterwards, so a report or a decision that does not match its assignment is refused "
294
+ "rather than repaired. The message names the document and the field it stopped on.",
295
+ "The Build itself is untouched: nothing about it changes when a review is refused, and "
296
+ "mr-data review --local runs every check this machine can run on its own.",
297
+ )
298
+ ),
299
+ "output": Family(
300
+ (
301
+ "This is about the path you asked the harness to write to.",
302
+ "Nothing is ever overwritten. Pick a path that does not exist yet, and make sure the "
303
+ "folder above it is a real directory you can write to.",
304
+ "A sealed folder is read-only on purpose, so clearing an old one takes "
305
+ "chmod -R u+w .work first, then remove it.",
306
+ )
307
+ ),
308
+ "sealing": Family(
309
+ (
310
+ "This is about a sealed Build and its Receipt: the fingerprints, the files the Receipt "
311
+ "lists, and the exact replay of the run.",
312
+ "Run mr-data verify on the Build folder to re-check it, and mr-data show to read its "
313
+ "Receipt.",
314
+ "A mismatch here means the folder is not the Build its Receipt describes. Do not "
315
+ "trust it, and do not edit anything inside it.",
316
+ )
317
+ ),
318
+ "network": Family(
319
+ (
320
+ "This is about fetching bytes over the network: the address, the name lookup, the "
321
+ "connection, and the limits on both.",
322
+ "The Courier fetches only the addresses a Recipe names and the allowlist permits.",
323
+ "Check the address in the Recipe first, then check the host is reachable and answers "
324
+ "within those limits.",
325
+ )
326
+ ),
327
+ "platform": Family(
328
+ (
329
+ "This step needs filesystem features your operating system does not offer, so the "
330
+ "harness stops rather than doing something it cannot prove.",
331
+ "Run mr-data preflight to see which features are missing and which commands still "
332
+ "work here.",
333
+ )
334
+ ),
335
+ "hosted lane": Family(
336
+ (
337
+ "This is about work running on the Mostly Right backend rather than on this computer: "
338
+ "signing in, queueing a run, watching one, and bringing its dataset back.",
339
+ "Nothing on this computer was changed. A run that was already queued keeps running "
340
+ "whatever this command answered -- watching one and downloading from one are reads, "
341
+ "and a submission that was refused never reached the backend at all.",
342
+ "The message names what did not hold. A run identifier is on the dashboard address "
343
+ "the submission printed; a credential is renewed with mr-data login; and a watch that "
344
+ "ended early can be started again, because it resumes from where it stopped.",
345
+ )
346
+ ),
347
+ "hosted authoring": Family(
348
+ (
349
+ "This is about authoring one hosted recipe proposal from a bundle of documents: the "
350
+ "Dataset, question, sources, table plan and recipe that mr-data propose reads and the "
351
+ "journal it writes beside them.",
352
+ "The bundle's journal records every resource that WAS created, so a stage that already "
353
+ "ran is not run again -- restore the journal if it is gone rather than re-registering. "
354
+ "A refused document costs nothing on the backend and is corrected and re-run; a "
355
+ "document changed after the resource built from it exists is refused with the document "
356
+ "named, and the remedy is to restore it or start a new bundle that adopts the created "
357
+ "resources by id.",
358
+ "The message names the document and what did not hold. mr-data list names the "
359
+ "workspace's datasets to adopt; the receipt's approve and build commands are the exact "
360
+ "next lines, and a confirmation in the dashboard may start the build itself.",
361
+ )
362
+ ),
363
+ "hosted resources": Family(
364
+ (
365
+ "This hosted worker does not have a usable finite memory boundary for graph work.",
366
+ "Check the worker's memory cgroup and selected memory class. The message says whether "
367
+ "the boundary was missing, unlimited, malformed, or below the supported graph budget.",
368
+ "No Build or Draft was sealed or published. Correct the worker memory allocation, "
369
+ "then run the hosted attempt again.",
370
+ )
371
+ ),
372
+ "dataset handoff": Family(
373
+ (
374
+ "This is about bringing a Build the hosted run released back to this computer and "
375
+ "writing its dataset notebook here.",
376
+ "The message names the step that stopped. Nothing on this computer was changed by a "
377
+ "step that did not finish: the rows are checked against the fingerprints the release "
378
+ "sealed, and checked again here, before anything is written.",
379
+ "Nothing hosted is affected either -- the released Build stays exactly where it is, "
380
+ "so running this again once the step the message names is fixed is safe.",
381
+ )
382
+ ),
383
+ "handoff": Family(
384
+ (
385
+ "This is about packaging a Build for Studio, or about a job Studio handed back.",
386
+ "The message names the step that stopped. Nothing was published.",
387
+ "The Build on your machine is untouched; you can package it again once the step the "
388
+ "message names is fixed.",
389
+ )
390
+ ),
391
+ "login": Family(
392
+ (
393
+ "This is about signing in to the Mostly Right cloud and the device credential stored "
394
+ "on this machine.",
395
+ "The message names the step that stopped: starting sign-in, waiting for browser "
396
+ "approval, or storing the credential.",
397
+ "A stopped sign-in stores nothing. Run mr-data login again to retry.",
398
+ )
399
+ ),
400
+ # Catalog facts and provenance require remediation distinct from Recipe source files.
401
+ "source catalog": Family(
402
+ (
403
+ "This is about one entry in the Source Catalog: the facts a public source is "
404
+ "described by, and the exact provider record each of those facts was read out of.",
405
+ "A fact is kept only together with the record and the path inside it that stated the "
406
+ "fact, so an entry whose facts and their provenance disagree is refused rather than "
407
+ "half-written.",
408
+ "The message names the field and the path inside the provider's record. Correct that "
409
+ "one field in the entry being written; the catalog you already have is unchanged.",
410
+ )
411
+ ),
412
+ # Filling that catalog from a public provider, which is the only part of it that leaves this
413
+ # machine.
414
+ "catalog fill": Family(
415
+ (
416
+ "This is about filling the Source Catalog from a public provider: the address it is "
417
+ "asked at, the key where one is needed to reach it, and the answers that came back.",
418
+ "A key is read from a file you own, one printable line of it, and it is never written "
419
+ "into the catalog or into a message. An answer that echoes it back, or that cannot be "
420
+ "read through in full before it is kept, is refused and nothing from it is retained.",
421
+ "The message names the part that did not hold. Nothing was published, so the catalog "
422
+ "you already have is unchanged and a fill that had got part-way can be started again "
423
+ "from its last checkpoint.",
424
+ )
425
+ ),
426
+ # Searching what is already on this machine, and the private index a search reads.
427
+ "local search": Family(
428
+ (
429
+ "This is about searching what is already on this machine: the sealed public catalog, "
430
+ "the private index over the Builds you have, and one search read from both.",
431
+ "Each store is opened at exactly the fingerprint it was published under, and every "
432
+ "filter, ordering and result is checked against the entries it claims to be about, so "
433
+ "a store or an answer that does not match its own record is refused rather than "
434
+ "answered from.",
435
+ "The message names the argument, the limit, or the root that did not hold. Correct "
436
+ "that one argument, or build the index again over the Builds you mean to search over; "
437
+ "docs/LOCAL-SEARCH.md lists the roots and fingerprints each store takes.",
438
+ )
439
+ ),
440
+ # Turning one converged provider sweep into catalog entries. This is not the `authoring`
441
+ # paragraph above it: that one is about the YAML file a person writes a Recipe in, and it is
442
+ # false of every one of these -- nobody is writing YAML here, and the answer to a policy
443
+ # coordinate that does not match, a title longer than an entry may carry, or a shard that will
444
+ # not read back is not "check your indentation". An answer written about a different thing is
445
+ # the defect this module exists to prevent, so this class gets its own paragraph.
446
+ "catalog authoring": Family(
447
+ (
448
+ "This is about writing the entries of one public catalog: the reviewed policy that "
449
+ "decides what may be said about a source, the provider records it is said from, and "
450
+ "the parts the entries are written into.",
451
+ "Only what the provider actually declared becomes a fact, under one reviewed policy "
452
+ "named by its own fingerprint, so a record the policy cannot honestly describe is set "
453
+ "aside with its reason rather than guessed at. The parts are written once and never "
454
+ "rewritten, and the list of them is published last.",
455
+ "The message names the record or the part it stopped on. Nothing was published, so the "
456
+ "catalog you already have is unchanged, and a run that had got part-way carries on "
457
+ "from what it already wrote.",
458
+ )
459
+ ),
460
+ # The durable generation itself: which entry a provider record is, what has happened to it,
461
+ # what changed since last time, and the receipt that says a generation was published.
462
+ "catalog generation": Family(
463
+ (
464
+ "This is about one published generation of a public catalog: which entry each "
465
+ "provider record is, the history kept for it, what changed since the last sweep, and "
466
+ "the receipt that describes the whole of it.",
467
+ "A record keeps one identity for as long as the provider keeps it, its history is "
468
+ "only ever added to, and a generation is compared against the exact predecessor it "
469
+ "names. The receipt carries the earlier stages' own receipts rather than a summary of "
470
+ "them, and a generation is read back off disk before that receipt is written.",
471
+ "The message names the coordinate, the count, or the bound that did not hold. Nothing "
472
+ "was published, so the generation you already have is unchanged, and a publication "
473
+ "that had got part-way carries on without doing its work again.",
474
+ )
475
+ ),
476
+ # Installing a published generation on this computer, with nothing but digests to trust.
477
+ "catalog update": Family(
478
+ (
479
+ "This is about installing a published catalog release on this computer: the archive "
480
+ "it arrives as, the exact SHA-256 published next to it, and the generation already "
481
+ "installed that the new one must descend from.",
482
+ "Nothing in the archive is trusted until its bytes match the published SHA-256, and "
483
+ "the installed folder is read back and checked end to end before the recorded state "
484
+ "advances. A release that does not name the installed generation as its predecessor "
485
+ "is refused rather than installed over it.",
486
+ "The message names the archive, the digest, or the link that did not hold. The "
487
+ "generation you already have is untouched, so searches keep working while you sort "
488
+ "it out.",
489
+ )
490
+ ),
491
+ # The optional model those searches can be ordered by, kept on this machine and pinned.
492
+ "search model": Family(
493
+ (
494
+ "This is about the search model kept on this machine: the pinned files it is made of, "
495
+ "the optional runtime that loads them, and the one bounded process it is asked in.",
496
+ "The whole model folder is opened without following links and checked against its "
497
+ "PIN.json before anything is loaded, and every answer it gives is held to the shape, "
498
+ "the size and the time it was pinned to.",
499
+ "The message names the file or the limit that did not hold. Put that folder in place "
500
+ "again from the pinned coordinate, install the optional runtime it names, and give "
501
+ "the command its absolute path; docs/LOCAL-SEARCH.md says how. Searching by words "
502
+ "alone needs none of it.",
503
+ )
504
+ ),
505
+ }
506
+
507
+
508
+ # ------------------------------------------------------------------------------------------------
509
+ # Layer 1: every code prefix, assigned
510
+ # ------------------------------------------------------------------------------------------------
511
+ #
512
+ # The prefix list is mechanical (an `ast` walk over the raise sites); the assignment below is a
513
+ # judgement, written out by hand. `tests/test_ux_remediation.py` fails when a new prefix appears
514
+ # here without an assignment.
515
+
516
+ PREFIX_FAMILY: dict[str, str] = {
517
+ "INGESTION": "ingestion",
518
+ "VIEWER": "viewer",
519
+ "VISUAL": "viewer",
520
+ # Finding and reading the files a plan names
521
+ "SOURCE": "sources",
522
+ "UNKNOWN": "sources",
523
+ "SAME": "sources",
524
+ "ROOT": "sources",
525
+ "MISSING": "sources",
526
+ "EVIDENCE": "sources",
527
+ "ENDPOINT": "sources",
528
+ "COUNT": "sources",
529
+ "UNSUPPORTED": "sources",
530
+ "HTTPS": "sources",
531
+ "SNAPSHOT": "sources",
532
+ "INPUT": "sources",
533
+ "DELAY": "sources",
534
+ "FITNESS": "sources",
535
+ "UNFIT": "sources",
536
+ "CATALOG": "sources",
537
+ "EMBEDDING": "sources",
538
+ "PROBE": "sources",
539
+ # The recorded publication-behaviour evidence, which is its own thing: not the source, not
540
+ # the connector that reached it, and not a Build.
541
+ "CADENCE": "cadence",
542
+ "RECOMMENDATION": "sources",
543
+ "REQUIREMENT": "sources",
544
+ "DISCOVERY": "sources",
545
+ # Whether a source may be used at all, and on what terms
546
+ "RIGHTS": "rights",
547
+ "LICENSE": "rights",
548
+ "CLASSIFICATION": "rights",
549
+ "OBLIGATION": "rights",
550
+ "CONDITIONAL": "rights",
551
+ "USE": "rights",
552
+ "DELETION": "rights",
553
+ "REMOVAL": "rights",
554
+ "RETENTION": "rights",
555
+ "TOMBSTONE": "rights",
556
+ "REJECTED": "rights",
557
+ # The packaged connector that reaches a source, and what it hands back
558
+ "ADAPTER": "connectors",
559
+ "STREAM": "connectors",
560
+ "SANDBOX": "connectors",
561
+ "QUERY": "connectors",
562
+ "SECRET": "connectors",
563
+ "TRUSTED": "connectors",
564
+ "AUTHENTICATED": "connectors",
565
+ "GOVERNED": "connectors",
566
+ "STORAGE": "connectors",
567
+ "ACQUIRED": "connectors",
568
+ "ACQUISITION": "connectors",
569
+ # `mr-data acquire`'s own refusals -- the document it was given, the allowlist it was given,
570
+ # and the hosted contract's caps. They are about reaching a source and the terms of doing so,
571
+ # which is what the connector family already answers for.
572
+ "ACQUIRE": "connectors",
573
+ "HOSTED": "connectors",
574
+ "PARSED": "connectors",
575
+ "RECORDED": "connectors",
576
+ "OBSERVATION": "connectors",
577
+ "EVENT": "connectors",
578
+ "WATERMARK": "connectors",
579
+ "VALUE": "connectors",
580
+ "RECEIPT": "connectors",
581
+ # The Receipt has to say which Reader opened the bytes, or say nothing at all; a half-stated
582
+ # decode identity is refused on the same side of the line as the rest of the Receipt's fields.
583
+ "DECODE": "connectors",
584
+ "TEMPORAL": "connectors",
585
+ "LIFECYCLE": "connectors",
586
+ # Opening the bytes
587
+ "DOCUMENT": "reading",
588
+ "PARSE": "reading",
589
+ "CSV": "reading",
590
+ "MEDIA": "reading",
591
+ "PEEK": "reading",
592
+ "GRIB": "reading",
593
+ "PLANNED": "reading",
594
+ "READER": "reading",
595
+ "SELECTOR": "reading",
596
+ "SIDECAR": "reading",
597
+ # Cleaning operations
598
+ "CAST": "cleaning",
599
+ "DECIMAL": "cleaning",
600
+ "CLEAN": "cleaning",
601
+ "RENAME": "cleaning",
602
+ "COLUMN": "cleaning",
603
+ "GRAIN": "cleaning",
604
+ "GRAPH": "recipes",
605
+ "OPERATION": "recipes",
606
+ "PREDICTION": "recipes",
607
+ "TARGET": "recipes",
608
+ # Joins
609
+ "JOIN": "joins",
610
+ # The checks a build must pass
611
+ "QUALITY": "quality",
612
+ "ROW": "quality",
613
+ "UNIQUE": "quality",
614
+ "VALIDATION": "quality",
615
+ "COVERAGE": "quality",
616
+ # Recipes, approvals, run settings, and the shape of the files they name
617
+ "RECIPE": "recipes",
618
+ # A declared unit and the grammar that resolves it. `UNIT_*` is raised where a contract reads
619
+ # the declaration; `UCUM_*` is raised inside the grammar itself and reaches a person through
620
+ # the same contract. Both are somebody writing a Recipe or a semantics document, so both
621
+ # answer where the rest of that writing does, and the ones a person actually meets are curated
622
+ # above so they can name `docs/UNITS.md` rather than a generic field correction.
623
+ "UNIT": "recipes",
624
+ "UCUM": "recipes",
625
+ "TYPE": "recipes",
626
+ "VERSION": "recipes",
627
+ "FIELDS": "recipes",
628
+ "IDENTIFIER": "recipes",
629
+ "INTEGER": "recipes",
630
+ "ENUM": "recipes",
631
+ "STRING": "recipes",
632
+ "SHA256": "recipes",
633
+ "RANGE": "recipes",
634
+ "UTC": "recipes",
635
+ "POSIX": "recipes",
636
+ "CONTROL": "recipes",
637
+ "NONCANONICAL": "recipes",
638
+ "EMPTY": "recipes",
639
+ "COLLECTION": "recipes",
640
+ "DUPLICATE": "recipes",
641
+ "REFERENCE": "recipes",
642
+ "CONTEXT": "recipes",
643
+ "ENGINE": "recipes",
644
+ "BACKEND": "recipes",
645
+ "TEXT": "recipes",
646
+ "UNICODE": "recipes",
647
+ "TIMESTAMP": "recipes",
648
+ "LIMIT": "recipes",
649
+ "SEMVER": "recipes",
650
+ "COMMIT": "recipes",
651
+ "DOTTED": "recipes",
652
+ "SAFE": "recipes",
653
+ "DATA": "recipes",
654
+ "BOOLEAN": "recipes",
655
+ # The exact text of a JSON file, whoever wrote it
656
+ "JSON": "json",
657
+ # Writing the entries of a public catalog. The one other thing spelled `AUTHOR_` in this tree
658
+ # is `AUTHOR_YAML_*`, which is a person writing a Recipe in YAML and is routed by the compound
659
+ # table below; everything else under this prefix is the catalog authoring stage.
660
+ "AUTHOR": "catalog authoring",
661
+ # Workbench folder state and the durable run. The split is by who can answer: `RESUME`,
662
+ # `REVIEW` and `PRODUCER` are about where the run got to, and `mr-data status` reads that and
663
+ # says so. `WORKSPACE`, `RUN` and the two `DEFINITION` prefixes are about the control tree
664
+ # itself -- and every command that opens the folder, `status` included, opens that tree first,
665
+ # so naming status as the fix for one of these names a loop.
666
+ "WORKSPACE": "workbench record",
667
+ "RUN": "workbench record",
668
+ "DEFINITION": "workbench record",
669
+ "DEFINITIONS": "workbench record",
670
+ "RESUME": "workbench",
671
+ "REVIEW": "workbench",
672
+ "PRODUCER": "workbench",
673
+ # The documents a sealed review is made of, and the envelope they are sealed into. Every one
674
+ # of these prefixes is raised in `review.py`, which is reached by `mr-data review`,
675
+ # `mr-data verify` and `mr-data sign-review`.
676
+ "ASSIGNMENT": "review",
677
+ "REPORT": "review",
678
+ "DECISION": "review",
679
+ "ENVELOPE": "review",
680
+ "LOCK": "review",
681
+ "KEY": "review",
682
+ # Paths written to
683
+ "OUTPUT": "output",
684
+ # Sealing and re-checking a build
685
+ "CANDIDATE": "sealing",
686
+ "PROFILE": "sealing",
687
+ "MANIFEST": "sealing",
688
+ "MEMBER": "sealing",
689
+ "REPLAY": "sealing",
690
+ "DIGEST": "sealing",
691
+ "DATASET": "sealing",
692
+ # Listing the builds under a folder. Both codes it raises today are curated; the family is here
693
+ # so a new one cannot arrive unassigned, and `sealing` is the right paragraph because reading a
694
+ # listing is reading sealed Builds -- with the class rule above keeping the tamper wording away
695
+ # from a folder that holds none.
696
+ "LIST": "sealing",
697
+ # Fetching over the network
698
+ "URL": "network",
699
+ "DNS": "network",
700
+ "EGRESS": "network",
701
+ "TLS": "network",
702
+ "HTTP": "network",
703
+ "PEER": "network",
704
+ "REDIRECT": "network",
705
+ "REQUEST": "network",
706
+ "RESPONSE": "network",
707
+ "RATE": "network",
708
+ "TIMEOUT": "network",
709
+ "TOTAL": "network",
710
+ "TRANSPORT": "network",
711
+ "CONCURRENCY": "network",
712
+ "CONTENT": "network",
713
+ "USER": "network",
714
+ "OBFUSCATED": "network",
715
+ "NON": "network",
716
+ "METADATA": "network",
717
+ "RESOLUTION": "network",
718
+ "BYTE": "network",
719
+ "LIMITER": "network",
720
+ "MULTIPART": "network",
721
+ "RETRIEVAL": "network",
722
+ "UNSOLICITED": "network",
723
+ # Operating-system support refusals
724
+ "PLATFORM": "platform",
725
+ # Packaging for Studio and the jobs it hands back
726
+ "ARTIFACT": "handoff",
727
+ "API": "handoff",
728
+ "STUDIO": "handoff",
729
+ "SIGNED": "handoff",
730
+ "SIGNING": "handoff",
731
+ "VERIFIER": "handoff",
732
+ "CRAWLER": "handoff",
733
+ "CREDENTIAL": "handoff",
734
+ "JOB": "handoff",
735
+ "DESCRIPTOR": "handoff",
736
+ "BOOTSTRAP": "handoff",
737
+ "WORKER": "handoff",
738
+ "WORKLOAD": "handoff",
739
+ # A research session's worker is a Builder-role attempt on a session-scoped run, so its
740
+ # refusals are the same subject as every other hosted attempt's: something about handing
741
+ # work to the backend did not hold. It sits here rather than under `sources` -- where
742
+ # `PROBE` sits, because a probe is about a source -- because a lease is about the worker.
743
+ "SESSION": "handoff",
744
+ "CAPABILITY": "handoff",
745
+ "PREDECESSOR": "handoff",
746
+ "PLAN": "handoff",
747
+ "DEPLOY": "handoff",
748
+ # Signing in to the cloud, and the device credential `mr-data login` stores locally.
749
+ "LOGIN": "login",
750
+ # The direct Cloud device-key client serves the same sign-in and credential lifecycle as the
751
+ # command's `LOGIN_*` outcomes. Its errors need the same human explanation, while keeping the
752
+ # Cloud-owned error names distinct on the wire.
753
+ "CLOUD": "login",
754
+ # A hosted run refused before any worker exists, because the workspace is over one of its
755
+ # standing allowances. It is raised where the job is taken up, beside the other worker codes.
756
+ "ADMISSION": "handoff",
757
+ # One entry in the Source Catalog. All five are raised while a v2 entry is being made: the
758
+ # harvester that stated a fact, the fact's own value and provenance, the order its provider
759
+ # paths are written in, the shape of one of those paths, and the whitespace inside one.
760
+ "COORDINATE": "source catalog",
761
+ "FACT": "source catalog",
762
+ "ORDER": "source catalog",
763
+ "PROVIDER": "source catalog",
764
+ "UNSAFE": "source catalog",
765
+ # Filling that catalog from a public provider. `DATAGOV` is the keyed metadata endpoint --
766
+ # the key file, the rate it allows, and what came back through it -- and `FILL` is the address
767
+ # the fill was pointed at. Neither is the `network` paragraph: that one is about the addresses
768
+ # a Recipe names and about a Courier that is never given a sign-in, and both halves are wrong
769
+ # here.
770
+ "DATAGOV": "catalog fill",
771
+ "FILL": "catalog fill",
772
+ # Searching the two stores on this machine, and publishing the private one. `LOCAL` covers the
773
+ # search itself and the record it keeps; `BUILD` and `INDEX` are raised while the private index
774
+ # is built over the Builds already sealed here.
775
+ "LOCAL": "local search",
776
+ "BUILD": "local search",
777
+ "INDEX": "local search",
778
+ # The optional model a search can be ordered by. It is not `local search`: that paragraph
779
+ # answers a filter or a fingerprint somebody passed, and none of these is about one -- a model
780
+ # folder that does not match its PIN.json is put in place again rather than corrected.
781
+ "NEURAL": "search model",
782
+ # Reading one page of a public provider's answer, and the harvester that knows how. `HARVEST`
783
+ # and `HARVESTER` are the parser and the registry it is looked up in, and `RECORD` is the one
784
+ # bare identifier check beside them; all three are reached only while a catalog is being
785
+ # filled, so they answer with the same paragraph the address and the key do.
786
+ "HARVEST": "catalog fill",
787
+ "HARVESTER": "catalog fill",
788
+ "RECORD": "catalog fill",
789
+ # The durable generation: which entry a provider record is, what changed since last time, and
790
+ # the receipt that describes a whole published generation.
791
+ "IDENTITY": "catalog generation",
792
+ "DELTA": "catalog generation",
793
+ "GENERATION": "catalog generation",
794
+ "PUBLISH": "catalog generation",
795
+ # The range-local parts one generation is published as, and read back from: its packs, their
796
+ # members, the postings and bounds over them, the head that names them and the work journal an
797
+ # interrupted publication leaves. Ninety-three codes, raised from three modules, every one of
798
+ # them about the durable generation this paragraph is written about -- including the ones
799
+ # raised while reading it back, because what a reader refuses there is the generation. The nine
800
+ # spelled `PACKED_SEARCH_` are the exception, and the compound table below routes them.
801
+ "PACKED": "catalog generation",
802
+ # A response over the byte budget kept across a redirect chain, raised beside the other
803
+ # retrieval limits.
804
+ "AGGREGATE": "network",
805
+ # Every refusal the thin hosted lane raises. The prefix is one word on purpose: the lane is one
806
+ # subject -- work that happens on the backend -- and splitting it by whether the credential,
807
+ # the submission, the stream or the download is what stopped would put four paragraphs in front
808
+ # of a reader who needs the one sentence the message already carries.
809
+ "THIN": "hosted lane",
810
+ }
811
+
812
+
813
+ # The same table one segment deeper, consulted first, for the few prefixes that are honestly two
814
+ # different subjects. It exists for exactly one shape: a prefix whose codes were written by two
815
+ # lanes about two things, where routing the whole prefix to either family would put a paragraph
816
+ # about one in front of somebody looking at the other.
817
+ #
818
+ # `AUTHOR_YAML_*` is a person writing a Recipe in YAML; every other `AUTHOR_*` code is the catalog
819
+ # authoring stage, and the plain answer for one is false of the other. `DIGEST` -- the bare code,
820
+ # not a prefix -- is a fingerprint field inside a provider's page, and the `sealing` paragraph it
821
+ # would otherwise reach is about a sealed Build and tells the reader to run `mr-data verify` on a
822
+ # folder that has nothing to do with it. `RECORD_ID` is the same case one file over.
823
+ #
824
+ # `PACKED_SEARCH_*` is the third case: every other `PACKED_*` code is about the published
825
+ # generation itself, and these nine are about one question asked of it -- the store it was asked
826
+ # in, the budget it was given, and the order of the answer. Somebody whose search was refused for
827
+ # a limit is not being told that nothing was published and their catalog is unchanged; they are
828
+ # being told which argument, limit or root did not hold, which is the `local search` paragraph.
829
+ COMPOUND_PREFIX_FAMILY: dict[str, str] = {
830
+ # Sealing a recording is not reaching a source: no connector runs, no Clean room opens, and
831
+ # the answer is about a file on this computer. The connector *configuration* stays with the
832
+ # connectors, because that is what it is about.
833
+ "STREAM_CAPTURE": "capture",
834
+ "STREAM_RECORDING": "capture",
835
+ "STREAM_MESSAGE": "capture",
836
+ "STREAM_EVENT": "capture",
837
+ # A rights-decision refusal is about deciding rights again, not about reading files.
838
+ "CATALOG_RIGHTS": "rights",
839
+ # An install refusal is about the release on this computer, not about the catalog's content.
840
+ "CATALOG_UPDATE": "catalog update",
841
+ "AUTHOR_YAML": "authoring",
842
+ "THIN_PROPOSE": "hosted authoring",
843
+ "DIGEST": "catalog fill",
844
+ "RECORD_ID": "catalog fill",
845
+ "PACKED_SEARCH": "local search",
846
+ # Worker memory provisioning is operational capacity, not a source connector.
847
+ "HOSTED_MEMORY": "hosted resources",
848
+ # Bringing a released Build back is the opposite direction from packaging one and sending it,
849
+ # which is what the deployment family's paragraph is about. Every code on the return trip
850
+ # answers through its own subject instead, so the ones with no sentence of their own still say
851
+ # something true.
852
+ "DEPLOY_DATASET": "dataset handoff",
853
+ }
854
+
855
+
856
+ def family_for(code: str) -> str:
857
+ """The family one code answers through: its first two segments, then its first.
858
+
859
+ Read in that order so a compound entry can correct a prefix rather than having to replace it,
860
+ which keeps the table above a list of subjects rather than a list of exceptions.
861
+ """
862
+
863
+ parts = code.split("_")
864
+ compound = "_".join(parts[:2])
865
+ if compound in COMPOUND_PREFIX_FAMILY:
866
+ return COMPOUND_PREFIX_FAMILY[compound]
867
+ return PREFIX_FAMILY.get(parts[0], "")
868
+
869
+
870
+ # ------------------------------------------------------------------------------------------------
871
+ # Layer 2: curated codes
872
+ # ------------------------------------------------------------------------------------------------
873
+ #
874
+ # The order below is the order the recorded quirks were found by driving the example build.
875
+
876
+ _PIN_DRIFT = (
877
+ "The run settings pin a Recipe fingerprint and an exact set of engine versions. One of them "
878
+ "no longer matches what is installed here.",
879
+ "Re-export the Recipe, read the fingerprint off the recipe-export receipt, and put it in "
880
+ "recipe-execution.json.",
881
+ "The message names the field that does not match.",
882
+ )
883
+
884
+ _OVERWRITE = (
885
+ "{path} already holds something, and builds are never overwritten.",
886
+ "Choose a name that is not in use, or look at what is already there.",
887
+ )
888
+
889
+ # The same refusal about a path that is taken by something that is not a Build. `OUTPUT_EXISTS` is
890
+ # raised by every writer that will not overwrite a folder, and the caller that looks before it
891
+ # speaks -- the command line's error boundary, and `mr-data preflight` -- reports what it found. A
892
+ # Recipe file, an approval, a signed report or any ordinary file in the way is not a build, and
893
+ # `mr-data show` on one answers with a path complaint harder to read than the refusal it was
894
+ # offered as the fix for. So the noun moves and the command goes, on the evidence rather than on a
895
+ # guess about which command was running.
896
+ # Two sentences, neither of which points at a neighbouring line: this wording is printed by a
897
+ # refusal, where what was being written is named above it, and by `mr-data preflight`, where the
898
+ # advice block is printed before the checks it is about.
899
+ # `ls -l` reports every path kind without opening a potentially blocking object.
900
+ _OVERWRITE_NOT_A_BUILD = (
901
+ "{path} already holds something that is not a build, and nothing here is ever overwritten.",
902
+ "Choose a name that is not in use, or run ls -l on that path to see what is there.",
903
+ )
904
+
905
+ # These refusals concern files, not Build folders. No trailing command is safe for every file type.
906
+ _RECIPE_OVERWRITE = (
907
+ "{path} already holds a file, and nothing written here is ever overwritten.",
908
+ "The line above says what was being written: a Recipe, or an approval of one.",
909
+ "Choose a name that is not in use, or open the file that is already there to see which it is.",
910
+ )
911
+
912
+ _PACKAGED_OVERWRITE = (
913
+ "{path} already holds a file, and nothing written here is ever overwritten.",
914
+ "The line above says what was being written: one Build, packaged for Studio.",
915
+ "Choose a name that is not in use, or open the file that is already there.",
916
+ )
917
+
918
+
919
+ # The same three refusals, about a taken path that is not a file at all.
920
+ #
921
+ # All three are raised on the one `FileExistsError` that `O_CREAT | O_EXCL | O_NOFOLLOW` gives back,
922
+ # and that errno is what a folder, pipe, socket, device, or link can return. The wording must not
923
+ # assert a file kind that was never observed or recommend opening a potentially blocking object.
924
+ #
925
+ # The repair is not a fourth sentence about pipes. The writer now looks, once, at the descriptor it
926
+ # already holds, and what it saw fills `{kind}`; where it saw nothing the noun is kind-neutral and
927
+ # nothing is claimed. `ls -l` is the named command because it is the one reading that answers on
928
+ # every one of those kinds -- and on a link, where it also says where the link points -- without
929
+ # opening anything.
930
+ def _taken_by_something_else(*middle: str) -> tuple[str, ...]:
931
+ return (
932
+ "{path} already holds {kind}, and nothing written here is ever overwritten.",
933
+ *middle,
934
+ "Choose a name that is not in use, or run ls -l on that path to see what is there.",
935
+ )
936
+
937
+
938
+ _RECIPE_OVERWRITE_KIND = _taken_by_something_else(
939
+ "The line above says what was being written: a Recipe, or an approval of one.",
940
+ )
941
+
942
+ _PACKAGED_OVERWRITE_KIND = _taken_by_something_else(
943
+ "The line above says what was being written: one Build, packaged for Studio.",
944
+ )
945
+
946
+ _SIGNED_REPORT_OVERWRITE_KIND = _taken_by_something_else()
947
+
948
+ _PLATFORM = (
949
+ "This step needs filesystem features your operating system does not offer, so the harness "
950
+ "stops rather than doing something it cannot prove.",
951
+ "Everything that does not need those features still works.",
952
+ )
953
+
954
+ # ------------------------------------------------------------------------------------------------
955
+ # The class rule: no answer about a Build may be given where there is no Build
956
+ # ------------------------------------------------------------------------------------------------
957
+ #
958
+ # Curating one code at a time fixes one instance at a time. The `sealing` family is written about a
959
+ # Build whose Receipt and bytes disagree, and its sentences -- "do not trust it", "do not edit
960
+ # anything inside it" -- are a tamper diagnosis. Given a folder with nothing built in it, every one
961
+ # of them is false, and false in the direction that frightens people. So the rule is stated once,
962
+ # over the whole family, rather than one curated code at a time: when the caller looked and found no
963
+ # Build, an uncurated sealing code does not fall through to that paragraph.
964
+ #
965
+ # Curated entries are not overridden. An entry is a sentence someone wrote for one exact failure,
966
+ # and some of them -- a link on the way in, a race, an output path already in use -- are true
967
+ # whether or not a Build is there. An entry that needs the alternative declares it, as
968
+ # `CANDIDATE_ABSENT` and `CANDIDATE_ANCESTOR_INVALID` do.
969
+
970
+ _ABOUT_AN_EXISTING_BUILD: frozenset[str] = frozenset({"sealing"})
971
+
972
+ NOTHING_BUILT_YET: tuple[str, ...] = (
973
+ "{path} is there, but nothing has been built in it yet, so there is no Build to read or check.",
974
+ "Run mr-data list to see the builds you have, or mr-data build to make one.",
975
+ "If you were expecting a build there, check the path: it is usually the folder just above or "
976
+ "just below the one you named.",
977
+ )
978
+
979
+
980
+ CODES: dict[str, Remediation] = {
981
+ "TABLE_PREVIEW_INVALID": Remediation(
982
+ lines=(
983
+ "The sealed Table could not be rendered as a bounded preview.",
984
+ "Verify the Build and rebuild the Table from its pinned Recipe and inputs.",
985
+ )
986
+ ),
987
+ # A retired operation is the one refusal where "correct that one field and run it again" is
988
+ # not merely vague but impossible: no value the field can take makes the plan valid, and the
989
+ # nearest thing an author reaches for next -- lag, lead, rolling, window_target -- is refused
990
+ # by name a layer down. So this code answers for itself rather than through its family.
991
+ "OPERATION_RETIRED": Remediation(
992
+ lines=(
993
+ "That operation has been retired, so a new plan cannot name it. This is not a "
994
+ "spelling or a version you can correct: there is no value that field can take that "
995
+ "makes the plan valid.",
996
+ "Remove the node and the output column it produced, and point the plan's terminal at "
997
+ "the node it followed. Nothing replaces it, and a window, lag, lead, or rolling "
998
+ "operation is not a substitute -- those are refused by name as well. Neither is a "
999
+ "derive expression or an extra join that reproduces the same thing.",
1000
+ "A Build already sealed with the operation, and a Recipe already approved with it, "
1001
+ "are unaffected: those still verify and still run.",
1002
+ )
1003
+ ),
1004
+ "UNIT_CODE": Remediation(
1005
+ lines=(
1006
+ "That is not a unit code. A unit is written in the accepted subset of the unit "
1007
+ "standard -- `m`, `cm`, `ug/m3`, `km/h`, `Cel` -- rather than in words, and the "
1008
+ "message names the part of what you wrote that put it outside that subset.",
1009
+ "The whole accepted subset, what it refuses, and the fifteen older names that still "
1010
+ "work are in docs/UNITS.md. Where a column carries no physical unit at all, a "
1011
+ "semantics document declares `none` -- but a plan declares nothing, so leave the "
1012
+ "column out of `column_units`, or leave `output_unit` off the expression.",
1013
+ )
1014
+ ),
1015
+ "UNIT_DIMENSION_MISMATCH": Remediation(
1016
+ lines=(
1017
+ "Two values the plan brings together are declared in units that do not measure the "
1018
+ "same kind of thing -- a length against a time, say. No conversion exists between "
1019
+ "them, so this is a declaration to correct rather than arithmetic to add.",
1020
+ "The message names both declarations. Check which of the two is wrong against the "
1021
+ "source it came from, and correct it where it was declared -- on the `column_units` "
1022
+ "of a source node, or on an expression's `output_unit`.",
1023
+ )
1024
+ ),
1025
+ "UNIT_CONVERSION_MISSING": Remediation(
1026
+ lines=(
1027
+ "Two values the plan brings together measure the same thing in different units, and "
1028
+ "nothing in the plan converts one to the other. Combining them would produce a value "
1029
+ "that is part one unit and part another.",
1030
+ "Add a derive node before the merge that converts one side, and declare the unit its "
1031
+ "result is in. That declared conversion is checked against the arithmetic you wrote, "
1032
+ "so a wrong factor is refused rather than trusted.",
1033
+ "The derive has to write a new column: a derive that reuses the name it reads from is "
1034
+ "refused when the plan runs. Project the original away and rename the new one, so the "
1035
+ "two sides of the merge carry the same columns in the same order.",
1036
+ )
1037
+ ),
1038
+ "UNIT_CONVERSION_FACTOR": Remediation(
1039
+ lines=(
1040
+ "The expression does not do what converting between those two units requires. The "
1041
+ "message states the arithmetic the units imply and the arithmetic the expression "
1042
+ "performs, in that order.",
1043
+ "There are three ways out. Correct the expression; correct the declared unit if the "
1044
+ "expression was right; or drop `output_unit` where the expression was never a "
1045
+ "conversion at all, because an undeclared result claims nothing and is checked "
1046
+ "against nothing.",
1047
+ "A temperature conversion has two parts -- Celsius to Fahrenheit is a multiply by 9/5 "
1048
+ "and an add of 32 -- and doing only the first is what this refusal usually means.",
1049
+ "There is no decimal literal in a plan, so write the fraction as whole numbers with "
1050
+ "the division last: `(x * 9 + 160) / 5` rather than `x * 9 / 5 + 32`. Every operation "
1051
+ "takes operands of one exact numeric type and a quotient lands in the decimal type, so "
1052
+ "a whole number added after a division is refused when the plan runs.",
1053
+ )
1054
+ ),
1055
+ "UNIT_CONVERSION_UNVERIFIABLE": Remediation(
1056
+ lines=(
1057
+ "The unit declared for this expression cannot be checked against anything, and a "
1058
+ "claim nobody can check is the thing these gates exist to stop. The message says "
1059
+ "which of three reasons applies.",
1060
+ "A unit is admitted only where something establishes it: the expression converts one "
1061
+ "declared column and the arithmetic is exactly that conversion; or its operands "
1062
+ "settle what it is in, every one read unscaled; or nothing it reads is declared. "
1063
+ "Write the conversion as whole-number multiplies, divides and adds over one declared "
1064
+ "column, and put any branching in another node.",
1065
+ "Declaring the other columns it reads does not help on its own -- what is checked is "
1066
+ "whether the arithmetic can be read, not who declared what.",
1067
+ "Dropping `output_unit` is always available: the result then carries no unit claim, "
1068
+ "and nothing downstream can rely on one.",
1069
+ )
1070
+ ),
1071
+ "UNIT_UNSUPPORTED_IN_VERSION": Remediation(
1072
+ lines=(
1073
+ "This document is written at a semantics generation whose unit vocabulary is the "
1074
+ "fifteen original names, and the unit named here is a code from the wider subset.",
1075
+ "Either write the older name for the same unit, or move the document to "
1076
+ "`table-semantics.v2`, which carries any code the grammar resolves. That is a "
1077
+ "change to the document's `schema_version` rather than to the field this message "
1078
+ "points at. Both generations are read forever; see docs/UNITS.md.",
1079
+ )
1080
+ ),
1081
+ "CATALOG_UPDATE_CHAIN": Remediation(
1082
+ lines=(
1083
+ "The release you gave does not descend from the generation installed here.",
1084
+ "If you skipped releases, install them in order. Only when you trust where this "
1085
+ "release's digest came from, run the update again with --replace to accept it as "
1086
+ "the new starting point.",
1087
+ )
1088
+ ),
1089
+ "CATALOG_UPDATE_DIGEST": Remediation(
1090
+ lines=(
1091
+ "The archive's bytes do not match the SHA-256 you typed.",
1092
+ "Copy the digest again from where the release was published, and download the "
1093
+ "archive again if it still does not match.",
1094
+ )
1095
+ ),
1096
+ "CATALOG_UPDATE_DIGEST_SPELLING": Remediation(
1097
+ lines=(
1098
+ "What you typed after --archive-sha256 is not the shape of a SHA-256.",
1099
+ "Copy the digest again exactly as published: 64 hex characters, all lowercase, "
1100
+ "with no prefix around them. The archive itself was not judged.",
1101
+ )
1102
+ ),
1103
+ "CATALOG_UPDATE_INSTALL": Remediation(
1104
+ lines=(
1105
+ "The release could not be written into the catalog folder.",
1106
+ "The message names the path in the way. Remove it only if it is an unfinished "
1107
+ "leftover or not this release's bytes, check free space and permissions, and run "
1108
+ "the update again; nothing recorded has changed.",
1109
+ )
1110
+ ),
1111
+ "VISUAL_RUN_TERMINAL": Remediation(
1112
+ lines=(
1113
+ "That visual research run has already ended and cannot accept more events.",
1114
+ "A completed run is read-only; use its existing Build. If it failed, start a new "
1115
+ "research run before retrying the Build.",
1116
+ )
1117
+ ),
1118
+ # Distinguish an absent Build from a present path that cannot be opened safely.
1119
+ "CANDIDATE_ANCESTOR_INVALID": Remediation(
1120
+ lines=(
1121
+ "There is no build at {path}.",
1122
+ "Run mr-data list to see the builds you have, or mr-data init to start one.",
1123
+ ),
1124
+ # The folder is there and holds nothing built. Diagnosing a link or a race here is the one
1125
+ # way this map can be worse than silence: it would send someone looking for tampering when
1126
+ # all they have done is not run the build yet.
1127
+ when_nothing_built_yet=(
1128
+ "{path} is there, but nothing has been built in it yet.",
1129
+ # This branch knows only that the path exists, not that it is a Workbench folder.
1130
+ "mr-data status says how far a workbench folder got, and mr-data run builds it.",
1131
+ "mr-data list shows the builds you already have.",
1132
+ ),
1133
+ # Folder-only advice is withheld when the observed object has another concrete kind.
1134
+ when_not_a_folder=(
1135
+ "There is {kind} at {path}, and a build is read out of a folder.",
1136
+ "Point the command at the Build folder itself, or at the workbench folder that holds "
1137
+ "one.",
1138
+ "mr-data list shows the builds you already have.",
1139
+ ),
1140
+ when_path_exists=(
1141
+ "{path} is there, but the folders leading to it could not be opened safely.",
1142
+ # This code establishes only a non-folder or a race; links have a distinct code.
1143
+ "A step of that path is not a plain folder, or it changed while it was being opened.",
1144
+ "Move the build to a plain, stable path and try again.",
1145
+ ),
1146
+ ),
1147
+ "CANDIDATE_ANCESTOR_SYMLINK": Remediation(
1148
+ lines=(
1149
+ "A folder on the way to {path} is a link, and the harness will not follow one.",
1150
+ "Use the real path, or move the build somewhere that has no links above it.",
1151
+ ),
1152
+ # `pipeline._open_candidate_run_chain` raises this only inside `if stat.S_ISLNK(...)`, on
1153
+ # the component it names. The kind is the reason the refusal exists, so the sentence may
1154
+ # say it without the boundary looking a second time and answering about a later moment.
1155
+ kinds_established=(A_LINK,),
1156
+ ),
1157
+ # Raised only by the hosted producer, while turning the Build's own sealed column summary
1158
+ # into the document the service's contract describes. Nothing the caller did on this machine
1159
+ # causes it, and nothing they can do on this machine fixes it -- so the lines say what is
1160
+ # known and point at the people who can act, rather than inventing a step.
1161
+ "PROFILE_INVALID": Remediation(
1162
+ lines=(
1163
+ "The column summary sealed into this Build could not be turned into the one the "
1164
+ "service reads.",
1165
+ "The Build itself is fine. This is a mismatch between the harness and the service, "
1166
+ "so report it rather than rebuilding.",
1167
+ ),
1168
+ ),
1169
+ "CANDIDATE_ANCESTOR_RACE": Remediation(
1170
+ lines=(
1171
+ "The folders leading to {path} changed while they were being opened.",
1172
+ "Something else is moving or replacing them. Stop that, then run the command again.",
1173
+ ),
1174
+ ),
1175
+ # Only the Build-folder refusal may name a command that opens what is in the way, and only when
1176
+ # the caller observed a Build there. `OUTPUT_EXISTS` is raised on a bare
1177
+ # "this path is taken" test by every writer that will not overwrite a folder, so the path in
1178
+ # the way is regularly a file; the alternative wording below is what a caller that looked gets.
1179
+ "OUTPUT_EXISTS": Remediation(
1180
+ lines=_OVERWRITE,
1181
+ command="mr-data show {path}",
1182
+ when_nothing_built_yet=_OVERWRITE_NOT_A_BUILD,
1183
+ # `mr-data show` is valid only when the caller observed a Build at this path.
1184
+ command_needs_a_build=True,
1185
+ ),
1186
+ "RECIPE_OUTPUT_EXISTS": Remediation(
1187
+ lines=_RECIPE_OVERWRITE,
1188
+ when_not_a_file=_RECIPE_OVERWRITE_KIND,
1189
+ ),
1190
+ "CANDIDATE_ENVELOPE_OUTPUT_EXISTS": Remediation(
1191
+ lines=_PACKAGED_OVERWRITE,
1192
+ when_not_a_file=_PACKAGED_OVERWRITE_KIND,
1193
+ ),
1194
+ "DEPLOY_REQUEST_INVALID": Remediation(
1195
+ lines=(
1196
+ "This Recipe cannot be sent to Studio in its current form.",
1197
+ "Export the Recipe again with the current harness, then review the new Recipe before "
1198
+ "approving it.",
1199
+ ),
1200
+ ),
1201
+ "DEPLOY_WORKER_POLICY_MISMATCH": Remediation(
1202
+ lines=(
1203
+ "This Recipe would be checked under different rules after it is sent.",
1204
+ "Nothing was approved or activated.",
1205
+ "Export the Recipe again with the current harness. If the fingerprints still differ, "
1206
+ "wait for the hosted service to be updated before trying again.",
1207
+ ),
1208
+ ),
1209
+ # Bringing a released Build back. The whole family paragraph is about packaging one and
1210
+ # sending it, which is the opposite direction, so the failures a person actually meets on the
1211
+ # return trip answer for themselves.
1212
+ "DEPLOY_DATASET_NOT_RELEASED": Remediation(
1213
+ lines=(
1214
+ "The run this deployment started has not released a Build yet.",
1215
+ "Nothing is wrong: hosted work takes as long as it takes, and the rows stay "
1216
+ "unavailable until it has passed its independent check.",
1217
+ "Check where the run is up to, and run this again once it says the dataset is ready.",
1218
+ ),
1219
+ ),
1220
+ "DEPLOY_DATASET_TARGET_OCCUPIED": Remediation(
1221
+ lines=(
1222
+ "Something is already in the folder the released Build has to go into, and it is "
1223
+ "not the Build that was released.",
1224
+ "Nothing was changed there.",
1225
+ "Point this at an empty folder with --into, or move what is in the way first.",
1226
+ ),
1227
+ ),
1228
+ "DEPLOY_DATASET_DOWNLOAD_INTEGRITY": Remediation(
1229
+ lines=(
1230
+ "The rows that came back do not match the fingerprint the release sealed, so none of "
1231
+ "them were kept.",
1232
+ "This is what the check is for; nothing was written and nothing was shown.",
1233
+ "Run this again. If it keeps happening, the copy being served is not the copy that "
1234
+ "was released, and that is worth reporting.",
1235
+ ),
1236
+ ),
1237
+ "DEPLOY_DATASET_ARTIFACT_CONTRACT_INVALID": Remediation(
1238
+ lines=(
1239
+ "What came back does not meet the agreed shape for what it claims to be, so it was "
1240
+ "refused rather than shown.",
1241
+ "Nothing was written here.",
1242
+ "This is a mismatch between what the cloud stored and what this version reads; it "
1243
+ "will not fix itself by retrying, so report it with the run this came from.",
1244
+ ),
1245
+ ),
1246
+ "DEPLOY_DATASET_LOCAL_CHECK_FAILED": Remediation(
1247
+ lines=(
1248
+ "The Build that came back did not pass the same checks a Build made on this machine "
1249
+ "has to pass, so no notebook was written from it.",
1250
+ "The rows are never shown on somebody else's word; they are re-checked here first, "
1251
+ "and this is that check refusing.",
1252
+ "Run this again into an empty folder. If it refuses again, report it with the run "
1253
+ "this came from.",
1254
+ ),
1255
+ ),
1256
+ "DEPLOY_DATASET_NARRATION_CONFLICT": Remediation(
1257
+ lines=(
1258
+ "The workbench window you pointed at is already showing a different piece of work.",
1259
+ "The dataset was not written, so nothing there has changed.",
1260
+ "Leave --research-dir off to write the dataset without telling a window about it, or "
1261
+ "point it at a fresh workbench folder.",
1262
+ ),
1263
+ ),
1264
+ "SIGNED_REPORT_OUTPUT_EXISTS": Remediation(
1265
+ # The strongest claim of the three, and the one furthest from what was checked: a taken
1266
+ # path that has never been opened is not a signed report, and a folder or a socket cannot
1267
+ # be one at all. The claim is kept for the one case the writer confirms.
1268
+ lines=(
1269
+ "{path} already holds a signed report, and reports are never overwritten.",
1270
+ "Choose a name that is not in use.",
1271
+ ),
1272
+ when_not_a_file=_SIGNED_REPORT_OVERWRITE_KIND,
1273
+ ),
1274
+ # The other half of writing a file: the target is free, but the folder above it is absent.
1275
+ # This error names the command that establishes the missing precondition.
1276
+ "OUTPUT_PARENT_ABSENT": Remediation(
1277
+ lines=(
1278
+ "The folder {path} would go into is not there. The line above names it.",
1279
+ "Create it first, with mkdir -p, then run the command again.",
1280
+ # Only the exclusive file writer raises this code; Build output creates missing parents.
1281
+ "Nothing is made for you on the way to a file these commands write, because a typo in "
1282
+ "a path would quietly build a folder tree nobody asked for.",
1283
+ ),
1284
+ # Both raise sites use `lstat`, so a dangling link does not masquerade as an absent parent.
1285
+ kinds_established=(NOTHING_THERE,),
1286
+ ),
1287
+ # The third way a path can be unwritable, and the one nobody expects: a link somewhere above
1288
+ # it. Sealing opens each folder on the way down without following anything, because a link that
1289
+ # is repointed mid-build would move where a sealed Build lands. `/tmp` is such a link on macOS
1290
+ # and is where a first build is most often written, so this one is met early and reads as
1291
+ # arbitrary until the fix is named.
1292
+ "OUTPUT_SYMLINK": Remediation(
1293
+ lines=(
1294
+ "One of the folders on the way to that path is a link, and a build never writes "
1295
+ "through one. The line above names it.",
1296
+ "A link can be repointed while a build is running, which would move where a sealed "
1297
+ "Build lands, so the whole chain is opened without following anything.",
1298
+ "Write to the path the link leads to instead: on this machine /tmp is usually a link "
1299
+ "to /private/tmp, and naming the second one works.",
1300
+ ),
1301
+ # Raised only from inside `if stat.S_ISLNK(...)` in `pipeline._require_output_directory`
1302
+ # and from the same test in `preflight._linked_ancestor`. The link is what the refusal is.
1303
+ kinds_established=(A_LINK,),
1304
+ ),
1305
+ # Input and output roots apply the same no-follow rule to every ancestor.
1306
+ "SOURCE_SYMLINK": Remediation(
1307
+ lines=(
1308
+ # "One of the folders on the way" would be false at the other raise site: the same code
1309
+ # is raised on a *source file* that is a link, where nothing above it is one. A step is
1310
+ # either, which is what both raise sites actually found.
1311
+ "A step on the way to those bytes is a link, and a build never reads through one. The "
1312
+ "line above names it.",
1313
+ "A link can be repointed while a build is running, which would change which bytes a "
1314
+ "sealed Build was made from, so every step down to a source is opened without "
1315
+ "following anything.",
1316
+ "Read from the path the link leads to instead: on this machine /tmp is usually a link "
1317
+ "to /private/tmp, and naming the second one works. A source that is a link has to be "
1318
+ "replaced by the file itself.",
1319
+ ),
1320
+ # Raised only from inside `if stat.S_ISLNK(...)` in `pipeline._require_source_directory`
1321
+ # and `pipeline._require_source_file`, and from the same test in `preflight._first_link_in`.
1322
+ kinds_established=(A_LINK,),
1323
+ ),
1324
+ # This code can describe a pipe, socket, device, or folder link, so it uses a kind-neutral noun.
1325
+ # `pipeline._open_output_parent` raises the same code when the chain could not be opened at
1326
+ # all, where nothing about the kind is known. So the noun comes from the caller that looked,
1327
+ # and a caller that did not look says `something` rather than guessing a file.
1328
+ #
1329
+ # The sentence keeps every word it had except the noun, which is the point: at a plain file --
1330
+ # the one case it was written for and the only one it was ever right about -- it renders exactly
1331
+ # as before, and it is the four other kinds that stop being called a file.
1332
+ "OUTPUT_PARENT_INVALID": Remediation(
1333
+ lines=(
1334
+ "Something on the way to {path} is {kind} rather than a folder. The line above names "
1335
+ "it.",
1336
+ "Choose an output path whose folders are all folders.",
1337
+ ),
1338
+ ),
1339
+ # One errno answers both directions. `EACCES` reaches this map from `init` writing into a folder
1340
+ # it may not write, and equally from `peek` opening a file it may not read and from `list`
1341
+ # walking a folder it may not enter. The boundary cannot tell those apart -- it has an errno and
1342
+ # a path, not an operation -- so the wording does not guess.
1343
+ "PERMISSION_DENIED": Remediation(
1344
+ lines=(
1345
+ "The operating system refused the harness access to {path}, for reading or for "
1346
+ "writing.",
1347
+ "Check what that path allows and who owns it: ls -l on it, and on the folders above "
1348
+ "it.",
1349
+ "A sealed Build folder is read-only on purpose, so an old run cannot be changed or "
1350
+ "cleared by accident. To clear one: chmod -R u+w .work first, then remove it.",
1351
+ ),
1352
+ ),
1353
+ # The two lookup keys for a refusal the operating system raised on a file a command was told to
1354
+ # read. Neither names {path}: the one path a command was pointed at is often not the file that
1355
+ # was missing -- `approve --recipe <missing> --output <new>` fails on the recipe while its
1356
+ # subject path is the output -- and a sentence that names the wrong file is worse than one that
1357
+ # names none. The line above always names the right one.
1358
+ "PATH_ABSENT": Remediation(
1359
+ lines=(
1360
+ # Not "a file this command had to read": the same errno can come from a folder that
1361
+ # vanished under a write. The sentence covers both because the boundary cannot tell
1362
+ # them apart, and the line above names the exact path either way.
1363
+ "Something on that path did not lead to anything. The line above names it.",
1364
+ "Check the spelling, and check you are in the folder you think you are in. Run ls -l "
1365
+ "on it as well: a link that points at a name with nothing at it reads this way too.",
1366
+ "mr-data peek shows what is in a file before you point a command at it.",
1367
+ ),
1368
+ ),
1369
+ "PATH_NOT_A_FILE": Remediation(
1370
+ lines=(
1371
+ "The line above names a path this command had to read as a file, and it is not one.",
1372
+ "A folder, or a name with a file part-way along it, both read this way.",
1373
+ "Point the command at the file itself.",
1374
+ ),
1375
+ ),
1376
+ # These typed path refusals must not fall through to raw numeric error text.
1377
+ "PATH_LINK_LOOP": Remediation(
1378
+ lines=(
1379
+ "The line above names a link that leads back to itself, directly or through a second "
1380
+ "link, so following it never arrives anywhere.",
1381
+ "Check what it points at: ls -l on it, and on anything it names.",
1382
+ "Point the command at the real file, or make the link point at one.",
1383
+ ),
1384
+ ),
1385
+ "PATH_NAME_TOO_LONG": Remediation(
1386
+ lines=(
1387
+ "The line above names a path longer than this machine's filesystem will accept, in "
1388
+ "one part of it or over the whole thing.",
1389
+ "This is a limit of the disk this path is on, not of the harness.",
1390
+ "Use a shorter name, or work from a folder closer to the top.",
1391
+ ),
1392
+ ),
1393
+ # The net under the other five. It says less than they do on purpose: a sentence invented for a
1394
+ # failure nobody has met would be a guess, and a guess in a remediation block is worse than an
1395
+ # honest pointer at the exact wording. The first line is the one that makes the block useful.
1396
+ "SYSTEM_REFUSED": Remediation(
1397
+ lines=(
1398
+ "The operating system stopped this step for a reason the harness has no plain sentence "
1399
+ "for yet. Add --json to the same command to read the exact reason it gave.",
1400
+ "The line above names what it was stopped on, where the system named one.",
1401
+ "A disk that is full, a disk mounted read-only, and too many files open at once all "
1402
+ "read this way; so does a path on a drive that has gone away.",
1403
+ ),
1404
+ ),
1405
+ # The two answers a document read has that the boundary above does not. A folder and a missing
1406
+ # path are answered there, in one place, for every command; these two are what is left over --
1407
+ # something that is not a file and is not a folder either, and a document past the bound.
1408
+ "DOCUMENT_TARGET_NOT_A_FILE": Remediation(
1409
+ lines=(
1410
+ "Something is at the path named above, but it is not a file: it is a pipe, a socket, "
1411
+ "or a device.",
1412
+ "This command reads a whole document and then checks it, and a pipe or a device holds "
1413
+ "nothing until something writes to it, so there is no document there to read.",
1414
+ "Write the document to a file first, and point the command at that file.",
1415
+ ),
1416
+ # `cli._read_json_document` reaches this only after `open_plain_file` refused on `S_ISREG`,
1417
+ # and only after the folder case has been turned into `EISDIR` on the line above it.
1418
+ kinds_established=(A_PIPE, A_SOCKET, A_DEVICE),
1419
+ ),
1420
+ # The third. `os.open` on a link that leads nowhere answers `ENOENT`, which the boundary reads
1421
+ # as "That file or folder is not there" -- said about an entry `ls -l` shows sitting in the
1422
+ # folder, with a fix about spelling. A link is a finding and it names its own fix.
1423
+ "DOCUMENT_TARGET_DANGLING": Remediation(
1424
+ lines=(
1425
+ "A link is at the path named above, and it leads nowhere: the name it points at has "
1426
+ "nothing at it.",
1427
+ "Run ls -l on that path to see where the link points.",
1428
+ "Repoint it at the Recipe, approval, or plan you meant, or point the command at that "
1429
+ "file directly.",
1430
+ ),
1431
+ kinds_established=(A_LINK,),
1432
+ ),
1433
+ "DOCUMENT_TOO_LARGE": Remediation(
1434
+ lines=(
1435
+ "The line above names the file and the size limit it went past.",
1436
+ "A Recipe, an approval, or a plan is a few kilobytes; a file this big is almost "
1437
+ "always the wrong file, or data rather than a Recipe.",
1438
+ "mr-data peek shows what is in a file before you point a command at it.",
1439
+ ),
1440
+ ),
1441
+ # The one Recipe field whose legal values a person cannot guess and the message does not list.
1442
+ # `recipes` answers "open that file and correct that one field", which is right and, for this
1443
+ # field, not enough: the reader has a value they got from somewhere and no way to learn what
1444
+ # was allowed instead. The three closed sets are short, so they are simply named.
1445
+ "RECIPE_OUTPUT_SEMANTICS": Remediation(
1446
+ lines=(
1447
+ "That is not a value this field takes. The line above names the exact field.",
1448
+ "A column type is one of: string, int64, float64, boolean, date, timestamp_utc. What "
1449
+ "the column is for is one of: identifier, entity, category, measure, dimension, "
1450
+ "target, other. A measure has to name a unit other than none.",
1451
+ "mr-data peek reports a column's type in these same words, so a type read out of a "
1452
+ "peek of your source is one this field takes.",
1453
+ ),
1454
+ ),
1455
+ # All version-pin drift shares one product explanation.
1456
+ "RECIPE_EXECUTION": Remediation(lines=_PIN_DRIFT),
1457
+ "RECIPE_RUNTIME": Remediation(lines=_PIN_DRIFT),
1458
+ "RECIPE_VALIDATION": Remediation(lines=_PIN_DRIFT),
1459
+ # An unknown verb gets a task-grouped list, not a dump of every accepted word.
1460
+ "CLI_USAGE": Remediation(
1461
+ lines=(
1462
+ "That is not a command. Run mr-data --help for the full list.",
1463
+ "Look at data: peek, show, list, diff.",
1464
+ "Build: init, plan, plan-check, run, build.",
1465
+ "Recipes: author, approve, recipe-run.",
1466
+ "Check: review --local, verify, preflight, inventory.",
1467
+ ),
1468
+ ),
1469
+ # The other half of a usage failure: the word was a real command, but what followed it was
1470
+ # not what that command takes. A grouped command list would answer a different question.
1471
+ "CLI_ARGUMENTS": Remediation(
1472
+ lines=(
1473
+ "That is a real command, but something it needs is missing or does not belong.",
1474
+ "The line above names the argument. Run the command with --help to see everything it "
1475
+ "takes.",
1476
+ ),
1477
+ ),
1478
+ # Sealing a decision, and signing a report, are done inside a session somebody else starts.
1479
+ # `REVIEW` is the workbench family, whose paragraph is about where a Workbench folder got to --
1480
+ # true of the other `REVIEW_` codes and not of this one, so this one is curated. The command it
1481
+ # names is the reading that needs no session at all, which is what the reader almost always
1482
+ # wanted.
1483
+ "REVIEW_AUTHORITY_ABSENT": Remediation(
1484
+ lines=(
1485
+ "Sealing a review decision, and signing a review report, happen inside a review "
1486
+ "session that the coordinator starts and hands to the command. This command was not "
1487
+ "started from one, so there is no authority to seal or sign with.",
1488
+ "Everything this machine can check on its own is one command away and needs no "
1489
+ "session: mr-data review --local on the Build folder runs the checks and writes "
1490
+ "nothing.",
1491
+ "docs/REVIEW-LOOP.md says who starts the session and what it hands over.",
1492
+ ),
1493
+ ),
1494
+ # Platform refusals identify the diagnostic command that reports available capabilities.
1495
+ "CANDIDATE_PLATFORM_UNSUPPORTED": Remediation(lines=_PLATFORM, command="mr-data preflight"),
1496
+ "SOURCE_PLATFORM_UNSUPPORTED": Remediation(lines=_PLATFORM, command="mr-data preflight"),
1497
+ "WORKSPACE_PLATFORM_UNSUPPORTED": Remediation(lines=_PLATFORM, command="mr-data preflight"),
1498
+ "PLATFORM_VERIFY_UNSUPPORTED": Remediation(
1499
+ lines=(
1500
+ *_PLATFORM,
1501
+ "Re-checking a Build needs them, so this machine cannot re-check one.",
1502
+ ),
1503
+ command="mr-data preflight",
1504
+ ),
1505
+ "PLATFORM_OUTPUT_UNSUPPORTED": Remediation(
1506
+ lines=(
1507
+ *_PLATFORM,
1508
+ "Writing this file needs them, so this machine cannot write it.",
1509
+ ),
1510
+ command="mr-data preflight",
1511
+ ),
1512
+ # peek is the first command anyone runs, so its refusals have to answer rather than just
1513
+ # refuse. None of them uses {path}: peek names what it was pointed at in the line above, and
1514
+ # its argument is not one of the subject paths the command line knows how to fill in.
1515
+ "PEEK_TARGET": Remediation(
1516
+ lines=(
1517
+ "Nothing is at the path named above, so there is nothing to look at.",
1518
+ "Check the spelling, and check you are in the folder you think you are in.",
1519
+ ),
1520
+ # `ux.plain_file` confirms both the failed open and a following `lstat` before declaring
1521
+ # that nothing is present; a dangling link has its own refusal below.
1522
+ kinds_established=(NOTHING_THERE,),
1523
+ ),
1524
+ # The other side of that split. The entry exists, it is a link, and the fix is about the link
1525
+ # rather than about spelling -- which is why it is a separate code and not a fourth sentence
1526
+ # under the one above.
1527
+ "PEEK_TARGET_DANGLING": Remediation(
1528
+ lines=(
1529
+ "A link is at the path named above, and it leads nowhere: the name it points at has "
1530
+ "nothing at it.",
1531
+ "Run ls -l on that path to see where the link points.",
1532
+ "Repoint it at the file you meant, or remove it and peek the file itself.",
1533
+ ),
1534
+ kinds_established=(A_LINK,),
1535
+ ),
1536
+ # A separate code because it is a separate finding, not a second wording of the one above. The
1537
+ # two answers contradict each other -- one says nothing is there, the other says something is
1538
+ # -- and telling somebody who typed the path they meant to check their spelling is advice
1539
+ # aimed at a mistake they did not make. Same shape as OUTPUT_PARENT_ABSENT and
1540
+ # OUTPUT_PARENT_INVALID, which are two codes for the same reason.
1541
+ "PEEK_TARGET_NOT_A_FILE": Remediation(
1542
+ lines=(
1543
+ "Something is at the path named above, but it is not a file: it is a folder, a pipe, "
1544
+ "a socket, or a device.",
1545
+ "A peek reads bytes that are already there, and a pipe or a device has none until "
1546
+ "something writes them, so there is nothing for it to look at.",
1547
+ "Name a file inside the folder, or write the stream to a file first and peek that.",
1548
+ ),
1549
+ # `ux.plain_file.open_plain_file` opens the target and asks the descriptor, and raises
1550
+ # `NOT_PLAIN` only when `S_ISREG` is false. Four kinds are left, and the sentence names
1551
+ # exactly those four: this is elimination on an observation, not a guess about which.
1552
+ kinds_established=(A_FOLDER, A_PIPE, A_SOCKET, A_DEVICE),
1553
+ ),
1554
+ "PEEK_FORMAT": Remediation(
1555
+ lines=(
1556
+ "peek reads csv, json, ndjson, and parquet files. The line above names what it was "
1557
+ "given instead.",
1558
+ "If the file really is one of those under a different name, say which one with "
1559
+ "--format; the bytes are still checked against it.",
1560
+ ),
1561
+ ),
1562
+ # Reached while building from a source, never while looking at one: peek shows these values as
1563
+ # their exact text, because a peek seals nothing. A Build does, so it stops here.
1564
+ "PARSE_VALUE": Remediation(
1565
+ lines=(
1566
+ "One value in that file is not a plain number, string, or true or false. Dates, "
1567
+ "times, decimals, and not-a-number all read this way.",
1568
+ "A Build seals what it reads, and it holds a value exactly or not at all, so it "
1569
+ "stops here rather than rounding it or quietly turning it into text.",
1570
+ "Use mr-data peek to see what is in there -- looking is not sealing, so peek shows "
1571
+ "those values -- then write those columns as text in the source you build from.",
1572
+ ),
1573
+ ),
1574
+ "PEEK_SIZE": Remediation(
1575
+ lines=(
1576
+ "That file is larger than anything this harness opens in one piece.",
1577
+ "Take a smaller extract of it and look at that instead.",
1578
+ ),
1579
+ ),
1580
+ "PEEK_SAMPLE": Remediation(
1581
+ lines=(
1582
+ "The number of rows to show is outside what peek will do.",
1583
+ "Ask for a whole number from 0 to 100. Zero shows the columns and no rows at all.",
1584
+ ),
1585
+ ),
1586
+ # Reading a build back. A folder with nothing in it is by far the commonest way to meet these,
1587
+ # and the reader's own refusal for a missing sealed tree reads like a break-in rather than the
1588
+ # typo it nearly always is.
1589
+ "CANDIDATE_ABSENT": Remediation(
1590
+ lines=(
1591
+ "There is no build at {path}.",
1592
+ "Run mr-data list to see the builds you have, or mr-data build to make one.",
1593
+ ),
1594
+ # Declared rather than inherited from the class rule above, because this is the code the
1595
+ # signpost raises: the folder being there is the ordinary case, not the exception.
1596
+ when_nothing_built_yet=NOTHING_BUILT_YET,
1597
+ ),
1598
+ # A folder that exists but was never set up. Every Workbench command reaches this, and until
1599
+ # the refusal was typed it surfaced as the operating system's own complaint about the control
1600
+ # directory inside the folder -- a name nobody types, carrying no code and no fix.
1601
+ "WORKSPACE_ABSENT": Remediation(
1602
+ lines=(
1603
+ "{path} is not a Workbench folder: nothing has been set up in it.",
1604
+ "mr-data init makes one, at a path that does not exist yet, out of your question, "
1605
+ "requirements, sources, and plan files.",
1606
+ "If you meant a folder you have already built in, mr-data list shows the builds you "
1607
+ "have.",
1608
+ ),
1609
+ ),
1610
+ # A folder a build is holding right now. Curated rather than left to the workbench family,
1611
+ # because that family's answer is "run mr-data status, then mr-data resume" -- and status waits
1612
+ # on the very lock this refusal is about, so the family would name a fix that hangs.
1613
+ "WORKSPACE_BUSY": Remediation(
1614
+ lines=(
1615
+ "A build is running in that folder, and only one command at a time may hold it.",
1616
+ "Wait for it to finish, then run this again.",
1617
+ "mr-data list says which folder is busy and carries on rather than waiting on it.",
1618
+ ),
1619
+ ),
1620
+ "LIST_ROOT_ABSENT": Remediation(
1621
+ lines=(
1622
+ # `is_dir` does not establish absence; it only establishes that listing cannot proceed.
1623
+ "A listing reads a folder, and the line above says what is at the path you named.",
1624
+ "Point mr-data list at a folder that exists, or run mr-data build to make your first "
1625
+ "build.",
1626
+ ),
1627
+ ),
1628
+ "LIST_DEPTH": Remediation(
1629
+ lines=(
1630
+ "How far down to look is outside what list will do.",
1631
+ "Ask for a whole number from 0 to 32. Six is the default, and it reaches a build six "
1632
+ "folders below the one you named.",
1633
+ ),
1634
+ ),
1635
+ # Trying a plan. Both of these are about the throwaway folder the try is built into, and both
1636
+ # are answered the same way: they are about where temporary folders are made on this machine,
1637
+ # which is a setting rather than anything wrong with the plan.
1638
+ "PLAN_CHECK_TEMP_OVERLAP": Remediation(
1639
+ lines=(
1640
+ "A build never reads from the folder it writes into, and the temporary folder this "
1641
+ "machine hands out sits inside the input folder you named.",
1642
+ "Set TMPDIR to a folder outside your input folder and try again.",
1643
+ "Nothing was built, so nothing has to be undone.",
1644
+ ),
1645
+ ),
1646
+ "PLAN_CHECK_TEMP_NOT_PRIVATE": Remediation(
1647
+ lines=(
1648
+ "The temporary folder this machine handed out can be read by other people on it, and a "
1649
+ "whole build was about to be written inside it.",
1650
+ "That usually means TMPDIR points somewhere shared. Set it to a folder only you can "
1651
+ "read and try again.",
1652
+ "Nothing was built, so nothing has to be undone.",
1653
+ ),
1654
+ ),
1655
+ # The four files a job is described in, handed to `init` on the command line. Their prefix sits
1656
+ # in the `workbench` family, which answers "run mr-data status on the folder, then mr-data
1657
+ # resume" -- and after one of these there is no folder to run status on, because `init` refuses
1658
+ # before it makes one. Following that advice earns a second refusal. Same shape of defect as the
1659
+ # sealing family's, at a different family: an answer written about an existing thing, given
1660
+ # about a thing that does not exist.
1661
+ "DEFINITION_INVALID": Remediation(
1662
+ lines=(
1663
+ "One of the files describing this job is not valid JSON. The line above says which "
1664
+ "one, where it is, and where the reading stopped.",
1665
+ "Nothing was created. There is no folder to look at and nothing to carry on from, so "
1666
+ "fix that one file and run the same init again.",
1667
+ "The four are your question, what the answer has to hold, the sources to consider, "
1668
+ "and the plan.",
1669
+ ),
1670
+ ),
1671
+ # The same two words, about the other end of the folder's life. `DEFINITION_INVALID` above is
1672
+ # `init` refusing one of the four files it was handed, before any folder exists. These two are
1673
+ # a folder that already exists whose sealed copies of those files no longer match the record
1674
+ # taken when it was made -- the state, not the input -- and the fix is not the same sentence.
1675
+ "DEFINITIONS_INVALID": Remediation(
1676
+ lines=(
1677
+ "The sealed copies of the four files this job was described in are no longer as they "
1678
+ "were when {path} was made. The line above says which part did not hold.",
1679
+ "Nothing in a Workbench folder is repaired in place, so there is no command that mends "
1680
+ "this one, and every command that opens it -- status and resume included -- will say "
1681
+ "exactly this.",
1682
+ "Make a fresh Workbench folder with mr-data init, at a path that does not exist yet, "
1683
+ "from the same four files. A Build already sealed in this one is untouched: point "
1684
+ "mr-data show at the result folder inside it.",
1685
+ ),
1686
+ ),
1687
+ "DEFINITION_DIGEST_MISMATCH": Remediation(
1688
+ lines=(
1689
+ "One of the four files this job was described in has changed inside {path} since the "
1690
+ "folder was made. The line above names which one.",
1691
+ "Those copies are sealed on purpose: what a build re-runs from has to be what the run "
1692
+ "was recorded against. A folder whose copies moved is not carried on from, and status "
1693
+ "and resume both stop here too.",
1694
+ "Make a fresh Workbench folder with mr-data init, at a path that does not exist yet, "
1695
+ "from the four files as you mean them now. A Build already sealed in this one is "
1696
+ "untouched: point mr-data show at the result folder inside it.",
1697
+ ),
1698
+ ),
1699
+ # `mr-data preflight --workload` reads one document and reports on the job it describes. Its
1700
+ # prefix sits in the `handoff` family, which answers about packaging a Build for Studio -- a
1701
+ # paragraph with nothing true to say about a path that is not a plan. Same shape of defect as
1702
+ # `DEFINITION_INVALID`'s above: an answer written about one thing, given about another.
1703
+ "WORKLOAD_UNREADABLE": Remediation(
1704
+ lines=(
1705
+ "The workload named could not be read. The line above says where it is and what "
1706
+ "stopped the read.",
1707
+ "It has to be a workbench folder, a plan, or a recipe: a plain file this account can "
1708
+ "read, or a folder mr-data init made.",
1709
+ "Nothing was measured, nothing was uploaded, and nothing has to be undone.",
1710
+ ),
1711
+ ),
1712
+ "WORKLOAD_INVALID": Remediation(
1713
+ lines=(
1714
+ "That document was read, and it is not a plan or a recipe this build understands. The "
1715
+ "line above carries the contract's own reason.",
1716
+ "Point --workload at a plan, at a recipe, or at the workbench folder that holds one.",
1717
+ "Nothing was measured, nothing was uploaded, and nothing has to be undone.",
1718
+ ),
1719
+ ),
1720
+ "DEFINITION_UNREADABLE": Remediation(
1721
+ lines=(
1722
+ "One of the files describing this job could not be read. The line above says which one "
1723
+ "and where it is.",
1724
+ "Check it is there, that it is a plain file rather than a folder or a link, and that "
1725
+ "you are allowed to read it.",
1726
+ "Nothing was created, so there is nothing to undo.",
1727
+ ),
1728
+ ),
1729
+ "DEFINITION_TOO_LARGE": Remediation(
1730
+ lines=(
1731
+ "One of the files describing this job is larger than the harness will read. The line "
1732
+ "above says which one and where it is.",
1733
+ "These four are short: a question, what the answer has to hold, the sources to "
1734
+ "consider, and a plan. One that is large is usually the data itself, passed by "
1735
+ "mistake.",
1736
+ "Nothing was created, so there is nothing to undo.",
1737
+ ),
1738
+ ),
1739
+ # The refusal `DEFINITION_UNREADABLE` above has no words for. Its advice -- check it is there,
1740
+ # check it is a plain file rather than a folder, check you may read it -- is all true of a pipe
1741
+ # nobody is writing to, so following it finds nothing wrong and the same command refuses again.
1742
+ # Same sentence as `DOCUMENT_TARGET_NOT_A_FILE`, about the four files `init` is handed rather
1743
+ # than the one document a later command reads.
1744
+ "DEFINITION_TARGET_NOT_A_FILE": Remediation(
1745
+ lines=(
1746
+ "Something is at the path named above, but it is not a file: it is a pipe, a socket, "
1747
+ "or a device.",
1748
+ "Each of the four files describing a job is read whole and then checked, and a pipe or "
1749
+ "a device holds nothing until something writes to it, so there is no description there "
1750
+ "to read.",
1751
+ "Write it to a file first, point mr-data init at that file, and run the same command "
1752
+ "again. Nothing was created, so there is nothing to undo.",
1753
+ ),
1754
+ # The same elimination as `PEEK_TARGET_NOT_A_FILE`, one kind shorter: a folder at one of
1755
+ # these four paths is answered by the command line's own boundary as `EISDIR` before it
1756
+ # reaches here.
1757
+ kinds_established=(A_PIPE, A_SOCKET, A_DEVICE),
1758
+ ),
1759
+ # The same finding at the four files `init` is handed. It reached `DEFINITION_UNREADABLE`,
1760
+ # whose advice is three things to confirm and none of them is the link.
1761
+ "DEFINITION_TARGET_DANGLING": Remediation(
1762
+ lines=(
1763
+ "A link is at the path named above, and it leads nowhere: the name it points at has "
1764
+ "nothing at it.",
1765
+ "Run ls -l on that path to see where the link points.",
1766
+ "Repoint it at the file describing that part of the job, or point mr-data init at "
1767
+ "that file directly. Nothing was created, so there is nothing to undo.",
1768
+ ),
1769
+ kinds_established=(A_LINK,),
1770
+ ),
1771
+ # A dangling authoring link has no readable target, so it needs link-specific advice.
1772
+ "VIEWER_DATASET_SIDECAR_UNAVAILABLE": Remediation(
1773
+ lines=(
1774
+ "The viewer has no table notebook to show for the Build at {path}, so the page keeps "
1775
+ "serving the research notebook and nothing else.",
1776
+ "Either there is none there, or the one there is not the notebook this Build derives. "
1777
+ "The viewer writes one only when there is none and never replaces one, so anything you "
1778
+ "added to a table notebook is still exactly as you left it.",
1779
+ "Make it again from the Build when you want the table notebook on the page. That "
1780
+ "writes the whole notebook, so copy anything you appended to it out first.",
1781
+ ),
1782
+ command="mr-data notebook {path} --json",
1783
+ command_needs_a_build=True,
1784
+ when_nothing_built_yet=(
1785
+ "The viewer is watching {path} for a table notebook, and no build has finished "
1786
+ "there for one to be made from.",
1787
+ "That is the ordinary state before a build completes. The research notebook keeps "
1788
+ "streaming on the page while you wait.",
1789
+ "If a build did complete here, read the receipt it printed: it says whether the "
1790
+ "table notebook was written and where.",
1791
+ ),
1792
+ ),
1793
+ "VIEWER_WATCHER_FAILED": Remediation(
1794
+ lines=(
1795
+ "The viewer stopped watching for notebook changes. The last good notebook is still on "
1796
+ "the page, and nothing new will appear on it.",
1797
+ "Stop this viewer with Ctrl-C and start another one with the same command you started "
1798
+ "this one with, then open the address it prints. The page you have now is stale.",
1799
+ "Nothing on disk was changed by this. The viewer only reads.",
1800
+ ),
1801
+ ),
1802
+ "LOGIN_CONFIG_INVALID": Remediation(
1803
+ lines=(
1804
+ "The address this command was pointed at is not a usable one.",
1805
+ "Check MOSTLYRIGHT_CLOUD_URL: it has to be a plain https address with nothing else "
1806
+ "in it -- no user name, no trailing slash, no fragment.",
1807
+ ),
1808
+ ),
1809
+ "LOGIN_TRANSPORT_FAILED": Remediation(
1810
+ lines=(
1811
+ "The cloud could not be reached, or answered in a way this command does not "
1812
+ "understand.",
1813
+ "Check the network and MOSTLYRIGHT_CLOUD_URL, then run mr-data login again.",
1814
+ ),
1815
+ ),
1816
+ "LOGIN_ACCESS_DENIED": Remediation(
1817
+ lines=(
1818
+ "Sign-in was denied in the browser, so nothing was stored.",
1819
+ "Run mr-data login again if you meant to approve it.",
1820
+ ),
1821
+ ),
1822
+ "LOGIN_EXPIRED": Remediation(
1823
+ lines=(
1824
+ "The sign-in code expired before it was approved, so nothing was stored.",
1825
+ "Run mr-data login again and approve the new code promptly.",
1826
+ ),
1827
+ ),
1828
+ "LOGIN_MINT_FAILED": Remediation(
1829
+ lines=(
1830
+ "Sign-in was approved, but the cloud could not issue a device credential afterwards.",
1831
+ "The approval this ran through is already spent. Run mr-data login again to get a "
1832
+ "new one.",
1833
+ ),
1834
+ ),
1835
+ "LOGIN_CREDENTIALS_EXIST": Remediation(
1836
+ lines=(
1837
+ "A credential is already stored at the path named above, and it was left unchanged.",
1838
+ "Run mr-data whoami to inspect its identity. If you intend to replace it, run "
1839
+ "mr-data login --force.",
1840
+ ),
1841
+ ),
1842
+ "LOGIN_CREDENTIALS_UNREADABLE": Remediation(
1843
+ lines=(
1844
+ "The stored credential at the path named above exists but cannot be read as the file "
1845
+ "the harness writes.",
1846
+ "Inspect that file and its permissions. If it is not a credential you need to keep, "
1847
+ "remove it and run mr-data login to create a new one.",
1848
+ ),
1849
+ ),
1850
+ "AUTHOR_YAML_DANGLING": Remediation(
1851
+ lines=(
1852
+ "A link is at the path named above, and it leads nowhere: the name it points at has "
1853
+ "nothing at it.",
1854
+ "Run ls -l on that path to see where the link points.",
1855
+ "Repoint it at the file you are writing the Recipe in, or remove it and write the "
1856
+ "Recipe at that name.",
1857
+ ),
1858
+ kinds_established=(A_LINK,),
1859
+ ),
1860
+ # The one code whose prefix family answers the wrong question. `DATA_FORMAT` is a field-shape
1861
+ # refusal and belongs with the Recipe fields; this one is a rights refusal wearing the same
1862
+ # prefix, and "open that file and correct that one field" is not what to do about it.
1863
+ "DATA_CLASSIFICATION_REJECTED": Remediation(
1864
+ lines=(
1865
+ "That source was looked at and refused, so nothing may be built from it.",
1866
+ "This is about the terms the source comes with, not about its bytes being broken. The "
1867
+ "message names what was found.",
1868
+ "Either supply the evidence that clears it, or choose a different source.",
1869
+ ),
1870
+ ),
1871
+ # Reading a JSON file. Every command that takes a --recipe, an --approval, a plan, or run
1872
+ # settings goes through the same strict reading, so these are met early and often -- and the
1873
+ # strictness is the part that surprises people, because the file usually opens fine everywhere
1874
+ # else. Each entry says what the rule is as well as what broke it.
1875
+ "JSON_SYNTAX": Remediation(
1876
+ lines=(
1877
+ "That file is not JSON at all. The line above gives the line and column where the "
1878
+ "reading stopped.",
1879
+ "A trailing comma, a missing quote, and a missing closing brace account for nearly "
1880
+ "all of these.",
1881
+ "Fix it there and run the same command again. Nothing was read past that point, so "
1882
+ "nothing has to be undone.",
1883
+ ),
1884
+ ),
1885
+ "JSON_DUPLICATE_KEY": Remediation(
1886
+ lines=(
1887
+ "That file writes the same key twice inside one object, and the harness will not "
1888
+ "choose between them for you.",
1889
+ "Most tools keep the last one silently. Which was meant is a question only you can "
1890
+ "answer, so it is asked rather than guessed.",
1891
+ "Delete the one you did not mean and run the command again.",
1892
+ ),
1893
+ ),
1894
+ "JSON_NONINTEGER_NUMBER": Remediation(
1895
+ lines=(
1896
+ "That file holds a number with a decimal point in it.",
1897
+ "Everything the harness fingerprints has to be written the same way every time, and a "
1898
+ "decimal cannot be, so numbers in these files are whole numbers only.",
1899
+ "Write it as a whole number, or as text when the exact digits matter.",
1900
+ ),
1901
+ ),
1902
+ "JSON_NONFINITE": Remediation(
1903
+ lines=(
1904
+ "That file holds NaN or Infinity. Neither is JSON, and neither has one exact spelling "
1905
+ "to fingerprint.",
1906
+ "Write the value as text, or leave it out and say so with null.",
1907
+ ),
1908
+ ),
1909
+ "JSON_BOM": Remediation(
1910
+ lines=(
1911
+ "That file starts with a byte-order mark -- invisible bytes some editors put at the "
1912
+ "front of a file -- and JSON does not allow one.",
1913
+ "Save it as UTF-8 without a byte-order mark. In most editors that is a separate "
1914
+ "choice sitting next to plain UTF-8.",
1915
+ ),
1916
+ ),
1917
+ "JSON_UTF8": Remediation(
1918
+ lines=(
1919
+ "That file is not UTF-8 text. The line above gives the byte where the reading stopped.",
1920
+ "A file saved as Latin-1 or Windows-1252 reads this way as soon as it holds an "
1921
+ "accented letter. Save it again as UTF-8.",
1922
+ ),
1923
+ ),
1924
+ "JSON_NONCANONICAL": Remediation(
1925
+ lines=(
1926
+ "That file is valid JSON, but it is not written the one exact way the harness "
1927
+ "fingerprints: keys in order, no spare spaces, no line breaks.",
1928
+ "This is only asked of a file the harness wrote itself, so a file that fails here has "
1929
+ "been rewritten since -- usually by an editor that tidied it on save.",
1930
+ "Use the original, or make it again with the command that wrote it.",
1931
+ ),
1932
+ ),
1933
+ # These refusals describe invalid command arguments rather than stored or provider data.
1934
+ "FILL_ENDPOINT": Remediation(
1935
+ lines=(
1936
+ "The address a fill is pointed at has to name one public host, and that one names "
1937
+ "none.",
1938
+ "Give the whole address, scheme and host together, rather than a bare path or a host "
1939
+ "with nothing in front of it.",
1940
+ "It has to be one public metadata address reached over https, and the protocol you "
1941
+ "passed has to be the one it speaks; docs/LOCAL-SEARCH.md lists the three.",
1942
+ ),
1943
+ ),
1944
+ "DATAGOV_SORT": Remediation(
1945
+ lines=(
1946
+ "The order a sweep pages in is part of its cursor contract, and that one is not an "
1947
+ "order this contract can page.",
1948
+ "Data.gov answers an order it does not recognise by quietly sorting by relevance "
1949
+ "instead, so an unrecognised name would page a different catalogue than the receipt "
1950
+ "claims. That is why the name is closed rather than passed through.",
1951
+ "Pass last_harvested_date or relevance. Prefer relevance for a sweep that has to "
1952
+ "converge: last_harvested_date is rewritten every time the provider re-harvests a "
1953
+ "dataset, so a record can move ahead of a cursor that already passed it.",
1954
+ ),
1955
+ ),
1956
+ "DATAGOV_CONFLICT_BOUND": Remediation(
1957
+ lines=(
1958
+ "The number of double-published identifiers a sweep may admit has to be a count, and "
1959
+ "that one is not.",
1960
+ "Pass a whole number that is zero or more. Zero admits none, which is what a sweep "
1961
+ "does when the flag is left off entirely.",
1962
+ ),
1963
+ ),
1964
+ "LOCAL_SEARCH_FILTER_PAIR": Remediation(
1965
+ lines=(
1966
+ "That filter is two halves of one thing, and only one half was given.",
1967
+ "A time filter takes its start and its end; a Recipe filter takes the Recipe's name, "
1968
+ "its version and its fingerprint; an exact-fingerprint filter takes the kind and the "
1969
+ "fingerprint. The line above names which of the three stopped.",
1970
+ "Pass the other half, or leave that whole filter out and search without it.",
1971
+ ),
1972
+ ),
1973
+ # Not a mistake anybody made, and the wording says so. This is the last look a search takes at
1974
+ # its own answer before handing it over: a private score, or a locator that may move, would
1975
+ # leave this machine if it went out. Telling the reader to check what they passed would send
1976
+ # them after a fault that is not theirs.
1977
+ "LOCAL_SEARCH_OUTPUT_BOUNDARY": Remediation(
1978
+ lines=(
1979
+ "The answer this search had built was held back rather than shown: it carried a "
1980
+ "private score or a locator that can move, and neither may leave the machine that "
1981
+ "made it.",
1982
+ "Nothing about your question caused this, and nothing was written. Both stores it "
1983
+ "read are exactly as they were.",
1984
+ "This is a fault in the harness rather than in what you asked for. Quote the code "
1985
+ "above when you report it.",
1986
+ ),
1987
+ ),
1988
+ "NEURAL_MODEL_ROOT_REQUIRED": Remediation(
1989
+ lines=(
1990
+ "This command was asked to work through the search model, and no model folder was "
1991
+ "named.",
1992
+ "Give the folder that was put in place on this machine, by its absolute path.",
1993
+ "Searching by words alone needs no model folder at all, and is what this harness does "
1994
+ "unless it is asked otherwise; docs/LOCAL-SEARCH.md says how the folder is put in "
1995
+ "place when you do want one.",
1996
+ ),
1997
+ ),
1998
+ "NEURAL_MODEL_ROOT_UNEXPECTED": Remediation(
1999
+ lines=(
2000
+ "This command was asked to work by words alone, and a model folder was named as well.",
2001
+ "Nothing would read that folder, so it is refused rather than accepted and quietly "
2002
+ "ignored: an argument that has no effect is worse than one that is turned down.",
2003
+ "Leave the model folder out, or ask for the model to be used.",
2004
+ ),
2005
+ ),
2006
+ "NEURAL_MODEL_ROOT_ABSOLUTE": Remediation(
2007
+ lines=(
2008
+ "The model folder has to be named by its absolute path, and that one is relative.",
2009
+ "What a search is ordered by is pinned to one exact folder, and a relative path names "
2010
+ "a different folder from every different place you run the command.",
2011
+ "Give the path from the top: on this machine it starts with a /.",
2012
+ ),
2013
+ ),
2014
+ "DATAGOV_KEY_FILE": Remediation(
2015
+ lines=(
2016
+ "The key file this fill was pointed at could not be read as a key.",
2017
+ "It has to be a plain file reached without following a link, holding one printable "
2018
+ "line and nothing else: no quotes around it, no name in front of it, and no second "
2019
+ "line.",
2020
+ "Write the key into a file only you can read, and point the command at that file. The "
2021
+ "key itself is never printed, so no message about it will ever quote one back at you.",
2022
+ ),
2023
+ ),
2024
+ }
2025
+
2026
+
2027
+ # Codes the command-line boundary uses itself, rather than through one of the typed error classes
2028
+ # the completeness gate walks for. Two kinds, and they are checked in two different ways because
2029
+ # they live in two different places.
2030
+ #
2031
+ # `CLI_USAGE` and `CLI_ARGUMENTS` are raised in `cli.py`, and are asserted to appear literally
2032
+ # there. The rest are lookup keys for a refusal that carries an errno and no typed code: they are
2033
+ # never printed as codes, because they are not ones a person could quote back at the source, and
2034
+ # they exist so that an untyped refusal a person can hit still gets plain sentences. They are
2035
+ # carried on `ux.headline.SYSTEM_REFUSALS` beside the sentence each answers, and
2036
+ # `tests/test_ux_remediation.py` asserts this set holds exactly the codes that table names -- an
2037
+ # equality between two modules rather than a string search, so a sentence added there with no
2038
+ # answer here fails, and an answer here that nothing looks up fails too.
2039
+ SYNTHETIC_CODES: frozenset[str] = frozenset(
2040
+ {
2041
+ "CLI_USAGE",
2042
+ "CLI_ARGUMENTS",
2043
+ "PATH_ABSENT",
2044
+ "PATH_LINK_LOOP",
2045
+ "PATH_NAME_TOO_LONG",
2046
+ "PATH_NOT_A_FILE",
2047
+ "PERMISSION_DENIED",
2048
+ "SYSTEM_REFUSED",
2049
+ }
2050
+ )
2051
+
2052
+
2053
+ # Source areas whose errors no workbench command can put in front of a person. The completeness
2054
+ # gate skips a code only when every file that raises it is in here, and a separate test asserts
2055
+ # each entry still names a path that exists -- so a deleted module cannot leave a stale excuse
2056
+ # behind. Paths are relative to `src/mostlyright/data_harness`.
2057
+ NOT_WORKBENCH_REACHABLE: frozenset[str] = frozenset(
2058
+ {
2059
+ # World B. Imported only by `repair/`, which has no command of its own, so nothing here
2060
+ # can reach a person through `mr-data`.
2061
+ "preparation",
2062
+ # World B's fix-it coordinator, and the reason `preparation` above is unreachable. Nothing
2063
+ # imports it: `cli.py` names `ImmutableRepairCoordinator` once, as a string inside the
2064
+ # workflow description `mr-data workflow` prints, which is a label rather than a call. A
2065
+ # fix-it cycle is driven from Studio, so its refusals are read there and never here.
2066
+ "repair",
2067
+ # The clean room's child process. Its stderr is read by the Builder daemon and written to
2068
+ # the daemon's log; a person at a workbench never sees it.
2069
+ "acquisition/sandbox.py",
2070
+ # Graph-kernel errors are converted to BuildError before the command boundary. Registry
2071
+ # errors are converted to ContractError while parsing. Their internal classes never reach
2072
+ # a workbench directly, so the public wrapper codes are the ones curated above.
2073
+ "plan_graph.py",
2074
+ "operation_registry.py",
2075
+ # Local acquisition-bundle errors are caught by deploy.py and converted into one
2076
+ # DeployRefused sentence carrying the original evidence code in brackets. Their internal
2077
+ # class never crosses the command boundary directly.
2078
+ "deployment_evidence.py",
2079
+ # Empirical cadence is a pure cross-system contract foundation. No mr-data command calls
2080
+ # it in this release; Studio persistence and scheduling remain follow-up work. Register
2081
+ # its typed refusals with the command boundary when a command actually consumes them.
2082
+ "sources/cadence.py",
2083
+ # Visual-run storage and query refusals are handled by the local viewer's HTTP envelope;
2084
+ # they are not emitted through the mr-data command error boundary this map governs.
2085
+ "visual_run",
2086
+ }
2087
+ )
2088
+
2089
+
2090
+ def remediation_for(
2091
+ code: str | None,
2092
+ *,
2093
+ path: str | None = None,
2094
+ path_exists: bool | None = None,
2095
+ build_present: bool | None = None,
2096
+ path_kind: str | None = None,
2097
+ ) -> tuple[str, ...]:
2098
+ """Plain sentences for one typed code, or an empty tuple when there is nothing honest to say.
2099
+
2100
+ A curated code answers for itself; anything else answers through its prefix family; an
2101
+ unrecognised prefix answers with nothing rather than raising. ``path`` is the subject path the
2102
+ command was given; ``path_exists``, ``build_present`` and ``path_kind`` are what the caller
2103
+ found when it looked -- supplied so an entry can avoid claiming a path is missing when it is
2104
+ not, avoid diagnosing a broken build where nothing has been built, and avoid calling a folder,
2105
+ a pipe or a socket a file. ``path_kind`` is one of the nouns in :mod:`ux.path_kind`, and
2106
+ ``None`` when the caller could not tell.
2107
+ """
2108
+
2109
+ if not code:
2110
+ return ()
2111
+ family_name = family_for(code)
2112
+ entry = CODES.get(code)
2113
+ if entry is not None:
2114
+ source, command = _wording(
2115
+ entry,
2116
+ path_exists=path_exists,
2117
+ build_present=build_present,
2118
+ path_kind=path_kind,
2119
+ )
2120
+ lines = [_fill(line, path, path_kind) for line in source]
2121
+ if command is not None:
2122
+ lines.append(f"Run: {_fill(command, path, path_kind)}")
2123
+ return tuple(lines)
2124
+ # The class rule. An uncurated code answers through its family, and a family written about an
2125
+ # existing Build has nothing true to say about a folder that holds none -- so it does not get
2126
+ # to answer. This sits above the family lookup rather than inside one more curated entry,
2127
+ # because the defect it fixes is the fallthrough itself.
2128
+ if path_exists and build_present is False and family_name in _ABOUT_AN_EXISTING_BUILD:
2129
+ return tuple(_fill(line, path) for line in NOTHING_BUILT_YET)
2130
+ family = FAMILIES.get(family_name)
2131
+ if family is None:
2132
+ return ()
2133
+ return family.lines
2134
+
2135
+
2136
+ def _wording(
2137
+ entry: Remediation,
2138
+ *,
2139
+ path_exists: bool | None,
2140
+ build_present: bool | None,
2141
+ path_kind: str | None,
2142
+ ) -> tuple[tuple[str, ...], str | None]:
2143
+ """Which of one entry's four wordings fits what the caller found on disk, and its command.
2144
+
2145
+ The kind is asked first, and it is the one test written as a refusal rather than as a
2146
+ confirmation: an entry that has written a wording for "not a file" keeps its default only where
2147
+ the caller looked and saw a plain file. Absence of evidence therefore falls to the kind-neutral
2148
+ wording rather than to the file assertion, which is the whole repair -- a wording that asserts a
2149
+ kind is reached only from an observation of that kind.
2150
+
2151
+ The rest is narrowest first. "The folder is there but empty of builds" is a strictly more
2152
+ specific finding than "the folder is there", so it claims the answer before the general
2153
+ path-exists wording gets to diagnose something the caller has no evidence for.
2154
+
2155
+ The command belongs to the wording it was written for, which is why it is returned from here
2156
+ rather than appended afterwards. An alternative wording is chosen precisely because the
2157
+ default's premise does not hold, and the command was written against that premise: `mr-data
2158
+ show` opens a Build, so a path that is taken by something that is not one has nothing for it
2159
+ to open. A block that ends in a command that fails is the defect this map exists to prevent.
2160
+
2161
+ ``command_needs_a_build`` drops Build-only commands unless a Build was observed. The folder
2162
+ check precedes the file check because ``when_not_a_file`` displaces a file assertion, while
2163
+ ``when_not_a_folder`` is selected only after another concrete kind was observed.
2164
+ """
2165
+
2166
+ command = entry.command
2167
+ if entry.command_needs_a_build and build_present is not True:
2168
+ command = None
2169
+ if entry.when_not_a_folder and path_kind not in (None, A_FOLDER):
2170
+ return entry.when_not_a_folder, None
2171
+ if entry.when_not_a_file and path_kind != A_FILE:
2172
+ return entry.when_not_a_file, None
2173
+ if not path_exists:
2174
+ return entry.lines, command
2175
+ if build_present is False and entry.when_nothing_built_yet:
2176
+ return entry.when_nothing_built_yet, None
2177
+ if entry.when_path_exists:
2178
+ return entry.when_path_exists, None
2179
+ return entry.lines, command
2180
+
2181
+
2182
+ def _fill(line: str, path: str | None, kind: str | None = None) -> str:
2183
+ return line.replace("{path}", path or _PATH_FALLBACK).replace("{kind}", kind or UNKNOWN_KIND)
2184
+
2185
+
2186
+ # Every fact about what is on disk that a wording here can turn on -- read off :func:`_wording`,
2187
+ # which is the one function that chooses between wordings, rather than written down beside it.
2188
+ #
2189
+ # Every observed fact that selects wording is a keyword argument of `_wording`; deriving this set
2190
+ # from the signature prevents a second manually maintained list from drifting.
2191
+ OBSERVATIONS: frozenset[str] = frozenset(
2192
+ name
2193
+ for name, parameter in inspect.signature(_wording).parameters.items()
2194
+ if parameter.kind is inspect.Parameter.KEYWORD_ONLY
2195
+ )