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.
- mostlyright/data_harness/__init__.py +158 -0
- mostlyright/data_harness/acquisition/__init__.py +55 -0
- mostlyright/data_harness/acquisition/http.py +2773 -0
- mostlyright/data_harness/acquisition/parsing.py +809 -0
- mostlyright/data_harness/acquisition/ranges.py +495 -0
- mostlyright/data_harness/acquisition/result_download.py +360 -0
- mostlyright/data_harness/acquisition/retention_admission.py +248 -0
- mostlyright/data_harness/acquisition/sandbox.py +4888 -0
- mostlyright/data_harness/acquisition/url_policy.py +530 -0
- mostlyright/data_harness/agent_runtime.py +2743 -0
- mostlyright/data_harness/assets/logo-ink.svg +31 -0
- mostlyright/data_harness/backends/__init__.py +28 -0
- mostlyright/data_harness/backends/pandas_backend.py +350 -0
- mostlyright/data_harness/backends/polars_backend.py +366 -0
- mostlyright/data_harness/backends/protocol.py +124 -0
- mostlyright/data_harness/backends/reference.py +83 -0
- mostlyright/data_harness/backends/registry.py +55 -0
- mostlyright/data_harness/backends/restrictions.py +126 -0
- mostlyright/data_harness/canonical.py +333 -0
- mostlyright/data_harness/catalog_job.py +625 -0
- mostlyright/data_harness/cli.py +5398 -0
- mostlyright/data_harness/contracts.py +53 -0
- mostlyright/data_harness/coordinator.py +1307 -0
- mostlyright/data_harness/deploy.py +924 -0
- mostlyright/data_harness/deploy_target.py +312 -0
- mostlyright/data_harness/deployment_evidence.py +1067 -0
- mostlyright/data_harness/event_presentation.py +576 -0
- mostlyright/data_harness/events.py +2152 -0
- mostlyright/data_harness/fast_delimited.py +239 -0
- mostlyright/data_harness/fleet.py +237 -0
- mostlyright/data_harness/formats.py +236 -0
- mostlyright/data_harness/governors.py +1163 -0
- mostlyright/data_harness/hosted_bootstrap.py +972 -0
- mostlyright/data_harness/hosted_crawler.py +1115 -0
- mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
- mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
- mostlyright/data_harness/hosted_crawler_job.py +1277 -0
- mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
- mostlyright/data_harness/hosted_dataset.py +1500 -0
- mostlyright/data_harness/hosted_deploy.py +3037 -0
- mostlyright/data_harness/hosted_handoff.py +62 -0
- mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
- mostlyright/data_harness/hosted_ingestion_job.py +356 -0
- mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
- mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
- mostlyright/data_harness/hosted_session_worker.py +3554 -0
- mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
- mostlyright/data_harness/hosted_worker.py +6784 -0
- mostlyright/data_harness/ingestion/__init__.py +56 -0
- mostlyright/data_harness/ingestion/contracts.py +461 -0
- mostlyright/data_harness/ingestion/faults.py +42 -0
- mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
- mostlyright/data_harness/ingestion/spool.py +130 -0
- mostlyright/data_harness/ingestion/store.py +885 -0
- mostlyright/data_harness/key_seam.py +434 -0
- mostlyright/data_harness/linux_process_boundary.py +262 -0
- mostlyright/data_harness/local_contracts.py +2880 -0
- mostlyright/data_harness/local_search/__init__.py +5 -0
- mostlyright/data_harness/local_search/build_index.py +1087 -0
- mostlyright/data_harness/local_search/contracts.py +920 -0
- mostlyright/data_harness/local_search/query_trace.py +266 -0
- mostlyright/data_harness/local_search/retrieval.py +700 -0
- mostlyright/data_harness/local_search/sealed.py +474 -0
- mostlyright/data_harness/local_search/service.py +784 -0
- mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
- mostlyright/data_harness/nbrender/__init__.py +12 -0
- mostlyright/data_harness/nbrender/chrome.py +359 -0
- mostlyright/data_harness/nbrender/code_body.py +266 -0
- mostlyright/data_harness/nbrender/document.py +407 -0
- mostlyright/data_harness/nbrender/frame.py +275 -0
- mostlyright/data_harness/nbrender/interactive.py +337 -0
- mostlyright/data_harness/nbrender/markdown_body.py +477 -0
- mostlyright/data_harness/nbrender/mr_components.py +134 -0
- mostlyright/data_harness/nbrender/outputs_data.py +595 -0
- mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
- mostlyright/data_harness/nbrender/outputs_source.py +260 -0
- mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
- mostlyright/data_harness/nbrender/outputs_text.py +400 -0
- mostlyright/data_harness/nbrender/parse.py +394 -0
- mostlyright/data_harness/nbrender/status.py +40 -0
- mostlyright/data_harness/nbrender/tokens.py +1295 -0
- mostlyright/data_harness/notebook.py +1710 -0
- mostlyright/data_harness/offline.py +2049 -0
- mostlyright/data_harness/operation_registry.py +1007 -0
- mostlyright/data_harness/operator_setup.py +239 -0
- mostlyright/data_harness/pipeline.py +6428 -0
- mostlyright/data_harness/plan_graph.py +2026 -0
- mostlyright/data_harness/preparation/__init__.py +104 -0
- mostlyright/data_harness/preparation/contracts.py +1017 -0
- mostlyright/data_harness/preparation/engine.py +221 -0
- mostlyright/data_harness/preparation/errors.py +14 -0
- mostlyright/data_harness/preparation/gates.py +751 -0
- mostlyright/data_harness/preparation/joins.py +574 -0
- mostlyright/data_harness/preparation/profile.py +384 -0
- mostlyright/data_harness/preparation/table.py +217 -0
- mostlyright/data_harness/preparation/transforms.py +568 -0
- mostlyright/data_harness/progress_events.py +534 -0
- mostlyright/data_harness/readers/__init__.py +46 -0
- mostlyright/data_harness/readers/containers.py +963 -0
- mostlyright/data_harness/readers/contracts.py +542 -0
- mostlyright/data_harness/readers/delimited.py +257 -0
- mostlyright/data_harness/readers/grib2/__init__.py +33 -0
- mostlyright/data_harness/readers/grib2/admission.py +722 -0
- mostlyright/data_harness/readers/grib2/decode.py +1009 -0
- mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
- mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
- mostlyright/data_harness/readers/json_tabular.py +485 -0
- mostlyright/data_harness/readers/registry.py +514 -0
- mostlyright/data_harness/readers/samples/README.md +110 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples.py +582 -0
- mostlyright/data_harness/readers/spreadsheet.py +803 -0
- mostlyright/data_harness/readers/tabular.py +510 -0
- mostlyright/data_harness/recipe.py +5321 -0
- mostlyright/data_harness/repair/__init__.py +78 -0
- mostlyright/data_harness/repair/adapters.py +274 -0
- mostlyright/data_harness/repair/contracts.py +872 -0
- mostlyright/data_harness/repair/coordinator.py +1099 -0
- mostlyright/data_harness/repair/errors.py +16 -0
- mostlyright/data_harness/review.py +2533 -0
- mostlyright/data_harness/rowset.py +283 -0
- mostlyright/data_harness/serving.py +1975 -0
- mostlyright/data_harness/serving_edge.py +590 -0
- mostlyright/data_harness/serving_http.py +1031 -0
- mostlyright/data_harness/session_probes.py +759 -0
- mostlyright/data_harness/signing.py +101 -0
- mostlyright/data_harness/source_discovery.py +898 -0
- mostlyright/data_harness/sources/__init__.py +209 -0
- mostlyright/data_harness/sources/_adapter_steps.py +213 -0
- mostlyright/data_harness/sources/adapters.py +1214 -0
- mostlyright/data_harness/sources/cadence.py +1428 -0
- mostlyright/data_harness/sources/cadence_emission.py +453 -0
- mostlyright/data_harness/sources/cadence_history.py +546 -0
- mostlyright/data_harness/sources/catalog/__init__.py +17 -0
- mostlyright/data_harness/sources/catalog/admission.py +477 -0
- mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
- mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
- mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
- mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
- mostlyright/data_harness/sources/catalog/channel.py +523 -0
- mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
- mostlyright/data_harness/sources/catalog/contracts.py +825 -0
- mostlyright/data_harness/sources/catalog/coverage.py +137 -0
- mostlyright/data_harness/sources/catalog/delta.py +1340 -0
- mostlyright/data_harness/sources/catalog/embedding.py +532 -0
- mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
- mostlyright/data_harness/sources/catalog/fill.py +3889 -0
- mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
- mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
- mostlyright/data_harness/sources/catalog/gating.py +374 -0
- mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
- mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
- mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
- mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
- mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
- mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
- mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
- mostlyright/data_harness/sources/catalog/health.py +447 -0
- mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
- mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
- mostlyright/data_harness/sources/catalog/neural.py +1618 -0
- mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
- mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
- mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
- mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
- mostlyright/data_harness/sources/catalog/recommend.py +171 -0
- mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
- mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
- mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
- mostlyright/data_harness/sources/catalog/sealed.py +560 -0
- mostlyright/data_harness/sources/catalog/search.py +230 -0
- mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
- mostlyright/data_harness/sources/catalog/update.py +891 -0
- mostlyright/data_harness/sources/collections.py +815 -0
- mostlyright/data_harness/sources/contracts.py +2223 -0
- mostlyright/data_harness/sources/deletion.py +761 -0
- mostlyright/data_harness/sources/fitness.py +162 -0
- mostlyright/data_harness/sources/governance.py +163 -0
- mostlyright/data_harness/sources/hosted.py +173 -0
- mostlyright/data_harness/sources/integration.py +218 -0
- mostlyright/data_harness/sources/range_reader.py +418 -0
- mostlyright/data_harness/sources/registry.py +514 -0
- mostlyright/data_harness/sources/rights_rule.py +59 -0
- mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
- mostlyright/data_harness/sources/sports.py +521 -0
- mostlyright/data_harness/sources/stream.py +524 -0
- mostlyright/data_harness/sources/stream_connector.py +418 -0
- mostlyright/data_harness/sources/stream_recorder.py +1404 -0
- mostlyright/data_harness/studio_boundary.py +2019 -0
- mostlyright/data_harness/thin/__init__.py +37 -0
- mostlyright/data_harness/thin/acquire.py +1137 -0
- mostlyright/data_harness/thin/acquire_cancel.py +579 -0
- mostlyright/data_harness/thin/approvals.py +617 -0
- mostlyright/data_harness/thin/commands.py +406 -0
- mostlyright/data_harness/thin/download.py +194 -0
- mostlyright/data_harness/thin/narrative.py +589 -0
- mostlyright/data_harness/thin/parity.py +1070 -0
- mostlyright/data_harness/thin/propose.py +2759 -0
- mostlyright/data_harness/thin/research.py +1663 -0
- mostlyright/data_harness/thin/router.py +924 -0
- mostlyright/data_harness/thin/runs.py +519 -0
- mostlyright/data_harness/thin/session.py +281 -0
- mostlyright/data_harness/thin/stream.py +501 -0
- mostlyright/data_harness/thin/transport.py +187 -0
- mostlyright/data_harness/thin/vocabulary.py +368 -0
- mostlyright/data_harness/thin/workers.py +164 -0
- mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
- mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
- mostlyright/data_harness/unit_flow.py +927 -0
- mostlyright/data_harness/units.py +572 -0
- mostlyright/data_harness/ux/__init__.py +9 -0
- mostlyright/data_harness/ux/approve.py +485 -0
- mostlyright/data_harness/ux/author_yaml.py +597 -0
- mostlyright/data_harness/ux/cloud_auth.py +447 -0
- mostlyright/data_harness/ux/commands/__init__.py +260 -0
- mostlyright/data_harness/ux/commands/approve.py +136 -0
- mostlyright/data_harness/ux/commands/auth.py +744 -0
- mostlyright/data_harness/ux/commands/author.py +79 -0
- mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
- mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
- mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
- mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
- mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
- mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
- mostlyright/data_harness/ux/commands/deploy.py +134 -0
- mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
- mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
- mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
- mostlyright/data_harness/ux/commands/diff.py +74 -0
- mostlyright/data_harness/ux/commands/index.py +84 -0
- mostlyright/data_harness/ux/commands/inventory.py +47 -0
- mostlyright/data_harness/ux/commands/list_builds.py +143 -0
- mostlyright/data_harness/ux/commands/login.py +63 -0
- mostlyright/data_harness/ux/commands/peek.py +236 -0
- mostlyright/data_harness/ux/commands/plan_check.py +90 -0
- mostlyright/data_harness/ux/commands/preflight.py +97 -0
- mostlyright/data_harness/ux/commands/record.py +107 -0
- mostlyright/data_harness/ux/commands/review_setup.py +47 -0
- mostlyright/data_harness/ux/commands/search.py +440 -0
- mostlyright/data_harness/ux/commands/show.py +61 -0
- mostlyright/data_harness/ux/commands/whoami.py +37 -0
- mostlyright/data_harness/ux/credential_native.py +551 -0
- mostlyright/data_harness/ux/credential_store.py +1055 -0
- mostlyright/data_harness/ux/credentials.py +631 -0
- mostlyright/data_harness/ux/diffing.py +444 -0
- mostlyright/data_harness/ux/headline.py +671 -0
- mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
- mostlyright/data_harness/ux/hosted_run_status.py +619 -0
- mostlyright/data_harness/ux/inventory.py +427 -0
- mostlyright/data_harness/ux/local_review.py +375 -0
- mostlyright/data_harness/ux/login.py +691 -0
- mostlyright/data_harness/ux/path_kind.py +147 -0
- mostlyright/data_harness/ux/peek.py +1000 -0
- mostlyright/data_harness/ux/plain_file.py +178 -0
- mostlyright/data_harness/ux/plan_check.py +311 -0
- mostlyright/data_harness/ux/preflight.py +918 -0
- mostlyright/data_harness/ux/readers.py +1124 -0
- mostlyright/data_harness/ux/remediation.py +2195 -0
- mostlyright/data_harness/ux/render.py +657 -0
- mostlyright/data_harness/ux/workload.py +1077 -0
- mostlyright/data_harness/viewer.py +3713 -0
- mostlyright/data_harness/visual_run/__init__.py +83 -0
- mostlyright/data_harness/visual_run/authoring.py +235 -0
- mostlyright/data_harness/visual_run/contracts.py +673 -0
- mostlyright/data_harness/visual_run/materialize.py +486 -0
- mostlyright/data_harness/visual_run/observations.py +874 -0
- mostlyright/data_harness/visual_run/query.py +259 -0
- mostlyright/data_harness/visual_run/reducer.py +280 -0
- mostlyright/data_harness/visual_run/sdk.py +892 -0
- mostlyright/data_harness/visual_run/store.py +584 -0
- mostlyright/data_harness/visual_run/transport.py +239 -0
- mostlyright/data_harness/watch.py +2999 -0
- mostlyright_data-0.9.0.dist-info/METADATA +607 -0
- mostlyright_data-0.9.0.dist-info/RECORD +314 -0
- mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
- 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)
|