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,1182 @@
1
+ """Honest version-two public catalog entries.
2
+
3
+ The frozen :mod:`contracts` module remains the complete v1 contract. This module adds a
4
+ side-by-side successor in which absence is data: every nullable semantic fact says exactly
5
+ ``known``, ``unknown``, or ``not_applicable``. A known fact is inseparable from the exact provider
6
+ record and canonical provider JSON path that stated it.
7
+
8
+ Semantic provenance and sweep provenance deliberately have different digests. The semantic
9
+ facts digest drives entry versioning and vector reuse; append-only observations record when and
10
+ from which response/page evidence those facts were seen without changing their meaning.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from collections.abc import Mapping
17
+ from dataclasses import dataclass, replace
18
+ from datetime import UTC, datetime
19
+ from types import MappingProxyType
20
+ from typing import Any
21
+
22
+ from mostlyright.data_harness.canonical import (
23
+ MAX_CANONICAL_DEPTH,
24
+ CanonicalJSONError,
25
+ canonical_sha256,
26
+ parse_canonical_json,
27
+ )
28
+ from mostlyright.data_harness.sources.catalog.contracts import (
29
+ ACCESS_KINDS,
30
+ CATALOG_ENTRY_CONTRACT_VERSION,
31
+ EMBEDDING_LAYERS,
32
+ MAX_DECLARED_COLUMNS,
33
+ MAX_DECLARED_NAME,
34
+ MAX_DECLARED_VOCABULARY,
35
+ MAX_DESCRIPTION,
36
+ MAX_ENTRY_VERSION,
37
+ MAX_SPATIAL_SCOPE,
38
+ CatalogEntry,
39
+ LayerVectorReference,
40
+ catalog_entry_from_dict,
41
+ )
42
+ from mostlyright.data_harness.sources.catalog.harvest.protocol import (
43
+ DATAGOV_V4_HARVESTER_COORDINATE,
44
+ )
45
+ from mostlyright.data_harness.sources.contracts import (
46
+ DATA_FORMATS,
47
+ MAX_SAFE_INTEGER,
48
+ RIGHTS_STATUSES,
49
+ SourceContractError,
50
+ _CanonicalContract,
51
+ )
52
+
53
+ CATALOG_ENTRY_V2_CONTRACT_VERSION = "harness-source-catalog-entry.v2"
54
+
55
+ # Keep a single entry below the existing four-MiB catalog-fact admission ceiling, and enforce it
56
+ # before ``json.loads`` can allocate a decoded object graph. Provider-scale collections use bounded
57
+ # shards/packs; no individual semantic record needs a wider spelling.
58
+ MAX_CATALOG_ENTRY_BYTES = 4 * 1024 * 1024
59
+ MAX_CATALOG_ENTRY_NESTING = MAX_CANONICAL_DEPTH
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class ProviderContract:
64
+ """Exact provider identity field and implementation coordinate; neither is inferred."""
65
+
66
+ harvester_coordinate: str
67
+ identifier_json_path: str
68
+
69
+
70
+ # One immutable provider contract prevents the identity field and observation producer from
71
+ # drifting into independently maintained maps. Adding a provider requires one explicit reviewed
72
+ # registry entry; names, prefixes, and semantic-version proximity never imply authority.
73
+ PROVIDER_CONTRACTS: Mapping[str, ProviderContract] = MappingProxyType(
74
+ {
75
+ "datagov_v4": ProviderContract(
76
+ harvester_coordinate=DATAGOV_V4_HARVESTER_COORDINATE,
77
+ identifier_json_path="$.identifier",
78
+ )
79
+ }
80
+ )
81
+
82
+ FACT_STATES = frozenset({"known", "unknown", "not_applicable"})
83
+ MAX_PROVIDER_RECORD_ID = 512
84
+ MAX_PROVIDER_PATHS = 64
85
+ MAX_OBSERVATIONS = 10_000
86
+ MAX_PROFILES = 256
87
+
88
+ FILTERABLE_FACTS = frozenset(
89
+ {
90
+ "publisher",
91
+ "description",
92
+ "spatial_scope",
93
+ "data_formats",
94
+ "access_kind",
95
+ "authentication_required",
96
+ "declared_columns",
97
+ "declared_vocabulary",
98
+ "declared_row_count",
99
+ "profiles",
100
+ }
101
+ )
102
+
103
+ _IDENTIFIER = re.compile(r"^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$")
104
+ _SHA256 = re.compile(r"^[0-9a-f]{64}$")
105
+ _HARVESTER_COORDINATE = re.compile(
106
+ r"^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*@[1-9][0-9]*\.[0-9]+\.[0-9]+$"
107
+ )
108
+ # Deliberately excludes numeric array positions. Provider records may reorder arrays between
109
+ # responses; a semantic author may cite the complete array (``[*]``), never an unstable local
110
+ # position. Keys are the closed portable spelling used by normalized provider records.
111
+ _PROVIDER_NAMED_SEGMENT = re.compile(r"\.[A-Za-z_][A-Za-z0-9_-]*")
112
+ _PROVIDER_QUOTED_KEYS = frozenset({"@type"})
113
+ _UTC_SECOND = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$")
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class ProviderRecordIdentity(_CanonicalContract):
118
+ """Stable provider record identity, directly cited from one normalized provider field."""
119
+
120
+ provider_id: str
121
+ provider_record_id: str
122
+ provider_record_sha256: str
123
+ identifier_json_path: str
124
+
125
+ def __post_init__(self) -> None:
126
+ _identifier(self.provider_id, "provider_record.provider_id")
127
+ _text(
128
+ self.provider_record_id,
129
+ "provider_record.provider_record_id",
130
+ maximum=MAX_PROVIDER_RECORD_ID,
131
+ )
132
+ _digest(self.provider_record_sha256, "provider_record.provider_record_sha256")
133
+ _provider_identifier_path(
134
+ self.identifier_json_path,
135
+ "provider_record.identifier_json_path",
136
+ )
137
+ contract = PROVIDER_CONTRACTS.get(self.provider_id)
138
+ if contract is None or self.identifier_json_path != contract.identifier_json_path:
139
+ raise SourceContractError(
140
+ "CATALOG_PROVIDER_IDENTITY_PATH",
141
+ "provider_record.identifier_json_path",
142
+ "must equal the sole registered identity path for this provider",
143
+ )
144
+
145
+ def to_dict(self) -> dict[str, str]:
146
+ return {
147
+ "provider_id": self.provider_id,
148
+ "provider_record_id": self.provider_record_id,
149
+ "provider_record_sha256": self.provider_record_sha256,
150
+ "identifier_json_path": self.identifier_json_path,
151
+ }
152
+
153
+
154
+ @dataclass(frozen=True)
155
+ class ProviderFactProvenance(_CanonicalContract):
156
+ """Exact provider record and JSON paths supporting one known fact."""
157
+
158
+ provider_id: str
159
+ provider_record_id: str
160
+ provider_record_sha256: str
161
+ json_paths: tuple[str, ...]
162
+
163
+ def __post_init__(self) -> None:
164
+ _identifier(self.provider_id, "fact.provenance.provider_id")
165
+ _text(
166
+ self.provider_record_id,
167
+ "fact.provenance.provider_record_id",
168
+ maximum=MAX_PROVIDER_RECORD_ID,
169
+ )
170
+ _digest(self.provider_record_sha256, "fact.provenance.provider_record_sha256")
171
+ if not isinstance(self.json_paths, tuple):
172
+ raise SourceContractError(
173
+ "TYPE",
174
+ "fact.provenance.json_paths",
175
+ "must be an immutable tuple",
176
+ )
177
+ if not self.json_paths or len(self.json_paths) > MAX_PROVIDER_PATHS:
178
+ raise SourceContractError(
179
+ "LIMIT",
180
+ "fact.provenance.json_paths",
181
+ f"must contain between 1 and {MAX_PROVIDER_PATHS} paths",
182
+ )
183
+ for index, path in enumerate(self.json_paths):
184
+ _provider_path(path, f"fact.provenance.json_paths[{index}]")
185
+ if tuple(sorted(set(self.json_paths))) != self.json_paths:
186
+ raise SourceContractError(
187
+ "ORDER",
188
+ "fact.provenance.json_paths",
189
+ "paths must be unique and sorted",
190
+ )
191
+
192
+ def to_dict(self) -> dict[str, Any]:
193
+ return {
194
+ "provider_id": self.provider_id,
195
+ "provider_record_id": self.provider_record_id,
196
+ "provider_record_sha256": self.provider_record_sha256,
197
+ "json_paths": list(self.json_paths),
198
+ }
199
+
200
+
201
+ @dataclass(frozen=True)
202
+ class CatalogFact(_CanonicalContract):
203
+ """One explicit nullable fact.
204
+
205
+ ``None`` is not a fourth state. It is only the canonical empty slot carried by the two states
206
+ that have no value. Known values cannot be null and cannot exist without provenance.
207
+ """
208
+
209
+ state: str
210
+ value: object | None = None
211
+ provenance: ProviderFactProvenance | None = None
212
+
213
+ def __post_init__(self) -> None:
214
+ _choice(self.state, FACT_STATES, "fact.state")
215
+ if self.state == "known":
216
+ if self.value is None:
217
+ raise SourceContractError("FACT_VALUE", "fact.value", "known requires a value")
218
+ if not isinstance(self.provenance, ProviderFactProvenance):
219
+ raise SourceContractError(
220
+ "FACT_PROVENANCE",
221
+ "fact.provenance",
222
+ "known requires exact provider provenance",
223
+ )
224
+ return
225
+ if self.value is not None:
226
+ raise SourceContractError(
227
+ "FACT_VALUE",
228
+ "fact.value",
229
+ f"{self.state} cannot carry a value",
230
+ )
231
+ if self.provenance is not None:
232
+ raise SourceContractError(
233
+ "FACT_PROVENANCE",
234
+ "fact.provenance",
235
+ f"{self.state} cannot carry provenance",
236
+ )
237
+
238
+ def to_dict(self) -> dict[str, Any]:
239
+ value = list(self.value) if isinstance(self.value, tuple) else self.value
240
+ return {
241
+ "state": self.state,
242
+ "value": value,
243
+ "provenance": self.provenance.to_dict() if self.provenance is not None else None,
244
+ }
245
+
246
+
247
+ @dataclass(frozen=True)
248
+ class CatalogObservation(_CanonicalContract):
249
+ """Append-only sweep evidence, excluded from semantic versioning and vector identity."""
250
+
251
+ harvester_coordinate: str
252
+ observed_at: str
253
+ response_evidence_sha256: str
254
+ page_evidence_sha256: str
255
+
256
+ def __post_init__(self) -> None:
257
+ coordinate = _text(
258
+ self.harvester_coordinate,
259
+ "observation.harvester_coordinate",
260
+ maximum=192,
261
+ )
262
+ if not _HARVESTER_COORDINATE.fullmatch(coordinate):
263
+ raise SourceContractError(
264
+ "COORDINATE",
265
+ "observation.harvester_coordinate",
266
+ "must be a provider/harvester identifier plus positive-major semantic version",
267
+ )
268
+ _utc_second(self.observed_at, "observation.observed_at")
269
+ _digest(self.response_evidence_sha256, "observation.response_evidence_sha256")
270
+ _digest(self.page_evidence_sha256, "observation.page_evidence_sha256")
271
+
272
+ def to_dict(self) -> dict[str, str]:
273
+ return {
274
+ "harvester_coordinate": self.harvester_coordinate,
275
+ "observed_at": self.observed_at,
276
+ "response_evidence_sha256": self.response_evidence_sha256,
277
+ "page_evidence_sha256": self.page_evidence_sha256,
278
+ }
279
+
280
+
281
+ @dataclass(frozen=True)
282
+ class CatalogFactFilter(_CanonicalContract):
283
+ """One closed exact filter over semantic facts, with no score-shaped input channel."""
284
+
285
+ field: str
286
+ expected: str | bool | int
287
+
288
+ def __post_init__(self) -> None:
289
+ _choice(self.field, FILTERABLE_FACTS, "filter.field")
290
+ path = "filter.expected"
291
+ if self.field == "authentication_required":
292
+ if type(self.expected) is not bool:
293
+ raise SourceContractError("BOOLEAN", path, "must be a boolean for this field")
294
+ elif self.field == "declared_row_count":
295
+ if type(self.expected) is not int or not 0 <= self.expected <= MAX_SAFE_INTEGER:
296
+ raise SourceContractError(
297
+ "INTEGER",
298
+ path,
299
+ f"must be an integer in [0, {MAX_SAFE_INTEGER}] for this field",
300
+ )
301
+ else:
302
+ _text(self.expected, path, maximum=MAX_DESCRIPTION)
303
+
304
+ def to_dict(self) -> dict[str, Any]:
305
+ return {"field": self.field, "expected": self.expected}
306
+
307
+
308
+ @dataclass(frozen=True)
309
+ class CatalogEntryV2Decision(_CanonicalContract):
310
+ """Score-blind retrieval/filter/admission projection for one already-safe v2 entry."""
311
+
312
+ entry_coordinate: str
313
+ searchable: bool
314
+ filters_match: bool
315
+ filter_failures: tuple[str, ...]
316
+ rights_disposition: str
317
+ recommendation: str
318
+ semantic_facts_digest: str
319
+ entry_digest: str
320
+
321
+ def __post_init__(self) -> None:
322
+ _text(self.entry_coordinate, "decision.entry_coordinate", maximum=256)
323
+ _boolean(self.searchable, "decision.searchable")
324
+ _boolean(self.filters_match, "decision.filters_match")
325
+ if not isinstance(self.filter_failures, tuple):
326
+ raise SourceContractError(
327
+ "TYPE",
328
+ "decision.filter_failures",
329
+ "must be an immutable tuple",
330
+ )
331
+ for index, failure in enumerate(self.filter_failures):
332
+ _text(failure, f"decision.filter_failures[{index}]", maximum=256)
333
+ _choice(
334
+ self.rights_disposition,
335
+ frozenset({"admitted", "flagged", "refused"}),
336
+ "decision.rights_disposition",
337
+ )
338
+ _choice(
339
+ self.recommendation,
340
+ frozenset({"use_source", "no_match"}),
341
+ "decision.recommendation",
342
+ )
343
+ _digest(self.semantic_facts_digest, "decision.semantic_facts_digest")
344
+ _digest(self.entry_digest, "decision.entry_digest")
345
+ if self.recommendation == "use_source" and (
346
+ not self.searchable or not self.filters_match or self.rights_disposition != "admitted"
347
+ ):
348
+ raise SourceContractError(
349
+ "CATALOG_AUTHORITY",
350
+ "decision.recommendation",
351
+ "use_source requires searchable admitted facts satisfying every filter",
352
+ )
353
+
354
+ def to_dict(self) -> dict[str, Any]:
355
+ return {
356
+ "entry_coordinate": self.entry_coordinate,
357
+ "searchable": self.searchable,
358
+ "filters_match": self.filters_match,
359
+ "filter_failures": list(self.filter_failures),
360
+ "rights_disposition": self.rights_disposition,
361
+ "recommendation": self.recommendation,
362
+ "semantic_facts_digest": self.semantic_facts_digest,
363
+ "entry_digest": self.entry_digest,
364
+ }
365
+
366
+
367
+ @dataclass(frozen=True)
368
+ class CatalogRetrievalRead:
369
+ """Durable parse disposition used by packed readers before a record can reach retrieval."""
370
+
371
+ entry: CatalogEntry | CatalogEntryV2 | None
372
+ schema_version: str | None
373
+ retrieval_disposition: str
374
+ searchable: bool
375
+ reason_code: str
376
+
377
+ def __post_init__(self) -> None:
378
+ if self.retrieval_disposition not in {"searchable", "skipped"}:
379
+ raise SourceContractError(
380
+ "ENUM",
381
+ "retrieval_read.retrieval_disposition",
382
+ "must be searchable or skipped",
383
+ )
384
+ if type(self.searchable) is not bool:
385
+ raise SourceContractError(
386
+ "BOOLEAN",
387
+ "retrieval_read.searchable",
388
+ "must be a boolean",
389
+ )
390
+ if (self.retrieval_disposition == "searchable") != self.searchable:
391
+ raise SourceContractError(
392
+ "CATALOG_RETRIEVAL_DISPOSITION",
393
+ "retrieval_read.searchable",
394
+ "must agree with retrieval_disposition",
395
+ )
396
+ if self.searchable != (self.entry is not None):
397
+ raise SourceContractError(
398
+ "CATALOG_RETRIEVAL_DISPOSITION",
399
+ "retrieval_read.entry",
400
+ "skipped reads carry no entry and searchable reads carry one",
401
+ )
402
+ _text(self.reason_code, "retrieval_read.reason_code", maximum=128)
403
+
404
+
405
+ _FACT_FIELDS = (
406
+ "title",
407
+ "publisher",
408
+ "description",
409
+ "spatial_scope",
410
+ "data_formats",
411
+ "access_kind",
412
+ "authentication_required",
413
+ "rights",
414
+ "declared_columns",
415
+ "declared_vocabulary",
416
+ "declared_row_count",
417
+ "profiles",
418
+ )
419
+
420
+
421
+ @dataclass(frozen=True)
422
+ class CatalogEntryV2(_CanonicalContract):
423
+ """One v2 public-source entry with explicit fact states and separate sweep history."""
424
+
425
+ entry_id: str
426
+ entry_version: int
427
+ provider_record: ProviderRecordIdentity
428
+ title: CatalogFact
429
+ publisher: CatalogFact
430
+ description: CatalogFact
431
+ spatial_scope: CatalogFact
432
+ data_formats: CatalogFact
433
+ access_kind: CatalogFact
434
+ authentication_required: CatalogFact
435
+ rights: CatalogFact
436
+ declared_columns: CatalogFact
437
+ declared_vocabulary: CatalogFact
438
+ declared_row_count: CatalogFact
439
+ profiles: CatalogFact
440
+ observations: tuple[CatalogObservation, ...]
441
+ vectors: tuple[LayerVectorReference, ...] = ()
442
+ previous_semantic_digest: str | None = None
443
+ schema_version: str = CATALOG_ENTRY_V2_CONTRACT_VERSION
444
+
445
+ def __post_init__(self) -> None:
446
+ if self.schema_version != CATALOG_ENTRY_V2_CONTRACT_VERSION:
447
+ raise SourceContractError(
448
+ "VERSION",
449
+ "catalog_entry_v2.schema_version",
450
+ f"must equal {CATALOG_ENTRY_V2_CONTRACT_VERSION!r}",
451
+ )
452
+ _identifier(self.entry_id, "catalog_entry_v2.entry_id")
453
+ if type(self.entry_version) is not int or not 1 <= self.entry_version <= MAX_ENTRY_VERSION:
454
+ raise SourceContractError(
455
+ "CATALOG_VERSION",
456
+ "catalog_entry_v2.entry_version",
457
+ f"must be an integer in [1, {MAX_ENTRY_VERSION}]",
458
+ )
459
+ if not isinstance(self.provider_record, ProviderRecordIdentity):
460
+ raise SourceContractError(
461
+ "TYPE",
462
+ "catalog_entry_v2.provider_record",
463
+ "must be a ProviderRecordIdentity",
464
+ )
465
+ for name in _FACT_FIELDS:
466
+ fact = getattr(self, name)
467
+ if not isinstance(fact, CatalogFact):
468
+ raise SourceContractError(
469
+ "TYPE",
470
+ f"catalog_entry_v2.{name}",
471
+ "must be a CatalogFact",
472
+ )
473
+ self._validate_fact_provenance(name, fact)
474
+ if self.title.state != "known":
475
+ raise SourceContractError(
476
+ "CATALOG_IDENTITY",
477
+ "catalog_entry_v2.title",
478
+ "safe searchable identity requires a directly evidenced known title",
479
+ )
480
+ self._validate_fact_types()
481
+ self._validate_observations()
482
+ self._validate_vectors()
483
+ if self.previous_semantic_digest is not None:
484
+ _digest(
485
+ self.previous_semantic_digest,
486
+ "catalog_entry_v2.previous_semantic_digest",
487
+ )
488
+ if self.entry_version == 1 and self.previous_semantic_digest is not None:
489
+ raise SourceContractError(
490
+ "CATALOG_VERSION_CHAIN",
491
+ "catalog_entry_v2.previous_semantic_digest",
492
+ "a first version has no semantic predecessor",
493
+ )
494
+ if self.entry_version > 1 and self.previous_semantic_digest is None:
495
+ raise SourceContractError(
496
+ "CATALOG_VERSION_CHAIN",
497
+ "catalog_entry_v2.previous_semantic_digest",
498
+ "a semantic successor must link to its predecessor",
499
+ )
500
+
501
+ def _validate_fact_provenance(self, name: str, fact: CatalogFact) -> None:
502
+ if fact.state != "known":
503
+ return
504
+ provenance = fact.provenance
505
+ assert provenance is not None
506
+ expected = (
507
+ self.provider_record.provider_id,
508
+ self.provider_record.provider_record_id,
509
+ self.provider_record.provider_record_sha256,
510
+ )
511
+ actual = (
512
+ provenance.provider_id,
513
+ provenance.provider_record_id,
514
+ provenance.provider_record_sha256,
515
+ )
516
+ if actual != expected:
517
+ raise SourceContractError(
518
+ "CATALOG_PROVIDER_BINDING",
519
+ f"catalog_entry_v2.{name}.provenance",
520
+ "known fact provenance must name this exact provider record and digest",
521
+ )
522
+
523
+ def _validate_fact_types(self) -> None:
524
+ _known_text(self.title, "catalog_entry_v2.title", maximum=200)
525
+ _known_text(self.publisher, "catalog_entry_v2.publisher", maximum=200)
526
+ _known_text(self.description, "catalog_entry_v2.description", maximum=MAX_DESCRIPTION)
527
+ _known_text_tuple(
528
+ self.spatial_scope,
529
+ "catalog_entry_v2.spatial_scope",
530
+ maximum=MAX_SPATIAL_SCOPE,
531
+ )
532
+ _known_choices(
533
+ self.data_formats,
534
+ "catalog_entry_v2.data_formats",
535
+ allowed=DATA_FORMATS,
536
+ maximum=len(DATA_FORMATS),
537
+ )
538
+ _known_choice(
539
+ self.access_kind,
540
+ "catalog_entry_v2.access_kind",
541
+ allowed=ACCESS_KINDS,
542
+ )
543
+ _known_boolean(
544
+ self.authentication_required,
545
+ "catalog_entry_v2.authentication_required",
546
+ )
547
+ _known_choice(self.rights, "catalog_entry_v2.rights", allowed=RIGHTS_STATUSES)
548
+ _known_text_tuple(
549
+ self.declared_columns,
550
+ "catalog_entry_v2.declared_columns",
551
+ maximum=MAX_DECLARED_COLUMNS,
552
+ )
553
+ _known_text_tuple(
554
+ self.declared_vocabulary,
555
+ "catalog_entry_v2.declared_vocabulary",
556
+ maximum=MAX_DECLARED_VOCABULARY,
557
+ )
558
+ _known_integer(
559
+ self.declared_row_count,
560
+ "catalog_entry_v2.declared_row_count",
561
+ minimum=0,
562
+ maximum=MAX_SAFE_INTEGER,
563
+ )
564
+ _known_text_tuple(self.profiles, "catalog_entry_v2.profiles", maximum=MAX_PROFILES)
565
+
566
+ def _validate_observations(self) -> None:
567
+ if not isinstance(self.observations, tuple):
568
+ raise SourceContractError(
569
+ "TYPE",
570
+ "catalog_entry_v2.observations",
571
+ "must be an immutable tuple",
572
+ )
573
+ if not self.observations or len(self.observations) > MAX_OBSERVATIONS:
574
+ raise SourceContractError(
575
+ "LIMIT",
576
+ "catalog_entry_v2.observations",
577
+ f"must contain between 1 and {MAX_OBSERVATIONS} observations",
578
+ )
579
+ for index, item in enumerate(self.observations):
580
+ if not isinstance(item, CatalogObservation):
581
+ raise SourceContractError(
582
+ "TYPE",
583
+ f"catalog_entry_v2.observations[{index}]",
584
+ "must be a CatalogObservation",
585
+ )
586
+ contract = PROVIDER_CONTRACTS.get(self.provider_record.provider_id)
587
+ if contract is None or item.harvester_coordinate != contract.harvester_coordinate:
588
+ raise SourceContractError(
589
+ "CATALOG_PROVIDER_HARVESTER",
590
+ f"catalog_entry_v2.observations[{index}].harvester_coordinate",
591
+ "must be an exact registered harvester coordinate for this provider",
592
+ )
593
+ keys = tuple((item.observed_at, item.digest) for item in self.observations)
594
+ if tuple(sorted(set(keys))) != keys:
595
+ raise SourceContractError(
596
+ "CATALOG_OBSERVATION_HISTORY",
597
+ "catalog_entry_v2.observations",
598
+ "observations must be unique and append-ordered by timestamp and digest",
599
+ )
600
+
601
+ def _validate_vectors(self) -> None:
602
+ if not isinstance(self.vectors, tuple):
603
+ raise SourceContractError(
604
+ "TYPE",
605
+ "catalog_entry_v2.vectors",
606
+ "must be an immutable tuple",
607
+ )
608
+ for index, item in enumerate(self.vectors):
609
+ if not isinstance(item, LayerVectorReference):
610
+ raise SourceContractError(
611
+ "TYPE",
612
+ f"catalog_entry_v2.vectors[{index}]",
613
+ "must be a LayerVectorReference",
614
+ )
615
+ if not self.vectors:
616
+ return
617
+ layers = tuple(item.layer for item in self.vectors)
618
+ if layers != EMBEDDING_LAYERS:
619
+ raise SourceContractError(
620
+ "CATALOG_VECTOR_LAYERS",
621
+ "catalog_entry_v2.vectors",
622
+ f"must carry exactly one vector for all layers in order {list(EMBEDDING_LAYERS)}",
623
+ )
624
+ for index, vector in enumerate(self.vectors):
625
+ if vector.entry_facts_digest != self.semantic_facts_digest:
626
+ raise SourceContractError(
627
+ "CATALOG_VECTOR_FACTS",
628
+ f"catalog_entry_v2.vectors[{index}].entry_facts_digest",
629
+ "must equal this entry's semantic facts digest",
630
+ )
631
+
632
+ @property
633
+ def coordinate(self) -> str:
634
+ return f"{self.entry_id}@v{self.entry_version}"
635
+
636
+ @property
637
+ def semantic_facts_digest(self) -> str:
638
+ """Digest meaning and provider paths, excluding observations and vector instances."""
639
+
640
+ return canonical_sha256(self._semantic_payload())
641
+
642
+ @property
643
+ def provenance_history_digest(self) -> str:
644
+ """Digest the stable meaning coordinate plus every append-only sweep observation."""
645
+
646
+ return canonical_sha256(
647
+ {
648
+ "schema_version": self.schema_version,
649
+ "semantic_facts_digest": self.semantic_facts_digest,
650
+ "observations": [item.to_dict() for item in self.observations],
651
+ }
652
+ )
653
+
654
+ def _semantic_payload(self) -> dict[str, Any]:
655
+ return {
656
+ "schema_version": self.schema_version,
657
+ "entry_id": self.entry_id,
658
+ "entry_version": self.entry_version,
659
+ "provider_record": self.provider_record.to_dict(),
660
+ **{name: getattr(self, name).to_dict() for name in _FACT_FIELDS},
661
+ "previous_semantic_digest": self.previous_semantic_digest,
662
+ }
663
+
664
+ def to_dict(self) -> dict[str, Any]:
665
+ return {
666
+ **self._semantic_payload(),
667
+ "observations": [item.to_dict() for item in self.observations],
668
+ "vectors": [item.to_dict() for item in self.vectors],
669
+ }
670
+
671
+
672
+ def append_catalog_observation(
673
+ entry: CatalogEntryV2,
674
+ observation: CatalogObservation,
675
+ ) -> CatalogEntryV2:
676
+ """Append one evidence observation without minting a new semantic entry version."""
677
+
678
+ if not isinstance(entry, CatalogEntryV2):
679
+ raise SourceContractError("TYPE", "entry", "must be a CatalogEntryV2")
680
+ if not isinstance(observation, CatalogObservation):
681
+ raise SourceContractError("TYPE", "observation", "must be a CatalogObservation")
682
+ return replace(entry, observations=(*entry.observations, observation))
683
+
684
+
685
+ def catalog_entry_from_canonical_bytes(data: bytes) -> CatalogEntry | CatalogEntryV2:
686
+ """Dispatch exact canonical bytes to frozen v1 or explicit v2 without reinterpretation."""
687
+
688
+ if not isinstance(data, (bytes, bytearray, memoryview)):
689
+ raise SourceContractError(
690
+ "TYPE",
691
+ "catalog_entry_bytes",
692
+ "must be a bytes-like value",
693
+ )
694
+ if len(data) > MAX_CATALOG_ENTRY_BYTES:
695
+ raise SourceContractError(
696
+ "CATALOG_ENTRY_BYTES",
697
+ "catalog_entry_bytes",
698
+ f"must contain at most {MAX_CATALOG_ENTRY_BYTES} bytes",
699
+ )
700
+ try:
701
+ decoded = parse_canonical_json(data)
702
+ except RecursionError:
703
+ # CPython's JSON decoder may reach its interpreter recursion limit before the canonical
704
+ # normalizer can issue JSON_DEPTH. Normalize both routes to one stable contract refusal.
705
+ raise SourceContractError(
706
+ "JSON_DEPTH",
707
+ "catalog_entry_bytes",
708
+ f"nesting exceeds {MAX_CATALOG_ENTRY_NESTING} levels",
709
+ ) from None
710
+ except CanonicalJSONError as error:
711
+ raise SourceContractError(
712
+ error.code,
713
+ "catalog_entry_bytes",
714
+ f"must be canonical JSON: {error.detail}",
715
+ ) from None
716
+ payload = _mapping(decoded, "catalog_entry")
717
+ schema_version = payload.get("schema_version")
718
+ if schema_version == CATALOG_ENTRY_CONTRACT_VERSION:
719
+ return catalog_entry_from_dict(payload)
720
+ if schema_version == CATALOG_ENTRY_V2_CONTRACT_VERSION:
721
+ return catalog_entry_v2_from_dict(payload)
722
+ raise SourceContractError(
723
+ "VERSION",
724
+ "catalog_entry.schema_version",
725
+ "must name the exact frozen v1 or explicit v2 catalog entry contract",
726
+ )
727
+
728
+
729
+ def read_catalog_entry_for_retrieval(data: bytes) -> CatalogRetrievalRead:
730
+ """Parse a record for retrieval, durably skipping unsafe identity or provenance."""
731
+
732
+ try:
733
+ entry = catalog_entry_from_canonical_bytes(data)
734
+ except RecursionError:
735
+ # Defense in depth: this adapter is the retrieval trust boundary, so a future parser
736
+ # branch must not accidentally restore an interpreter exception as a public outcome.
737
+ return CatalogRetrievalRead(
738
+ entry=None,
739
+ schema_version=None,
740
+ retrieval_disposition="skipped",
741
+ searchable=False,
742
+ reason_code="JSON_DEPTH",
743
+ )
744
+ except SourceContractError as error:
745
+ return CatalogRetrievalRead(
746
+ entry=None,
747
+ schema_version=None,
748
+ retrieval_disposition="skipped",
749
+ searchable=False,
750
+ reason_code=error.code,
751
+ )
752
+ return CatalogRetrievalRead(
753
+ entry=entry,
754
+ schema_version=entry.schema_version,
755
+ retrieval_disposition="searchable",
756
+ searchable=True,
757
+ reason_code="OK",
758
+ )
759
+
760
+
761
+ def evaluate_catalog_entry_v2(
762
+ entry: CatalogEntryV2,
763
+ *,
764
+ filters: tuple[CatalogFactFilter, ...] = (),
765
+ ) -> CatalogEntryV2Decision:
766
+ """Project searchable metadata and fail-closed fact authority without retrieval scores."""
767
+
768
+ if not isinstance(entry, CatalogEntryV2):
769
+ raise SourceContractError("TYPE", "entry", "must be a CatalogEntryV2")
770
+ if not isinstance(filters, tuple):
771
+ raise SourceContractError("TYPE", "filters", "must be an immutable tuple")
772
+ failures: list[str] = []
773
+ seen: set[str] = set()
774
+ for index, required in enumerate(filters):
775
+ if not isinstance(required, CatalogFactFilter):
776
+ raise SourceContractError(
777
+ "TYPE",
778
+ f"filters[{index}]",
779
+ "must be a CatalogFactFilter",
780
+ )
781
+ if required.field in seen:
782
+ raise SourceContractError(
783
+ "DUPLICATE",
784
+ f"filters[{index}].field",
785
+ "each structured fact may be filtered at most once",
786
+ )
787
+ seen.add(required.field)
788
+ fact = getattr(entry, required.field)
789
+ if fact.state != "known":
790
+ failures.append(f"{required.field}:{fact.state}")
791
+ continue
792
+ value = fact.value
793
+ matched = (
794
+ required.expected in value if isinstance(value, tuple) else value == required.expected
795
+ )
796
+ if not matched:
797
+ failures.append(f"{required.field}:mismatch")
798
+
799
+ rights_disposition = _v2_rights_disposition(entry.rights)
800
+ explicitly_public = (
801
+ entry.authentication_required.state == "known"
802
+ and entry.authentication_required.value is False
803
+ and entry.access_kind.state == "known"
804
+ and entry.data_formats.state == "known"
805
+ )
806
+ recommendation = (
807
+ "use_source"
808
+ if rights_disposition == "admitted" and explicitly_public and not failures
809
+ else "no_match"
810
+ )
811
+ return CatalogEntryV2Decision(
812
+ entry_coordinate=entry.coordinate,
813
+ searchable=True,
814
+ filters_match=not failures,
815
+ filter_failures=tuple(failures),
816
+ rights_disposition=rights_disposition,
817
+ recommendation=recommendation,
818
+ semantic_facts_digest=entry.semantic_facts_digest,
819
+ entry_digest=entry.digest,
820
+ )
821
+
822
+
823
+ def _v2_rights_disposition(rights: CatalogFact) -> str:
824
+ if rights.state != "known":
825
+ return "flagged"
826
+ if rights.value == "approved":
827
+ return "admitted"
828
+ if rights.value == "prohibited":
829
+ return "refused"
830
+ # ``conditional`` lacks a v2 obligation fact, and ``unclear`` explicitly requires escalation.
831
+ return "flagged"
832
+
833
+
834
+ def catalog_entry_v2_from_dict(data: Mapping[str, Any]) -> CatalogEntryV2:
835
+ """Strictly rebuild the one exact canonical v2 object shape."""
836
+
837
+ payload = _mapping(data, "catalog_entry_v2")
838
+ _exact(
839
+ payload,
840
+ {
841
+ "schema_version",
842
+ "entry_id",
843
+ "entry_version",
844
+ "provider_record",
845
+ *_FACT_FIELDS,
846
+ "observations",
847
+ "vectors",
848
+ "previous_semantic_digest",
849
+ },
850
+ "catalog_entry_v2",
851
+ )
852
+ provider_record = _provider_record_from_dict(payload["provider_record"])
853
+ facts = {
854
+ name: _fact_from_dict(
855
+ payload[name],
856
+ f"catalog_entry_v2.{name}",
857
+ tuple_value=name
858
+ in {
859
+ "spatial_scope",
860
+ "data_formats",
861
+ "declared_columns",
862
+ "declared_vocabulary",
863
+ "profiles",
864
+ },
865
+ )
866
+ for name in _FACT_FIELDS
867
+ }
868
+ return CatalogEntryV2(
869
+ schema_version=payload["schema_version"],
870
+ entry_id=payload["entry_id"],
871
+ entry_version=payload["entry_version"],
872
+ provider_record=provider_record,
873
+ **facts,
874
+ observations=tuple(
875
+ _observation_from_dict(item, f"catalog_entry_v2.observations[{index}]")
876
+ for index, item in enumerate(
877
+ _sequence(payload["observations"], "catalog_entry_v2.observations")
878
+ )
879
+ ),
880
+ vectors=tuple(
881
+ _vector_from_dict(item, f"catalog_entry_v2.vectors[{index}]")
882
+ for index, item in enumerate(_sequence(payload["vectors"], "catalog_entry_v2.vectors"))
883
+ ),
884
+ previous_semantic_digest=payload["previous_semantic_digest"],
885
+ )
886
+
887
+
888
+ def _provider_record_from_dict(data: Any) -> ProviderRecordIdentity:
889
+ path = "catalog_entry_v2.provider_record"
890
+ payload = _mapping(data, path)
891
+ _exact(
892
+ payload,
893
+ {
894
+ "provider_id",
895
+ "provider_record_id",
896
+ "provider_record_sha256",
897
+ "identifier_json_path",
898
+ },
899
+ path,
900
+ )
901
+ return ProviderRecordIdentity(
902
+ provider_id=payload["provider_id"],
903
+ provider_record_id=payload["provider_record_id"],
904
+ provider_record_sha256=payload["provider_record_sha256"],
905
+ identifier_json_path=payload["identifier_json_path"],
906
+ )
907
+
908
+
909
+ def _fact_from_dict(data: Any, path: str, *, tuple_value: bool) -> CatalogFact:
910
+ payload = _mapping(data, path)
911
+ _exact(payload, {"state", "value", "provenance"}, path)
912
+ value = payload["value"]
913
+ if tuple_value and value is not None:
914
+ value = tuple(_sequence(value, f"{path}.value"))
915
+ provenance_payload = payload["provenance"]
916
+ provenance = None
917
+ if provenance_payload is not None:
918
+ provenance_data = _mapping(provenance_payload, f"{path}.provenance")
919
+ _exact(
920
+ provenance_data,
921
+ {"provider_id", "provider_record_id", "provider_record_sha256", "json_paths"},
922
+ f"{path}.provenance",
923
+ )
924
+ provenance = ProviderFactProvenance(
925
+ provider_id=provenance_data["provider_id"],
926
+ provider_record_id=provenance_data["provider_record_id"],
927
+ provider_record_sha256=provenance_data["provider_record_sha256"],
928
+ json_paths=tuple(
929
+ _sequence(provenance_data["json_paths"], f"{path}.provenance.json_paths")
930
+ ),
931
+ )
932
+ return CatalogFact(state=payload["state"], value=value, provenance=provenance)
933
+
934
+
935
+ def _observation_from_dict(data: Any, path: str) -> CatalogObservation:
936
+ payload = _mapping(data, path)
937
+ _exact(
938
+ payload,
939
+ {
940
+ "harvester_coordinate",
941
+ "observed_at",
942
+ "response_evidence_sha256",
943
+ "page_evidence_sha256",
944
+ },
945
+ path,
946
+ )
947
+ return CatalogObservation(
948
+ harvester_coordinate=payload["harvester_coordinate"],
949
+ observed_at=payload["observed_at"],
950
+ response_evidence_sha256=payload["response_evidence_sha256"],
951
+ page_evidence_sha256=payload["page_evidence_sha256"],
952
+ )
953
+
954
+
955
+ def _vector_from_dict(data: Any, path: str) -> LayerVectorReference:
956
+ payload = _mapping(data, path)
957
+ _exact(
958
+ payload,
959
+ {
960
+ "layer",
961
+ "backend_id",
962
+ "backend_version",
963
+ "dimensions",
964
+ "quantization",
965
+ "vector_sha256",
966
+ "entry_facts_digest",
967
+ },
968
+ path,
969
+ )
970
+ return LayerVectorReference(
971
+ layer=payload["layer"],
972
+ backend_id=payload["backend_id"],
973
+ backend_version=payload["backend_version"],
974
+ dimensions=payload["dimensions"],
975
+ quantization=payload["quantization"],
976
+ vector_sha256=payload["vector_sha256"],
977
+ entry_facts_digest=payload["entry_facts_digest"],
978
+ )
979
+
980
+
981
+ def _mapping(value: Any, path: str) -> Mapping[str, Any]:
982
+ if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value):
983
+ raise SourceContractError("TYPE", path, "must be an object with string keys")
984
+ return value
985
+
986
+
987
+ def _sequence(value: Any, path: str) -> list[Any]:
988
+ if not isinstance(value, list):
989
+ raise SourceContractError("TYPE", path, "must be an array")
990
+ return value
991
+
992
+
993
+ def _exact(data: Mapping[str, Any], expected: set[str], path: str) -> None:
994
+ actual = set(data)
995
+ if actual != expected:
996
+ raise SourceContractError(
997
+ "FIELDS",
998
+ path,
999
+ f"must contain exactly {sorted(expected)}; missing={sorted(expected - actual)}, "
1000
+ f"extra={sorted(actual - expected)}",
1001
+ )
1002
+
1003
+
1004
+ def _text(value: Any, path: str, *, maximum: int) -> str:
1005
+ if not isinstance(value, str) or not value or len(value) > maximum:
1006
+ raise SourceContractError(
1007
+ "TEXT",
1008
+ path,
1009
+ f"must be a non-empty string no longer than {maximum} characters",
1010
+ )
1011
+ for index, char in enumerate(value):
1012
+ if 0xD800 <= ord(char) <= 0xDFFF:
1013
+ raise SourceContractError(
1014
+ "UNICODE",
1015
+ path,
1016
+ f"contains an unpaired surrogate at character {index}",
1017
+ )
1018
+ if ord(char) < 0x20 or 0x7F <= ord(char) <= 0x9F:
1019
+ raise SourceContractError(
1020
+ "CONTROL_CHARACTER",
1021
+ path,
1022
+ f"contains a C0/C1 control character at character {index}",
1023
+ )
1024
+ if char.isspace() and char != " ":
1025
+ raise SourceContractError(
1026
+ "UNSAFE_WHITESPACE",
1027
+ path,
1028
+ f"contains non-ASCII-space whitespace at character {index}",
1029
+ )
1030
+ return value
1031
+
1032
+
1033
+ def _identifier(value: Any, path: str) -> str:
1034
+ text = _text(value, path, maximum=MAX_DECLARED_NAME)
1035
+ if not _IDENTIFIER.fullmatch(text):
1036
+ raise SourceContractError(
1037
+ "IDENTIFIER",
1038
+ path,
1039
+ "must be a lowercase dotted/dashed/underscored identifier",
1040
+ )
1041
+ return text
1042
+
1043
+
1044
+ def _digest(value: Any, path: str) -> str:
1045
+ text = _text(value, path, maximum=64)
1046
+ if not _SHA256.fullmatch(text):
1047
+ raise SourceContractError("SHA256", path, "must be a lowercase 64-character SHA-256")
1048
+ return text
1049
+
1050
+
1051
+ def _choice(value: Any, allowed: frozenset[str], path: str) -> str:
1052
+ text = _text(value, path, maximum=MAX_DECLARED_NAME)
1053
+ if text not in allowed:
1054
+ raise SourceContractError("ENUM", path, f"must be one of {sorted(allowed)}")
1055
+ return text
1056
+
1057
+
1058
+ def _boolean(value: Any, path: str) -> bool:
1059
+ if type(value) is not bool:
1060
+ raise SourceContractError("BOOLEAN", path, "must be a boolean")
1061
+ return value
1062
+
1063
+
1064
+ def _provider_path(value: Any, path: str) -> str:
1065
+ text = _text(value, path, maximum=512)
1066
+ valid_root = text.startswith("$")
1067
+ position = 1 if valid_root else len(text)
1068
+ segments = 0
1069
+ while position < len(text):
1070
+ named = _PROVIDER_NAMED_SEGMENT.match(text, position)
1071
+ if named is not None:
1072
+ position = named.end()
1073
+ segments += 1
1074
+ continue
1075
+ if text.startswith("[*]", position):
1076
+ position += 3
1077
+ segments += 1
1078
+ continue
1079
+ if text.startswith('["', position):
1080
+ end = text.find('"]', position + 2)
1081
+ if end != -1:
1082
+ key = text[position + 2 : end]
1083
+ if key in _PROVIDER_QUOTED_KEYS:
1084
+ position = end + 2
1085
+ segments += 1
1086
+ continue
1087
+ break
1088
+ if not valid_root or position != len(text) or segments == 0:
1089
+ raise SourceContractError(
1090
+ "PROVIDER_PATH",
1091
+ path,
1092
+ "must use canonical named keys, optional [*], and only closed bracket-quoted keys",
1093
+ )
1094
+ return text
1095
+
1096
+
1097
+ def _provider_identifier_path(value: Any, path: str) -> str:
1098
+ text = _provider_path(value, path)
1099
+ if "[*]" in text:
1100
+ raise SourceContractError(
1101
+ "PROVIDER_IDENTIFIER_PATH",
1102
+ path,
1103
+ "must identify one singular named provider field without an array wildcard",
1104
+ )
1105
+ return text
1106
+
1107
+
1108
+ def _utc_second(value: Any, path: str) -> str:
1109
+ text = _text(value, path, maximum=20)
1110
+ if not _UTC_SECOND.fullmatch(text):
1111
+ raise SourceContractError("TIMESTAMP", path, "must be canonical UTC to whole seconds")
1112
+ try:
1113
+ parsed = datetime.strptime(text, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)
1114
+ except ValueError:
1115
+ raise SourceContractError("TIMESTAMP", path, "must be a real UTC timestamp") from None
1116
+ if parsed.strftime("%Y-%m-%dT%H:%M:%SZ") != text:
1117
+ raise SourceContractError("TIMESTAMP", path, "must be canonical UTC to whole seconds")
1118
+ return text
1119
+
1120
+
1121
+ def _known_text(fact: CatalogFact, path: str, *, maximum: int) -> None:
1122
+ if fact.state == "known":
1123
+ _text(fact.value, f"{path}.value", maximum=maximum)
1124
+
1125
+
1126
+ def _known_text_tuple(fact: CatalogFact, path: str, *, maximum: int) -> None:
1127
+ if fact.state != "known":
1128
+ return
1129
+ value = fact.value
1130
+ if not isinstance(value, tuple):
1131
+ raise SourceContractError("TYPE", f"{path}.value", "must be an immutable tuple")
1132
+ if len(value) > maximum:
1133
+ raise SourceContractError("LIMIT", f"{path}.value", f"must contain at most {maximum}")
1134
+ for index, item in enumerate(value):
1135
+ _text(item, f"{path}.value[{index}]", maximum=MAX_DECLARED_NAME)
1136
+ if len(set(value)) != len(value):
1137
+ raise SourceContractError("DUPLICATE", f"{path}.value", "values must be unique")
1138
+
1139
+
1140
+ def _known_choice(fact: CatalogFact, path: str, *, allowed: frozenset[str]) -> None:
1141
+ if fact.state == "known":
1142
+ _choice(fact.value, allowed, f"{path}.value")
1143
+
1144
+
1145
+ def _known_choices(
1146
+ fact: CatalogFact,
1147
+ path: str,
1148
+ *,
1149
+ allowed: frozenset[str],
1150
+ maximum: int,
1151
+ ) -> None:
1152
+ if fact.state != "known":
1153
+ return
1154
+ value = fact.value
1155
+ if not isinstance(value, tuple):
1156
+ raise SourceContractError("TYPE", f"{path}.value", "must be an immutable tuple")
1157
+ if not value or len(value) > maximum:
1158
+ raise SourceContractError(
1159
+ "LIMIT",
1160
+ f"{path}.value",
1161
+ f"must contain between 1 and {maximum} values",
1162
+ )
1163
+ for index, item in enumerate(value):
1164
+ _choice(item, allowed, f"{path}.value[{index}]")
1165
+ if len(set(value)) != len(value):
1166
+ raise SourceContractError("DUPLICATE", f"{path}.value", "values must be unique")
1167
+
1168
+
1169
+ def _known_boolean(fact: CatalogFact, path: str) -> None:
1170
+ if fact.state == "known" and type(fact.value) is not bool:
1171
+ raise SourceContractError("BOOLEAN", f"{path}.value", "must be a boolean")
1172
+
1173
+
1174
+ def _known_integer(fact: CatalogFact, path: str, *, minimum: int, maximum: int) -> None:
1175
+ if fact.state != "known":
1176
+ return
1177
+ if type(fact.value) is not int or not minimum <= fact.value <= maximum:
1178
+ raise SourceContractError(
1179
+ "INTEGER",
1180
+ f"{path}.value",
1181
+ f"must be an integer in [{minimum}, {maximum}]",
1182
+ )