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.

Potentially problematic release.


This version of mostlyright-data might be problematic. Click here for more details.

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,212 @@
1
+ # nbrender component contract
2
+
3
+ `nbrender` turns nbformat 4.x JSON into one self-contained HTML string using the stdlib alone.
4
+ This file records how its modules fit together: which module owns which surface, what shared API
5
+ each may rely on, and the conventions all of them follow. Every hex, size, and geometry of the
6
+ design system lives in `tokens.py`; no other module writes a literal color or pixel value.
7
+
8
+ Component ids (`A01` through `MR01`) name the individual rendered surfaces — header, gutter prompt,
9
+ DataFrame, traceback, and so on. They appear throughout the code comments and are defined by the
10
+ ownership map below.
11
+
12
+ ## Hard rules
13
+
14
+ - **stdlib-only**: `json`, `html`, `html.parser`, `tokenize`, `re`, `textwrap`. No pip deps — no
15
+ nbconvert, pygments, markdown, or webfont fetch.
16
+ - **Escape everything from notebook JSON** through `parse.esc` (or a helper that calls it) at the
17
+ point of interpolation. Untrusted HTML output (D21) is sandboxed by its owner, not trusted.
18
+ - **No emoji / decorative unicode.** The glyphs `▸ ⋯ ↯ − ·` are the sanctioned iconography.
19
+ - **Terse technical copy** in any user-visible string: one fact per sentence, no fluff.
20
+ - **Sanctioned deviations** — the deliberate limits of this renderer; do not add others:
21
+ stdlib-only; `static` + `embed` modes (`connected` = CSS/states only, no kernel wiring);
22
+ C14 renders TeX **source** in the display well labeled `math · TeX source`; D19 omits the
23
+ "open in table view →" link in static mode; `mr-data view` keeps its SSE reload script.
24
+
25
+ ## Ownership map
26
+
27
+ Shared layers:
28
+
29
+ | File | Owns |
30
+ |---|---|
31
+ | `tokens.py` | Every design-system constant; `build_css(mode)` — the **only** stylesheet. |
32
+ | `parse.py` | Model dataclasses, `RenderContext`, and every shared parse/escape helper. |
33
+ | `document.py` | Shell + layout, `.ipynb` -> component routing, pending/unsupported wells. |
34
+ | `__init__.py` | Re-exports `render_notebook`. |
35
+
36
+ Component modules:
37
+
38
+ | File | Component IDs | Public functions |
39
+ |---|---|---|
40
+ | `chrome.py` | A01–A05 | `render_header`, `render_toolbar`, `render_kernel_pill`, `render_outline`, `render_footer` |
41
+ | `frame.py` | B06–B11 | `render_prompt`, `cell_state_classes`, `render_hover_toolbar`, `render_collapse_bar`, `render_tags`, `render_exec_meta` |
42
+ | `code_body.py` | C12, C14, C15 | `render_code_input`, `render_math`, `render_raw` |
43
+ | `markdown_body.py` | C13, F29 | `render_markdown`, `render_entity_chip` |
44
+ | `outputs_text.py` | D16, D17, D18, D23, D24 | `render_stdout`, `render_stderr`, `render_result_text`, `render_traceback`, `render_progress` |
45
+ | `outputs_rich.py` | D19, D20, D21, D22 | `render_dataframe`, `render_figure`, `render_html`, `render_json` |
46
+ | `interactive.py` | E25, E26 | `render_widget`, `render_stale_banner` |
47
+ | `mr_components.py` | F27, F28 | `render_provenance`, `render_leakage` |
48
+ | `outputs_data.py` | MR01 | `render_column_profiles` |
49
+
50
+ Tests live in `tests/nbrender/`: `test_chrome.py`, `test_frame.py`, `test_code_body.py`,
51
+ `test_markdown.py`, `test_outputs_text.py`, `test_outputs_rich.py`, `test_interactive_mr.py`
52
+ (covers both `interactive.py` and `mr_components.py`), `test_acceptance.py` over whole rendered
53
+ documents, and the shared `test_scaffold.py` — do not weaken its assertions.
54
+
55
+ Every public function returns an **HTML string** (except `cell_state_classes`, which returns a
56
+ space-joined class fragment or `""`). `document._slot` wraps each call and substitutes a pending
57
+ well if a component raises `NotImplementedError`; no module raises it, so that path is a defensive
58
+ fallback, not an expected state.
59
+
60
+ ## How `document` dispatches
61
+
62
+ Per cell, in the 62px grid:
63
+
64
+ - **Gutter** = `frame.render_prompt` (B06).
65
+ - **Code cell content** = `[frame.render_hover_toolbar` (B08)`] + code_body.render_code_input` (C12),
66
+ wrapped in `.nb-panel.nb-code` with `frame.cell_state_classes` (B07) applied; then each output via
67
+ the output router below; then `frame.render_exec_meta` (B11) for executed cells.
68
+ - **Markdown cell content** = `[frame.render_tags` (B10) when tags present`] + markdown_body.render_markdown`
69
+ (C13). C13 calls `parse.split_entity_refs` and `markdown_body.render_entity_chip` (F29) for refs,
70
+ and `code_body.render_math` (C14) for `$…$`/`$$…$$`.
71
+ - **Raw cell content** = `code_body.render_raw` (C15).
72
+
73
+ The `.nb-io` row owns the 10px (the value `IO_GAP_PX` records) input-to-output gap and resets the
74
+ top margin of every child of its `.nb-io-body`, so the row supplies that one gap and no output adds
75
+ a second — not even an output that renders as several sibling roots, as E26 + E25 does. The one
76
+ exception is the welded first output: a first plain stdout stream (`document._is_weldable_well`)
77
+ is appended outside any row and welds flush to the panel, with no gap at all. D17's own
78
+ `margin-top: 12px` was retired for that uniform 10px (SPEC D17, amended 2026-08-12); the stderr
79
+ well keeps its dashed `border-top` and 11px top padding.
80
+
81
+ Output router (`document._render_output`), by `Output.kind` then `Output.primary_mimetype()`:
82
+
83
+ | Condition | Component |
84
+ |---|---|
85
+ | `stream-stdout`, not tqdm | D16 |
86
+ | `stream-stdout`, tqdm pattern | D24 |
87
+ | `stream-stderr` | D17 |
88
+ | `error` | D23 |
89
+ | `text/html` containing `class="dataframe"` | D19 |
90
+ | `text/html` otherwise | D21 |
91
+ | `image/svg+xml` or `image/png` | D20 |
92
+ | `application/json` | D22 |
93
+ | `text/plain`, tqdm pattern | D24 |
94
+ | `text/plain` otherwise | D18 |
95
+ | `application/vnd.jupyter.widget-view+json` | E26 (static/embed) then E25 |
96
+ | `application/vnd.mostlyright.column-profile.v1+json` | MR01 |
97
+ | any other mimetype | unsupported well (owned by `document`) |
98
+
99
+ Notebook level: F27 once at document top when `notebook.provenance` present; F28 after the cell an
100
+ entry's integer `cell` index names (else trailing).
101
+
102
+ ## `RenderContext` and model reference (`parse.py`)
103
+
104
+ `RenderContext`: `mode: str` (static/connected/embed) · `cell_index: int` · `cell_count: int` ·
105
+ `prev_execution_count: int | None` (previous **code** cell's count, for B11 out-of-order).
106
+
107
+ `Notebook`: `nbformat: int` · `nbformat_minor: int` · `metadata: dict` · `cells: list[Cell]` ·
108
+ `.provenance -> dict | None` (F27) · `.leakage -> list[dict]` (F28).
109
+
110
+ `Cell`: `cell_id: str` (nbformat `id`, or a deterministic parse fallback) · `cell_type: str`
111
+ (markdown/code/raw/unknown) · `source: str` (raw, unescaped) ·
112
+ `metadata: dict` · `execution_count: int | None` · `outputs: list[Output]` ·
113
+ `.tags -> list[str]` (the four known B10 tags, in display order).
114
+
115
+ `Output`: `kind: str` (stream-stdout/stream-stderr/execute_result/display_data/error/unknown) ·
116
+ `text: str` (raw stream/plain text, ANSI intact) · `data: dict` (normalized mimetype bundle) ·
117
+ `metadata: dict` · `execution_count: int | None` · `ename/evalue: str | None` ·
118
+ `traceback: list[str]` · `.primary_mimetype() -> str | None` · `.text_of(mimetype) -> str`.
119
+
120
+ ## MR01 portable column-profile output
121
+
122
+ The generator stores the inspector in a normal `display_data` MIME bundle. The structured
123
+ `application/vnd.mostlyright.column-profile.v1+json` member is the rich contract. The same bundle
124
+ must also contain `text/html` and `text/plain` fallbacks so an ordinary Jupyter viewer never shows
125
+ an empty or unsupported output. The structured member is data only; it contains no executable
126
+ markup.
127
+
128
+ Every Parquet column appears exactly once and records logical type, physical type, declared unit,
129
+ missing count/rate, distinct count/rate and exact-or-sampled status, a type-appropriate summary and
130
+ chart, source column, and transformations. Units come from a sealed Recipe's `output_semantics`.
131
+ The renderer never guesses a unit: older Builds say `Not declared`, while non-measure fields
132
+ with an explicit `unit: none` say `Not applicable`.
133
+
134
+ The v1 envelope may add an optional `mostlyright.column-semantics.v1` sub-document and additive
135
+ per-column semantic fields. Bump the outer MIME version only when an existing field changes
136
+ meaning or a required field is added or removed. An absent sub-document means “not declared,” not
137
+ an invalid payload. The four unit states are `declared`, `normalized`, `not_applicable`, and
138
+ `unknown`; the legacy `unit.label` must remain a truthful fallback for each state.
139
+
140
+ The generator may reduce optional rich fields to remain below the 1 MiB MIME bound. The complete
141
+ sealed text remains in the notebook Overview and Data dictionary markdown cells and in the HTML
142
+ and plain-text fallbacks. Every renderer escapes semantic strings and never treats source content
143
+ as approval.
144
+
145
+ The portable fallbacks carry bounded dataset and column semantic sections independently of any
146
+ rich-MIME reduction. Fixed limits cover text, limitations, categorical definitions, and columns;
147
+ an omitted tail always produces an explicit deterministic truncation notice. Recipe semantics may
148
+ say `approved_recipe` only after the generator has verified the sealed approval digest and current
149
+ Recipe execution/acquisition bindings, never from member names alone.
150
+
151
+ The interactive semantic banner renders summary, row grain, time/population/completeness coverage,
152
+ and limitations. If the bounded MIME reduction ladder omits semantic detail, `render_scope` remains
153
+ present and the banner explicitly names the omitted field groups and points to the portable
154
+ fallbacks; reduction never silently turns a semantic-bearing payload into a no-semantics payload.
155
+ Large verified string values may produce oversized summary or chart labels. The final rich-profile
156
+ reduction step truncates those derived presentation strings with an explicit `[truncated]` marker,
157
+ while retaining semantic authority and the analytical profile shape, before it may refuse the MIME.
158
+ Portable categorical definitions always include their `complete` or `partial` mapping status.
159
+
160
+ Semantic evidence references are bounded `source_id` values. The generator accepts them only
161
+ after the table-semantics contract has bound each identifier to a source in the same sealed
162
+ Recipe or local plan; the renderer presents them as references, never as approvals.
163
+
164
+ `outputs_data.py` escapes every value and emits all profiles into one in-place selector. The
165
+ selector is progressive enhancement owned by the local workbench; without JavaScript the first
166
+ profile and the portable fallbacks remain readable. Charts are passive inline SVG authored only by
167
+ the trusted renderer from escaped scalar values—never notebook-supplied SVG. Distribution-axis
168
+ labels use a deterministic two-row layout and geometry-bounded middle abbreviation. Each bar's
169
+ escaped `<title>` retains the complete label and count, and a visually hidden table exposes the
170
+ same bounded data to assistive technology.
171
+
172
+ ## Helper inventory
173
+
174
+ `parse.py`:
175
+ - `esc(value) -> str` — HTML-escape any value, quotes included; coerces non-strings.
176
+ - `strip_ansi(value) -> StrippedAnsi(text, bold)` — strip SGR; `bold` flags any SGR-1 seen.
177
+ - `detect_tqdm(text) -> TqdmProgress(fraction, label, stat) | None`.
178
+ - `split_entity_refs(source) -> list[str | EntityRef(type, id)]`.
179
+ - `parse_notebook(nb: dict) -> Notebook` — tolerant; never raises on bad shapes.
180
+ - Constants: `MIMETYPE_PRECEDENCE`, `KNOWN_TAGS`.
181
+
182
+ `tokens.py`:
183
+ - Color constants (core + viewer-only), `CSS_TOKENS` (css-var name -> hex).
184
+ - `FONT_SANS`, `FONT_MONO`, `TYPE_SCALE` (role -> family/size/weight/line-height/notes).
185
+ - Geometry: `GUTTER_PX`, `CELL_GAP_PX`, `IO_GAP_PX`, `RADIUS`, `SHELL_*`, `OUTLINE_WIDTH_PX`.
186
+ - `KEYFRAMES`, `HOVER_TRANSITION`, `VALID_MODES`.
187
+ - `build_css(mode) -> str` — the whole stylesheet.
188
+
189
+ ## Class-name convention
190
+
191
+ All classes are `nb-`-prefixed, kebab-case: block `nb-<component>` (e.g. `nb-df`), element
192
+ `nb-<component>-<part>` (e.g. `nb-df-caption`), modifier `nb-<component>--<variant>` (e.g.
193
+ `nb-kernel--busy`). Transient state uses `is-<state>` (e.g. `nb-panel.is-running`). Every class a
194
+ component emits must resolve against a rule in `build_css`.
195
+
196
+ ## Changing shared behavior
197
+
198
+ `tokens.py` is the single stylesheet: add a rule or a token there rather than inlining a style in a
199
+ component, so one edit reaches every module and the palette cannot drift. The same holds for
200
+ `parse.py` (shared parse and escape helpers) and `document.py` (dispatch and layout) — a component
201
+ that needs new shared behavior gets it added to the shared file rather than copied into its own
202
+ module.
203
+
204
+ ## Notes for a host stylesheet
205
+
206
+ A host may override `.nb-cell`'s gutter geometry, but it must then re-state `.nb-io` with
207
+ `margin-left` equal to its own gutter track plus its column gap — the row re-enters the gutter by
208
+ that exact distance, so leaving nbrender's base pull-back in place skews `Out [n]` away from
209
+ `In [n]`. Restack the host's notebook rules under `@container`, never `@media`: nbrender's
210
+ breakpoints resolve against `.nb-shell`'s inline size (`container-type: inline-size`), which is
211
+ narrower than the viewport by whatever the host's own layout spends on padding, so a viewport-keyed
212
+ override disagrees with nbrender across a band of widths.
@@ -0,0 +1,12 @@
1
+ """Stdlib-only, static-first Jupyter notebook renderer.
2
+
3
+ nbformat 4.x JSON in, one self-contained HTML string out — no nbconvert, no pygments, no markdown
4
+ library, no webfont fetch. ``render_notebook`` is the single public entry point; the modules
5
+ behind it are documented in ``CONTRACT.md``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from mostlyright.data_harness.nbrender.document import render_notebook
11
+
12
+ __all__ = ["render_notebook"]
@@ -0,0 +1,359 @@
1
+ """Group A · chrome (A01-A05): header, toolbar, kernel pill, outline rail, repro footer.
2
+
3
+ Dispatched from ``document.render_notebook``. Every string interpolated from notebook JSON —
4
+ filename, breadcrumb path, kernelspec, env hash — passes through :func:`parse.esc` at the point of
5
+ interpolation. Two header strings are fixed design copy rather than notebook data: the "saved"
6
+ line and the kernel pill labels (an exported notebook carries neither a save clock nor a kernel).
7
+ Header height is mode-invariant: in ``static`` the primary button reads "Download .ipynb" rather
8
+ than vanishing. Toolbar (A02) is emitted only in ``connected``; ``build_css`` already hides
9
+ ``.nb-toolbar`` elsewhere.
10
+
11
+ Class-name convention (CONTRACT.md): every class emitted resolves against a rule in
12
+ ``tokens.build_css`` — including the toolbar Run accent, the minimap type tints/heights (the two
13
+ tints ``#E5E9F6``/``#FBE4DA`` are ``--nb-minimap-*`` tokens, the three bar heights are
14
+ ``.nb-minimap-h-*`` rules), and the outline "Cells" top gap (``.nb-outline-label--cells``). No
15
+ styling is inlined here; every design-system value lives in ``tokens``.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+
22
+ from mostlyright.data_harness.nbrender.parse import (
23
+ Notebook,
24
+ RenderContext,
25
+ esc,
26
+ heading_text,
27
+ slugify,
28
+ )
29
+
30
+ # --- A03 kernel pill --------------------------------------------------------------------------
31
+ # state key -> label; the key doubles as the ``nb-kernel--<state>`` class modifier. The labels are
32
+ # fixed design copy, not notebook data — nothing here reads the kernelspec, and the interpreter
33
+ # version and queue depth are literal text. ``render_header`` only ever asks for ``idle``
34
+ # (connected mode) or ``none``. The middot and em dash are literal punctuation, not iconography.
35
+ _KERNEL_LABELS: dict[str, str] = {
36
+ "idle": "python 3.12 · idle",
37
+ "busy": "python 3.12 · busy",
38
+ "queued": "python 3.12 · queued (3)",
39
+ "dead": "kernel dead — restart",
40
+ "none": "no kernel",
41
+ }
42
+
43
+
44
+ def render_kernel_pill(state: str, ctx: RenderContext) -> str:
45
+ """A03 · kernel status pill — idle/busy/queued/dead/none dot and label. Composed by A01.
46
+
47
+ An unknown ``state`` degrades to ``none`` ("no kernel") rather than raising, so a caller that
48
+ passes a stale kernel status still yields a well-formed pill.
49
+ """
50
+ key = state if state in _KERNEL_LABELS else "none"
51
+ label = _KERNEL_LABELS[key]
52
+ return (
53
+ f'<span class="nb-kernel nb-kernel--{key}">'
54
+ '<span class="nb-kernel-dot" aria-hidden="true"></span>'
55
+ f"{esc(label)}"
56
+ "</span>"
57
+ )
58
+
59
+
60
+ # --- A01 header -------------------------------------------------------------------------------
61
+
62
+
63
+ def _path_segments(filename: str) -> list[str]:
64
+ """Split a notebook path into non-empty segments, tolerant of both slash styles."""
65
+ text = str(filename).replace("\\", "/")
66
+ return [seg for seg in text.split("/") if seg]
67
+
68
+
69
+ def _basename(filename: str) -> str:
70
+ """Last path segment — the filename shown in the header title line."""
71
+ segs = _path_segments(filename)
72
+ return segs[-1] if segs else str(filename)
73
+
74
+
75
+ def _brand_mark(size: int = 26) -> str:
76
+ """The multi-color facet mark: ``--m1`` paper plus the standard orange/yellow/cobalt facets.
77
+
78
+ Rendered as inline SVG so no external asset is fetched; facet fills reference palette tokens
79
+ (``--m1`` == paper) rather than re-declaring hexes. Decorative, so ``aria-hidden``.
80
+ """
81
+ return (
82
+ f'<svg width="{size}" height="{size}" viewBox="0 0 26 26" '
83
+ 'aria-hidden="true" focusable="false">'
84
+ '<rect x="0" y="0" width="11" height="11" rx="2" fill="var(--nb-paper)"></rect>'
85
+ '<rect x="15" y="0" width="11" height="11" rx="2" fill="var(--nb-orange)"></rect>'
86
+ '<rect x="0" y="15" width="11" height="11" rx="2" fill="var(--nb-yellow)"></rect>'
87
+ '<rect x="15" y="15" width="11" height="11" rx="2" fill="var(--nb-cobalt)"></rect>'
88
+ "</svg>"
89
+ )
90
+
91
+
92
+ def _breadcrumb_html(filename: str) -> str:
93
+ """Breadcrumb trail from the path: ``/`` separators, the deepest crumb highlighted."""
94
+ segs = _path_segments(filename) or [str(filename)]
95
+ last = len(segs) - 1
96
+ pieces: list[str] = []
97
+ for i, seg in enumerate(segs):
98
+ if i == last:
99
+ pieces.append(f'<span class="nb-header-crumb-last">{esc(seg)}</span>')
100
+ else:
101
+ pieces.append(f"<span>{esc(seg)}</span>")
102
+ sep = '<span class="nb-header-crumb-sep"> / </span>'
103
+ return '<div class="nb-header-crumbs">' + sep.join(pieces) + "</div>"
104
+
105
+
106
+ def render_header(nb: Notebook, ctx: RenderContext, filename: str) -> str:
107
+ """A01 · notebook header — brand mark, breadcrumb + filename block, kernel pill, actions.
108
+
109
+ Height is mode-invariant. In ``connected`` the kernel pill shows ``idle`` and the primary
110
+ button runs the notebook; in every other mode the pill reads ``no kernel`` and the primary
111
+ button downloads the source, so the bar never collapses. The breadcrumb and filename come from
112
+ ``filename``; the "saved" line below them is fixed copy.
113
+ """
114
+ mark = _brand_mark(26)
115
+ crumbs = _breadcrumb_html(filename)
116
+ base = _basename(filename)
117
+ kernel_state = "idle" if ctx.mode == "connected" else "none"
118
+ pill = render_kernel_pill(kernel_state, ctx)
119
+ ghost = '<button class="nb-btn-ghost" type="button">Raw</button>'
120
+ primary_label = "Run all" if ctx.mode == "connected" else "Download .ipynb"
121
+ primary = f'<button class="nb-btn-primary" type="button">{esc(primary_label)}</button>'
122
+ return (
123
+ '<header class="nb-header">'
124
+ '<div class="nb-header-id">'
125
+ f"{mark}"
126
+ '<div class="nb-header-block">'
127
+ f"{crumbs}"
128
+ f'<div class="nb-header-file" title="{esc(base)}">{esc(base)}</div>'
129
+ # Fixed design copy, not a measured value: an exported notebook has no save clock.
130
+ '<div class="nb-header-saved">saved 4m ago</div>'
131
+ "</div>"
132
+ "</div>"
133
+ '<div class="nb-header-actions">'
134
+ f"{pill}{ghost}{primary}"
135
+ "</div>"
136
+ "</header>"
137
+ )
138
+
139
+
140
+ # --- A02 toolbar (connected only; CSS hides it in static/embed) -------------------------------
141
+
142
+
143
+ def render_toolbar(nb: Notebook, ctx: RenderContext) -> str:
144
+ """A02 · toolbar — run/stop/restart, add-cell, cell count, clear/download.
145
+
146
+ Full markup is always produced; visibility is mode-gated in ``build_css`` (hidden in static
147
+ and embed). ``document`` only calls this in ``connected``. The cell count is derived from the
148
+ notebook: total cells, then code cells carrying an execution count.
149
+ """
150
+ total = len(nb.cells)
151
+ executed = sum(1 for c in nb.cells if c.cell_type == "code" and c.execution_count is not None)
152
+ count = f"{total} cells · {executed} executed"
153
+
154
+ # 9x10 solid play triangle · A02. Fill references the ink token; the rest accent (ink
155
+ # color + 6px gap) is carried by the registered `.nb-toolbar-btn--run` rule, no inline style.
156
+ triangle = (
157
+ '<svg width="9" height="10" viewBox="0 0 9 10" aria-hidden="true" focusable="false">'
158
+ '<path d="M0 0 L9 5 L0 10 Z" fill="var(--nb-ink)"></path></svg>'
159
+ )
160
+ run = f'<button class="nb-toolbar-btn nb-toolbar-btn--run" type="button">{triangle}Run</button>'
161
+
162
+ def btn(label: str) -> str:
163
+ return f'<button class="nb-toolbar-btn" type="button">{esc(label)}</button>'
164
+
165
+ divider = '<span class="nb-toolbar-divider" aria-hidden="true"></span>'
166
+ return (
167
+ '<div class="nb-toolbar">'
168
+ f"{run}{btn('Stop')}{btn('Restart')}"
169
+ f"{divider}"
170
+ f"{btn('+ Code')}{btn('+ Markdown')}"
171
+ '<span class="nb-toolbar-spacer"></span>'
172
+ f'<span class="nb-toolbar-count">{esc(count)}</span>'
173
+ f"{divider}"
174
+ f"{btn('Clear outputs')}{btn('Download .ipynb')}"
175
+ "</div>"
176
+ )
177
+
178
+
179
+ # --- A04 outline rail -------------------------------------------------------------------------
180
+
181
+ _HEADING_RE = re.compile(r"^(#{1,6})\s+(.*?)\s*#*\s*$")
182
+ _FENCE_RE = re.compile(r"^\s*(```|~~~)")
183
+
184
+
185
+ def _headings(nb: Notebook) -> list[tuple[int, str]]:
186
+ """ATX headings from markdown cells only, skipping fenced code. Returns (level, text)."""
187
+ out: list[tuple[int, str]] = []
188
+ for cell in nb.cells:
189
+ if cell.cell_type != "markdown":
190
+ continue
191
+ in_fence = False
192
+ for line in cell.source.splitlines():
193
+ if _FENCE_RE.match(line):
194
+ in_fence = not in_fence
195
+ continue
196
+ if in_fence:
197
+ continue
198
+ m = _HEADING_RE.match(line)
199
+ if m:
200
+ text = heading_text(m.group(2))
201
+ if text:
202
+ out.append((len(m.group(1)), text))
203
+ return out
204
+
205
+
206
+ def _minimap_type(cell) -> str:
207
+ """Minimap stripe/bar class for a cell: md/code/err/raw (unknown recedes to raw)."""
208
+ if cell.cell_type == "markdown":
209
+ return "md"
210
+ if cell.cell_type == "raw":
211
+ return "raw"
212
+ if cell.cell_type == "code":
213
+ if any(o.kind == "error" for o in cell.outputs):
214
+ return "err"
215
+ return "code"
216
+ return "raw"
217
+
218
+
219
+ def _length_bucket(source: str) -> str:
220
+ """Bucket a cell's length into the three minimap bar heights by trimmed source size."""
221
+ n = len((source or "").strip())
222
+ if n <= 120:
223
+ return "short"
224
+ if n <= 480:
225
+ return "medium"
226
+ return "long"
227
+
228
+
229
+ def _minimap(nb: Notebook) -> str:
230
+ """One decorative row per cell: a type stripe plus a length-encoded bar.
231
+
232
+ Stripe/bar tints (by cell type) and the three bar heights (by length bucket) are class-driven —
233
+ the two non-palette tints live as ``--nb-minimap-*`` tokens and the heights as
234
+ ``.nb-minimap-h-*`` rules — so no styling is inlined here.
235
+ """
236
+ rows: list[str] = []
237
+ for cell in nb.cells:
238
+ kind = _minimap_type(cell)
239
+ height = f"nb-minimap-h-{_length_bucket(cell.source)}"
240
+ rows.append(
241
+ '<div class="nb-minimap-row" aria-hidden="true">'
242
+ f'<span class="nb-minimap-stripe nb-minimap-stripe--{kind} {height}"></span>'
243
+ f'<span class="nb-minimap-bar nb-minimap-bar--{kind} {height}"></span>'
244
+ "</div>"
245
+ )
246
+ return "".join(rows)
247
+
248
+
249
+ def render_outline(nb: Notebook, ctx: RenderContext) -> str:
250
+ """A04 · outline rail — heading tree plus per-cell minimap. Collapses below 1100px (CSS).
251
+
252
+ The heading tree is built from markdown cells; depth maps h1->0 through h4+->3 for the indent
253
+ ramp. The first heading is marked active as the document-top default. Anchor targets
254
+ (``#slug``) match the heading ids ``markdown_body`` emits, because both sides derive them from
255
+ :func:`parse.slugify` over the same heading text.
256
+ """
257
+ headings = _headings(nb)
258
+ links: list[str] = []
259
+ for i, (level, text) in enumerate(headings):
260
+ depth = min(level - 1, 3)
261
+ cls = "nb-outline-link"
262
+ if i == 0:
263
+ cls += " nb-outline-link--active"
264
+ links.append(
265
+ f'<a class="{cls}" data-depth="{depth}" href="#{esc(slugify(text))}">{esc(text)}</a>'
266
+ )
267
+
268
+ tree = ""
269
+ if links:
270
+ tree = '<div class="nb-outline-label">Outline</div>' + "".join(links)
271
+
272
+ # A small top gap before the second label only when a heading tree precedes it — carried by the
273
+ # registered `.nb-outline-label--cells` rule rather than an inline style.
274
+ cells_class = "nb-outline-label nb-outline-label--cells" if links else "nb-outline-label"
275
+ return (
276
+ '<nav class="nb-outline" aria-label="Notebook outline">'
277
+ f"{tree}"
278
+ f'<div class="{cells_class}">Cells</div>'
279
+ f"{_minimap(nb)}"
280
+ "</nav>"
281
+ )
282
+
283
+
284
+ # --- A05 repro footer -------------------------------------------------------------------------
285
+
286
+
287
+ def _mostlyright_block(nb: Notebook) -> dict:
288
+ block = nb.metadata.get("mostlyright")
289
+ return block if isinstance(block, dict) else {}
290
+
291
+
292
+ def _kernel_name(nb: Notebook) -> str | None:
293
+ ks = nb.metadata.get("kernelspec")
294
+ if not isinstance(ks, dict):
295
+ return None
296
+ name = ks.get("display_name") or ks.get("name")
297
+ return str(name) if name else None
298
+
299
+
300
+ def _footer_facts(nb: Notebook) -> list[str]:
301
+ """Repro facts in display order; absent fields are omitted, never placeholdered.
302
+
303
+ ``nbformat`` is always derivable, so the footer never renders empty. ``kernelspec`` comes from
304
+ notebook metadata; the env hash, ``mostlyright`` version, and wall time come from
305
+ ``metadata.mostlyright`` when the run recorded them.
306
+ """
307
+ facts: list[str] = []
308
+ mr = _mostlyright_block(nb)
309
+ env = mr.get("env") or mr.get("env_hash")
310
+ if env:
311
+ facts.append(f"env {env}")
312
+ version = mr.get("version")
313
+ if version:
314
+ facts.append(f"mostlyright {version}")
315
+ wall = mr.get("wall_time") or mr.get("walltime")
316
+ if wall:
317
+ facts.append(f"wall {wall}")
318
+ kernel = _kernel_name(nb)
319
+ if kernel:
320
+ facts.append(kernel)
321
+ facts.append(f"nbformat {nb.nbformat}.{nb.nbformat_minor}")
322
+ return facts
323
+
324
+
325
+ def _replay_oneliner(nb: Notebook) -> str:
326
+ """The ``mr.replay(...)`` one-liner the reproduce link carries: env hash, else revision.
327
+
328
+ A provenance ``revision`` is display copy carrying a parenthetical tail (the harness emits
329
+ ``"<digest12> (<manifest_version>)"``), and the tail is not part of the identifier. Only the
330
+ leading token is used, so the link reads ``mr.replay("1ef73f4ff396")``.
331
+ """
332
+ mr = _mostlyright_block(nb)
333
+ ident = mr.get("env") or mr.get("env_hash")
334
+ if not ident:
335
+ prov = nb.provenance or {}
336
+ tokens = str(prov.get("revision") or "").split()
337
+ ident = tokens[0] if tokens else ""
338
+ if ident:
339
+ return f'mr.replay("{ident}")'
340
+ return "mr.replay()"
341
+
342
+
343
+ def render_footer(nb: Notebook, ctx: RenderContext) -> str:
344
+ """A05 · repro footer — env hash, version, wall time, kernel spec, nbformat, replay link.
345
+
346
+ The reproduce link stashes the one-liner in ``data-replay``/``title`` so a connected-mode
347
+ script can copy it; static has no script, so the link is inert by design.
348
+ """
349
+ facts = _footer_facts(nb)
350
+ mid = '<span class="nb-footer-mid" aria-hidden="true">·</span>'
351
+ fact_html = mid.join(f"<span>{esc(text)}</span>" for text in facts)
352
+ replay = _replay_oneliner(nb)
353
+ link = (
354
+ '<span class="nb-footer-link" role="button" tabindex="0" '
355
+ f'data-replay="{esc(replay)}" title="{esc(replay)}">reproduce this run →</span>'
356
+ )
357
+ return (
358
+ f'<footer class="nb-footer"><div class="nb-footer-facts">{fact_html}</div>{link}</footer>'
359
+ )