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,560 @@
1
+ """Query time: load a sealed catalog, verify every byte of it, and compute nothing.
2
+
3
+ That sentence is the query-time trust boundary on this side of the line. ``load`` reads a canonical
4
+ index, rebuilds each entry strictly, checks every vector file against the digest carried in the
5
+ entry it belongs to, and resolves an encoder for later query use. It never calls a backend's
6
+ ``encode``, opens a socket, or loads a model. If a catalog was sealed by a backend that is not
7
+ safe to run in-process, the load fails rather than degrading into one that would.
8
+
9
+ Failures are split three ways, in the taxonomy the sealed-artifact replay path already uses,
10
+ because a missing artifact, a drifted artifact and a policy refusal are different operational
11
+ events and reading them as one loses the distinction that matters when something breaks:
12
+
13
+ * ``CatalogMissError`` -- an artifact the index references is not there.
14
+ * ``CatalogTamperError`` -- an artifact is there but is not what it should be. Three different
15
+ things count: it disagrees with the index that references it, the index itself disagrees with
16
+ the backend that will read it, or a sealed vector disagrees with the facts of the entry that
17
+ references it. The last two are the ones a self-consistent forgery survives, because every check
18
+ that reads only the artifact agrees with the forgery by construction.
19
+ * ``CatalogRuntimeModelError`` -- the artifacts are fine, but running this catalog at query time
20
+ would mean running a model at query time.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import re
26
+ import struct
27
+ from collections.abc import Mapping
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+
31
+ from mostlyright.data_harness.canonical import parse_canonical_json, sha256_bytes
32
+ from mostlyright.data_harness.sources.catalog.contracts import (
33
+ CATALOG_INDEX_CONTRACT_VERSION,
34
+ EMBEDDING_LAYERS,
35
+ CatalogEntry,
36
+ catalog_entry_from_dict,
37
+ )
38
+ from mostlyright.data_harness.sources.catalog.embedding import (
39
+ DEFAULT_BACKENDS,
40
+ BackendDescriptor,
41
+ BackendRegistry,
42
+ EmbeddingBackend,
43
+ )
44
+ from mostlyright.data_harness.sources.contracts import SourceContractError
45
+
46
+ CATALOG_FILENAME = "catalog.json"
47
+ VECTORS_DIRNAME = "vectors"
48
+ VECTOR_SUFFIX = ".vec"
49
+ MAX_SEALED_ENTRIES = 10_000
50
+
51
+ _SHA256 = re.compile(r"^[0-9a-f]{64}$")
52
+
53
+
54
+ class CatalogMissError(SourceContractError):
55
+ """A sealed artifact the index references is absent."""
56
+
57
+
58
+ class CatalogTamperError(SourceContractError):
59
+ """A sealed artifact is present but does not match what the index says it is."""
60
+
61
+
62
+ class CatalogRuntimeModelError(SourceContractError):
63
+ """The catalog was sealed by a backend that must not run on the query path."""
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class SealedEmbeddingIndex:
68
+ """A verified catalog: its entries, its integer vectors, and its query-side encoder.
69
+
70
+ The query-safe refusal lives on this type and not only in :meth:`load`. ``retrieval`` calls
71
+ ``index.backend.encode(question)`` on every question, so this is the object that decides whether
72
+ a model runs on the runtime path. Putting the only check in ``load`` would make this boundary a
73
+ statement about how callers happen to build one -- an unenforced convention -- and
74
+ ``SealedEmbeddingIndex(entries=..., vectors=..., backend=...)``
75
+ is an ordinary public constructor that any caller, or any later convenience path inside this
76
+ package, can reach without going near ``load``. A check here holds for every construction route
77
+ there is, which is what lets ``retrieval`` state its precondition as a property of its argument
78
+ rather than as a provenance claim.
79
+
80
+ The same argument reaches the entry/vector binding. ``load`` checks it
81
+ two ways -- ``entry_facts_digest`` against the entry's own facts, and each vector's bytes
82
+ against the digest the entry references. This constructor checked neither, so an index built
83
+ directly out of one entry's entries and another's vectors was accepted in silence and then
84
+ ranked the wrong source with no error at any point. Both checks now run here, over the same
85
+ references, which is what makes ``entry_facts_digest`` a binding rather than a field.
86
+
87
+ It also reaches the whole backend identity. ``load`` binds all four of ``backend_id``,
88
+ ``backend_version``, ``dimensions``
89
+ and ``quantization`` to the backend it resolved; this constructor bound none of them, so an
90
+ index pairing one query-safe backend's sealed entries with a *different* query-safe backend of
91
+ the same dimensions and the same quantization constructed in silence and answered the gate-three
92
+ question with the wrong source. Both routes now run one shared checker,
93
+ :func:`_require_sealed_vectors_come_from_this_backend`, so neither can enumerate a different
94
+ rule than the other.
95
+
96
+ The checks cost a descriptor read and one digest per sealed vector. They compute no embedding,
97
+ so they are legal where they stand.
98
+ """
99
+
100
+ entries: tuple[CatalogEntry, ...]
101
+ vectors: Mapping[tuple[str, str], tuple[int, ...]]
102
+ backend: EmbeddingBackend
103
+
104
+ def __post_init__(self) -> None:
105
+ if not isinstance(self.entries, tuple) or not self.entries:
106
+ raise SourceContractError(
107
+ "TYPE",
108
+ "index.entries",
109
+ "must be a non-empty immutable tuple of catalog entries",
110
+ )
111
+ for position, entry in enumerate(self.entries):
112
+ if not isinstance(entry, CatalogEntry):
113
+ raise SourceContractError(
114
+ "TYPE",
115
+ f"index.entries[{position}]",
116
+ "must be a CatalogEntry",
117
+ )
118
+ # One current version per source, refused here and not only in `load`. Two entries sharing
119
+ # an id make the vector key `(entry_id, layer)` ambiguous, and `recommend_sources` builds
120
+ # `{entry.entry_id: entry}` -- so the second one wins, the first is dropped from the
121
+ # answer, and nothing raises. `load` refuses this on the artifact and keeps its own
122
+ # tamper-taxonomy code for that; this is the same refusal for every other construction
123
+ # route, by the argument that put the query-safe refusal here.
124
+ identifiers = [entry.entry_id for entry in self.entries]
125
+ if len(set(identifiers)) != len(identifiers):
126
+ raise SourceContractError(
127
+ "DUPLICATE",
128
+ "index.entries",
129
+ "two entries name the same source; an index holds one version of each",
130
+ )
131
+ if not isinstance(self.vectors, Mapping):
132
+ raise SourceContractError(
133
+ "TYPE",
134
+ "index.vectors",
135
+ "must be a mapping of (entry_id, layer) to a sealed integer vector",
136
+ )
137
+ descriptor = self.backend.descriptor
138
+ if not isinstance(descriptor, BackendDescriptor):
139
+ raise SourceContractError(
140
+ "EMBEDDING_BACKEND_DESCRIPTOR",
141
+ "index.backend.descriptor",
142
+ "backend descriptor must use the strict contract",
143
+ )
144
+ if not descriptor.query_safe:
145
+ raise CatalogRuntimeModelError(
146
+ "CATALOG_RUNTIME_MODEL",
147
+ "index.backend",
148
+ f"{descriptor.coordinate} is not query-safe; an index carrying it would run a "
149
+ "model on the runtime path the first time it was asked a question",
150
+ )
151
+ _require_vectors_the_query_path_can_read(
152
+ self.vectors, known_entry_ids=frozenset(identifiers), dimensions=descriptor.dimensions
153
+ )
154
+ # Runs only once the map is known to be well-formed and complete: it indexes the map by
155
+ # every reference an entry carries, and it packs each vector back into the sealed
156
+ # encoding, both of which need the shape checks above to have passed.
157
+ _require_vectors_bound_to_the_entries_that_reference_them(self.entries, self.vectors)
158
+ # Runs last, and it is the same function `load` runs against the backend it resolved. It
159
+ # is what stops a cross-backend index: two backends of identical dimensions and identical
160
+ # quantization produce vectors that multiply against each other without complaint and rank
161
+ # a different source first, so nothing above can see the substitution.
162
+ _require_sealed_vectors_come_from_this_backend(
163
+ self.entries, descriptor, path="index.entries"
164
+ )
165
+
166
+ @classmethod
167
+ def load(
168
+ cls,
169
+ directory: Path,
170
+ *,
171
+ registry: BackendRegistry = DEFAULT_BACKENDS,
172
+ ) -> SealedEmbeddingIndex:
173
+ """Read and verify a sealed catalog directory. Computes no embedding."""
174
+
175
+ if not isinstance(directory, Path):
176
+ raise SourceContractError("TYPE", "directory", "must be a Path")
177
+ index_path = directory / CATALOG_FILENAME
178
+ try:
179
+ raw = index_path.read_bytes()
180
+ except FileNotFoundError:
181
+ raise CatalogMissError(
182
+ "CATALOG_INDEX_MISSING",
183
+ str(index_path),
184
+ "the sealed catalog index is absent",
185
+ ) from None
186
+ # Canonical, not merely parseable: a re-serialised or hand-edited index is refused here,
187
+ # before a single field of it is trusted.
188
+ payload = parse_canonical_json(raw)
189
+ if not isinstance(payload, dict):
190
+ raise CatalogTamperError(
191
+ "CATALOG_INDEX_SHAPE",
192
+ CATALOG_FILENAME,
193
+ "the sealed catalog index must be an object",
194
+ )
195
+ if set(payload) != {"schema_version", "entries"}:
196
+ raise CatalogTamperError(
197
+ "CATALOG_INDEX_SHAPE",
198
+ CATALOG_FILENAME,
199
+ "must contain exactly ['entries', 'schema_version']",
200
+ )
201
+ if payload["schema_version"] != CATALOG_INDEX_CONTRACT_VERSION:
202
+ raise CatalogTamperError(
203
+ "VERSION",
204
+ "catalog.schema_version",
205
+ f"must equal {CATALOG_INDEX_CONTRACT_VERSION!r}",
206
+ )
207
+ raw_entries = payload["entries"]
208
+ if not isinstance(raw_entries, list) or not raw_entries:
209
+ raise CatalogTamperError(
210
+ "CATALOG_INDEX_SHAPE",
211
+ "catalog.entries",
212
+ "must be a non-empty array",
213
+ )
214
+ if len(raw_entries) > MAX_SEALED_ENTRIES:
215
+ raise CatalogTamperError(
216
+ "LIMIT",
217
+ "catalog.entries",
218
+ f"must contain at most {MAX_SEALED_ENTRIES} entries",
219
+ )
220
+
221
+ entries = tuple(catalog_entry_from_dict(item) for item in raw_entries)
222
+ # A sealed catalog holds one current version per source. Allowing two versions of one
223
+ # entry id would make the vector key ambiguous and silently drop half of them.
224
+ identifiers = [entry.entry_id for entry in entries]
225
+ if len(set(identifiers)) != len(identifiers):
226
+ raise CatalogTamperError(
227
+ "DUPLICATE",
228
+ "catalog.entries",
229
+ "two entries name the same source",
230
+ )
231
+
232
+ vectors: dict[tuple[str, str], tuple[int, ...]] = {}
233
+ for entry in entries:
234
+ layers = tuple(reference.layer for reference in entry.vectors)
235
+ if sorted(layers) != sorted(EMBEDDING_LAYERS):
236
+ raise CatalogMissError(
237
+ "CATALOG_VECTOR_LAYERS",
238
+ f"{entry.coordinate}.vectors",
239
+ f"a sealed entry must carry one vector per layer {list(EMBEDDING_LAYERS)}",
240
+ )
241
+ facts_digest = entry.facts_digest
242
+ for reference in entry.vectors:
243
+ # Bind the vector to the facts it was computed from. Every other check in this
244
+ # loop reads only the artifact, so a catalog assembled from entries carrying some
245
+ # other entry's vectors satisfies all of them: four layers present, one backend
246
+ # identity, files present and digest-matching. It would then rank each entry on the
247
+ # other one's text with no error at any point -- the only silent failure this
248
+ # module has. `facts_digest` excludes `vectors`, so this is a digest comparison and
249
+ # not a re-encode; nothing on this path computes an embedding.
250
+ if reference.entry_facts_digest != facts_digest:
251
+ raise CatalogTamperError(
252
+ "CATALOG_VECTOR_ENTRY_BINDING",
253
+ f"{entry.coordinate}.{reference.layer}",
254
+ "the sealed vector was computed from different entry facts than the entry "
255
+ "that references it",
256
+ )
257
+ vectors[(entry.entry_id, reference.layer)] = _read_vector(
258
+ directory,
259
+ digest=reference.vector_sha256,
260
+ dimensions=reference.dimensions,
261
+ path=f"{entry.coordinate}.{reference.layer}",
262
+ )
263
+
264
+ # The sealed identity is read once, by the shared checker, so this route and the public
265
+ # constructor cannot enumerate different rules again. Here it names the coordinate the
266
+ # catalog must be resolved against; below, the same function checks it against what was
267
+ # resolved.
268
+ backend_id, backend_version = _require_one_sealed_backend_identity(
269
+ entries, path="catalog.entries"
270
+ )[:2]
271
+ backend = registry.resolve(backend_id, backend_version)
272
+ # Bind the sealed vector identity to the backend that will read it. The checks above prove
273
+ # each vector file is internally consistent with the entry that references it, which a
274
+ # forged index satisfies trivially by declaring its own shorter dimension count -- the
275
+ # length check and the digest check both agree with the forgery. Only the resolved
276
+ # descriptor is outside the artifact, so it is the only thing that can catch one.
277
+ #
278
+ # Left unbound, a mismatch escapes the load path entirely and surfaces at query time: a
279
+ # dimension mismatch as a bare ValueError out of the strict `zip` in `retrieval`, which is
280
+ # outside this module's three-way error taxonomy, and a quantization mismatch not as an
281
+ # error at all -- two vectors on different scales still multiply, so retrieval would just
282
+ # rank wrongly and silently. That is the worse of the two, which is why quantization is
283
+ # bound here as well as the dimension count.
284
+ #
285
+ # This runs before the query-safe refusal because it is a statement about the artifact
286
+ # rather than about policy, and it groups with the integrity checks above. It is the same
287
+ # function `__post_init__` runs, so `load` grants nothing the constructor does not.
288
+ _require_sealed_vectors_come_from_this_backend(
289
+ entries, backend.descriptor, path="catalog.entries"
290
+ )
291
+ if not backend.descriptor.query_safe:
292
+ raise CatalogRuntimeModelError(
293
+ "CATALOG_RUNTIME_MODEL",
294
+ "catalog.backend",
295
+ f"{backend_id}@{backend_version} is not query-safe; a catalog sealed by it "
296
+ "cannot be queried in-process, because answering a query would mean running a "
297
+ "model on the runtime path",
298
+ )
299
+ return cls(entries=entries, vectors=vectors, backend=backend)
300
+
301
+
302
+ def _require_one_sealed_backend_identity(
303
+ entries: tuple[CatalogEntry, ...],
304
+ *,
305
+ path: str,
306
+ ) -> tuple[str, str, int, str]:
307
+ """The single ``(backend_id, backend_version, dimensions, quantization)`` the entries declare.
308
+
309
+ An index whose entries disagree about which backend sealed them has no single encoder that
310
+ could read all of it, so there is nothing to bind and the mixed set is refused here rather
311
+ than resolved arbitrarily.
312
+ """
313
+
314
+ identities = {
315
+ (
316
+ reference.backend_id,
317
+ reference.backend_version,
318
+ reference.dimensions,
319
+ reference.quantization,
320
+ )
321
+ for entry in entries
322
+ for reference in entry.vectors
323
+ }
324
+ if len(identities) != 1:
325
+ raise CatalogTamperError(
326
+ "CATALOG_MIXED_BACKEND",
327
+ path,
328
+ "every sealed vector must come from one backend, version, dimension count and "
329
+ f"quantization; found {len(identities)}",
330
+ )
331
+ return identities.pop()
332
+
333
+
334
+ def _require_sealed_vectors_come_from_this_backend(
335
+ entries: tuple[CatalogEntry, ...],
336
+ descriptor: BackendDescriptor,
337
+ *,
338
+ path: str,
339
+ ) -> None:
340
+ """Refuse an index whose sealed vectors were not produced by the backend that will read them.
341
+
342
+ Four fields make up a sealed vector's identity. ``load`` resolves a backend *by* the declared
343
+ coordinate, so on
344
+ that route ``backend_id`` and ``backend_version`` agree by construction and only the dimension
345
+ count and the quantization needed checking. The public constructor takes the backend as an
346
+ argument, so on that route nothing agreed by construction and nothing was checked: an index
347
+ built from entries sealed by one query-safe backend, paired with a different query-safe backend
348
+ of identical dimensions and identical quantization, constructed in silence and then answered
349
+ the gate-three question with the wrong source. Every other check passed, because every other
350
+ check reads the artifact against itself -- the digests match, the lengths match, the layers are
351
+ all present. Only the descriptor of the encoder that will be asked the question is outside the
352
+ artifact.
353
+
354
+ The failure is the silent kind, which is why it is bound at all four fields rather than the two
355
+ ``load`` happened to need. Vectors from two different embedding schemes still multiply; a
356
+ mismatch is not an error at query time, it is a confidently wrong answer -- the same argument
357
+ that put quantization into the binding in the first place.
358
+
359
+ Nothing here computes an embedding. It is a comparison of five strings and one integer.
360
+ """
361
+
362
+ backend_id, backend_version, dimensions, quantization = _require_one_sealed_backend_identity(
363
+ entries, path=path
364
+ )
365
+ if (backend_id, backend_version) != (descriptor.backend_id, descriptor.backend_version):
366
+ raise CatalogTamperError(
367
+ "CATALOG_BACKEND_IDENTITY",
368
+ path,
369
+ f"sealed vectors were produced by {backend_id}@{backend_version} but the backend "
370
+ f"that would read them is {descriptor.coordinate}",
371
+ )
372
+ if descriptor.dimensions != dimensions:
373
+ raise CatalogTamperError(
374
+ "CATALOG_VECTOR_DIMENSIONS",
375
+ path,
376
+ f"sealed vectors declare {dimensions} dimensions but backend "
377
+ f"{backend_id}@{backend_version} produces {descriptor.dimensions}",
378
+ )
379
+ if descriptor.quantization != quantization:
380
+ raise CatalogTamperError(
381
+ "CATALOG_VECTOR_QUANTIZATION",
382
+ path,
383
+ f"sealed vectors declare quantization {quantization!r} but backend "
384
+ f"{backend_id}@{backend_version} produces {descriptor.quantization!r}",
385
+ )
386
+
387
+
388
+ def _require_vectors_bound_to_the_entries_that_reference_them(
389
+ entries: tuple[CatalogEntry, ...],
390
+ vectors: Mapping[tuple[str, str], tuple[int, ...]],
391
+ ) -> None:
392
+ """Refuse an index whose vectors are not the vectors its entries reference.
393
+
394
+ This is the module's one silent failure, stated on the type rather than only on the load path.
395
+ Every other check reads the map alone -- keys well-shaped, four layers present, dimensions and
396
+ integrality right, nothing stray -- and an index assembled from one entry's entries and another
397
+ entry's vectors satisfies all of them. It then ranks each entry on the other one's text and
398
+ raises nothing, anywhere. Reproduced on the shipped eval catalog: swapping the four vectors of
399
+ ``faostat_qcl`` with those of ``sentinel_2_l2a`` constructed cleanly and answered the banana
400
+ question with ``sentinel_2_l2a``.
401
+
402
+ Two things are checked, because either one alone leaves the swap open. ``entry_facts_digest``
403
+ ties the reference to the facts the vector was computed from, and re-packing the map's vector
404
+ into its sealed 32-bit encoding ties the *bytes in this map* to the digest the entry carries --
405
+ without the second, ``entry_facts_digest`` binds a reference to an entry and says nothing about
406
+ which numbers ended up under that key.
407
+
408
+ Reference completeness is required here rather than only in ``load`` for the same reason the
409
+ map's completeness is: an entry carrying no references at all would be bound by nothing, and
410
+ ``_require_vectors_the_query_path_can_read`` would still hand ``_best_layer_score`` four
411
+ vectors to rank it on.
412
+
413
+ Nothing here computes an embedding. It is a digest comparison and a struct pack.
414
+ """
415
+
416
+ for entry in entries:
417
+ layers = tuple(reference.layer for reference in entry.vectors)
418
+ if sorted(layers) != sorted(EMBEDDING_LAYERS):
419
+ raise CatalogMissError(
420
+ "CATALOG_VECTOR_LAYERS",
421
+ f"{entry.coordinate}.vectors",
422
+ f"a sealed entry must reference one vector per layer {list(EMBEDDING_LAYERS)}",
423
+ )
424
+ facts_digest = entry.facts_digest
425
+ for reference in entry.vectors:
426
+ path = f"{entry.coordinate}.{reference.layer}"
427
+ if reference.entry_facts_digest != facts_digest:
428
+ raise CatalogTamperError(
429
+ "CATALOG_VECTOR_ENTRY_BINDING",
430
+ path,
431
+ "the sealed vector was computed from different entry facts than the entry "
432
+ "that references it",
433
+ )
434
+ vector = vectors[(entry.entry_id, reference.layer)]
435
+ try:
436
+ sealed = struct.pack(f">{len(vector)}i", *vector)
437
+ except struct.error:
438
+ raise CatalogTamperError(
439
+ "CATALOG_VECTOR_ENCODING",
440
+ path,
441
+ "a sealed vector holds a value outside the signed 32-bit encoding sealed "
442
+ "vectors are written in, so it cannot be the vector this entry references",
443
+ ) from None
444
+ if sha256_bytes(sealed) != reference.vector_sha256:
445
+ raise CatalogTamperError(
446
+ "CATALOG_VECTOR_DIGEST",
447
+ path,
448
+ "the vector under this key is not the sealed vector this entry references",
449
+ )
450
+
451
+
452
+ def _require_vectors_the_query_path_can_read(
453
+ vectors: Mapping[tuple[str, str], tuple[int, ...]],
454
+ *,
455
+ known_entry_ids: frozenset[str],
456
+ dimensions: int,
457
+ ) -> None:
458
+ """Refuse a vector map ``retrieval`` could not multiply against a question.
459
+
460
+ ``_best_layer_score`` does ``zip(asked, sealed, strict=True)`` and multiplies. Two things break
461
+ it and neither is caught by anything else once a caller builds an index directly. A wrong
462
+ dimension count raises a bare ``ValueError`` at query time, outside this module's three-way
463
+ taxonomy and far from the artifact that caused it. A non-integer element is worse: it does not
464
+ raise at all, it just ranks on a different scale -- the same silent-wrong failure the
465
+ quantization binding in :meth:`load` exists to stop.
466
+
467
+ ``load`` cannot produce either, because ``struct.unpack`` returns integers of the length it was
468
+ given and the descriptor binding checks that length. The constructor is public, so the property
469
+ belongs on the type as well.
470
+
471
+ A key naming an entry the index does not carry is refused too. It is dead weight rather than a
472
+ hazard -- ``_best_layer_score`` looks up by entry id and would never read it -- but a vector for
473
+ an entry that is not here means the map and the entries came from different places, and that is
474
+ worth failing on rather than ignoring.
475
+
476
+ **Completeness is checked as well as shape**, because checking only what is present validates
477
+ the wrong half. ``_best_layer_score`` skips a layer it cannot find (``if sealed is None:
478
+ continue``), so an index missing three of an entry's four layers answers every question without
479
+ error and ranks on a quarter of the signal. Both ``load`` and the public constructor therefore
480
+ require each entry to carry one reference per layer.
481
+ The map must be exactly the cross product of the entries and the four layers.
482
+ """
483
+
484
+ known_layers = frozenset(EMBEDDING_LAYERS)
485
+ required = {(entry_id, layer) for entry_id in known_entry_ids for layer in EMBEDDING_LAYERS}
486
+ for key, vector in vectors.items():
487
+ if not isinstance(key, tuple) or len(key) != 2 or not all(isinstance(p, str) for p in key):
488
+ raise SourceContractError(
489
+ "TYPE",
490
+ "index.vectors",
491
+ f"every key must be an (entry_id, layer) pair of strings; found {key!r}",
492
+ )
493
+ entry_id, layer = key
494
+ path = f"index.vectors[{entry_id!r}, {layer!r}]"
495
+ if layer not in known_layers:
496
+ raise SourceContractError(
497
+ "CATALOG_VECTOR_LAYERS",
498
+ path,
499
+ f"layer must be one of {list(EMBEDDING_LAYERS)}",
500
+ )
501
+ if entry_id not in known_entry_ids:
502
+ raise SourceContractError(
503
+ "CATALOG_VECTOR_ENTRY_BINDING",
504
+ path,
505
+ "a sealed vector names an entry this index does not carry",
506
+ )
507
+ if not isinstance(vector, tuple) or any(type(value) is not int for value in vector):
508
+ raise SourceContractError(
509
+ "TYPE",
510
+ path,
511
+ "a sealed vector must be an immutable tuple of integers",
512
+ )
513
+ if len(vector) != dimensions:
514
+ raise CatalogTamperError(
515
+ "CATALOG_VECTOR_DIMENSIONS",
516
+ path,
517
+ f"sealed vector has {len(vector)} dimensions but the backend reading it produces "
518
+ f"{dimensions}",
519
+ )
520
+ missing = sorted(required - set(vectors))
521
+ if missing:
522
+ raise CatalogMissError(
523
+ "CATALOG_VECTOR_LAYERS",
524
+ "index.vectors",
525
+ f"every entry must carry one vector per layer {list(EMBEDDING_LAYERS)}; "
526
+ f"{len(missing)} are absent, the first being {missing[0]}",
527
+ )
528
+
529
+
530
+ def _read_vector(directory: Path, *, digest: str, dimensions: int, path: str) -> tuple[int, ...]:
531
+ # Validate the digest before it is used to build a path. The filename is derived from a
532
+ # checked 64-character hex string and never from anything an artifact supplies verbatim.
533
+ if not isinstance(digest, str) or not _SHA256.fullmatch(digest):
534
+ raise CatalogTamperError(
535
+ "SHA256",
536
+ path,
537
+ "vector digest must be a lowercase 64-character SHA-256",
538
+ )
539
+ vector_path = directory / VECTORS_DIRNAME / f"{digest}{VECTOR_SUFFIX}"
540
+ try:
541
+ raw = vector_path.read_bytes()
542
+ except FileNotFoundError:
543
+ raise CatalogMissError(
544
+ "CATALOG_VECTOR_MISSING",
545
+ path,
546
+ f"sealed vector {digest} is absent",
547
+ ) from None
548
+ if len(raw) != dimensions * 4:
549
+ raise CatalogTamperError(
550
+ "CATALOG_VECTOR_LENGTH",
551
+ path,
552
+ f"sealed vector must be {dimensions * 4} bytes, found {len(raw)}",
553
+ )
554
+ if sha256_bytes(raw) != digest:
555
+ raise CatalogTamperError(
556
+ "CATALOG_VECTOR_DIGEST",
557
+ path,
558
+ "sealed vector bytes do not match the digest the entry carries",
559
+ )
560
+ return struct.unpack(f">{dimensions}i", raw)