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,1701 @@
|
|
|
1
|
+
"""Authoring time: compute the four embedding layers once and seal them.
|
|
2
|
+
|
|
3
|
+
This module is the only place in the catalog where text becomes a vector. It runs when a catalog
|
|
4
|
+
is built, never when one is queried, and the query-side load path (``sealed.py``) computes
|
|
5
|
+
nothing at all. That split is the whole of "models at authoring time only": the runtime path can
|
|
6
|
+
read sealed integers and verify digests, and there is no code on it that could do anything else.
|
|
7
|
+
|
|
8
|
+
The layer projections below are fixed and documented because they are silently load-bearing --
|
|
9
|
+
changing how an entry is rendered into text changes every vector sealed from it, and a catalog
|
|
10
|
+
whose vectors were computed under an older projection would rank differently while looking
|
|
11
|
+
identical. Treat a projection change as a re-seal of the whole catalog.
|
|
12
|
+
|
|
13
|
+
Nothing here reads a tenant's data. ``declared_columns`` and ``declared_vocabulary`` hold
|
|
14
|
+
published names only, which is what makes the ``columns`` layer a fact about a public source
|
|
15
|
+
rather than a summary of somebody's rows.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import secrets
|
|
23
|
+
import stat
|
|
24
|
+
import struct
|
|
25
|
+
import tempfile
|
|
26
|
+
from collections.abc import Iterable, Sequence
|
|
27
|
+
from dataclasses import dataclass, replace
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from mostlyright.data_harness.acquisition.url_policy import EgressPolicy
|
|
31
|
+
from mostlyright.data_harness.canonical import (
|
|
32
|
+
canonical_json_bytes,
|
|
33
|
+
parse_canonical_json,
|
|
34
|
+
sha256_bytes,
|
|
35
|
+
)
|
|
36
|
+
from mostlyright.data_harness.sources.catalog.admission import (
|
|
37
|
+
admit_public_fact_bytes,
|
|
38
|
+
require_public_source_evidence,
|
|
39
|
+
)
|
|
40
|
+
from mostlyright.data_harness.sources.catalog.contracts import (
|
|
41
|
+
CATALOG_INDEX_CONTRACT_VERSION,
|
|
42
|
+
EMBEDDING_LAYERS,
|
|
43
|
+
CatalogEntry,
|
|
44
|
+
LayerVectorReference,
|
|
45
|
+
SourceQuirk,
|
|
46
|
+
catalog_entry_from_dict,
|
|
47
|
+
)
|
|
48
|
+
from mostlyright.data_harness.sources.catalog.embedding import (
|
|
49
|
+
BackendDescriptor,
|
|
50
|
+
EmbeddingBackend,
|
|
51
|
+
)
|
|
52
|
+
from mostlyright.data_harness.sources.catalog.harvest.protocol import HarvestedRecord
|
|
53
|
+
from mostlyright.data_harness.sources.catalog.retrieval_manifest import (
|
|
54
|
+
LEXICAL_REPRESENTATION,
|
|
55
|
+
MAX_REPRESENTATION_DESCRIPTOR_BYTES,
|
|
56
|
+
NEURAL_REPRESENTATION,
|
|
57
|
+
REPRESENTATION_DESCRIPTOR_FILENAME,
|
|
58
|
+
RETRIEVAL_MANIFEST_FILENAME,
|
|
59
|
+
LoadedRetrievalCatalog,
|
|
60
|
+
LoadedRetrievalRepresentation,
|
|
61
|
+
RepresentationMember,
|
|
62
|
+
RetrievalManifestV2,
|
|
63
|
+
RetrievalRepresentation,
|
|
64
|
+
backend_descriptor_bytes,
|
|
65
|
+
backend_descriptor_sha256,
|
|
66
|
+
load_retrieval_catalog,
|
|
67
|
+
)
|
|
68
|
+
from mostlyright.data_harness.sources.contracts import (
|
|
69
|
+
RightsEvidence,
|
|
70
|
+
SourceContractError,
|
|
71
|
+
SourceObservation,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
CATALOG_FILENAME = "catalog.json"
|
|
75
|
+
VECTORS_DIRNAME = "vectors"
|
|
76
|
+
VECTOR_SUFFIX = ".vec"
|
|
77
|
+
MAX_CATALOG_MANIFEST_BYTES = 16 * 1024 * 1024
|
|
78
|
+
|
|
79
|
+
_ENTRY_ID = re.compile(r"^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class CatalogSealResult:
|
|
84
|
+
"""Exact result of one incremental durable v1 catalog seal."""
|
|
85
|
+
|
|
86
|
+
entries: tuple[CatalogEntry, ...]
|
|
87
|
+
manifest_sha256: str
|
|
88
|
+
reused_vector_sha256s: tuple[str, ...]
|
|
89
|
+
created_vector_sha256s: tuple[str, ...]
|
|
90
|
+
encoder_invocations: int
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class RetrievalSealResult:
|
|
95
|
+
"""Result of one atomic v2 outer-manifest publication."""
|
|
96
|
+
|
|
97
|
+
manifest: RetrievalManifestV2
|
|
98
|
+
manifest_sha256: str
|
|
99
|
+
lexical_entries: tuple[CatalogEntry, ...]
|
|
100
|
+
neural_entries: tuple[CatalogEntry, ...] | None
|
|
101
|
+
lexical_reused_vector_sha256s: tuple[str, ...]
|
|
102
|
+
lexical_created_vector_sha256s: tuple[str, ...]
|
|
103
|
+
neural_reused_vector_sha256s: tuple[str, ...]
|
|
104
|
+
neural_created_vector_sha256s: tuple[str, ...]
|
|
105
|
+
lexical_encoder_invocations: int
|
|
106
|
+
neural_encoder_invocations: int
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def layer_text(entry: CatalogEntry, layer: str) -> str:
|
|
110
|
+
"""Render one entry onto one retrieval layer, deterministically.
|
|
111
|
+
|
|
112
|
+
``description`` is what the source calls itself. ``metadata`` is the facts a searcher would
|
|
113
|
+
phrase a filter with. ``columns`` is the published names a searcher would name a variable
|
|
114
|
+
with. ``profiles`` is the shape and cadence a searcher would sanity-check coverage against.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
if not isinstance(entry, CatalogEntry):
|
|
118
|
+
raise SourceContractError("TYPE", "entry", "must be a CatalogEntry")
|
|
119
|
+
if layer not in EMBEDDING_LAYERS:
|
|
120
|
+
raise SourceContractError(
|
|
121
|
+
"ENUM",
|
|
122
|
+
"layer",
|
|
123
|
+
f"must be one of {list(EMBEDDING_LAYERS)}",
|
|
124
|
+
)
|
|
125
|
+
if layer == "description":
|
|
126
|
+
return f"{entry.title}\n{entry.description}"
|
|
127
|
+
if layer == "metadata":
|
|
128
|
+
return " ".join(
|
|
129
|
+
(
|
|
130
|
+
entry.publisher,
|
|
131
|
+
*entry.spatial_scope,
|
|
132
|
+
*entry.data_formats,
|
|
133
|
+
entry.access_kind,
|
|
134
|
+
entry.rights.status,
|
|
135
|
+
entry.rights.lawful_basis,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
if layer == "columns":
|
|
139
|
+
parts: list[str] = [*entry.declared_columns, *entry.declared_vocabulary]
|
|
140
|
+
for quirk in entry.quirks:
|
|
141
|
+
parts.append(quirk.code)
|
|
142
|
+
parts.append(quirk.detail)
|
|
143
|
+
return " ".join(parts)
|
|
144
|
+
observation = entry.observation
|
|
145
|
+
row_count = "unknown" if entry.declared_row_count is None else str(entry.declared_row_count)
|
|
146
|
+
return " ".join(
|
|
147
|
+
(
|
|
148
|
+
observation.historical_start,
|
|
149
|
+
observation.historical_end,
|
|
150
|
+
observation.live_status,
|
|
151
|
+
f"update_frequency_seconds {observation.update_frequency_seconds}",
|
|
152
|
+
f"publication_delay_seconds {observation.publication_delay_seconds}",
|
|
153
|
+
*entry.data_formats,
|
|
154
|
+
f"rows {row_count}",
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def seal_catalog(
|
|
160
|
+
entries: Iterable[CatalogEntry],
|
|
161
|
+
*,
|
|
162
|
+
backend: EmbeddingBackend,
|
|
163
|
+
directory: Path,
|
|
164
|
+
egress_policy: EgressPolicy,
|
|
165
|
+
schema_version: str = CATALOG_INDEX_CONTRACT_VERSION,
|
|
166
|
+
) -> tuple[CatalogEntry, ...]:
|
|
167
|
+
"""Encode every layer of every entry, write the sealed catalog, and return the sealed entries.
|
|
168
|
+
|
|
169
|
+
This function is the only writer of the shared cross-tenant artifact, so the public-source
|
|
170
|
+
check runs here too and ``egress_policy`` is required rather than optional. Composition
|
|
171
|
+
(:func:`compose_catalog_entry`) checks the harvest seam, but an entry can also reach a catalog
|
|
172
|
+
by being authored or reconstructed from a file, and a check that only one of two doors runs is
|
|
173
|
+
the "enforced by review" pattern the tenancy rule rejects. Every entry is checked before any
|
|
174
|
+
byte is written, so a refused entry cannot leave a half-written tree behind.
|
|
175
|
+
|
|
176
|
+
Vector files are content-addressed, so two entries whose layer text is identical share one
|
|
177
|
+
file. The index is sorted by ``(entry_id, entry_version)`` before serialisation, which is what
|
|
178
|
+
makes a re-seal byte-identical rather than merely equivalent.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
supplied = tuple(entries)
|
|
182
|
+
canonical_entries: list[CatalogEntry] = []
|
|
183
|
+
for index, entry in enumerate(supplied):
|
|
184
|
+
# Exact type at the boundary, followed by one immutable snapshot of the whole graph. A
|
|
185
|
+
# nested contract may itself be a subclass with a stateful to_dict; reading the original
|
|
186
|
+
# graph again would let admission inspect one projection and sealing serialize another.
|
|
187
|
+
if type(entry) is not CatalogEntry:
|
|
188
|
+
raise SourceContractError("TYPE", f"entries[{index}]", "must be a CatalogEntry")
|
|
189
|
+
captured_bytes = canonical_json_bytes(entry.to_dict())
|
|
190
|
+
admit_public_fact_bytes(captured_bytes)
|
|
191
|
+
canonical_entry = catalog_entry_from_dict(parse_canonical_json(captured_bytes))
|
|
192
|
+
if canonical_json_bytes(canonical_entry.to_dict()) != captured_bytes:
|
|
193
|
+
raise SourceContractError(
|
|
194
|
+
"CATALOG_ENTRY_PROJECTION",
|
|
195
|
+
f"entries[{index}]",
|
|
196
|
+
"the entry's captured projection cannot be rebuilt byte-for-byte as an exact "
|
|
197
|
+
"CatalogEntry; admission and sealing must inspect the same exact facts",
|
|
198
|
+
)
|
|
199
|
+
if canonical_entry.vectors:
|
|
200
|
+
raise SourceContractError(
|
|
201
|
+
"CATALOG_ALREADY_SEALED",
|
|
202
|
+
f"entries[{index}].vectors",
|
|
203
|
+
f"{canonical_entry.coordinate} already carries sealed vectors; re-sealing must "
|
|
204
|
+
"be explicit so a stale vector cannot outlive a fact change",
|
|
205
|
+
)
|
|
206
|
+
canonical_entries.append(canonical_entry)
|
|
207
|
+
materialized: Sequence[CatalogEntry] = tuple(canonical_entries)
|
|
208
|
+
if not isinstance(directory, Path):
|
|
209
|
+
raise SourceContractError("TYPE", "directory", "must be a Path")
|
|
210
|
+
for entry in materialized:
|
|
211
|
+
require_public_source_evidence(entry, egress_policy=egress_policy)
|
|
212
|
+
|
|
213
|
+
descriptor = backend.descriptor
|
|
214
|
+
vector_directory = directory / VECTORS_DIRNAME
|
|
215
|
+
vector_directory.mkdir(parents=True, exist_ok=True)
|
|
216
|
+
|
|
217
|
+
sealed: list[CatalogEntry] = []
|
|
218
|
+
for entry in materialized:
|
|
219
|
+
references: list[LayerVectorReference] = []
|
|
220
|
+
# Taken once, before any vector is written. `facts_digest` excludes `vectors`, so this is
|
|
221
|
+
# the same value the sealed entry will carry and the same one `SealedEmbeddingIndex.load`
|
|
222
|
+
# will re-derive -- which is what makes the binding checkable without re-encoding anything.
|
|
223
|
+
facts_digest = entry.facts_digest
|
|
224
|
+
for layer in EMBEDDING_LAYERS:
|
|
225
|
+
vector = backend.encode(layer_text(entry, layer))
|
|
226
|
+
if len(vector) != descriptor.dimensions:
|
|
227
|
+
raise SourceContractError(
|
|
228
|
+
"EMBEDDING_DIMENSIONS",
|
|
229
|
+
f"{entry.coordinate}.{layer}",
|
|
230
|
+
f"backend returned {len(vector)} values, expected {descriptor.dimensions}",
|
|
231
|
+
)
|
|
232
|
+
packed = struct.pack(f">{descriptor.dimensions}i", *vector)
|
|
233
|
+
digest = sha256_bytes(packed)
|
|
234
|
+
_write_vector(vector_directory / f"{digest}{VECTOR_SUFFIX}", packed)
|
|
235
|
+
references.append(
|
|
236
|
+
LayerVectorReference(
|
|
237
|
+
layer=layer,
|
|
238
|
+
backend_id=descriptor.backend_id,
|
|
239
|
+
backend_version=descriptor.backend_version,
|
|
240
|
+
dimensions=descriptor.dimensions,
|
|
241
|
+
quantization=descriptor.quantization,
|
|
242
|
+
vector_sha256=digest,
|
|
243
|
+
entry_facts_digest=facts_digest,
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
sealed.append(replace(entry, vectors=tuple(references)))
|
|
247
|
+
|
|
248
|
+
ordered = tuple(sorted(sealed, key=lambda item: (item.entry_id, item.entry_version)))
|
|
249
|
+
index_bytes = canonical_json_bytes(
|
|
250
|
+
{
|
|
251
|
+
"schema_version": schema_version,
|
|
252
|
+
"entries": [item.to_dict() for item in ordered],
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
_atomic_write(directory / CATALOG_FILENAME, index_bytes)
|
|
256
|
+
return ordered
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def seal_retrieval_catalog(
|
|
260
|
+
entries: Iterable[CatalogEntry],
|
|
261
|
+
*,
|
|
262
|
+
lexical_backend: EmbeddingBackend,
|
|
263
|
+
neural_backend: EmbeddingBackend | None,
|
|
264
|
+
directory: Path,
|
|
265
|
+
egress_policy: EgressPolicy,
|
|
266
|
+
fallback_reason: str | None = None,
|
|
267
|
+
expected_existing_manifest_sha256: str | None = None,
|
|
268
|
+
) -> RetrievalSealResult:
|
|
269
|
+
"""Seal independent v1 representations and publish one explicit v2 head.
|
|
270
|
+
|
|
271
|
+
Both representations are derived from one captured tuple of canonical facts. Their immutable
|
|
272
|
+
generations are complete and read back before ``retrieval.json`` is replaced, making the
|
|
273
|
+
outer replacement the sole public linearization point.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
supplied = tuple(entries)
|
|
277
|
+
captured = tuple(
|
|
278
|
+
_capture_entry(entry, index=index, sealed=False) for index, entry in enumerate(supplied)
|
|
279
|
+
)
|
|
280
|
+
ordered = tuple(sorted(captured, key=_entry_key))
|
|
281
|
+
if len({entry.entry_id for entry in ordered}) != len(ordered):
|
|
282
|
+
raise SourceContractError(
|
|
283
|
+
"DUPLICATE", "retrieval.entries", "a retrieval catalog has one entry per source"
|
|
284
|
+
)
|
|
285
|
+
if neural_backend is None and fallback_reason is None:
|
|
286
|
+
fallback_reason = "NEURAL_NOT_REQUESTED"
|
|
287
|
+
if neural_backend is not None and fallback_reason is not None:
|
|
288
|
+
raise SourceContractError(
|
|
289
|
+
"RETRIEVAL_FALLBACK",
|
|
290
|
+
"fallback_reason",
|
|
291
|
+
"a hybrid seal cannot also claim lexical-only fallback",
|
|
292
|
+
)
|
|
293
|
+
for entry in ordered:
|
|
294
|
+
require_public_source_evidence(entry, egress_policy=egress_policy)
|
|
295
|
+
|
|
296
|
+
shared_facts_sha256 = _shared_facts_sha256(ordered)
|
|
297
|
+
trusted_resume = _load_trusted_resume_catalog(
|
|
298
|
+
directory=directory,
|
|
299
|
+
expected_manifest_sha256=expected_existing_manifest_sha256,
|
|
300
|
+
expected_facts=ordered,
|
|
301
|
+
lexical_backend=lexical_backend,
|
|
302
|
+
neural_backend=neural_backend,
|
|
303
|
+
fallback_reason=fallback_reason,
|
|
304
|
+
)
|
|
305
|
+
if trusted_resume is not None:
|
|
306
|
+
return _result_from_trusted_resume(trusted_resume)
|
|
307
|
+
lexical_generation = _representation_generation(shared_facts_sha256, lexical_backend.descriptor)
|
|
308
|
+
lexical_directory = directory / LEXICAL_REPRESENTATION / lexical_generation
|
|
309
|
+
lexical_result = _seal_representation_generation(
|
|
310
|
+
reused_entries=(),
|
|
311
|
+
fresh_entries=ordered,
|
|
312
|
+
backend=lexical_backend,
|
|
313
|
+
directory=lexical_directory,
|
|
314
|
+
predecessor_directory=None,
|
|
315
|
+
predecessor_manifest_sha256=None,
|
|
316
|
+
egress_policy=egress_policy,
|
|
317
|
+
)
|
|
318
|
+
lexical_representation = _representation_from_seal(
|
|
319
|
+
root=directory,
|
|
320
|
+
name=LEXICAL_REPRESENTATION,
|
|
321
|
+
generation=lexical_generation,
|
|
322
|
+
descriptor=lexical_backend.descriptor,
|
|
323
|
+
result=lexical_result,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
neural_result: CatalogSealResult | None = None
|
|
327
|
+
representations = [lexical_representation]
|
|
328
|
+
if neural_backend is not None:
|
|
329
|
+
neural_generation = _representation_generation(
|
|
330
|
+
shared_facts_sha256, neural_backend.descriptor
|
|
331
|
+
)
|
|
332
|
+
neural_directory = directory / NEURAL_REPRESENTATION / neural_generation
|
|
333
|
+
neural_result = _seal_representation_generation(
|
|
334
|
+
reused_entries=(),
|
|
335
|
+
fresh_entries=ordered,
|
|
336
|
+
backend=neural_backend,
|
|
337
|
+
directory=neural_directory,
|
|
338
|
+
predecessor_directory=None,
|
|
339
|
+
predecessor_manifest_sha256=None,
|
|
340
|
+
egress_policy=egress_policy,
|
|
341
|
+
)
|
|
342
|
+
representations.append(
|
|
343
|
+
_representation_from_seal(
|
|
344
|
+
root=directory,
|
|
345
|
+
name=NEURAL_REPRESENTATION,
|
|
346
|
+
generation=neural_generation,
|
|
347
|
+
descriptor=neural_backend.descriptor,
|
|
348
|
+
result=neural_result,
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
manifest = RetrievalManifestV2(
|
|
353
|
+
shared_facts_sha256=shared_facts_sha256,
|
|
354
|
+
representations=tuple(representations),
|
|
355
|
+
fallback_reason=fallback_reason,
|
|
356
|
+
)
|
|
357
|
+
# Reparse before publication so a writer bug cannot create a public envelope the reader would
|
|
358
|
+
# reject. All referenced members have already been durably installed and read back.
|
|
359
|
+
if RetrievalManifestV2.from_bytes(manifest.canonical_bytes) != manifest:
|
|
360
|
+
raise SourceContractError(
|
|
361
|
+
"RETRIEVAL_MANIFEST_READBACK", RETRIEVAL_MANIFEST_FILENAME, "reparse differs"
|
|
362
|
+
)
|
|
363
|
+
_publish_retrieval_manifest(directory, manifest.canonical_bytes)
|
|
364
|
+
return RetrievalSealResult(
|
|
365
|
+
manifest=manifest,
|
|
366
|
+
manifest_sha256=manifest.digest,
|
|
367
|
+
lexical_entries=lexical_result.entries,
|
|
368
|
+
neural_entries=None if neural_result is None else neural_result.entries,
|
|
369
|
+
lexical_reused_vector_sha256s=lexical_result.reused_vector_sha256s,
|
|
370
|
+
lexical_created_vector_sha256s=lexical_result.created_vector_sha256s,
|
|
371
|
+
neural_reused_vector_sha256s=()
|
|
372
|
+
if neural_result is None
|
|
373
|
+
else neural_result.reused_vector_sha256s,
|
|
374
|
+
neural_created_vector_sha256s=()
|
|
375
|
+
if neural_result is None
|
|
376
|
+
else neural_result.created_vector_sha256s,
|
|
377
|
+
lexical_encoder_invocations=lexical_result.encoder_invocations,
|
|
378
|
+
neural_encoder_invocations=0
|
|
379
|
+
if neural_result is None
|
|
380
|
+
else neural_result.encoder_invocations,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def seal_retrieval_catalog_successor(
|
|
385
|
+
*,
|
|
386
|
+
lexical_reused_entries: tuple[CatalogEntry, ...],
|
|
387
|
+
fresh_entries: tuple[CatalogEntry, ...],
|
|
388
|
+
lexical_backend: EmbeddingBackend,
|
|
389
|
+
neural_reused_entries: tuple[CatalogEntry, ...],
|
|
390
|
+
neural_backend: EmbeddingBackend | None,
|
|
391
|
+
directory: Path,
|
|
392
|
+
predecessor_retrieval_directory: Path | None,
|
|
393
|
+
predecessor_retrieval_manifest_sha256: str | None,
|
|
394
|
+
egress_policy: EgressPolicy,
|
|
395
|
+
fallback_reason: str | None = None,
|
|
396
|
+
expected_existing_manifest_sha256: str | None = None,
|
|
397
|
+
) -> RetrievalSealResult:
|
|
398
|
+
"""Seal a v2 successor while preserving exact predecessor members for unchanged facts."""
|
|
399
|
+
|
|
400
|
+
if neural_backend is None:
|
|
401
|
+
if neural_reused_entries:
|
|
402
|
+
raise SourceContractError(
|
|
403
|
+
"RETRIEVAL_NEURAL_REUSE",
|
|
404
|
+
"retrieval.neural_reused_entries",
|
|
405
|
+
"neural reuse requires an explicit exact backend",
|
|
406
|
+
)
|
|
407
|
+
if fallback_reason is None:
|
|
408
|
+
fallback_reason = "NEURAL_NOT_REQUESTED"
|
|
409
|
+
elif fallback_reason is not None:
|
|
410
|
+
raise SourceContractError(
|
|
411
|
+
"RETRIEVAL_FALLBACK",
|
|
412
|
+
"fallback_reason",
|
|
413
|
+
"a hybrid successor cannot also claim lexical-only fallback",
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
lexical_facts = tuple(sorted((*lexical_reused_entries, *fresh_entries), key=_entry_key))
|
|
417
|
+
neural_reused_ids = {entry.entry_id for entry in neural_reused_entries}
|
|
418
|
+
neural_fresh = tuple(
|
|
419
|
+
replace(entry, vectors=())
|
|
420
|
+
for entry in lexical_facts
|
|
421
|
+
if entry.entry_id not in neural_reused_ids
|
|
422
|
+
)
|
|
423
|
+
if len(neural_reused_ids) != len(neural_reused_entries):
|
|
424
|
+
raise SourceContractError(
|
|
425
|
+
"DUPLICATE", "retrieval.neural_reused_entries", "entry ids must be unique"
|
|
426
|
+
)
|
|
427
|
+
lexical_reused_facts = tuple(
|
|
428
|
+
(entry.coordinate, entry.facts_digest) for entry in lexical_reused_entries
|
|
429
|
+
)
|
|
430
|
+
neural_reused_facts = tuple(
|
|
431
|
+
(entry.coordinate, entry.facts_digest) for entry in neural_reused_entries
|
|
432
|
+
)
|
|
433
|
+
if neural_reused_facts and neural_reused_facts != lexical_reused_facts:
|
|
434
|
+
raise SourceContractError(
|
|
435
|
+
"RETRIEVAL_SHARED_FACTS",
|
|
436
|
+
"retrieval.neural_reused_entries",
|
|
437
|
+
"neural reused entries must exactly match lexical reuse order and facts",
|
|
438
|
+
)
|
|
439
|
+
for entry in lexical_facts:
|
|
440
|
+
require_public_source_evidence(entry, egress_policy=egress_policy)
|
|
441
|
+
trusted_predecessor = None
|
|
442
|
+
if lexical_reused_entries or neural_reused_entries:
|
|
443
|
+
trusted_predecessor = _capture_trusted_retrieval_predecessor(
|
|
444
|
+
predecessor_directory=predecessor_retrieval_directory,
|
|
445
|
+
predecessor_manifest_sha256=predecessor_retrieval_manifest_sha256,
|
|
446
|
+
lexical_reused_entries=lexical_reused_entries,
|
|
447
|
+
neural_reused_entries=neural_reused_entries,
|
|
448
|
+
lexical_backend=lexical_backend,
|
|
449
|
+
neural_backend=neural_backend,
|
|
450
|
+
)
|
|
451
|
+
trusted_resume = _load_trusted_resume_catalog(
|
|
452
|
+
directory=directory,
|
|
453
|
+
expected_manifest_sha256=expected_existing_manifest_sha256,
|
|
454
|
+
expected_facts=lexical_facts,
|
|
455
|
+
lexical_backend=lexical_backend,
|
|
456
|
+
neural_backend=neural_backend,
|
|
457
|
+
fallback_reason=fallback_reason,
|
|
458
|
+
)
|
|
459
|
+
if trusted_resume is not None:
|
|
460
|
+
return _result_from_trusted_resume(trusted_resume)
|
|
461
|
+
shared_facts_sha256 = _shared_facts_sha256(lexical_facts)
|
|
462
|
+
|
|
463
|
+
lexical_generation = _representation_generation(shared_facts_sha256, lexical_backend.descriptor)
|
|
464
|
+
lexical_directory = directory / LEXICAL_REPRESENTATION / lexical_generation
|
|
465
|
+
if trusted_predecessor is None:
|
|
466
|
+
lexical_result = _seal_representation_generation(
|
|
467
|
+
reused_entries=(),
|
|
468
|
+
fresh_entries=fresh_entries,
|
|
469
|
+
backend=lexical_backend,
|
|
470
|
+
directory=lexical_directory,
|
|
471
|
+
predecessor_directory=None,
|
|
472
|
+
predecessor_manifest_sha256=None,
|
|
473
|
+
egress_policy=egress_policy,
|
|
474
|
+
)
|
|
475
|
+
else:
|
|
476
|
+
with tempfile.TemporaryDirectory(prefix="mr-lexical-predecessor-") as snapshot_raw:
|
|
477
|
+
snapshot_directory, snapshot_digest = _materialize_representation_snapshot(
|
|
478
|
+
trusted_predecessor.lexical,
|
|
479
|
+
directory=Path(snapshot_raw),
|
|
480
|
+
)
|
|
481
|
+
lexical_result = _seal_representation_generation(
|
|
482
|
+
reused_entries=lexical_reused_entries,
|
|
483
|
+
fresh_entries=fresh_entries,
|
|
484
|
+
backend=lexical_backend,
|
|
485
|
+
directory=lexical_directory,
|
|
486
|
+
predecessor_directory=snapshot_directory,
|
|
487
|
+
predecessor_manifest_sha256=snapshot_digest,
|
|
488
|
+
egress_policy=egress_policy,
|
|
489
|
+
)
|
|
490
|
+
representations = [
|
|
491
|
+
_representation_from_seal(
|
|
492
|
+
root=directory,
|
|
493
|
+
name=LEXICAL_REPRESENTATION,
|
|
494
|
+
generation=lexical_generation,
|
|
495
|
+
descriptor=lexical_backend.descriptor,
|
|
496
|
+
result=lexical_result,
|
|
497
|
+
)
|
|
498
|
+
]
|
|
499
|
+
|
|
500
|
+
neural_result: CatalogSealResult | None = None
|
|
501
|
+
if neural_backend is not None:
|
|
502
|
+
neural_generation = _representation_generation(
|
|
503
|
+
shared_facts_sha256, neural_backend.descriptor
|
|
504
|
+
)
|
|
505
|
+
neural_directory = directory / NEURAL_REPRESENTATION / neural_generation
|
|
506
|
+
trusted_neural = None if trusted_predecessor is None else trusted_predecessor.neural
|
|
507
|
+
if trusted_neural is None:
|
|
508
|
+
neural_result = _seal_representation_generation(
|
|
509
|
+
reused_entries=(),
|
|
510
|
+
fresh_entries=neural_fresh,
|
|
511
|
+
backend=neural_backend,
|
|
512
|
+
directory=neural_directory,
|
|
513
|
+
predecessor_directory=None,
|
|
514
|
+
predecessor_manifest_sha256=None,
|
|
515
|
+
egress_policy=egress_policy,
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
with tempfile.TemporaryDirectory(prefix="mr-neural-predecessor-") as snapshot_raw:
|
|
519
|
+
snapshot_directory, snapshot_digest = _materialize_representation_snapshot(
|
|
520
|
+
trusted_neural,
|
|
521
|
+
directory=Path(snapshot_raw),
|
|
522
|
+
)
|
|
523
|
+
neural_result = _seal_representation_generation(
|
|
524
|
+
reused_entries=neural_reused_entries,
|
|
525
|
+
fresh_entries=neural_fresh,
|
|
526
|
+
backend=neural_backend,
|
|
527
|
+
directory=neural_directory,
|
|
528
|
+
predecessor_directory=snapshot_directory,
|
|
529
|
+
predecessor_manifest_sha256=snapshot_digest,
|
|
530
|
+
egress_policy=egress_policy,
|
|
531
|
+
)
|
|
532
|
+
if tuple(
|
|
533
|
+
(entry.coordinate, entry.facts_digest) for entry in neural_result.entries
|
|
534
|
+
) != tuple((entry.coordinate, entry.facts_digest) for entry in lexical_result.entries):
|
|
535
|
+
raise SourceContractError(
|
|
536
|
+
"RETRIEVAL_SHARED_FACTS",
|
|
537
|
+
"retrieval.neural",
|
|
538
|
+
"neural successor differs from lexical canonical facts",
|
|
539
|
+
)
|
|
540
|
+
representations.append(
|
|
541
|
+
_representation_from_seal(
|
|
542
|
+
root=directory,
|
|
543
|
+
name=NEURAL_REPRESENTATION,
|
|
544
|
+
generation=neural_generation,
|
|
545
|
+
descriptor=neural_backend.descriptor,
|
|
546
|
+
result=neural_result,
|
|
547
|
+
)
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
manifest = RetrievalManifestV2(
|
|
551
|
+
shared_facts_sha256=shared_facts_sha256,
|
|
552
|
+
representations=tuple(representations),
|
|
553
|
+
fallback_reason=fallback_reason,
|
|
554
|
+
)
|
|
555
|
+
if RetrievalManifestV2.from_bytes(manifest.canonical_bytes) != manifest:
|
|
556
|
+
raise SourceContractError(
|
|
557
|
+
"RETRIEVAL_MANIFEST_READBACK", RETRIEVAL_MANIFEST_FILENAME, "reparse differs"
|
|
558
|
+
)
|
|
559
|
+
_publish_retrieval_manifest(directory, manifest.canonical_bytes)
|
|
560
|
+
return RetrievalSealResult(
|
|
561
|
+
manifest=manifest,
|
|
562
|
+
manifest_sha256=manifest.digest,
|
|
563
|
+
lexical_entries=lexical_result.entries,
|
|
564
|
+
neural_entries=None if neural_result is None else neural_result.entries,
|
|
565
|
+
lexical_reused_vector_sha256s=lexical_result.reused_vector_sha256s,
|
|
566
|
+
lexical_created_vector_sha256s=lexical_result.created_vector_sha256s,
|
|
567
|
+
neural_reused_vector_sha256s=()
|
|
568
|
+
if neural_result is None
|
|
569
|
+
else neural_result.reused_vector_sha256s,
|
|
570
|
+
neural_created_vector_sha256s=()
|
|
571
|
+
if neural_result is None
|
|
572
|
+
else neural_result.created_vector_sha256s,
|
|
573
|
+
lexical_encoder_invocations=lexical_result.encoder_invocations,
|
|
574
|
+
neural_encoder_invocations=0
|
|
575
|
+
if neural_result is None
|
|
576
|
+
else neural_result.encoder_invocations,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def compose_catalog_entry(
|
|
581
|
+
*,
|
|
582
|
+
record: HarvestedRecord,
|
|
583
|
+
rights: RightsEvidence,
|
|
584
|
+
observation: SourceObservation,
|
|
585
|
+
spatial_scope: tuple[str, ...],
|
|
586
|
+
access_kind: str,
|
|
587
|
+
authentication_required: bool,
|
|
588
|
+
entry_id: str,
|
|
589
|
+
egress_policy: EgressPolicy,
|
|
590
|
+
data_formats: tuple[str, ...] | None = None,
|
|
591
|
+
quirks: tuple[SourceQuirk, ...] = (),
|
|
592
|
+
declared_columns: tuple[str, ...] = (),
|
|
593
|
+
declared_vocabulary: tuple[str, ...] = (),
|
|
594
|
+
declared_row_count: int | None = None,
|
|
595
|
+
) -> CatalogEntry:
|
|
596
|
+
"""Turn one harvested record into a first-version catalog entry, publicly cited.
|
|
597
|
+
|
|
598
|
+
This is the seam where a stranger's document becomes shared knowledge, so it is where the
|
|
599
|
+
public-source checks actually run. Returning an unchecked entry and expecting the caller to
|
|
600
|
+
validate it is the "enforced by review" pattern the tenancy rule rejects.
|
|
601
|
+
|
|
602
|
+
``entry_id`` is supplied rather than derived from ``record.record_id``: publisher ids do not
|
|
603
|
+
satisfy the harness identifier grammar in general, and silently transliterating one would make
|
|
604
|
+
the ``faostat_qcl@v3`` coordinate unstable across harvests. The author chooses the id; the
|
|
605
|
+
record is the cited evidence for it.
|
|
606
|
+
|
|
607
|
+
``data_formats`` defaults to what the record declares and exists because some protocols declare
|
|
608
|
+
nothing. An SDMX structure message describes dataflows, not payloads, so every SDMX record
|
|
609
|
+
carries ``data_formats=()``; a CKAN package whose resources name only formats outside the closed
|
|
610
|
+
vocabulary carries the same. Without an override those records could not be composed at all, and
|
|
611
|
+
the only remaining route into a catalog would be hand-building a ``CatalogEntry`` -- the
|
|
612
|
+
unchecked route this seam exists to replace. Supplying formats is an author's assertion about
|
|
613
|
+
what the source really publishes, and the record stays cited as the evidence for the rest of the
|
|
614
|
+
entry. When neither the record nor the author supplies one, composition refuses and says so.
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
if not isinstance(record, HarvestedRecord):
|
|
618
|
+
raise SourceContractError("TYPE", "record", "must be a HarvestedRecord")
|
|
619
|
+
if not isinstance(entry_id, str) or not _ENTRY_ID.fullmatch(entry_id):
|
|
620
|
+
raise SourceContractError(
|
|
621
|
+
"CATALOG_ENTRY_ID",
|
|
622
|
+
"entry_id",
|
|
623
|
+
f"{entry_id!r} is not a valid entry identifier for record {record.record_id!r}; "
|
|
624
|
+
"an entry id is chosen by the author, never transliterated from a publisher id",
|
|
625
|
+
)
|
|
626
|
+
if data_formats is not None and not isinstance(data_formats, tuple):
|
|
627
|
+
raise SourceContractError("TYPE", "data_formats", "must be a tuple of format names")
|
|
628
|
+
resolved_formats = record.data_formats if data_formats is None else data_formats
|
|
629
|
+
if not resolved_formats:
|
|
630
|
+
raise SourceContractError(
|
|
631
|
+
"CATALOG_ENTRY_FORMATS",
|
|
632
|
+
"data_formats",
|
|
633
|
+
f"record {record.record_id!r} on protocol {record.protocol!r} declares no payload "
|
|
634
|
+
"format the harness can read, so the author must supply data_formats explicitly; a "
|
|
635
|
+
"structure or metadata document describes a source without describing its payload",
|
|
636
|
+
)
|
|
637
|
+
entry = CatalogEntry(
|
|
638
|
+
entry_id=entry_id,
|
|
639
|
+
entry_version=1,
|
|
640
|
+
publisher=record.publisher,
|
|
641
|
+
title=record.title,
|
|
642
|
+
description=record.description,
|
|
643
|
+
spatial_scope=spatial_scope,
|
|
644
|
+
data_formats=resolved_formats,
|
|
645
|
+
access_kind=access_kind,
|
|
646
|
+
authentication_required=authentication_required,
|
|
647
|
+
observation=observation,
|
|
648
|
+
rights=rights,
|
|
649
|
+
quirks=quirks,
|
|
650
|
+
declared_columns=declared_columns,
|
|
651
|
+
declared_vocabulary=declared_vocabulary,
|
|
652
|
+
declared_row_count=declared_row_count,
|
|
653
|
+
evidence=(record.evidence,),
|
|
654
|
+
vectors=(),
|
|
655
|
+
previous_digest=None,
|
|
656
|
+
)
|
|
657
|
+
require_public_source_evidence(entry, egress_policy=egress_policy)
|
|
658
|
+
return entry
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def seal_catalog_successor(
|
|
662
|
+
*,
|
|
663
|
+
reused_entries: tuple[CatalogEntry, ...],
|
|
664
|
+
fresh_entries: tuple[CatalogEntry, ...],
|
|
665
|
+
backend: EmbeddingBackend,
|
|
666
|
+
directory: Path,
|
|
667
|
+
predecessor_directory: Path | None,
|
|
668
|
+
predecessor_manifest_sha256: str | None,
|
|
669
|
+
egress_policy: EgressPolicy,
|
|
670
|
+
) -> CatalogSealResult:
|
|
671
|
+
"""Durably publish one complete v1 current-head manifest.
|
|
672
|
+
|
|
673
|
+
Existing sealed entries retain their exact vector references and bytes. New and changed
|
|
674
|
+
unsealed entries are encoded once per layer. Every member is installed through retained root
|
|
675
|
+
descriptors with exclusive no-follow creation, exact readback, and mode ``0444`` before the
|
|
676
|
+
manifest is atomically replaced and read back.
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
if not isinstance(directory, Path):
|
|
680
|
+
raise SourceContractError("TYPE", "directory", "must be a Path")
|
|
681
|
+
if predecessor_directory is not None and not isinstance(predecessor_directory, Path):
|
|
682
|
+
raise SourceContractError("TYPE", "predecessor_directory", "must be a Path or None")
|
|
683
|
+
if not isinstance(reused_entries, tuple) or not isinstance(fresh_entries, tuple):
|
|
684
|
+
raise SourceContractError(
|
|
685
|
+
"TYPE", "catalog.successor.entries", "entry collections must be tuples"
|
|
686
|
+
)
|
|
687
|
+
captured_reused = tuple(
|
|
688
|
+
_capture_entry(entry, index=index, sealed=True)
|
|
689
|
+
for index, entry in enumerate(reused_entries)
|
|
690
|
+
)
|
|
691
|
+
captured_fresh = tuple(
|
|
692
|
+
_capture_entry(entry, index=index, sealed=False)
|
|
693
|
+
for index, entry in enumerate(fresh_entries)
|
|
694
|
+
)
|
|
695
|
+
all_ids = [entry.entry_id for entry in (*captured_reused, *captured_fresh)]
|
|
696
|
+
if len(set(all_ids)) != len(all_ids):
|
|
697
|
+
raise SourceContractError(
|
|
698
|
+
"DUPLICATE", "catalog.entries", "a current-head catalog has one entry per source"
|
|
699
|
+
)
|
|
700
|
+
for entry in (*captured_reused, *captured_fresh):
|
|
701
|
+
require_public_source_evidence(entry, egress_policy=egress_policy)
|
|
702
|
+
|
|
703
|
+
_refuse_build_ancestor(directory)
|
|
704
|
+
root_fd, publication_fds, publication_chain = _open_or_create_catalog_root(directory)
|
|
705
|
+
source_root_fd = -1
|
|
706
|
+
vector_fd = -1
|
|
707
|
+
source_vector_fd = -1
|
|
708
|
+
try:
|
|
709
|
+
_verify_publication_chain(publication_chain)
|
|
710
|
+
_refuse_build_root(root_fd)
|
|
711
|
+
try:
|
|
712
|
+
os.mkdir(VECTORS_DIRNAME, mode=0o755, dir_fd=root_fd)
|
|
713
|
+
except FileExistsError:
|
|
714
|
+
pass
|
|
715
|
+
vector_fd = _open_directory_at(root_fd, VECTORS_DIRNAME, "catalog vectors")
|
|
716
|
+
|
|
717
|
+
if predecessor_manifest_sha256 is None:
|
|
718
|
+
if captured_reused:
|
|
719
|
+
raise SourceContractError(
|
|
720
|
+
"CATALOG_PREDECESSOR",
|
|
721
|
+
"catalog.predecessor",
|
|
722
|
+
"reused entries require an exact predecessor manifest digest",
|
|
723
|
+
)
|
|
724
|
+
else:
|
|
725
|
+
if predecessor_directory is None:
|
|
726
|
+
raise SourceContractError(
|
|
727
|
+
"CATALOG_PREDECESSOR",
|
|
728
|
+
"catalog.predecessor",
|
|
729
|
+
"an exact predecessor directory is required",
|
|
730
|
+
)
|
|
731
|
+
source_root_fd = _open_directory_nofollow(
|
|
732
|
+
predecessor_directory, "catalog predecessor root"
|
|
733
|
+
)
|
|
734
|
+
predecessor_raw = _read_regular_at(
|
|
735
|
+
source_root_fd,
|
|
736
|
+
CATALOG_FILENAME,
|
|
737
|
+
maximum=MAX_CATALOG_MANIFEST_BYTES,
|
|
738
|
+
label="catalog predecessor manifest",
|
|
739
|
+
)
|
|
740
|
+
if sha256_bytes(predecessor_raw) != predecessor_manifest_sha256:
|
|
741
|
+
raise SourceContractError(
|
|
742
|
+
"CATALOG_PREDECESSOR",
|
|
743
|
+
"catalog.predecessor",
|
|
744
|
+
"predecessor manifest digest differs",
|
|
745
|
+
)
|
|
746
|
+
previous = _entries_from_manifest(predecessor_raw)
|
|
747
|
+
previous_by_id = {entry.entry_id: entry for entry in previous}
|
|
748
|
+
for entry in captured_reused:
|
|
749
|
+
observed = previous_by_id.get(entry.entry_id)
|
|
750
|
+
if observed is None or observed.to_dict() != entry.to_dict():
|
|
751
|
+
raise SourceContractError(
|
|
752
|
+
"CATALOG_PREDECESSOR",
|
|
753
|
+
f"catalog.predecessor.{entry.entry_id}",
|
|
754
|
+
"reused entry is not the exact predecessor member",
|
|
755
|
+
)
|
|
756
|
+
source_vector_fd = _open_directory_at(
|
|
757
|
+
source_root_fd, VECTORS_DIRNAME, "catalog predecessor vectors"
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
descriptor = backend.descriptor
|
|
761
|
+
payloads: dict[str, bytes] = {}
|
|
762
|
+
reused_digests: set[str] = set()
|
|
763
|
+
for entry in captured_reused:
|
|
764
|
+
for reference in entry.vectors:
|
|
765
|
+
if (
|
|
766
|
+
reference.backend_id != descriptor.backend_id
|
|
767
|
+
or reference.backend_version != descriptor.backend_version
|
|
768
|
+
or reference.dimensions != descriptor.dimensions
|
|
769
|
+
or reference.quantization != descriptor.quantization
|
|
770
|
+
):
|
|
771
|
+
raise SourceContractError(
|
|
772
|
+
"CATALOG_BACKEND_IDENTITY",
|
|
773
|
+
f"catalog.entries.{entry.entry_id}",
|
|
774
|
+
"reused vectors do not match the selected backend",
|
|
775
|
+
)
|
|
776
|
+
raw = _read_regular_at(
|
|
777
|
+
source_vector_fd,
|
|
778
|
+
f"{reference.vector_sha256}{VECTOR_SUFFIX}",
|
|
779
|
+
maximum=descriptor.dimensions * 4,
|
|
780
|
+
label=f"reused vector {reference.vector_sha256}",
|
|
781
|
+
)
|
|
782
|
+
if (
|
|
783
|
+
len(raw) != descriptor.dimensions * 4
|
|
784
|
+
or sha256_bytes(raw) != reference.vector_sha256
|
|
785
|
+
):
|
|
786
|
+
raise SourceContractError(
|
|
787
|
+
"CATALOG_VECTOR_DIGEST",
|
|
788
|
+
reference.vector_sha256,
|
|
789
|
+
"reused vector bytes differ from their content address",
|
|
790
|
+
)
|
|
791
|
+
prior = payloads.setdefault(reference.vector_sha256, raw)
|
|
792
|
+
if prior != raw:
|
|
793
|
+
raise SourceContractError(
|
|
794
|
+
"CATALOG_VECTOR_COLLISION",
|
|
795
|
+
reference.vector_sha256,
|
|
796
|
+
"one content address resolved to different bytes",
|
|
797
|
+
)
|
|
798
|
+
reused_digests.add(reference.vector_sha256)
|
|
799
|
+
|
|
800
|
+
sealed_fresh: list[CatalogEntry] = []
|
|
801
|
+
encoder_invocations = 0
|
|
802
|
+
for entry in captured_fresh:
|
|
803
|
+
facts_digest = entry.facts_digest
|
|
804
|
+
references: list[LayerVectorReference] = []
|
|
805
|
+
for layer in EMBEDDING_LAYERS:
|
|
806
|
+
vector = backend.encode(layer_text(entry, layer))
|
|
807
|
+
encoder_invocations += 1
|
|
808
|
+
if len(vector) != descriptor.dimensions:
|
|
809
|
+
raise SourceContractError(
|
|
810
|
+
"EMBEDDING_DIMENSIONS",
|
|
811
|
+
f"{entry.coordinate}.{layer}",
|
|
812
|
+
f"backend returned {len(vector)} values, expected {descriptor.dimensions}",
|
|
813
|
+
)
|
|
814
|
+
packed = struct.pack(f">{descriptor.dimensions}i", *vector)
|
|
815
|
+
digest = sha256_bytes(packed)
|
|
816
|
+
prior = payloads.setdefault(digest, packed)
|
|
817
|
+
if prior != packed:
|
|
818
|
+
raise SourceContractError(
|
|
819
|
+
"CATALOG_VECTOR_COLLISION",
|
|
820
|
+
digest,
|
|
821
|
+
"one content address resolved to different bytes",
|
|
822
|
+
)
|
|
823
|
+
references.append(
|
|
824
|
+
LayerVectorReference(
|
|
825
|
+
layer=layer,
|
|
826
|
+
backend_id=descriptor.backend_id,
|
|
827
|
+
backend_version=descriptor.backend_version,
|
|
828
|
+
dimensions=descriptor.dimensions,
|
|
829
|
+
quantization=descriptor.quantization,
|
|
830
|
+
vector_sha256=digest,
|
|
831
|
+
entry_facts_digest=facts_digest,
|
|
832
|
+
)
|
|
833
|
+
)
|
|
834
|
+
sealed_fresh.append(replace(entry, vectors=tuple(references)))
|
|
835
|
+
|
|
836
|
+
ordered = tuple(
|
|
837
|
+
sorted((*captured_reused, *sealed_fresh), key=lambda item: _entry_key(item))
|
|
838
|
+
)
|
|
839
|
+
index_bytes = canonical_json_bytes(
|
|
840
|
+
{
|
|
841
|
+
"schema_version": CATALOG_INDEX_CONTRACT_VERSION,
|
|
842
|
+
"entries": [item.to_dict() for item in ordered],
|
|
843
|
+
}
|
|
844
|
+
)
|
|
845
|
+
# Strict reconstruction before the first output mutation makes the manifest a checked
|
|
846
|
+
# successor, not merely canonical bytes produced by this process.
|
|
847
|
+
if _entries_from_manifest(index_bytes) != ordered:
|
|
848
|
+
raise SourceContractError(
|
|
849
|
+
"CATALOG_ENTRY_PROJECTION", "catalog.entries", "manifest reconstruction differs"
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
for digest in sorted(payloads):
|
|
853
|
+
_install_immutable_member(
|
|
854
|
+
vector_fd,
|
|
855
|
+
f"{digest}{VECTOR_SUFFIX}",
|
|
856
|
+
payloads[digest],
|
|
857
|
+
digest=digest,
|
|
858
|
+
)
|
|
859
|
+
_catalog_durability_barrier(vector_fd, "vectors-directory")
|
|
860
|
+
manifest_digest = sha256_bytes(index_bytes)
|
|
861
|
+
temporary = f".{CATALOG_FILENAME}.tmp-{secrets.token_hex(16)}"
|
|
862
|
+
try:
|
|
863
|
+
manifest_fd = _create_regular_at(root_fd, temporary, mode=0o600)
|
|
864
|
+
try:
|
|
865
|
+
_write_all(manifest_fd, index_bytes)
|
|
866
|
+
_catalog_durability_barrier(manifest_fd, "manifest-temporary")
|
|
867
|
+
if _read_open_descriptor(manifest_fd, maximum=len(index_bytes)) != index_bytes:
|
|
868
|
+
raise SourceContractError(
|
|
869
|
+
"CATALOG_MANIFEST_READBACK",
|
|
870
|
+
CATALOG_FILENAME,
|
|
871
|
+
"temporary manifest readback differs",
|
|
872
|
+
)
|
|
873
|
+
finally:
|
|
874
|
+
os.close(manifest_fd)
|
|
875
|
+
os.replace(
|
|
876
|
+
temporary,
|
|
877
|
+
CATALOG_FILENAME,
|
|
878
|
+
src_dir_fd=root_fd,
|
|
879
|
+
dst_dir_fd=root_fd,
|
|
880
|
+
)
|
|
881
|
+
_verify_publication_chain(publication_chain)
|
|
882
|
+
_catalog_durability_barrier(root_fd, "manifest-replaced")
|
|
883
|
+
_catalog_durability_barrier(root_fd, "catalog-directory")
|
|
884
|
+
observed = _read_regular_at(
|
|
885
|
+
root_fd,
|
|
886
|
+
CATALOG_FILENAME,
|
|
887
|
+
maximum=len(index_bytes),
|
|
888
|
+
label="published catalog manifest",
|
|
889
|
+
)
|
|
890
|
+
if observed != index_bytes or sha256_bytes(observed) != manifest_digest:
|
|
891
|
+
raise SourceContractError(
|
|
892
|
+
"CATALOG_MANIFEST_READBACK",
|
|
893
|
+
CATALOG_FILENAME,
|
|
894
|
+
"published manifest readback differs",
|
|
895
|
+
)
|
|
896
|
+
_catalog_durability_barrier(root_fd, "manifest-readback")
|
|
897
|
+
finally:
|
|
898
|
+
try:
|
|
899
|
+
os.unlink(temporary, dir_fd=root_fd)
|
|
900
|
+
except FileNotFoundError:
|
|
901
|
+
pass
|
|
902
|
+
return CatalogSealResult(
|
|
903
|
+
entries=ordered,
|
|
904
|
+
manifest_sha256=manifest_digest,
|
|
905
|
+
reused_vector_sha256s=tuple(sorted(reused_digests)),
|
|
906
|
+
created_vector_sha256s=tuple(sorted(set(payloads) - reused_digests)),
|
|
907
|
+
encoder_invocations=encoder_invocations,
|
|
908
|
+
)
|
|
909
|
+
finally:
|
|
910
|
+
for descriptor_to_close in (
|
|
911
|
+
source_vector_fd,
|
|
912
|
+
vector_fd,
|
|
913
|
+
source_root_fd,
|
|
914
|
+
):
|
|
915
|
+
if descriptor_to_close >= 0:
|
|
916
|
+
os.close(descriptor_to_close)
|
|
917
|
+
for descriptor_to_close in reversed(publication_fds):
|
|
918
|
+
os.close(descriptor_to_close)
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def _capture_entry(entry: CatalogEntry, *, index: int, sealed: bool) -> CatalogEntry:
|
|
922
|
+
if type(entry) is not CatalogEntry:
|
|
923
|
+
raise SourceContractError("TYPE", f"entries[{index}]", "must be a CatalogEntry")
|
|
924
|
+
captured = canonical_json_bytes(entry.to_dict())
|
|
925
|
+
admit_public_fact_bytes(captured)
|
|
926
|
+
canonical_entry = catalog_entry_from_dict(parse_canonical_json(captured))
|
|
927
|
+
if canonical_json_bytes(canonical_entry.to_dict()) != captured:
|
|
928
|
+
raise SourceContractError(
|
|
929
|
+
"CATALOG_ENTRY_PROJECTION",
|
|
930
|
+
f"entries[{index}]",
|
|
931
|
+
"entry cannot be rebuilt byte-for-byte",
|
|
932
|
+
)
|
|
933
|
+
if sealed and not canonical_entry.vectors:
|
|
934
|
+
raise SourceContractError(
|
|
935
|
+
"CATALOG_REUSE_UNSEALED", f"entries[{index}]", "reused entry must be sealed"
|
|
936
|
+
)
|
|
937
|
+
if not sealed and canonical_entry.vectors:
|
|
938
|
+
raise SourceContractError(
|
|
939
|
+
"CATALOG_ALREADY_SEALED", f"entries[{index}]", "fresh entry must be unsealed"
|
|
940
|
+
)
|
|
941
|
+
return canonical_entry
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _shared_facts_sha256(entries: tuple[CatalogEntry, ...]) -> str:
|
|
945
|
+
return sha256_bytes(
|
|
946
|
+
canonical_json_bytes(
|
|
947
|
+
[
|
|
948
|
+
{"coordinate": entry.coordinate, "facts_sha256": entry.facts_digest}
|
|
949
|
+
for entry in entries
|
|
950
|
+
]
|
|
951
|
+
)
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def _seal_representation_generation(
|
|
956
|
+
*,
|
|
957
|
+
reused_entries: tuple[CatalogEntry, ...],
|
|
958
|
+
fresh_entries: tuple[CatalogEntry, ...],
|
|
959
|
+
backend: EmbeddingBackend,
|
|
960
|
+
directory: Path,
|
|
961
|
+
predecessor_directory: Path | None,
|
|
962
|
+
predecessor_manifest_sha256: str | None,
|
|
963
|
+
egress_policy: EgressPolicy,
|
|
964
|
+
) -> CatalogSealResult:
|
|
965
|
+
"""Seal a new generation; an existing one needs a trusted outer-head resume."""
|
|
966
|
+
|
|
967
|
+
_seal_generation_descriptor(directory, backend.descriptor)
|
|
968
|
+
if (directory / CATALOG_FILENAME).exists():
|
|
969
|
+
raise SourceContractError(
|
|
970
|
+
"RETRIEVAL_GENERATION_UNTRUSTED",
|
|
971
|
+
str(directory),
|
|
972
|
+
"existing generation requires an explicit authenticated outer manifest digest",
|
|
973
|
+
)
|
|
974
|
+
return seal_catalog_successor(
|
|
975
|
+
reused_entries=reused_entries,
|
|
976
|
+
fresh_entries=fresh_entries,
|
|
977
|
+
backend=backend,
|
|
978
|
+
directory=directory,
|
|
979
|
+
predecessor_directory=predecessor_directory,
|
|
980
|
+
predecessor_manifest_sha256=predecessor_manifest_sha256,
|
|
981
|
+
egress_policy=egress_policy,
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def _representation_generation(shared_facts_sha256: str, descriptor: BackendDescriptor) -> str:
|
|
986
|
+
return sha256_bytes(
|
|
987
|
+
canonical_json_bytes(
|
|
988
|
+
{
|
|
989
|
+
"backend_descriptor_sha256": backend_descriptor_sha256(descriptor),
|
|
990
|
+
"shared_facts_sha256": shared_facts_sha256,
|
|
991
|
+
}
|
|
992
|
+
)
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def _capture_trusted_retrieval_predecessor(
|
|
997
|
+
*,
|
|
998
|
+
predecessor_directory: Path | None,
|
|
999
|
+
predecessor_manifest_sha256: str | None,
|
|
1000
|
+
lexical_reused_entries: tuple[CatalogEntry, ...],
|
|
1001
|
+
neural_reused_entries: tuple[CatalogEntry, ...],
|
|
1002
|
+
lexical_backend: EmbeddingBackend,
|
|
1003
|
+
neural_backend: EmbeddingBackend | None,
|
|
1004
|
+
) -> LoadedRetrievalCatalog:
|
|
1005
|
+
if predecessor_directory is None or predecessor_manifest_sha256 is None:
|
|
1006
|
+
raise SourceContractError(
|
|
1007
|
+
"CATALOG_PREDECESSOR",
|
|
1008
|
+
"retrieval.predecessor",
|
|
1009
|
+
"representation reuse requires an exact predecessor outer root and manifest digest",
|
|
1010
|
+
)
|
|
1011
|
+
loaded = load_retrieval_catalog(
|
|
1012
|
+
predecessor_directory,
|
|
1013
|
+
expected_manifest_sha256=predecessor_manifest_sha256,
|
|
1014
|
+
neural_backend=neural_backend,
|
|
1015
|
+
)
|
|
1016
|
+
if backend_descriptor_bytes(loaded.lexical.backend.descriptor) != backend_descriptor_bytes(
|
|
1017
|
+
lexical_backend.descriptor
|
|
1018
|
+
):
|
|
1019
|
+
raise SourceContractError(
|
|
1020
|
+
"RETRIEVAL_BACKEND_MISMATCH",
|
|
1021
|
+
"retrieval.lexical_predecessor",
|
|
1022
|
+
"trusted predecessor uses a different lexical backend descriptor",
|
|
1023
|
+
)
|
|
1024
|
+
previous_lexical = {entry.entry_id: entry for entry in loaded.lexical.entries}
|
|
1025
|
+
for entry in lexical_reused_entries:
|
|
1026
|
+
observed = previous_lexical.get(entry.entry_id)
|
|
1027
|
+
if observed is None or observed.to_dict() != entry.to_dict():
|
|
1028
|
+
raise SourceContractError(
|
|
1029
|
+
"CATALOG_PREDECESSOR",
|
|
1030
|
+
f"retrieval.lexical_predecessor.{entry.entry_id}",
|
|
1031
|
+
"reused lexical entry is not the exact authenticated predecessor member",
|
|
1032
|
+
)
|
|
1033
|
+
if neural_reused_entries and loaded.neural is None:
|
|
1034
|
+
raise SourceContractError(
|
|
1035
|
+
"CATALOG_PREDECESSOR",
|
|
1036
|
+
"retrieval.neural_predecessor",
|
|
1037
|
+
"trusted predecessor has no resolved neural representation",
|
|
1038
|
+
)
|
|
1039
|
+
previous_neural = (
|
|
1040
|
+
{} if loaded.neural is None else {entry.entry_id: entry for entry in loaded.neural.entries}
|
|
1041
|
+
)
|
|
1042
|
+
for entry in neural_reused_entries:
|
|
1043
|
+
observed = previous_neural.get(entry.entry_id)
|
|
1044
|
+
if observed is None or observed.to_dict() != entry.to_dict():
|
|
1045
|
+
raise SourceContractError(
|
|
1046
|
+
"CATALOG_PREDECESSOR",
|
|
1047
|
+
f"retrieval.neural_predecessor.{entry.entry_id}",
|
|
1048
|
+
"reused neural entry is not the exact authenticated predecessor member",
|
|
1049
|
+
)
|
|
1050
|
+
return loaded
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def _materialize_representation_snapshot(
|
|
1054
|
+
snapshot: LoadedRetrievalRepresentation,
|
|
1055
|
+
*,
|
|
1056
|
+
directory: Path,
|
|
1057
|
+
) -> tuple[Path, str]:
|
|
1058
|
+
vectors_directory = directory / VECTORS_DIRNAME
|
|
1059
|
+
vectors_directory.mkdir(parents=True)
|
|
1060
|
+
catalog_raw = canonical_json_bytes(
|
|
1061
|
+
{
|
|
1062
|
+
"entries": [entry.to_dict() for entry in snapshot.entries],
|
|
1063
|
+
"schema_version": CATALOG_INDEX_CONTRACT_VERSION,
|
|
1064
|
+
}
|
|
1065
|
+
)
|
|
1066
|
+
_atomic_write(directory / CATALOG_FILENAME, catalog_raw)
|
|
1067
|
+
written: set[str] = set()
|
|
1068
|
+
for entry in snapshot.entries:
|
|
1069
|
+
for reference in entry.vectors:
|
|
1070
|
+
if reference.vector_sha256 in written:
|
|
1071
|
+
continue
|
|
1072
|
+
vector = snapshot.vectors[(entry.entry_id, reference.layer)]
|
|
1073
|
+
raw = struct.pack(f">{reference.dimensions}i", *vector)
|
|
1074
|
+
if sha256_bytes(raw) != reference.vector_sha256:
|
|
1075
|
+
raise SourceContractError(
|
|
1076
|
+
"CATALOG_VECTOR_DIGEST",
|
|
1077
|
+
reference.vector_sha256,
|
|
1078
|
+
"authenticated snapshot vector no longer matches its content address",
|
|
1079
|
+
)
|
|
1080
|
+
_atomic_write(vectors_directory / f"{reference.vector_sha256}{VECTOR_SUFFIX}", raw)
|
|
1081
|
+
written.add(reference.vector_sha256)
|
|
1082
|
+
return directory, sha256_bytes(catalog_raw)
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def _load_trusted_resume_catalog(
|
|
1086
|
+
*,
|
|
1087
|
+
directory: Path,
|
|
1088
|
+
expected_manifest_sha256: str | None,
|
|
1089
|
+
expected_facts: tuple[CatalogEntry, ...],
|
|
1090
|
+
lexical_backend: EmbeddingBackend,
|
|
1091
|
+
neural_backend: EmbeddingBackend | None,
|
|
1092
|
+
fallback_reason: str | None,
|
|
1093
|
+
) -> LoadedRetrievalCatalog | None:
|
|
1094
|
+
if expected_manifest_sha256 is None:
|
|
1095
|
+
return None
|
|
1096
|
+
loaded = load_retrieval_catalog(
|
|
1097
|
+
directory,
|
|
1098
|
+
expected_manifest_sha256=expected_manifest_sha256,
|
|
1099
|
+
neural_backend=neural_backend,
|
|
1100
|
+
)
|
|
1101
|
+
expected = tuple((entry.coordinate, entry.facts_digest) for entry in expected_facts)
|
|
1102
|
+
observed = tuple((entry.coordinate, entry.facts_digest) for entry in loaded.lexical.entries)
|
|
1103
|
+
if observed != expected:
|
|
1104
|
+
raise SourceContractError(
|
|
1105
|
+
"RETRIEVAL_GENERATION_COLLISION",
|
|
1106
|
+
str(directory),
|
|
1107
|
+
"authenticated existing head differs from the requested canonical facts",
|
|
1108
|
+
)
|
|
1109
|
+
if backend_descriptor_bytes(loaded.lexical.backend.descriptor) != backend_descriptor_bytes(
|
|
1110
|
+
lexical_backend.descriptor
|
|
1111
|
+
):
|
|
1112
|
+
raise SourceContractError(
|
|
1113
|
+
"RETRIEVAL_BACKEND_MISMATCH",
|
|
1114
|
+
str(directory),
|
|
1115
|
+
"authenticated existing head uses a different lexical backend",
|
|
1116
|
+
)
|
|
1117
|
+
requested_hybrid = neural_backend is not None
|
|
1118
|
+
loaded_hybrid = loaded.manifest.neural is not None
|
|
1119
|
+
if requested_hybrid != loaded_hybrid:
|
|
1120
|
+
raise SourceContractError(
|
|
1121
|
+
"RETRIEVAL_BACKEND_MISMATCH",
|
|
1122
|
+
str(directory),
|
|
1123
|
+
"authenticated existing head has a different representation set",
|
|
1124
|
+
)
|
|
1125
|
+
if loaded.manifest.fallback_reason != fallback_reason:
|
|
1126
|
+
raise SourceContractError(
|
|
1127
|
+
"RETRIEVAL_MANIFEST_FALLBACK",
|
|
1128
|
+
str(directory),
|
|
1129
|
+
"authenticated existing head has a different fallback reason",
|
|
1130
|
+
)
|
|
1131
|
+
if requested_hybrid:
|
|
1132
|
+
assert neural_backend is not None
|
|
1133
|
+
assert loaded.neural is not None
|
|
1134
|
+
if backend_descriptor_bytes(loaded.neural.backend.descriptor) != backend_descriptor_bytes(
|
|
1135
|
+
neural_backend.descriptor
|
|
1136
|
+
):
|
|
1137
|
+
raise SourceContractError(
|
|
1138
|
+
"RETRIEVAL_BACKEND_MISMATCH",
|
|
1139
|
+
str(directory),
|
|
1140
|
+
"authenticated existing head uses a different complete neural descriptor",
|
|
1141
|
+
)
|
|
1142
|
+
return loaded
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _result_from_trusted_resume(loaded: LoadedRetrievalCatalog) -> RetrievalSealResult:
|
|
1146
|
+
lexical_digests = tuple(
|
|
1147
|
+
sorted(
|
|
1148
|
+
{
|
|
1149
|
+
reference.vector_sha256
|
|
1150
|
+
for entry in loaded.lexical.entries
|
|
1151
|
+
for reference in entry.vectors
|
|
1152
|
+
}
|
|
1153
|
+
)
|
|
1154
|
+
)
|
|
1155
|
+
neural_digests: tuple[str, ...] = ()
|
|
1156
|
+
if loaded.neural is not None:
|
|
1157
|
+
neural_digests = tuple(
|
|
1158
|
+
sorted(
|
|
1159
|
+
{
|
|
1160
|
+
reference.vector_sha256
|
|
1161
|
+
for entry in loaded.neural.entries
|
|
1162
|
+
for reference in entry.vectors
|
|
1163
|
+
}
|
|
1164
|
+
)
|
|
1165
|
+
)
|
|
1166
|
+
return RetrievalSealResult(
|
|
1167
|
+
manifest=loaded.manifest,
|
|
1168
|
+
manifest_sha256=loaded.manifest.digest,
|
|
1169
|
+
lexical_entries=loaded.lexical.entries,
|
|
1170
|
+
neural_entries=None if loaded.neural is None else loaded.neural.entries,
|
|
1171
|
+
lexical_reused_vector_sha256s=lexical_digests,
|
|
1172
|
+
lexical_created_vector_sha256s=(),
|
|
1173
|
+
neural_reused_vector_sha256s=neural_digests,
|
|
1174
|
+
neural_created_vector_sha256s=(),
|
|
1175
|
+
lexical_encoder_invocations=0,
|
|
1176
|
+
neural_encoder_invocations=0,
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def _seal_generation_descriptor(directory: Path, descriptor: BackendDescriptor) -> None:
|
|
1181
|
+
raw = backend_descriptor_bytes(descriptor)
|
|
1182
|
+
digest = sha256_bytes(raw)
|
|
1183
|
+
generation_fd, owned, chain = _open_or_create_catalog_root(directory)
|
|
1184
|
+
try:
|
|
1185
|
+
_verify_publication_chain(chain)
|
|
1186
|
+
try:
|
|
1187
|
+
os.stat(CATALOG_FILENAME, dir_fd=generation_fd, follow_symlinks=False)
|
|
1188
|
+
catalog_exists = True
|
|
1189
|
+
except FileNotFoundError:
|
|
1190
|
+
catalog_exists = False
|
|
1191
|
+
try:
|
|
1192
|
+
os.stat(
|
|
1193
|
+
REPRESENTATION_DESCRIPTOR_FILENAME,
|
|
1194
|
+
dir_fd=generation_fd,
|
|
1195
|
+
follow_symlinks=False,
|
|
1196
|
+
)
|
|
1197
|
+
descriptor_exists = True
|
|
1198
|
+
except FileNotFoundError:
|
|
1199
|
+
descriptor_exists = False
|
|
1200
|
+
if catalog_exists and not descriptor_exists:
|
|
1201
|
+
raise SourceContractError(
|
|
1202
|
+
"RETRIEVAL_BACKEND_MISMATCH",
|
|
1203
|
+
str(directory),
|
|
1204
|
+
"existing generation has no authenticated backend descriptor",
|
|
1205
|
+
)
|
|
1206
|
+
try:
|
|
1207
|
+
_install_immutable_member(
|
|
1208
|
+
generation_fd,
|
|
1209
|
+
REPRESENTATION_DESCRIPTOR_FILENAME,
|
|
1210
|
+
raw,
|
|
1211
|
+
digest=digest,
|
|
1212
|
+
)
|
|
1213
|
+
except SourceContractError as error:
|
|
1214
|
+
raise SourceContractError(
|
|
1215
|
+
"RETRIEVAL_BACKEND_MISMATCH",
|
|
1216
|
+
str(directory),
|
|
1217
|
+
"existing generation backend descriptor differs",
|
|
1218
|
+
) from error
|
|
1219
|
+
finally:
|
|
1220
|
+
for owned_fd in reversed(owned):
|
|
1221
|
+
os.close(owned_fd)
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def _representation_from_seal(
|
|
1225
|
+
*,
|
|
1226
|
+
root: Path,
|
|
1227
|
+
name: str,
|
|
1228
|
+
generation: str,
|
|
1229
|
+
descriptor: BackendDescriptor,
|
|
1230
|
+
result: CatalogSealResult,
|
|
1231
|
+
) -> RetrievalRepresentation:
|
|
1232
|
+
root_fd = _open_directory_nofollow(root, "retrieval publication root")
|
|
1233
|
+
name_fd = -1
|
|
1234
|
+
generation_fd = -1
|
|
1235
|
+
vectors_fd = -1
|
|
1236
|
+
try:
|
|
1237
|
+
name_fd = _open_directory_at(root_fd, name, f"{name} representation")
|
|
1238
|
+
generation_fd = _open_directory_at(name_fd, generation, f"{name} generation")
|
|
1239
|
+
catalog_raw = _read_regular_at(
|
|
1240
|
+
generation_fd,
|
|
1241
|
+
CATALOG_FILENAME,
|
|
1242
|
+
maximum=MAX_CATALOG_MANIFEST_BYTES,
|
|
1243
|
+
label=f"{name} catalog manifest",
|
|
1244
|
+
)
|
|
1245
|
+
if sha256_bytes(catalog_raw) != result.manifest_sha256:
|
|
1246
|
+
raise SourceContractError(
|
|
1247
|
+
"RETRIEVAL_MEMBER_READBACK",
|
|
1248
|
+
f"{name}/{generation}/{CATALOG_FILENAME}",
|
|
1249
|
+
"inner manifest differs after durable sealing",
|
|
1250
|
+
)
|
|
1251
|
+
prefix = f"{name}/{generation}"
|
|
1252
|
+
members = [
|
|
1253
|
+
RepresentationMember(
|
|
1254
|
+
path=f"{prefix}/{CATALOG_FILENAME}",
|
|
1255
|
+
size=len(catalog_raw),
|
|
1256
|
+
sha256=result.manifest_sha256,
|
|
1257
|
+
)
|
|
1258
|
+
]
|
|
1259
|
+
descriptor_raw = _read_regular_at(
|
|
1260
|
+
generation_fd,
|
|
1261
|
+
REPRESENTATION_DESCRIPTOR_FILENAME,
|
|
1262
|
+
maximum=MAX_REPRESENTATION_DESCRIPTOR_BYTES,
|
|
1263
|
+
label=f"{name} backend descriptor",
|
|
1264
|
+
)
|
|
1265
|
+
if descriptor_raw != backend_descriptor_bytes(descriptor):
|
|
1266
|
+
raise SourceContractError(
|
|
1267
|
+
"RETRIEVAL_BACKEND_MISMATCH",
|
|
1268
|
+
f"{prefix}/{REPRESENTATION_DESCRIPTOR_FILENAME}",
|
|
1269
|
+
"generation descriptor differs after durable sealing",
|
|
1270
|
+
)
|
|
1271
|
+
members.append(
|
|
1272
|
+
RepresentationMember(
|
|
1273
|
+
path=f"{prefix}/{REPRESENTATION_DESCRIPTOR_FILENAME}",
|
|
1274
|
+
size=len(descriptor_raw),
|
|
1275
|
+
sha256=sha256_bytes(descriptor_raw),
|
|
1276
|
+
)
|
|
1277
|
+
)
|
|
1278
|
+
vectors_fd = _open_directory_at(generation_fd, VECTORS_DIRNAME, f"{name} vectors")
|
|
1279
|
+
vector_digests = sorted(
|
|
1280
|
+
{reference.vector_sha256 for entry in result.entries for reference in entry.vectors}
|
|
1281
|
+
)
|
|
1282
|
+
for digest in vector_digests:
|
|
1283
|
+
raw = _read_regular_at(
|
|
1284
|
+
vectors_fd,
|
|
1285
|
+
f"{digest}{VECTOR_SUFFIX}",
|
|
1286
|
+
maximum=descriptor.dimensions * 4,
|
|
1287
|
+
label=f"{name} vector {digest}",
|
|
1288
|
+
)
|
|
1289
|
+
if len(raw) != descriptor.dimensions * 4 or sha256_bytes(raw) != digest:
|
|
1290
|
+
raise SourceContractError(
|
|
1291
|
+
"RETRIEVAL_MEMBER_READBACK",
|
|
1292
|
+
f"{prefix}/{VECTORS_DIRNAME}/{digest}{VECTOR_SUFFIX}",
|
|
1293
|
+
"sealed vector differs after durable sealing",
|
|
1294
|
+
)
|
|
1295
|
+
members.append(
|
|
1296
|
+
RepresentationMember(
|
|
1297
|
+
path=f"{prefix}/{VECTORS_DIRNAME}/{digest}{VECTOR_SUFFIX}",
|
|
1298
|
+
size=len(raw),
|
|
1299
|
+
sha256=digest,
|
|
1300
|
+
)
|
|
1301
|
+
)
|
|
1302
|
+
return RetrievalRepresentation.create(
|
|
1303
|
+
name=name,
|
|
1304
|
+
descriptor=descriptor,
|
|
1305
|
+
members=tuple(members),
|
|
1306
|
+
)
|
|
1307
|
+
finally:
|
|
1308
|
+
for descriptor_to_close in (vectors_fd, generation_fd, name_fd, root_fd):
|
|
1309
|
+
if descriptor_to_close >= 0:
|
|
1310
|
+
os.close(descriptor_to_close)
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
def _publish_retrieval_manifest(directory: Path, raw: bytes) -> None:
|
|
1314
|
+
root_fd, owned, chain = _open_or_create_catalog_root(directory)
|
|
1315
|
+
temporary = f".{RETRIEVAL_MANIFEST_FILENAME}.tmp-{secrets.token_hex(16)}"
|
|
1316
|
+
try:
|
|
1317
|
+
_verify_publication_chain(chain)
|
|
1318
|
+
descriptor = _create_regular_at(root_fd, temporary, mode=0o600)
|
|
1319
|
+
try:
|
|
1320
|
+
_write_all(descriptor, raw)
|
|
1321
|
+
_catalog_durability_barrier(descriptor, "retrieval-manifest-temporary")
|
|
1322
|
+
if _read_open_descriptor(descriptor, maximum=len(raw)) != raw:
|
|
1323
|
+
raise SourceContractError(
|
|
1324
|
+
"RETRIEVAL_MANIFEST_READBACK",
|
|
1325
|
+
RETRIEVAL_MANIFEST_FILENAME,
|
|
1326
|
+
"temporary manifest readback differs",
|
|
1327
|
+
)
|
|
1328
|
+
os.fchmod(descriptor, 0o444)
|
|
1329
|
+
finally:
|
|
1330
|
+
os.close(descriptor)
|
|
1331
|
+
_verify_publication_chain(chain)
|
|
1332
|
+
os.replace(
|
|
1333
|
+
temporary,
|
|
1334
|
+
RETRIEVAL_MANIFEST_FILENAME,
|
|
1335
|
+
src_dir_fd=root_fd,
|
|
1336
|
+
dst_dir_fd=root_fd,
|
|
1337
|
+
)
|
|
1338
|
+
_catalog_durability_barrier(root_fd, "retrieval-manifest-replaced")
|
|
1339
|
+
observed = _read_regular_at(
|
|
1340
|
+
root_fd,
|
|
1341
|
+
RETRIEVAL_MANIFEST_FILENAME,
|
|
1342
|
+
maximum=len(raw),
|
|
1343
|
+
label="published retrieval manifest",
|
|
1344
|
+
)
|
|
1345
|
+
if observed != raw:
|
|
1346
|
+
raise SourceContractError(
|
|
1347
|
+
"RETRIEVAL_MANIFEST_READBACK",
|
|
1348
|
+
RETRIEVAL_MANIFEST_FILENAME,
|
|
1349
|
+
"published manifest readback differs",
|
|
1350
|
+
)
|
|
1351
|
+
_catalog_durability_barrier(root_fd, "retrieval-manifest-readback")
|
|
1352
|
+
finally:
|
|
1353
|
+
try:
|
|
1354
|
+
os.unlink(temporary, dir_fd=root_fd)
|
|
1355
|
+
except FileNotFoundError:
|
|
1356
|
+
pass
|
|
1357
|
+
for descriptor_to_close in reversed(owned):
|
|
1358
|
+
os.close(descriptor_to_close)
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
def _entries_from_manifest(raw: bytes) -> tuple[CatalogEntry, ...]:
|
|
1362
|
+
try:
|
|
1363
|
+
payload = parse_canonical_json(raw)
|
|
1364
|
+
except ValueError as error:
|
|
1365
|
+
raise SourceContractError(
|
|
1366
|
+
"CATALOG_MANIFEST", CATALOG_FILENAME, "manifest is not canonical"
|
|
1367
|
+
) from error
|
|
1368
|
+
if (
|
|
1369
|
+
not isinstance(payload, dict)
|
|
1370
|
+
or set(payload) != {"schema_version", "entries"}
|
|
1371
|
+
or payload["schema_version"] != CATALOG_INDEX_CONTRACT_VERSION
|
|
1372
|
+
or not isinstance(payload["entries"], list)
|
|
1373
|
+
):
|
|
1374
|
+
raise SourceContractError(
|
|
1375
|
+
"CATALOG_MANIFEST", CATALOG_FILENAME, "manifest fields or version differ"
|
|
1376
|
+
)
|
|
1377
|
+
entries = tuple(catalog_entry_from_dict(item) for item in payload["entries"])
|
|
1378
|
+
if tuple(sorted(entries, key=_entry_key)) != entries:
|
|
1379
|
+
raise SourceContractError(
|
|
1380
|
+
"CATALOG_MANIFEST", CATALOG_FILENAME, "manifest entries are not sorted"
|
|
1381
|
+
)
|
|
1382
|
+
if len({entry.entry_id for entry in entries}) != len(entries):
|
|
1383
|
+
raise SourceContractError(
|
|
1384
|
+
"CATALOG_MANIFEST", CATALOG_FILENAME, "manifest entry ids are not unique"
|
|
1385
|
+
)
|
|
1386
|
+
return entries
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def _open_directory_nofollow(path: Path, label: str) -> int:
|
|
1390
|
+
flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
1391
|
+
try:
|
|
1392
|
+
descriptor = os.open(path, flags)
|
|
1393
|
+
except OSError as error:
|
|
1394
|
+
raise SourceContractError(
|
|
1395
|
+
"CATALOG_PUBLICATION_ROOT", label, "directory cannot be opened safely"
|
|
1396
|
+
) from error
|
|
1397
|
+
info = os.fstat(descriptor)
|
|
1398
|
+
if not stat.S_ISDIR(info.st_mode):
|
|
1399
|
+
os.close(descriptor)
|
|
1400
|
+
raise SourceContractError("CATALOG_PUBLICATION_ROOT", label, "must be a directory")
|
|
1401
|
+
return descriptor
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
def _open_or_create_catalog_root(
|
|
1405
|
+
directory: Path,
|
|
1406
|
+
) -> tuple[int, list[int], list[tuple[int, str, int, tuple[int, int, int]]]]:
|
|
1407
|
+
"""Create/open a catalog only through one retained no-follow ancestor chain."""
|
|
1408
|
+
|
|
1409
|
+
owned: list[int] = []
|
|
1410
|
+
chain: list[tuple[int, str, int, tuple[int, int, int]]] = []
|
|
1411
|
+
try:
|
|
1412
|
+
parent_fd = os.open(
|
|
1413
|
+
"/", os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
1414
|
+
)
|
|
1415
|
+
owned.append(parent_fd)
|
|
1416
|
+
for part in Path(os.path.abspath(directory)).parts[1:]:
|
|
1417
|
+
if _directory_contains_build_manifest(parent_fd):
|
|
1418
|
+
raise SourceContractError(
|
|
1419
|
+
"CATALOG_PUBLICATION_ROOT",
|
|
1420
|
+
"catalog.directory",
|
|
1421
|
+
"a catalog may not be written through or inside a sealed Build root",
|
|
1422
|
+
)
|
|
1423
|
+
try:
|
|
1424
|
+
named = os.stat(part, dir_fd=parent_fd, follow_symlinks=False)
|
|
1425
|
+
except FileNotFoundError:
|
|
1426
|
+
os.mkdir(part, mode=0o755, dir_fd=parent_fd)
|
|
1427
|
+
os.fsync(parent_fd)
|
|
1428
|
+
named = os.stat(part, dir_fd=parent_fd, follow_symlinks=False)
|
|
1429
|
+
child_fd = os.open(
|
|
1430
|
+
part,
|
|
1431
|
+
os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0),
|
|
1432
|
+
dir_fd=parent_fd,
|
|
1433
|
+
)
|
|
1434
|
+
owned.append(child_fd)
|
|
1435
|
+
opened = os.fstat(child_fd)
|
|
1436
|
+
identity = _directory_identity(opened)
|
|
1437
|
+
if not stat.S_ISDIR(named.st_mode) or _directory_identity(named) != identity:
|
|
1438
|
+
raise SourceContractError(
|
|
1439
|
+
"CATALOG_PUBLICATION_ROOT",
|
|
1440
|
+
"catalog.directory",
|
|
1441
|
+
"named and opened ancestor identities differ",
|
|
1442
|
+
)
|
|
1443
|
+
chain.append((parent_fd, part, child_fd, identity))
|
|
1444
|
+
parent_fd = child_fd
|
|
1445
|
+
_verify_publication_chain(chain)
|
|
1446
|
+
if _directory_contains_build_manifest(parent_fd):
|
|
1447
|
+
raise SourceContractError(
|
|
1448
|
+
"CATALOG_PUBLICATION_ROOT",
|
|
1449
|
+
"catalog.directory",
|
|
1450
|
+
"a catalog may not be written through or inside a sealed Build root",
|
|
1451
|
+
)
|
|
1452
|
+
return parent_fd, owned, chain
|
|
1453
|
+
except BaseException:
|
|
1454
|
+
for descriptor in reversed(owned):
|
|
1455
|
+
os.close(descriptor)
|
|
1456
|
+
raise
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
def _verify_publication_chain(
|
|
1460
|
+
chain: list[tuple[int, str, int, tuple[int, int, int]]],
|
|
1461
|
+
) -> None:
|
|
1462
|
+
for parent_fd, name, child_fd, identity in chain:
|
|
1463
|
+
try:
|
|
1464
|
+
named = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
|
|
1465
|
+
opened = os.fstat(child_fd)
|
|
1466
|
+
except OSError as error:
|
|
1467
|
+
raise SourceContractError(
|
|
1468
|
+
"CATALOG_PUBLICATION_ROOT",
|
|
1469
|
+
"catalog.directory",
|
|
1470
|
+
"retained publication ancestor disappeared",
|
|
1471
|
+
) from error
|
|
1472
|
+
if _directory_identity(named) != identity or _directory_identity(opened) != identity:
|
|
1473
|
+
raise SourceContractError(
|
|
1474
|
+
"CATALOG_PUBLICATION_ROOT",
|
|
1475
|
+
"catalog.directory",
|
|
1476
|
+
"retained publication ancestor was replaced",
|
|
1477
|
+
)
|
|
1478
|
+
|
|
1479
|
+
|
|
1480
|
+
def _directory_identity(info: os.stat_result) -> tuple[int, int, int]:
|
|
1481
|
+
return (info.st_dev, info.st_ino, stat.S_IFMT(info.st_mode))
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
def _open_directory_at(parent_fd: int, name: str, label: str) -> int:
|
|
1485
|
+
flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
1486
|
+
try:
|
|
1487
|
+
descriptor = os.open(name, flags, dir_fd=parent_fd)
|
|
1488
|
+
except OSError as error:
|
|
1489
|
+
raise SourceContractError(
|
|
1490
|
+
"CATALOG_PUBLICATION_ROOT", label, "directory cannot be opened safely"
|
|
1491
|
+
) from error
|
|
1492
|
+
info = os.fstat(descriptor)
|
|
1493
|
+
if not stat.S_ISDIR(info.st_mode):
|
|
1494
|
+
os.close(descriptor)
|
|
1495
|
+
raise SourceContractError("CATALOG_PUBLICATION_ROOT", label, "must be a directory")
|
|
1496
|
+
return descriptor
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
def _refuse_build_root(root_fd: int) -> None:
|
|
1500
|
+
if _directory_contains_build_manifest(root_fd):
|
|
1501
|
+
raise SourceContractError(
|
|
1502
|
+
"CATALOG_PUBLICATION_ROOT",
|
|
1503
|
+
"catalog.directory",
|
|
1504
|
+
"a catalog may not be written through or inside a sealed Build root",
|
|
1505
|
+
)
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
def _refuse_build_ancestor(directory: Path) -> None:
|
|
1509
|
+
absolute = directory.absolute()
|
|
1510
|
+
for candidate in (absolute, *absolute.parents):
|
|
1511
|
+
try:
|
|
1512
|
+
info = candidate.lstat()
|
|
1513
|
+
except FileNotFoundError:
|
|
1514
|
+
continue
|
|
1515
|
+
if not stat.S_ISDIR(info.st_mode) or stat.S_ISLNK(info.st_mode):
|
|
1516
|
+
raise SourceContractError(
|
|
1517
|
+
"CATALOG_PUBLICATION_ROOT",
|
|
1518
|
+
"catalog.directory",
|
|
1519
|
+
"catalog path ancestors must be real directories",
|
|
1520
|
+
)
|
|
1521
|
+
descriptor = _open_directory_nofollow(candidate, "catalog path ancestor")
|
|
1522
|
+
try:
|
|
1523
|
+
if _directory_contains_build_manifest(descriptor):
|
|
1524
|
+
raise SourceContractError(
|
|
1525
|
+
"CATALOG_PUBLICATION_ROOT",
|
|
1526
|
+
"catalog.directory",
|
|
1527
|
+
"a catalog may not be written through or inside a sealed Build root",
|
|
1528
|
+
)
|
|
1529
|
+
finally:
|
|
1530
|
+
os.close(descriptor)
|
|
1531
|
+
|
|
1532
|
+
|
|
1533
|
+
def _directory_contains_build_manifest(root_fd: int) -> bool:
|
|
1534
|
+
try:
|
|
1535
|
+
info = os.stat("manifest.json", dir_fd=root_fd, follow_symlinks=False)
|
|
1536
|
+
except FileNotFoundError:
|
|
1537
|
+
return False
|
|
1538
|
+
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode) or info.st_nlink != 1:
|
|
1539
|
+
return False
|
|
1540
|
+
try:
|
|
1541
|
+
raw = _read_regular_at(
|
|
1542
|
+
root_fd,
|
|
1543
|
+
"manifest.json",
|
|
1544
|
+
maximum=MAX_CATALOG_MANIFEST_BYTES,
|
|
1545
|
+
label="possible Build manifest",
|
|
1546
|
+
)
|
|
1547
|
+
payload = parse_canonical_json(raw)
|
|
1548
|
+
except (SourceContractError, ValueError):
|
|
1549
|
+
return False
|
|
1550
|
+
return (
|
|
1551
|
+
isinstance(payload, dict)
|
|
1552
|
+
and isinstance(payload.get("schema_version"), str)
|
|
1553
|
+
and payload["schema_version"].startswith("candidate-manifest.")
|
|
1554
|
+
)
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
def _create_regular_at(parent_fd: int, name: str, *, mode: int) -> int:
|
|
1558
|
+
flags = (
|
|
1559
|
+
os.O_RDWR
|
|
1560
|
+
| os.O_CREAT
|
|
1561
|
+
| os.O_EXCL
|
|
1562
|
+
| getattr(os, "O_NOFOLLOW", 0)
|
|
1563
|
+
| getattr(os, "O_CLOEXEC", 0)
|
|
1564
|
+
)
|
|
1565
|
+
return os.open(name, flags, mode, dir_fd=parent_fd)
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
def _install_immutable_member(parent_fd: int, name: str, raw: bytes, *, digest: str) -> None:
|
|
1569
|
+
created = False
|
|
1570
|
+
try:
|
|
1571
|
+
descriptor = _create_regular_at(parent_fd, name, mode=0o600)
|
|
1572
|
+
created = True
|
|
1573
|
+
except FileExistsError:
|
|
1574
|
+
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0)
|
|
1575
|
+
try:
|
|
1576
|
+
descriptor = os.open(name, flags, dir_fd=parent_fd)
|
|
1577
|
+
except OSError as error:
|
|
1578
|
+
raise SourceContractError(
|
|
1579
|
+
"CATALOG_VECTOR_COLLISION", name, "existing member cannot be opened safely"
|
|
1580
|
+
) from error
|
|
1581
|
+
try:
|
|
1582
|
+
before = os.fstat(descriptor)
|
|
1583
|
+
if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1:
|
|
1584
|
+
raise SourceContractError(
|
|
1585
|
+
"CATALOG_VECTOR_COLLISION", name, "member is not single-link regular"
|
|
1586
|
+
)
|
|
1587
|
+
if created:
|
|
1588
|
+
_write_all(descriptor, raw)
|
|
1589
|
+
os.fsync(descriptor)
|
|
1590
|
+
stable_before = os.fstat(descriptor)
|
|
1591
|
+
observed = _read_open_descriptor(descriptor, maximum=len(raw))
|
|
1592
|
+
after_read = os.fstat(descriptor)
|
|
1593
|
+
if (
|
|
1594
|
+
observed != raw
|
|
1595
|
+
or sha256_bytes(observed) != digest
|
|
1596
|
+
or after_read.st_size != len(raw)
|
|
1597
|
+
or _identity(stable_before) != _identity(after_read)
|
|
1598
|
+
):
|
|
1599
|
+
raise SourceContractError(
|
|
1600
|
+
"CATALOG_VECTOR_COLLISION", name, "member readback or identity differs"
|
|
1601
|
+
)
|
|
1602
|
+
os.fchmod(descriptor, 0o444)
|
|
1603
|
+
_catalog_durability_barrier(descriptor, "vector-member")
|
|
1604
|
+
sealed = os.fstat(descriptor)
|
|
1605
|
+
if stat.S_IMODE(sealed.st_mode) != 0o444:
|
|
1606
|
+
raise SourceContractError(
|
|
1607
|
+
"CATALOG_VECTOR_MODE", name, "member did not become immutable"
|
|
1608
|
+
)
|
|
1609
|
+
except BaseException:
|
|
1610
|
+
if created:
|
|
1611
|
+
try:
|
|
1612
|
+
os.unlink(name, dir_fd=parent_fd)
|
|
1613
|
+
except FileNotFoundError:
|
|
1614
|
+
pass
|
|
1615
|
+
raise
|
|
1616
|
+
finally:
|
|
1617
|
+
os.close(descriptor)
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
def _read_regular_at(parent_fd: int, name: str, *, maximum: int, label: str) -> bytes:
|
|
1621
|
+
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0)
|
|
1622
|
+
try:
|
|
1623
|
+
descriptor = os.open(name, flags, dir_fd=parent_fd)
|
|
1624
|
+
except OSError as error:
|
|
1625
|
+
raise SourceContractError(
|
|
1626
|
+
"CATALOG_MEMBER", label, "member cannot be opened safely"
|
|
1627
|
+
) from error
|
|
1628
|
+
try:
|
|
1629
|
+
before = os.fstat(descriptor)
|
|
1630
|
+
if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1 or before.st_size > maximum:
|
|
1631
|
+
raise SourceContractError(
|
|
1632
|
+
"CATALOG_MEMBER", label, "member is not bounded single-link regular"
|
|
1633
|
+
)
|
|
1634
|
+
raw = _read_open_descriptor(descriptor, maximum=maximum)
|
|
1635
|
+
after = os.fstat(descriptor)
|
|
1636
|
+
if _identity(before) != _identity(after) or len(raw) != after.st_size:
|
|
1637
|
+
raise SourceContractError("CATALOG_MEMBER", label, "member changed during read")
|
|
1638
|
+
return raw
|
|
1639
|
+
finally:
|
|
1640
|
+
os.close(descriptor)
|
|
1641
|
+
|
|
1642
|
+
|
|
1643
|
+
def _read_open_descriptor(descriptor: int, *, maximum: int) -> bytes:
|
|
1644
|
+
os.lseek(descriptor, 0, os.SEEK_SET)
|
|
1645
|
+
observed = bytearray()
|
|
1646
|
+
while len(observed) <= maximum:
|
|
1647
|
+
chunk = os.read(descriptor, min(1024 * 1024, maximum + 1 - len(observed)))
|
|
1648
|
+
if not chunk:
|
|
1649
|
+
break
|
|
1650
|
+
observed.extend(chunk)
|
|
1651
|
+
if len(observed) > maximum:
|
|
1652
|
+
raise SourceContractError("CATALOG_MEMBER", "catalog.member", "member exceeds its bound")
|
|
1653
|
+
return bytes(observed)
|
|
1654
|
+
|
|
1655
|
+
|
|
1656
|
+
def _write_all(descriptor: int, raw: bytes) -> None:
|
|
1657
|
+
offset = 0
|
|
1658
|
+
while offset < len(raw):
|
|
1659
|
+
written = os.write(descriptor, raw[offset:])
|
|
1660
|
+
if written < 1:
|
|
1661
|
+
raise SourceContractError(
|
|
1662
|
+
"CATALOG_PUBLICATION", "catalog.member", "write made no progress"
|
|
1663
|
+
)
|
|
1664
|
+
offset += written
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
def _identity(info: os.stat_result) -> tuple[int, int, int, int, int, int]:
|
|
1668
|
+
base = (info.st_dev, info.st_ino, info.st_mode, info.st_nlink, info.st_mtime_ns)
|
|
1669
|
+
return (*base, info.st_size)
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
def _entry_key(entry: CatalogEntry) -> tuple[str, int]:
|
|
1673
|
+
return (entry.entry_id, entry.entry_version)
|
|
1674
|
+
|
|
1675
|
+
|
|
1676
|
+
def _catalog_durability_barrier(descriptor: int, boundary: str) -> None:
|
|
1677
|
+
"""Persist one publication boundary; the label supports deterministic crash injection."""
|
|
1678
|
+
|
|
1679
|
+
del boundary
|
|
1680
|
+
os.fsync(descriptor)
|
|
1681
|
+
|
|
1682
|
+
|
|
1683
|
+
def _write_vector(path: Path, payload: bytes) -> None:
|
|
1684
|
+
if path.exists():
|
|
1685
|
+
existing = path.read_bytes()
|
|
1686
|
+
if existing != payload:
|
|
1687
|
+
# Content addressing must hold. A mismatch means a corrupted tree (or, absurdly, a
|
|
1688
|
+
# sha256 collision). Either way, overwriting would destroy the evidence.
|
|
1689
|
+
raise SourceContractError(
|
|
1690
|
+
"CATALOG_VECTOR_COLLISION",
|
|
1691
|
+
str(path.name),
|
|
1692
|
+
"an existing vector file with this digest holds different bytes",
|
|
1693
|
+
)
|
|
1694
|
+
return
|
|
1695
|
+
_atomic_write(path, payload)
|
|
1696
|
+
|
|
1697
|
+
|
|
1698
|
+
def _atomic_write(path: Path, payload: bytes) -> None:
|
|
1699
|
+
temporary = path.with_name(f".{path.name}.partial")
|
|
1700
|
+
temporary.write_bytes(payload)
|
|
1701
|
+
os.replace(temporary, path)
|