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,1340 @@
1
+ """Deterministic public-catalog deltas over one complete fill generation.
2
+
3
+ Publisher identifiers and Harness entry identifiers are deliberately separate. A caller must
4
+ therefore provide both mappings explicitly: the current provider record to its authored candidate
5
+ entry, and each predecessor provider record to the entry it previously produced. This module
6
+ never transliterates or guesses an identifier.
7
+
8
+ Classification is read-only. It binds one complete, converged ``FillStaging`` generation and
9
+ produces admitted entries plus receipt-only absence and rights-review records. Publication lives
10
+ below so an incomplete page checkpoint can never accidentally become a public catalog.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Mapping
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from mostlyright.data_harness.acquisition.url_policy import EgressPolicy
21
+ from mostlyright.data_harness.canonical import canonical_sha256
22
+ from mostlyright.data_harness.sources.catalog.authoring import (
23
+ seal_catalog_successor,
24
+ seal_retrieval_catalog_successor,
25
+ )
26
+ from mostlyright.data_harness.sources.catalog.contracts import (
27
+ CatalogEntry,
28
+ catalog_entry_from_dict,
29
+ next_entry_version,
30
+ verify_version_chain,
31
+ )
32
+ from mostlyright.data_harness.sources.catalog.embedding import EmbeddingBackend
33
+ from mostlyright.data_harness.sources.catalog.fill import CatalogFillResult
34
+ from mostlyright.data_harness.sources.catalog.fill_staging import FillStaging
35
+ from mostlyright.data_harness.sources.catalog.harvest.ckan import ckan_licence_rights
36
+ from mostlyright.data_harness.sources.catalog.harvest.protocol import HarvestedRecord
37
+ from mostlyright.data_harness.sources.catalog.harvest.stac import stac_licence_rights
38
+ from mostlyright.data_harness.sources.catalog.retrieval_manifest import (
39
+ LoadedRetrievalCatalog,
40
+ RetrievalManifestError,
41
+ load_retrieval_catalog,
42
+ )
43
+ from mostlyright.data_harness.sources.catalog.rights_decisions import RightsDecision
44
+ from mostlyright.data_harness.sources.contracts import (
45
+ EvidenceReference,
46
+ LicenseObligation,
47
+ RightsEvidence,
48
+ SourceContractError,
49
+ SourceObservation,
50
+ )
51
+ from mostlyright.data_harness.sources.rights_rule import rights_disposition
52
+
53
+ CATALOG_DELTA_SCHEMA = "harness-source-catalog-delta.v1"
54
+
55
+
56
+ class CatalogDeltaError(SourceContractError):
57
+ """A stable refusal while classifying or publishing a catalog successor."""
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class CatalogAbsenceObservation:
62
+ """One predecessor provider record absent from a complete converged fill."""
63
+
64
+ record_id: str
65
+ entry_id: str
66
+ previous_coordinate: str
67
+ previous_facts_digest: str
68
+ observed_at: str
69
+ fill_receipt_sha256: str
70
+
71
+ def to_dict(self) -> dict[str, str]:
72
+ return {
73
+ "record_id": self.record_id,
74
+ "entry_id": self.entry_id,
75
+ "previous_coordinate": self.previous_coordinate,
76
+ "previous_facts_digest": self.previous_facts_digest,
77
+ "observed_at": self.observed_at,
78
+ "fill_receipt_sha256": self.fill_receipt_sha256,
79
+ }
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class CatalogFlaggedRecord:
84
+ """One provider record kept out of admission pending a rights decision."""
85
+
86
+ protocol: str
87
+ record_id: str
88
+ record_sha256: str
89
+ license_id: str | None
90
+ license_uri: str | None
91
+ rights_status: str
92
+ authority: str = "human_review_required"
93
+ entry_id: str | None = None
94
+ human_decision_reference: str | None = None
95
+ decided_at: str | None = None
96
+
97
+ def to_dict(self) -> dict[str, Any]:
98
+ return {
99
+ "protocol": self.protocol,
100
+ "record_id": self.record_id,
101
+ "record_sha256": self.record_sha256,
102
+ "license_id": self.license_id,
103
+ "license_uri": self.license_uri,
104
+ "rights_status": self.rights_status,
105
+ "authority": self.authority,
106
+ "entry_id": self.entry_id,
107
+ "human_decision_reference": self.human_decision_reference,
108
+ "decided_at": self.decided_at,
109
+ }
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class CatalogDeltaClassification:
114
+ """A complete deterministic partition of one fill against its predecessor."""
115
+
116
+ predecessor_sha256: str | None
117
+ fill_receipt_sha256: str
118
+ staging_sha256: str
119
+ records_sha256: str
120
+ unchanged: tuple[CatalogEntry, ...]
121
+ changed: tuple[CatalogEntry, ...]
122
+ new: tuple[CatalogEntry, ...]
123
+ absent: tuple[CatalogAbsenceObservation, ...]
124
+ flagged: tuple[CatalogFlaggedRecord, ...]
125
+ version_chains: tuple[tuple[CatalogEntry, ...], ...]
126
+ rights_decisions_sha256: str | None = None
127
+ schema_version: str = CATALOG_DELTA_SCHEMA
128
+
129
+ @property
130
+ def admitted_entries(self) -> tuple[CatalogEntry, ...]:
131
+ return tuple(sorted((*self.unchanged, *self.changed, *self.new), key=_entry_key))
132
+
133
+ @property
134
+ def digest(self) -> str:
135
+ return canonical_sha256(self.to_dict())
136
+
137
+ def to_dict(self) -> dict[str, Any]:
138
+ return {
139
+ "schema_version": self.schema_version,
140
+ "predecessor_sha256": self.predecessor_sha256,
141
+ "fill_receipt_sha256": self.fill_receipt_sha256,
142
+ "staging_sha256": self.staging_sha256,
143
+ "records_sha256": self.records_sha256,
144
+ "rights_decisions_sha256": self.rights_decisions_sha256,
145
+ "counts": {
146
+ "unchanged": len(self.unchanged),
147
+ "changed": len(self.changed),
148
+ "new": len(self.new),
149
+ "absent": len(self.absent),
150
+ "flagged": len(self.flagged),
151
+ "admitted": len(self.admitted_entries),
152
+ },
153
+ "unchanged": [_entry_receipt(item) for item in self.unchanged],
154
+ "changed": [_entry_receipt(item) for item in self.changed],
155
+ "new": [_entry_receipt(item) for item in self.new],
156
+ "absent": [item.to_dict() for item in self.absent],
157
+ "flagged": [item.to_dict() for item in self.flagged],
158
+ "version_chains": [
159
+ [_entry_receipt(item) for item in chain] for chain in self.version_chains
160
+ ],
161
+ }
162
+
163
+
164
+ @dataclass(frozen=True)
165
+ class CatalogDeltaPublication:
166
+ """The exact durable v1 successor and its incremental-work receipt."""
167
+
168
+ delta_sha256: str
169
+ manifest_sha256: str
170
+ entries: tuple[CatalogEntry, ...]
171
+ reused_vector_sha256s: tuple[str, ...]
172
+ created_vector_sha256s: tuple[str, ...]
173
+ encoder_invocations: int
174
+
175
+ @property
176
+ def digest(self) -> str:
177
+ return canonical_sha256(self.to_dict())
178
+
179
+ def to_dict(self) -> dict[str, Any]:
180
+ return {
181
+ "schema_version": "harness-source-catalog-publication.v1",
182
+ "delta_sha256": self.delta_sha256,
183
+ "manifest_sha256": self.manifest_sha256,
184
+ "entries": [_entry_receipt(entry) for entry in self.entries],
185
+ "reused_vector_sha256s": list(self.reused_vector_sha256s),
186
+ "created_vector_sha256s": list(self.created_vector_sha256s),
187
+ "encoder_invocations": self.encoder_invocations,
188
+ }
189
+
190
+
191
+ @dataclass(frozen=True)
192
+ class RetrievalCatalogDeltaPublication:
193
+ """Canonical v2 delta receipt with independent representation work accounting."""
194
+
195
+ delta_sha256: str
196
+ classification_checkpoint_sha256: str
197
+ manifest_sha256: str
198
+ entries: tuple[CatalogEntry, ...]
199
+ unchanged: int
200
+ changed: int
201
+ new: int
202
+ absent: int
203
+ flagged: int
204
+ lexical_reused_vector_sha256s: tuple[str, ...]
205
+ lexical_created_vector_sha256s: tuple[str, ...]
206
+ neural_reused_vector_sha256s: tuple[str, ...]
207
+ neural_created_vector_sha256s: tuple[str, ...]
208
+ lexical_encoder_invocations: int
209
+ neural_encoder_invocations: int
210
+ fallback_reason: str | None
211
+
212
+ @property
213
+ def digest(self) -> str:
214
+ return canonical_sha256(self.to_dict())
215
+
216
+ def to_dict(self) -> dict[str, Any]:
217
+ return {
218
+ "schema_version": "harness-source-catalog-retrieval-publication.v2",
219
+ "delta_sha256": self.delta_sha256,
220
+ "classification_checkpoint_sha256": self.classification_checkpoint_sha256,
221
+ "manifest_sha256": self.manifest_sha256,
222
+ "counts": {
223
+ "unchanged": self.unchanged,
224
+ "changed": self.changed,
225
+ "new": self.new,
226
+ "absent": self.absent,
227
+ "flagged": self.flagged,
228
+ },
229
+ "entries": [_entry_receipt(entry) for entry in self.entries],
230
+ "representations": {
231
+ "lexical": {
232
+ "reused_vector_sha256s": list(self.lexical_reused_vector_sha256s),
233
+ "created_vector_sha256s": list(self.lexical_created_vector_sha256s),
234
+ "encoder_invocations": self.lexical_encoder_invocations,
235
+ },
236
+ "neural": {
237
+ "reused_vector_sha256s": list(self.neural_reused_vector_sha256s),
238
+ "created_vector_sha256s": list(self.neural_created_vector_sha256s),
239
+ "encoder_invocations": self.neural_encoder_invocations,
240
+ },
241
+ },
242
+ "fallback_reason": self.fallback_reason,
243
+ }
244
+
245
+
246
+ @dataclass(frozen=True)
247
+ class _FillSnapshot:
248
+ records: tuple[HarvestedRecord, ...]
249
+ flagged: tuple[CatalogFlaggedRecord, ...]
250
+ observed_at: str
251
+ records_sha256: str
252
+
253
+
254
+ def classify_catalog_delta(
255
+ *,
256
+ predecessor: tuple[CatalogEntry, ...],
257
+ predecessor_record_ids: Mapping[str, str],
258
+ candidates_by_record_id: Mapping[str, CatalogEntry],
259
+ fill_result: CatalogFillResult,
260
+ staging_root: Path,
261
+ predecessor_chains: Mapping[str, tuple[CatalogEntry, ...]] | None = None,
262
+ rights_decisions: Mapping[str, RightsDecision] | None = None,
263
+ rights_decisions_sha256: str | None = None,
264
+ ) -> CatalogDeltaClassification:
265
+ """Partition a complete fill into unchanged, changed, new, absent, and flagged facts.
266
+
267
+ ``candidates_by_record_id`` contains authored, unsealed entries for every non-flagged provider
268
+ record. ``predecessor_record_ids`` preserves the explicit provider-to-Harness identity chosen
269
+ when the predecessor was authored. Neither map is inferred from publisher text.
270
+
271
+ ``rights_decisions`` is the queue's one exit. Each decision must name a record the queue
272
+ holds and bind the exact bytes the person decided against; a cleared record leaves the
273
+ queue and demands a candidate whose rights are exactly the decided rights, and a
274
+ prohibited record stays out of admission carrying the decision, so the queue
275
+ distinguishes "nobody has looked" from "somebody looked and said no".
276
+ """
277
+
278
+ previous_by_id, chains = _validate_predecessor(
279
+ predecessor, predecessor_record_ids, predecessor_chains or {}
280
+ )
281
+ snapshot = _capture_complete_fill(fill_result, staging_root)
282
+ records_by_id = {record.record_id: record for record in snapshot.records}
283
+ staged_flagged = {item.record_id: item for item in snapshot.flagged}
284
+
285
+ decisions = dict(rights_decisions or {})
286
+ if decisions:
287
+ _validate_mapping(decisions, "rights_decisions")
288
+ prohibited_ids: set[str] = set()
289
+ for record_id in sorted(decisions):
290
+ decision = decisions[record_id]
291
+ if not isinstance(decision, RightsDecision):
292
+ raise CatalogDeltaError(
293
+ "TYPE", f"delta.rights_decisions.{record_id}", "must be a RightsDecision"
294
+ )
295
+ if decision.record_id != record_id:
296
+ raise CatalogDeltaError(
297
+ "CATALOG_RIGHTS_DECISION_TARGET",
298
+ f"delta.rights_decisions.{record_id}",
299
+ "the decision names a different record than the mapping key; one decision "
300
+ "is about one record, spelled once",
301
+ )
302
+ record = records_by_id.get(record_id)
303
+ if record is None:
304
+ raise CatalogDeltaError(
305
+ "CATALOG_RIGHTS_DECISION_TARGET",
306
+ f"delta.rights_decisions.{record_id}",
307
+ "names a record this fill did not observe",
308
+ )
309
+ if decision.record_facts_sha256 != record.facts_sha256:
310
+ raise CatalogDeltaError(
311
+ "CATALOG_RIGHTS_DECISION_DRIFT",
312
+ f"delta.rights_decisions.{record_id}",
313
+ "the record's published facts changed after this decision was made; a "
314
+ "decision is about one spelling of the facts, so a person must decide "
315
+ "again against the current facts",
316
+ )
317
+ staged = staged_flagged.get(record_id)
318
+ if decision.cleared:
319
+ if staged is None:
320
+ raise CatalogDeltaError(
321
+ "CATALOG_RIGHTS_DECISION_TARGET",
322
+ f"delta.rights_decisions.{record_id}",
323
+ "names a record the rights queue does not hold; the provider states "
324
+ "its own licence, so there is nothing left for a clearance to clear",
325
+ )
326
+ if staged.rights_status == "prohibited":
327
+ raise CatalogDeltaError(
328
+ "CATALOG_RIGHTS_DECISION_OVERRULE",
329
+ f"delta.rights_decisions.{record_id}",
330
+ "the provider's own licence prohibits reuse; a decision may clear "
331
+ "silence, never overrule a provider's explicit no",
332
+ )
333
+ else:
334
+ # A person's no outranks a provider's yes, so a prohibiting decision holds
335
+ # whether or not the provider's own licence would have admitted the record.
336
+ prohibited_ids.add(record_id)
337
+ cleared_ids = {record_id for record_id, item in decisions.items() if item.cleared}
338
+
339
+ _validate_mapping(candidates_by_record_id, "candidates_by_record_id")
340
+ candidate_keys = set(candidates_by_record_id)
341
+ required_candidate_keys = (
342
+ (set(records_by_id) - set(staged_flagged)) | cleared_ids
343
+ ) - prohibited_ids
344
+ if candidate_keys != required_candidate_keys:
345
+ raise CatalogDeltaError(
346
+ "CATALOG_DELTA_CANDIDATE_MAPPING",
347
+ "delta.candidates_by_record_id",
348
+ "must name exactly every current provider record not held by the rights queue, "
349
+ "plus every record a decision cleared out of it, minus every record a decision "
350
+ "prohibits; "
351
+ f"missing={sorted(required_candidate_keys - candidate_keys)}, "
352
+ f"extra={sorted(candidate_keys - required_candidate_keys)}",
353
+ )
354
+
355
+ unchanged: list[CatalogEntry] = []
356
+ changed: list[CatalogEntry] = []
357
+ new: list[CatalogEntry] = []
358
+ flagged: dict[str, CatalogFlaggedRecord] = dict(staged_flagged)
359
+ for record_id, decision in decisions.items():
360
+ if decision.cleared:
361
+ flagged.pop(record_id, None)
362
+ else:
363
+ record = records_by_id[record_id]
364
+ flagged[record_id] = CatalogFlaggedRecord(
365
+ protocol=record.protocol,
366
+ record_id=record.record_id,
367
+ record_sha256=record.digest,
368
+ license_id=record.license_id,
369
+ license_uri=record.license_uri,
370
+ rights_status=decision.rights.status,
371
+ authority="human_decision",
372
+ entry_id=None,
373
+ human_decision_reference=decision.human_decision_reference,
374
+ decided_at=decision.decided_at,
375
+ )
376
+ resulting_chains: list[tuple[CatalogEntry, ...]] = []
377
+
378
+ for record_id in sorted(required_candidate_keys):
379
+ record = records_by_id[record_id]
380
+ decision = decisions.get(record_id)
381
+ candidate = _validated_candidate(
382
+ record_id,
383
+ record,
384
+ candidates_by_record_id[record_id],
385
+ decided_rights=decision.rights if decision is not None else None,
386
+ )
387
+ disposition = rights_disposition(candidate.rights.status)
388
+ if disposition != "admit":
389
+ flagged[record_id] = CatalogFlaggedRecord(
390
+ protocol=record.protocol,
391
+ record_id=record.record_id,
392
+ record_sha256=record.digest,
393
+ license_id=record.license_id,
394
+ license_uri=record.license_uri,
395
+ rights_status=candidate.rights.status,
396
+ entry_id=candidate.entry_id,
397
+ )
398
+ continue
399
+ previous_id = predecessor_record_ids.get(record_id)
400
+ if previous_id is None:
401
+ if candidate.entry_id in previous_by_id:
402
+ raise CatalogDeltaError(
403
+ "CATALOG_DELTA_PREDECESSOR_MAPPING",
404
+ f"delta.candidates_by_record_id.{record_id}",
405
+ "a current entry reuses a predecessor entry_id without an explicit provider "
406
+ "record mapping",
407
+ )
408
+ new.append(candidate)
409
+ resulting_chains.append((candidate,))
410
+ verify_version_chain((candidate,))
411
+ continue
412
+ previous = previous_by_id[previous_id]
413
+ if candidate.entry_id != previous.entry_id:
414
+ raise CatalogDeltaError(
415
+ "CATALOG_DELTA_ENTRY_ID",
416
+ f"delta.candidates_by_record_id.{record_id}.entry_id",
417
+ f"must remain {previous.entry_id!r} for this predecessor mapping",
418
+ )
419
+ chain = chains[previous.entry_id]
420
+ # ``CatalogEntry.facts_digest`` correctly commits the version and predecessor link. The
421
+ # author's current spelling is deliberately v1 so those managed fields necessarily differ
422
+ # from a v2+ head even when every observed/public fact is unchanged. Compare the canonical
423
+ # non-managed projection for classification; keep ``facts_digest`` for the chain link.
424
+ if _classifiable_facts_digest(candidate) == _classifiable_facts_digest(previous):
425
+ unchanged.append(previous)
426
+ resulting_chains.append(chain)
427
+ continue
428
+ successor = next_entry_version(previous, **_successor_changes(candidate))
429
+ successor_chain = (*chain, successor)
430
+ verify_version_chain(successor_chain)
431
+ changed.append(successor)
432
+ resulting_chains.append(successor_chain)
433
+
434
+ current_record_ids = set(records_by_id)
435
+ absent: list[CatalogAbsenceObservation] = []
436
+ for record_id in sorted(set(predecessor_record_ids) - current_record_ids):
437
+ previous = previous_by_id[predecessor_record_ids[record_id]]
438
+ absent.append(
439
+ CatalogAbsenceObservation(
440
+ record_id=record_id,
441
+ entry_id=previous.entry_id,
442
+ previous_coordinate=previous.coordinate,
443
+ previous_facts_digest=previous.facts_digest,
444
+ observed_at=snapshot.observed_at,
445
+ fill_receipt_sha256=fill_result.receipt.digest,
446
+ )
447
+ )
448
+
449
+ # A current flagged record is neither admitted nor absent. Its prior public head is removed
450
+ # from the current-head manifest, while the canonical queue preserves why that happened.
451
+ ordered_flags = tuple(
452
+ sorted(flagged.values(), key=lambda item: (item.protocol, item.record_id))
453
+ )
454
+ ordered_chains = tuple(sorted(resulting_chains, key=lambda chain: _entry_key(chain[-1])))
455
+ return CatalogDeltaClassification(
456
+ predecessor_sha256=fill_result.receipt.predecessor_sha256,
457
+ fill_receipt_sha256=fill_result.receipt.digest,
458
+ staging_sha256=fill_result.eligible_staging_sha256 or "",
459
+ records_sha256=snapshot.records_sha256,
460
+ unchanged=tuple(sorted(unchanged, key=_entry_key)),
461
+ changed=tuple(sorted(changed, key=_entry_key)),
462
+ new=tuple(sorted(new, key=_entry_key)),
463
+ absent=tuple(absent),
464
+ flagged=ordered_flags,
465
+ version_chains=ordered_chains,
466
+ rights_decisions_sha256=rights_decisions_sha256,
467
+ )
468
+
469
+
470
+ def publish_catalog_successor(
471
+ classification: CatalogDeltaClassification,
472
+ *,
473
+ backend: EmbeddingBackend,
474
+ directory: Path,
475
+ predecessor_directory: Path | None,
476
+ egress_policy: EgressPolicy,
477
+ ) -> CatalogDeltaPublication:
478
+ """Publish a fully classified current-head successor with durable exact readback."""
479
+
480
+ _validate_classification_for_publication(classification)
481
+ try:
482
+ sealed = seal_catalog_successor(
483
+ reused_entries=classification.unchanged,
484
+ fresh_entries=tuple((*classification.changed, *classification.new)),
485
+ backend=backend,
486
+ directory=directory,
487
+ predecessor_directory=predecessor_directory,
488
+ predecessor_manifest_sha256=classification.predecessor_sha256,
489
+ egress_policy=egress_policy,
490
+ )
491
+ except CatalogDeltaError:
492
+ raise
493
+ except SourceContractError as error:
494
+ raise CatalogDeltaError(error.code, error.path, error.detail) from error
495
+ except OSError as error:
496
+ raise CatalogDeltaError(
497
+ "CATALOG_PUBLICATION",
498
+ "catalog.directory",
499
+ "durable catalog publication failed",
500
+ ) from error
501
+ return CatalogDeltaPublication(
502
+ delta_sha256=classification.digest,
503
+ manifest_sha256=sealed.manifest_sha256,
504
+ entries=sealed.entries,
505
+ reused_vector_sha256s=sealed.reused_vector_sha256s,
506
+ created_vector_sha256s=sealed.created_vector_sha256s,
507
+ encoder_invocations=sealed.encoder_invocations,
508
+ )
509
+
510
+
511
+ def checkpoint_catalog_delta_classification(
512
+ classification: CatalogDeltaClassification,
513
+ *,
514
+ staging_root: Path,
515
+ lexical_backend: EmbeddingBackend,
516
+ neural_backend: EmbeddingBackend | None,
517
+ ) -> str:
518
+ """Persist the complete classification before either representation is encoded."""
519
+
520
+ _validate_classification_for_publication(classification)
521
+ staging = FillStaging(staging_root)
522
+ with staging.locked():
523
+ state = staging.load()
524
+ if state is None or staging.state_digest() != classification.staging_sha256:
525
+ raise CatalogDeltaError(
526
+ "CATALOG_DELTA_STAGING",
527
+ "delta.classification.staging_sha256",
528
+ "classification does not bind the live complete staging generation",
529
+ )
530
+ if not state["completed"] or state["reason_code"] is not None or state["pass_drift"]:
531
+ raise CatalogDeltaError(
532
+ "CATALOG_DELTA_INCOMPLETE",
533
+ "delta.staging",
534
+ "classification checkpoints require complete converged staging",
535
+ )
536
+ return staging.write_shard(
537
+ "classification",
538
+ _classification_checkpoint_payload(
539
+ classification,
540
+ lexical_coordinate=lexical_backend.descriptor.coordinate,
541
+ neural_coordinate=None
542
+ if neural_backend is None
543
+ else neural_backend.descriptor.coordinate,
544
+ ),
545
+ )
546
+
547
+
548
+ def load_catalog_delta_classification(
549
+ checkpoint_sha256: str,
550
+ *,
551
+ staging_root: Path,
552
+ expected_staging_sha256: str,
553
+ expected_predecessor_sha256: str | None,
554
+ lexical_backend: EmbeddingBackend,
555
+ neural_backend: EmbeddingBackend | None,
556
+ ) -> CatalogDeltaClassification:
557
+ """Restore a checkpoint only under the exact staging, predecessor, and model tuple."""
558
+
559
+ staging = FillStaging(staging_root)
560
+ with staging.locked():
561
+ state = staging.load()
562
+ if state is None or staging.state_digest() != expected_staging_sha256:
563
+ raise CatalogDeltaError(
564
+ "CATALOG_DELTA_STAGING",
565
+ "delta.classification.staging_sha256",
566
+ "live staging differs from the expected complete generation",
567
+ )
568
+ payload = staging.read_shard("classification", checkpoint_sha256)
569
+ exact = {
570
+ "schema_version",
571
+ "classification_sha256",
572
+ "staging_sha256",
573
+ "predecessor_sha256",
574
+ "lexical_coordinate",
575
+ "neural_coordinate",
576
+ "classification",
577
+ }
578
+ if not isinstance(payload, dict) or set(payload) != exact:
579
+ raise CatalogDeltaError(
580
+ "CATALOG_DELTA_CHECKPOINT", "delta.classification", "checkpoint fields differ"
581
+ )
582
+ expected_neural = None if neural_backend is None else neural_backend.descriptor.coordinate
583
+ if (
584
+ payload["schema_version"] != "harness-source-catalog-delta-checkpoint.v1"
585
+ or payload["staging_sha256"] != expected_staging_sha256
586
+ or payload["predecessor_sha256"] != expected_predecessor_sha256
587
+ or payload["lexical_coordinate"] != lexical_backend.descriptor.coordinate
588
+ or payload["neural_coordinate"] != expected_neural
589
+ ):
590
+ raise CatalogDeltaError(
591
+ "CATALOG_DELTA_CHECKPOINT_BINDING",
592
+ "delta.classification",
593
+ "checkpoint staging, predecessor, or representation coordinate differs",
594
+ )
595
+ rebuilt = _classification_from_checkpoint(payload["classification"])
596
+ if rebuilt.digest != payload["classification_sha256"]:
597
+ raise CatalogDeltaError(
598
+ "CATALOG_DELTA_CHECKPOINT",
599
+ "delta.classification",
600
+ "checkpoint classification digest differs",
601
+ )
602
+ _validate_classification_for_publication(rebuilt)
603
+ return rebuilt
604
+
605
+
606
+ def publish_retrieval_catalog_successor(
607
+ classification: CatalogDeltaClassification,
608
+ *,
609
+ classification_checkpoint_sha256: str,
610
+ staging_root: Path,
611
+ lexical_backend: EmbeddingBackend,
612
+ neural_backend: EmbeddingBackend | None,
613
+ directory: Path,
614
+ predecessor_directory: Path | None,
615
+ egress_policy: EgressPolicy,
616
+ fallback_reason: str | None = None,
617
+ expected_existing_manifest_sha256: str | None = None,
618
+ ) -> RetrievalCatalogDeltaPublication:
619
+ """Publish one v2 successor from the exact complete checkpointed classification."""
620
+
621
+ restored = load_catalog_delta_classification(
622
+ classification_checkpoint_sha256,
623
+ staging_root=staging_root,
624
+ expected_staging_sha256=classification.staging_sha256,
625
+ expected_predecessor_sha256=classification.predecessor_sha256,
626
+ lexical_backend=lexical_backend,
627
+ neural_backend=neural_backend,
628
+ )
629
+ if restored != classification:
630
+ raise CatalogDeltaError(
631
+ "CATALOG_DELTA_CHECKPOINT_BINDING",
632
+ "delta.classification",
633
+ "supplied classification differs from its immutable checkpoint",
634
+ )
635
+
636
+ predecessor: LoadedRetrievalCatalog | None = None
637
+ if classification.predecessor_sha256 is not None:
638
+ if predecessor_directory is None:
639
+ raise CatalogDeltaError(
640
+ "CATALOG_DELTA_PREDECESSOR",
641
+ "delta.predecessor",
642
+ "an exact predecessor directory is required",
643
+ )
644
+ try:
645
+ predecessor = load_retrieval_catalog(
646
+ predecessor_directory,
647
+ expected_manifest_sha256=classification.predecessor_sha256,
648
+ neural_backend=neural_backend,
649
+ )
650
+ except (RetrievalManifestError, SourceContractError) as error:
651
+ raise CatalogDeltaError(error.code, error.path, error.detail) from error
652
+
653
+ lexical_reused = classification.unchanged
654
+ neural_reused: tuple[CatalogEntry, ...] = ()
655
+ if predecessor is not None:
656
+ previous_lexical = {entry.entry_id: entry for entry in predecessor.lexical.entries}
657
+ if any(previous_lexical.get(entry.entry_id) != entry for entry in lexical_reused):
658
+ raise CatalogDeltaError(
659
+ "CATALOG_DELTA_PREDECESSOR",
660
+ "delta.unchanged",
661
+ "unchanged lexical entries differ from the exact predecessor representation",
662
+ )
663
+ if neural_backend is not None and predecessor.neural is not None:
664
+ previous_neural = {entry.entry_id: entry for entry in predecessor.neural.entries}
665
+ neural_reused = tuple(previous_neural[entry.entry_id] for entry in lexical_reused)
666
+
667
+ try:
668
+ sealed = seal_retrieval_catalog_successor(
669
+ lexical_reused_entries=lexical_reused,
670
+ fresh_entries=tuple((*classification.changed, *classification.new)),
671
+ lexical_backend=lexical_backend,
672
+ neural_reused_entries=neural_reused,
673
+ neural_backend=neural_backend,
674
+ directory=directory,
675
+ predecessor_retrieval_directory=predecessor_directory,
676
+ predecessor_retrieval_manifest_sha256=classification.predecessor_sha256,
677
+ egress_policy=egress_policy,
678
+ fallback_reason=fallback_reason,
679
+ expected_existing_manifest_sha256=expected_existing_manifest_sha256,
680
+ )
681
+ except SourceContractError as error:
682
+ raise CatalogDeltaError(error.code, error.path, error.detail) from error
683
+ except OSError as error:
684
+ raise CatalogDeltaError(
685
+ "CATALOG_PUBLICATION",
686
+ "catalog.directory",
687
+ "durable retrieval publication failed",
688
+ ) from error
689
+ return RetrievalCatalogDeltaPublication(
690
+ delta_sha256=classification.digest,
691
+ classification_checkpoint_sha256=classification_checkpoint_sha256,
692
+ manifest_sha256=sealed.manifest_sha256,
693
+ entries=sealed.lexical_entries,
694
+ unchanged=len(classification.unchanged),
695
+ changed=len(classification.changed),
696
+ new=len(classification.new),
697
+ absent=len(classification.absent),
698
+ flagged=len(classification.flagged),
699
+ lexical_reused_vector_sha256s=sealed.lexical_reused_vector_sha256s,
700
+ lexical_created_vector_sha256s=sealed.lexical_created_vector_sha256s,
701
+ neural_reused_vector_sha256s=sealed.neural_reused_vector_sha256s,
702
+ neural_created_vector_sha256s=sealed.neural_created_vector_sha256s,
703
+ lexical_encoder_invocations=sealed.lexical_encoder_invocations,
704
+ neural_encoder_invocations=sealed.neural_encoder_invocations,
705
+ fallback_reason=sealed.manifest.fallback_reason,
706
+ )
707
+
708
+
709
+ def _validate_classification_for_publication(
710
+ classification: CatalogDeltaClassification,
711
+ ) -> None:
712
+ if not isinstance(classification, CatalogDeltaClassification):
713
+ raise CatalogDeltaError(
714
+ "TYPE", "delta.classification", "must be a CatalogDeltaClassification"
715
+ )
716
+ if classification.schema_version != CATALOG_DELTA_SCHEMA:
717
+ raise CatalogDeltaError(
718
+ "CATALOG_DELTA_VERSION", "delta.schema_version", "unsupported version"
719
+ )
720
+ partitions = (classification.unchanged, classification.changed, classification.new)
721
+ for name, entries in zip(("unchanged", "changed", "new"), partitions, strict=True):
722
+ if tuple(sorted(entries, key=_entry_key)) != entries:
723
+ raise CatalogDeltaError(
724
+ "CATALOG_DELTA_ORDER", f"delta.{name}", "entries must be sorted"
725
+ )
726
+ if any(not entry.vectors for entry in classification.unchanged):
727
+ raise CatalogDeltaError(
728
+ "CATALOG_DELTA_REUSE", "delta.unchanged", "unchanged entries must remain sealed"
729
+ )
730
+ if any(entry.vectors for entry in (*classification.changed, *classification.new)):
731
+ raise CatalogDeltaError(
732
+ "CATALOG_DELTA_REUSE",
733
+ "delta.changed",
734
+ "changed and new entries must be encoded from unsealed facts",
735
+ )
736
+ admitted = classification.admitted_entries
737
+ if len({entry.entry_id for entry in admitted}) != len(admitted):
738
+ raise CatalogDeltaError(
739
+ "CATALOG_DELTA_DUPLICATE", "delta.entries", "entry ids must be unique"
740
+ )
741
+ if any(rights_disposition(entry.rights.status) != "admit" for entry in admitted):
742
+ raise CatalogDeltaError(
743
+ "CATALOG_DELTA_RIGHTS",
744
+ "delta.entries",
745
+ "a non-admitted rights status cannot enter the public manifest",
746
+ )
747
+ if (
748
+ tuple(sorted(classification.absent, key=lambda item: item.record_id))
749
+ != classification.absent
750
+ ):
751
+ raise CatalogDeltaError(
752
+ "CATALOG_DELTA_ORDER", "delta.absent", "absence observations must be sorted"
753
+ )
754
+ if (
755
+ tuple(
756
+ sorted(
757
+ classification.flagged,
758
+ key=lambda item: (item.protocol, item.record_id),
759
+ )
760
+ )
761
+ != classification.flagged
762
+ ):
763
+ raise CatalogDeltaError(
764
+ "CATALOG_DELTA_ORDER", "delta.flagged", "rights queue must be sorted"
765
+ )
766
+ heads: dict[str, CatalogEntry] = {}
767
+ for chain in classification.version_chains:
768
+ verify_version_chain(chain)
769
+ head = chain[-1]
770
+ if head.entry_id in heads:
771
+ raise CatalogDeltaError(
772
+ "CATALOG_DELTA_VERSION_CHAIN",
773
+ "delta.version_chains",
774
+ "one version chain is allowed per admitted entry",
775
+ )
776
+ heads[head.entry_id] = head
777
+ if set(heads) != {entry.entry_id for entry in admitted}:
778
+ raise CatalogDeltaError(
779
+ "CATALOG_DELTA_VERSION_CHAIN",
780
+ "delta.version_chains",
781
+ "verified chains must cover every admitted entry",
782
+ )
783
+ for entry in admitted:
784
+ head = heads[entry.entry_id]
785
+ if head.coordinate != entry.coordinate or head.facts_digest != entry.facts_digest:
786
+ raise CatalogDeltaError(
787
+ "CATALOG_DELTA_VERSION_CHAIN",
788
+ f"delta.version_chains.{entry.entry_id}",
789
+ "verified chain head differs from the admitted entry",
790
+ )
791
+
792
+
793
+ def _validate_predecessor(
794
+ predecessor: tuple[CatalogEntry, ...],
795
+ record_ids: Mapping[str, str],
796
+ supplied_chains: Mapping[str, tuple[CatalogEntry, ...]],
797
+ ) -> tuple[dict[str, CatalogEntry], dict[str, tuple[CatalogEntry, ...]]]:
798
+ if not isinstance(predecessor, tuple) or any(
799
+ type(entry) is not CatalogEntry for entry in predecessor
800
+ ):
801
+ raise CatalogDeltaError("TYPE", "delta.predecessor", "must be a tuple of CatalogEntry")
802
+ previous_by_id = {entry.entry_id: entry for entry in predecessor}
803
+ if len(previous_by_id) != len(predecessor):
804
+ raise CatalogDeltaError(
805
+ "CATALOG_DELTA_PREDECESSOR", "delta.predecessor", "entry ids must be unique"
806
+ )
807
+ _validate_mapping(record_ids, "predecessor_record_ids")
808
+ if len(set(record_ids.values())) != len(record_ids):
809
+ raise CatalogDeltaError(
810
+ "CATALOG_DELTA_PREDECESSOR_MAPPING",
811
+ "delta.predecessor_record_ids",
812
+ "one predecessor entry must map to exactly one provider record",
813
+ )
814
+ if set(record_ids.values()) != set(previous_by_id):
815
+ raise CatalogDeltaError(
816
+ "CATALOG_DELTA_PREDECESSOR_MAPPING",
817
+ "delta.predecessor_record_ids",
818
+ "must map every predecessor entry exactly once; "
819
+ f"missing={sorted(set(previous_by_id) - set(record_ids.values()))}, "
820
+ f"unknown={sorted(set(record_ids.values()) - set(previous_by_id))}",
821
+ )
822
+ if set(supplied_chains) - set(previous_by_id):
823
+ raise CatalogDeltaError(
824
+ "CATALOG_DELTA_VERSION_CHAIN",
825
+ "delta.predecessor_chains",
826
+ "contains a chain for an unknown predecessor entry",
827
+ )
828
+ chains: dict[str, tuple[CatalogEntry, ...]] = {}
829
+ for entry_id, previous in previous_by_id.items():
830
+ chain = supplied_chains.get(entry_id)
831
+ if chain is None:
832
+ if previous.entry_version != 1:
833
+ raise CatalogDeltaError(
834
+ "CATALOG_DELTA_VERSION_CHAIN",
835
+ f"delta.predecessor_chains.{entry_id}",
836
+ "a predecessor after v1 requires its complete explicit version chain",
837
+ )
838
+ chain = (previous,)
839
+ verify_version_chain(chain)
840
+ head = chain[-1]
841
+ if head.coordinate != previous.coordinate or head.facts_digest != previous.facts_digest:
842
+ raise CatalogDeltaError(
843
+ "CATALOG_DELTA_VERSION_CHAIN",
844
+ f"delta.predecessor_chains.{entry_id}",
845
+ "the verified chain head does not match the predecessor snapshot",
846
+ )
847
+ chains[entry_id] = chain
848
+ return previous_by_id, chains
849
+
850
+
851
+ def _capture_complete_fill(fill_result: CatalogFillResult, staging_root: Path) -> _FillSnapshot:
852
+ if not isinstance(fill_result, CatalogFillResult):
853
+ raise CatalogDeltaError("TYPE", "delta.fill_result", "must be a CatalogFillResult")
854
+ receipt = fill_result.receipt
855
+ eligible = fill_result.eligible_staging_sha256
856
+ if (
857
+ fill_result.status != "complete"
858
+ or fill_result.reason_code is not None
859
+ or receipt.status != "complete"
860
+ or receipt.reason_code is not None
861
+ or eligible is None
862
+ or eligible != fill_result.checkpoint_sha256
863
+ ):
864
+ raise CatalogDeltaError(
865
+ "CATALOG_DELTA_INCOMPLETE",
866
+ "delta.fill_result",
867
+ "only one complete reconciled fill generation may be classified",
868
+ )
869
+ if not isinstance(staging_root, Path):
870
+ raise CatalogDeltaError("TYPE", "delta.staging_root", "must be a Path")
871
+ staging = FillStaging(staging_root)
872
+ with staging.locked():
873
+ state = staging.load()
874
+ if state is None or staging.state_digest() != eligible:
875
+ raise CatalogDeltaError(
876
+ "CATALOG_DELTA_STAGING",
877
+ "delta.staging",
878
+ "the eligible fill generation is not the live staged generation",
879
+ )
880
+ if not state["completed"] or state["reason_code"] is not None or state["pass_drift"]:
881
+ raise CatalogDeltaError(
882
+ "CATALOG_DELTA_INCOMPLETE",
883
+ "delta.staging",
884
+ "staging is not complete and converged",
885
+ )
886
+ bindings = (
887
+ receipt.config_sha256 == state["config_sha256"]
888
+ and receipt.endpoint == state["endpoint"]
889
+ and receipt.harvester_coordinate == state["harvester_coordinate"]
890
+ and receipt.predecessor_sha256 == state["predecessor_sha256"]
891
+ and receipt.page_shard_digests == tuple(state["page_shards"])
892
+ and receipt.response_evidence_digests == tuple(state["response_shards"])
893
+ and receipt.pages == state["budget"]["pages"]
894
+ and receipt.requests == state["budget"]["requests"]
895
+ and receipt.responses == state["budget"]["responses"]
896
+ and receipt.records == state["budget"]["records"]
897
+ and receipt.network_bytes == state["budget"]["network_bytes"]
898
+ and receipt.elapsed_ns == state["budget"]["elapsed_ns"]
899
+ and receipt.skipped_records == state["budget"]["skipped_records"]
900
+ and receipt.provider_count == state["provider_count"]
901
+ and receipt.count_basis == state["count_basis"]
902
+ and receipt.convergence_passes == state["pass_number"]
903
+ )
904
+ if not bindings:
905
+ raise CatalogDeltaError(
906
+ "CATALOG_DELTA_RECEIPT",
907
+ "delta.fill_result.receipt",
908
+ "receipt does not bind the live complete staging generation",
909
+ )
910
+ records: dict[str, HarvestedRecord] = {}
911
+ flags: dict[tuple[str, str], CatalogFlaggedRecord] = {}
912
+ observed_at: str | None = None
913
+ for digest in state["page_shards"][state["pass_start_index"] :]:
914
+ page = staging.read_shard("page", digest)
915
+ page_evidence = _evidence_from_dict(page.get("evidence"), "delta.page.evidence")
916
+ if observed_at is None:
917
+ observed_at = page_evidence.observed_at
918
+ elif observed_at != page_evidence.observed_at:
919
+ raise CatalogDeltaError(
920
+ "CATALOG_DELTA_OBSERVATION",
921
+ "delta.page.evidence.observed_at",
922
+ "one fill generation must use one observation time",
923
+ )
924
+ raw_records = page.get("records")
925
+ if not isinstance(raw_records, list):
926
+ raise CatalogDeltaError(
927
+ "CATALOG_DELTA_STAGING", "delta.page.records", "must be an array"
928
+ )
929
+ page_map = page.get("id_facts")
930
+ if not isinstance(page_map, dict):
931
+ raise CatalogDeltaError(
932
+ "CATALOG_DELTA_STAGING", "delta.page.id_facts", "must be an object"
933
+ )
934
+ for raw in raw_records:
935
+ record = _record_from_dict(raw)
936
+ if page_map.get(record.record_id) != record.digest:
937
+ raise CatalogDeltaError(
938
+ "CATALOG_DELTA_STAGING",
939
+ f"delta.page.id_facts.{record.record_id}",
940
+ "does not match the staged record facts",
941
+ )
942
+ prior = records.get(record.record_id)
943
+ if prior is not None and prior.digest != record.digest:
944
+ raise CatalogDeltaError(
945
+ "CATALOG_DELTA_STAGING",
946
+ f"delta.records.{record.record_id}",
947
+ "complete generation contains conflicting record facts",
948
+ )
949
+ records[record.record_id] = record
950
+ raw_flags = page.get("flagged_rights")
951
+ if not isinstance(raw_flags, list):
952
+ raise CatalogDeltaError(
953
+ "CATALOG_DELTA_STAGING", "delta.page.flagged_rights", "must be an array"
954
+ )
955
+ for raw_flag in raw_flags:
956
+ flag = _flag_from_dict(raw_flag)
957
+ flags[(flag.protocol, flag.record_id)] = flag
958
+ if observed_at is None:
959
+ raise CatalogDeltaError(
960
+ "CATALOG_DELTA_STAGING", "delta.page_shards", "complete fill has no final page"
961
+ )
962
+ record_map = {record_id: records[record_id].digest for record_id in sorted(records)}
963
+ if canonical_sha256(record_map) != state["final_map_sha256"]:
964
+ raise CatalogDeltaError(
965
+ "CATALOG_DELTA_STAGING",
966
+ "delta.staging.final_map_sha256",
967
+ "final staged records do not match the converged map",
968
+ )
969
+ ordered_flags = tuple(flags[key] for key in sorted(flags))
970
+ # The receipt binds exactly what the fill staged. Decision facts join a flag only at
971
+ # classification, downstream of staging, so the queue digest is computed over the
972
+ # staged shape rather than over ``to_dict``, which now also carries them.
973
+ fill_queue = [
974
+ {
975
+ "protocol": item.protocol,
976
+ "record_id": item.record_id,
977
+ "record_sha256": item.record_sha256,
978
+ "license_id": item.license_id,
979
+ "license_uri": item.license_uri,
980
+ "rights_status": item.rights_status,
981
+ "authority": item.authority,
982
+ }
983
+ for item in ordered_flags
984
+ ]
985
+ if (
986
+ receipt.unique_records != len(records)
987
+ or receipt.flagged_rights != len(ordered_flags)
988
+ or receipt.flagged_queue_sha256 != canonical_sha256(fill_queue)
989
+ ):
990
+ raise CatalogDeltaError(
991
+ "CATALOG_DELTA_RECEIPT",
992
+ "delta.fill_result.receipt",
993
+ "receipt counts or rights queue do not match final staged facts",
994
+ )
995
+ return _FillSnapshot(
996
+ records=tuple(records[key] for key in sorted(records)),
997
+ flagged=ordered_flags,
998
+ observed_at=observed_at,
999
+ records_sha256=canonical_sha256(record_map),
1000
+ )
1001
+
1002
+
1003
+ def _validate_mapping(mapping: Mapping[str, Any], name: str) -> None:
1004
+ if not isinstance(mapping, Mapping) or any(
1005
+ not isinstance(key, str) or not key for key in mapping
1006
+ ):
1007
+ raise CatalogDeltaError("TYPE", f"delta.{name}", "must be a string-keyed mapping")
1008
+
1009
+
1010
+ def _validated_candidate(
1011
+ record_id: str,
1012
+ record: HarvestedRecord,
1013
+ candidate: CatalogEntry,
1014
+ *,
1015
+ decided_rights: RightsEvidence | None = None,
1016
+ ) -> CatalogEntry:
1017
+ if type(candidate) is not CatalogEntry:
1018
+ raise CatalogDeltaError(
1019
+ "TYPE", f"delta.candidates_by_record_id.{record_id}", "must be a CatalogEntry"
1020
+ )
1021
+ if candidate.entry_version != 1 or candidate.previous_digest is not None or candidate.vectors:
1022
+ raise CatalogDeltaError(
1023
+ "CATALOG_DELTA_CANDIDATE",
1024
+ f"delta.candidates_by_record_id.{record_id}",
1025
+ "must be an unsealed first-version spelling; version links and vectors are managed",
1026
+ )
1027
+ try:
1028
+ # Capture one strict immutable spelling before comparing or classifying it. Nested
1029
+ # contract subclasses may override ``to_dict``; retaining the caller's object after a
1030
+ # successful comparison would let a later publication observe different facts.
1031
+ captured = catalog_entry_from_dict(candidate.to_dict())
1032
+ authored_formats = captured.data_formats if record.protocol == "sdmx" else None
1033
+ expected = _candidate_from_record(
1034
+ record,
1035
+ entry_id=captured.entry_id,
1036
+ authored_formats=authored_formats,
1037
+ decided_rights=decided_rights,
1038
+ )
1039
+ except SourceContractError as error:
1040
+ raise CatalogDeltaError(error.code, error.path, error.detail) from error
1041
+ if captured.to_dict() != expected.to_dict():
1042
+ raise CatalogDeltaError(
1043
+ "CATALOG_DELTA_RECORD_BINDING",
1044
+ f"delta.candidates_by_record_id.{record_id}",
1045
+ "authored candidate differs from the complete deterministic provider-fact projection",
1046
+ )
1047
+ return captured
1048
+
1049
+
1050
+ def _candidate_from_record(
1051
+ record: HarvestedRecord,
1052
+ *,
1053
+ entry_id: str,
1054
+ authored_formats: tuple[str, ...] | None = None,
1055
+ decided_rights: RightsEvidence | None = None,
1056
+ ) -> CatalogEntry:
1057
+ """Derive every publishable v1 fact from the captured provider record.
1058
+
1059
+ The legacy v1 entry contract cannot represent unknown spatial and temporal facts directly.
1060
+ Its closed sentinel values state only what this metadata harvest established; authored input
1061
+ cannot replace them with source coverage, cadence, liveness, schema, or access claims.
1062
+
1063
+ ``decided_rights`` is the one authored substitution, and only for a record a human rights
1064
+ decision cleared out of the queue: the provider stated no licence, so the rights facts are
1065
+ the person's claim with the person's evidence, not a derivation from provider metadata.
1066
+ Every other fact stays the deterministic projection.
1067
+ """
1068
+
1069
+ data_formats = record.data_formats or authored_formats or ()
1070
+ if not data_formats:
1071
+ raise CatalogDeltaError(
1072
+ "CATALOG_DELTA_RECORD_FORMATS",
1073
+ f"delta.records.{record.record_id}.data_formats",
1074
+ "the provider record declares no supported payload format",
1075
+ )
1076
+ if decided_rights is not None:
1077
+ rights = decided_rights
1078
+ else:
1079
+ status, allowed_uses, obligation = _record_rights(record)
1080
+ obligations: tuple[LicenseObligation, ...] = ()
1081
+ if obligation == "attribution":
1082
+ obligations = (
1083
+ LicenseObligation(
1084
+ obligation_id="attribution",
1085
+ applies_at=("publication",),
1086
+ permitted_uses=allowed_uses,
1087
+ prohibited_uses=(),
1088
+ required_artifact_digests=(),
1089
+ escalation_when_ambiguous=True,
1090
+ ),
1091
+ )
1092
+ rights = RightsEvidence(
1093
+ status=status,
1094
+ lawful_basis=record.license_id or "No recognized license declared",
1095
+ allowed_uses=allowed_uses,
1096
+ evidence=(record.evidence,),
1097
+ obligations=obligations,
1098
+ )
1099
+ observed_time = record.declared_updated_at or record.evidence.observed_at
1100
+ return CatalogEntry(
1101
+ entry_id=entry_id,
1102
+ entry_version=1,
1103
+ publisher=record.publisher,
1104
+ title=record.title,
1105
+ description=record.description,
1106
+ spatial_scope=("unspecified",),
1107
+ data_formats=data_formats,
1108
+ access_kind="open_catalog",
1109
+ authentication_required=False,
1110
+ observation=SourceObservation(
1111
+ source_id=entry_id,
1112
+ observed_at=record.evidence.observed_at,
1113
+ event_time_field=None,
1114
+ available_at_field=None,
1115
+ historical_start=observed_time,
1116
+ historical_end=observed_time,
1117
+ live_status="not_applicable",
1118
+ publication_delay_seconds=0,
1119
+ update_frequency_seconds=0,
1120
+ response_status=None,
1121
+ evidence=(record.evidence,),
1122
+ ),
1123
+ rights=rights,
1124
+ evidence=(record.evidence,),
1125
+ )
1126
+
1127
+
1128
+ def _record_rights(record: HarvestedRecord) -> tuple[str, tuple[str, ...], str | None]:
1129
+ if record.protocol == "ckan":
1130
+ return ckan_licence_rights(record.license_id)
1131
+ if record.protocol == "stac":
1132
+ return stac_licence_rights(record.license_id)
1133
+ return ("unclear", (), None)
1134
+
1135
+
1136
+ def _successor_changes(candidate: CatalogEntry) -> dict[str, object]:
1137
+ return {
1138
+ "publisher": candidate.publisher,
1139
+ "title": candidate.title,
1140
+ "description": candidate.description,
1141
+ "spatial_scope": candidate.spatial_scope,
1142
+ "data_formats": candidate.data_formats,
1143
+ "access_kind": candidate.access_kind,
1144
+ "authentication_required": candidate.authentication_required,
1145
+ "observation": candidate.observation,
1146
+ "rights": candidate.rights,
1147
+ "quirks": candidate.quirks,
1148
+ "declared_columns": candidate.declared_columns,
1149
+ "declared_vocabulary": candidate.declared_vocabulary,
1150
+ "declared_row_count": candidate.declared_row_count,
1151
+ "evidence": candidate.evidence,
1152
+ }
1153
+
1154
+
1155
+ def _classifiable_facts_digest(entry: CatalogEntry) -> str:
1156
+ payload = entry.to_dict()
1157
+ for managed in ("entry_version", "previous_digest", "vectors"):
1158
+ del payload[managed]
1159
+ return canonical_sha256(payload)
1160
+
1161
+
1162
+ def _classification_checkpoint_payload(
1163
+ classification: CatalogDeltaClassification,
1164
+ *,
1165
+ lexical_coordinate: str,
1166
+ neural_coordinate: str | None,
1167
+ ) -> dict[str, Any]:
1168
+ return {
1169
+ "schema_version": "harness-source-catalog-delta-checkpoint.v1",
1170
+ "classification_sha256": classification.digest,
1171
+ "staging_sha256": classification.staging_sha256,
1172
+ "predecessor_sha256": classification.predecessor_sha256,
1173
+ "lexical_coordinate": lexical_coordinate,
1174
+ "neural_coordinate": neural_coordinate,
1175
+ "classification": {
1176
+ "predecessor_sha256": classification.predecessor_sha256,
1177
+ "rights_decisions_sha256": classification.rights_decisions_sha256,
1178
+ "fill_receipt_sha256": classification.fill_receipt_sha256,
1179
+ "staging_sha256": classification.staging_sha256,
1180
+ "records_sha256": classification.records_sha256,
1181
+ "unchanged": [entry.to_dict() for entry in classification.unchanged],
1182
+ "changed": [entry.to_dict() for entry in classification.changed],
1183
+ "new": [entry.to_dict() for entry in classification.new],
1184
+ "absent": [item.to_dict() for item in classification.absent],
1185
+ "flagged": [item.to_dict() for item in classification.flagged],
1186
+ "version_chains": [
1187
+ [entry.to_dict() for entry in chain] for chain in classification.version_chains
1188
+ ],
1189
+ },
1190
+ }
1191
+
1192
+
1193
+ def _classification_from_checkpoint(value: Any) -> CatalogDeltaClassification:
1194
+ exact = {
1195
+ "predecessor_sha256",
1196
+ "rights_decisions_sha256",
1197
+ "fill_receipt_sha256",
1198
+ "staging_sha256",
1199
+ "records_sha256",
1200
+ "unchanged",
1201
+ "changed",
1202
+ "new",
1203
+ "absent",
1204
+ "flagged",
1205
+ "version_chains",
1206
+ }
1207
+ if not isinstance(value, dict) or set(value) != exact:
1208
+ raise CatalogDeltaError(
1209
+ "CATALOG_DELTA_CHECKPOINT",
1210
+ "delta.classification",
1211
+ "serialized classification fields differ",
1212
+ )
1213
+ try:
1214
+ unchanged = tuple(catalog_entry_from_dict(item) for item in value["unchanged"])
1215
+ changed = tuple(catalog_entry_from_dict(item) for item in value["changed"])
1216
+ new = tuple(catalog_entry_from_dict(item) for item in value["new"])
1217
+ absent = tuple(CatalogAbsenceObservation(**item) for item in value["absent"])
1218
+ flagged = tuple(CatalogFlaggedRecord(**item) for item in value["flagged"])
1219
+ chains = tuple(
1220
+ tuple(catalog_entry_from_dict(item) for item in chain)
1221
+ for chain in value["version_chains"]
1222
+ )
1223
+ return CatalogDeltaClassification(
1224
+ predecessor_sha256=value["predecessor_sha256"],
1225
+ rights_decisions_sha256=value["rights_decisions_sha256"],
1226
+ fill_receipt_sha256=value["fill_receipt_sha256"],
1227
+ staging_sha256=value["staging_sha256"],
1228
+ records_sha256=value["records_sha256"],
1229
+ unchanged=unchanged,
1230
+ changed=changed,
1231
+ new=new,
1232
+ absent=absent,
1233
+ flagged=flagged,
1234
+ version_chains=chains,
1235
+ )
1236
+ except (KeyError, TypeError, ValueError, SourceContractError) as error:
1237
+ raise CatalogDeltaError(
1238
+ "CATALOG_DELTA_CHECKPOINT",
1239
+ "delta.classification",
1240
+ "serialized classification is invalid",
1241
+ ) from error
1242
+
1243
+
1244
+ def _entry_receipt(entry: CatalogEntry) -> dict[str, Any]:
1245
+ return {
1246
+ "coordinate": entry.coordinate,
1247
+ "facts_sha256": entry.facts_digest,
1248
+ "entry_sha256": entry.digest,
1249
+ "vector_sha256s": [reference.vector_sha256 for reference in entry.vectors],
1250
+ }
1251
+
1252
+
1253
+ def _entry_key(entry: CatalogEntry) -> tuple[str, int]:
1254
+ return (entry.entry_id, entry.entry_version)
1255
+
1256
+
1257
+ def _record_from_dict(value: Any) -> HarvestedRecord:
1258
+ if not isinstance(value, dict):
1259
+ raise CatalogDeltaError("CATALOG_DELTA_STAGING", "delta.page.records[]", "must be object")
1260
+ expected = {
1261
+ "schema_version",
1262
+ "protocol",
1263
+ "record_id",
1264
+ "title",
1265
+ "description",
1266
+ "publisher",
1267
+ "landing_uri",
1268
+ "data_formats",
1269
+ "evidence",
1270
+ "license_id",
1271
+ "license_uri",
1272
+ "declared_updated_at",
1273
+ }
1274
+ if set(value) != expected or not isinstance(value.get("data_formats"), list):
1275
+ raise CatalogDeltaError(
1276
+ "CATALOG_DELTA_STAGING", "delta.page.records[]", "record fields differ"
1277
+ )
1278
+ return HarvestedRecord(
1279
+ schema_version=value["schema_version"],
1280
+ protocol=value["protocol"],
1281
+ record_id=value["record_id"],
1282
+ title=value["title"],
1283
+ description=value["description"],
1284
+ publisher=value["publisher"],
1285
+ landing_uri=value["landing_uri"],
1286
+ data_formats=tuple(value["data_formats"]),
1287
+ evidence=_evidence_from_dict(value["evidence"], "delta.page.records[].evidence"),
1288
+ license_id=value["license_id"],
1289
+ license_uri=value["license_uri"],
1290
+ declared_updated_at=value["declared_updated_at"],
1291
+ )
1292
+
1293
+
1294
+ def _evidence_from_dict(value: Any, path: str) -> EvidenceReference:
1295
+ if not isinstance(value, dict) or set(value) != {
1296
+ "uri",
1297
+ "observed_at",
1298
+ "content_sha256",
1299
+ "media_type",
1300
+ }:
1301
+ raise CatalogDeltaError("CATALOG_DELTA_STAGING", path, "evidence fields differ")
1302
+ return EvidenceReference(
1303
+ uri=value["uri"],
1304
+ observed_at=value["observed_at"],
1305
+ content_sha256=value["content_sha256"],
1306
+ media_type=value["media_type"],
1307
+ )
1308
+
1309
+
1310
+ def _flag_from_dict(value: Any) -> CatalogFlaggedRecord:
1311
+ expected = {
1312
+ "protocol",
1313
+ "record_id",
1314
+ "record_sha256",
1315
+ "license_id",
1316
+ "license_uri",
1317
+ "rights_status",
1318
+ "authority",
1319
+ }
1320
+ if not isinstance(value, dict) or set(value) != expected:
1321
+ raise CatalogDeltaError(
1322
+ "CATALOG_DELTA_STAGING", "delta.page.flagged_rights[]", "flag fields differ"
1323
+ )
1324
+ if value["authority"] != "human_review_required" or value["rights_status"] not in {
1325
+ "unclear",
1326
+ "prohibited",
1327
+ }:
1328
+ raise CatalogDeltaError(
1329
+ "CATALOG_DELTA_RIGHTS",
1330
+ "delta.page.flagged_rights[]",
1331
+ "flag must require human review for unclear or prohibited rights",
1332
+ )
1333
+ return CatalogFlaggedRecord(
1334
+ protocol=value["protocol"],
1335
+ record_id=value["record_id"],
1336
+ record_sha256=value["record_sha256"],
1337
+ license_id=value["license_id"],
1338
+ license_uri=value["license_uri"],
1339
+ rights_status=value["rights_status"],
1340
+ )