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