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,898 @@
|
|
|
1
|
+
"""Strict structural evidence for host-owned source discovery.
|
|
2
|
+
|
|
3
|
+
Search and capability receipts are untrusted host evidence. This module does not claim the host
|
|
4
|
+
ran a browser. It deterministically binds every reported lane, query, source comparison, and
|
|
5
|
+
refusal-recovery chain to one question and one proposal set without granting acquisition authority.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from itertools import pairwise
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from mostlyright.data_harness.canonical import canonical_json_bytes, canonical_sha256
|
|
16
|
+
from mostlyright.data_harness.local_contracts import (
|
|
17
|
+
ContractError,
|
|
18
|
+
SourceProposal,
|
|
19
|
+
source_locator_identity,
|
|
20
|
+
validate_source_locator,
|
|
21
|
+
)
|
|
22
|
+
from mostlyright.data_harness.readers.contracts import ReaderError, identifier, semver
|
|
23
|
+
from mostlyright.data_harness.readers.registry import TOOLBOX
|
|
24
|
+
from mostlyright.data_harness.sources.contracts import FITNESS_DIMENSIONS, MAX_EVIDENCE
|
|
25
|
+
|
|
26
|
+
SOURCE_DISCOVERY_REPORT_SCHEMA_VERSION = "local-source-discovery-report.v2"
|
|
27
|
+
SOURCE_DISCOVERY_POLICY_VERSION = "multi-lane-source-discovery.v2"
|
|
28
|
+
|
|
29
|
+
DISCOVERY_LANES = (
|
|
30
|
+
"registered_adapters",
|
|
31
|
+
"sealed_catalog",
|
|
32
|
+
"host_web_search",
|
|
33
|
+
"official_provider",
|
|
34
|
+
"user_supplied",
|
|
35
|
+
)
|
|
36
|
+
FITNESS_CRITERIA = tuple(sorted(FITNESS_DIMENSIONS))
|
|
37
|
+
DISCOVERY_LANE_STATUSES = ("searched", "unavailable")
|
|
38
|
+
DISCOVERY_CRITERION_RESULTS = ("fail", "pass", "unknown")
|
|
39
|
+
DISCOVERY_FITNESS_DECISIONS = ("eligible", "rejected", "selected")
|
|
40
|
+
DISCOVERY_RECOVERY_OUTCOMES = ("lawful_routes_exhausted", "recovered", "still_rejected")
|
|
41
|
+
DISCOVERY_REJECTION_KINDS = ("fitness_failure", "typed_refusal")
|
|
42
|
+
DISCOVERY_UNAVAILABLE_REASONS = {
|
|
43
|
+
"registered_adapters": ("no_registered_capability",),
|
|
44
|
+
"sealed_catalog": ("catalog_not_configured",),
|
|
45
|
+
"host_web_search": ("host_search_unavailable", "policy_prohibits_network"),
|
|
46
|
+
"official_provider": ("policy_prohibits_network", "provider_search_unavailable"),
|
|
47
|
+
"user_supplied": ("no_user_source_supplied",),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_LANE_STATUSES = frozenset(DISCOVERY_LANE_STATUSES)
|
|
51
|
+
_CRITERION_RESULTS = frozenset(DISCOVERY_CRITERION_RESULTS)
|
|
52
|
+
_FITNESS_DECISIONS = frozenset(DISCOVERY_FITNESS_DECISIONS)
|
|
53
|
+
_RECOVERY_OUTCOMES = frozenset(DISCOVERY_RECOVERY_OUTCOMES)
|
|
54
|
+
_REJECTION_KINDS = frozenset(DISCOVERY_REJECTION_KINDS)
|
|
55
|
+
_IDENTIFIER = re.compile(r"^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$")
|
|
56
|
+
_REFUSAL_CODE = re.compile(r"^[A-Z][A-Z0-9_]{1,127}$")
|
|
57
|
+
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _object(value: Any, path: str, fields: set[str]) -> dict[str, Any]:
|
|
61
|
+
if not isinstance(value, dict):
|
|
62
|
+
raise ContractError(path, "TYPE", "must be an object")
|
|
63
|
+
if set(value) != fields:
|
|
64
|
+
raise ContractError(path, "FIELDS", f"must contain exactly {sorted(fields)!r}")
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _array(value: Any, path: str, *, maximum: int, nonempty: bool = False) -> list[Any]:
|
|
69
|
+
if not isinstance(value, list):
|
|
70
|
+
raise ContractError(path, "TYPE", "must be an array")
|
|
71
|
+
minimum = 1 if nonempty else 0
|
|
72
|
+
if not minimum <= len(value) <= maximum:
|
|
73
|
+
raise ContractError(path, "COLLECTION_LIMIT", f"must contain {minimum}..{maximum} items")
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _text(value: Any, path: str, *, maximum: int = 2_000) -> str:
|
|
78
|
+
if not isinstance(value, str) or not value or len(value) > maximum:
|
|
79
|
+
raise ContractError(path, "TEXT", f"must be non-empty text of at most {maximum} characters")
|
|
80
|
+
if value != value.strip():
|
|
81
|
+
raise ContractError(path, "NONCANONICAL_TEXT", "must not have surrounding whitespace")
|
|
82
|
+
if any(ord(character) < 0x20 and character not in {"\t", "\n", "\r"} for character in value):
|
|
83
|
+
raise ContractError(path, "CONTROL_CHARACTER", "contains a control character")
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _identifier(value: Any, path: str) -> str:
|
|
88
|
+
text = _text(value, path, maximum=64)
|
|
89
|
+
if _IDENTIFIER.fullmatch(text) is None:
|
|
90
|
+
raise ContractError(path, "IDENTIFIER", "must be a lower-case identifier")
|
|
91
|
+
return text
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _choice(value: Any, choices: frozenset[str], path: str) -> str:
|
|
95
|
+
text = _text(value, path, maximum=64)
|
|
96
|
+
if text not in choices:
|
|
97
|
+
raise ContractError(path, "DISCOVERY_CHOICE", f"must be one of {sorted(choices)!r}")
|
|
98
|
+
return text
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _sha256(value: Any, path: str) -> str:
|
|
102
|
+
text = _text(value, path, maximum=64)
|
|
103
|
+
if _SHA256.fullmatch(text) is None:
|
|
104
|
+
raise ContractError(path, "DIGEST", "must be a lower-case SHA-256 digest")
|
|
105
|
+
return text
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _unique(values: tuple[str, ...], path: str) -> None:
|
|
109
|
+
if len(set(values)) != len(values):
|
|
110
|
+
raise ContractError(path, "DUPLICATE", "items must be unique")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class DiscoveryQuery:
|
|
115
|
+
"""One host query and the immutable host-tool receipt it reports."""
|
|
116
|
+
|
|
117
|
+
query: str
|
|
118
|
+
result_source_ids: tuple[str, ...]
|
|
119
|
+
search_receipt_sha256: str
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def from_value(cls, value: Any, path: str) -> DiscoveryQuery:
|
|
123
|
+
data = _object(value, path, {"query", "result_source_ids", "search_receipt_sha256"})
|
|
124
|
+
source_ids = tuple(
|
|
125
|
+
_identifier(item, f"{path}.result_source_ids[{index}]")
|
|
126
|
+
for index, item in enumerate(
|
|
127
|
+
_array(data["result_source_ids"], f"{path}.result_source_ids", maximum=64)
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
_unique(source_ids, f"{path}.result_source_ids")
|
|
131
|
+
return cls(
|
|
132
|
+
query=_text(data["query"], f"{path}.query"),
|
|
133
|
+
result_source_ids=source_ids,
|
|
134
|
+
search_receipt_sha256=_sha256(
|
|
135
|
+
data["search_receipt_sha256"], f"{path}.search_receipt_sha256"
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def to_dict(self) -> dict[str, Any]:
|
|
140
|
+
return {
|
|
141
|
+
"query": self.query,
|
|
142
|
+
"result_source_ids": list(self.result_source_ids),
|
|
143
|
+
"search_receipt_sha256": self.search_receipt_sha256,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True)
|
|
148
|
+
class DiscoveryLane:
|
|
149
|
+
"""Reported availability and bounded query trace for one mandatory lane."""
|
|
150
|
+
|
|
151
|
+
lane: str
|
|
152
|
+
status: str
|
|
153
|
+
reason_code: str | None
|
|
154
|
+
capability_receipt_sha256: str
|
|
155
|
+
queries: tuple[DiscoveryQuery, ...]
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def from_value(cls, value: Any, path: str) -> DiscoveryLane:
|
|
159
|
+
data = _object(
|
|
160
|
+
value,
|
|
161
|
+
path,
|
|
162
|
+
{"lane", "status", "reason_code", "capability_receipt_sha256", "queries"},
|
|
163
|
+
)
|
|
164
|
+
lane = _choice(data["lane"], frozenset(DISCOVERY_LANES), f"{path}.lane")
|
|
165
|
+
status = _choice(data["status"], _LANE_STATUSES, f"{path}.status")
|
|
166
|
+
queries = tuple(
|
|
167
|
+
DiscoveryQuery.from_value(item, f"{path}.queries[{index}]")
|
|
168
|
+
for index, item in enumerate(_array(data["queries"], f"{path}.queries", maximum=32))
|
|
169
|
+
)
|
|
170
|
+
reason_value = data["reason_code"]
|
|
171
|
+
if status == "searched":
|
|
172
|
+
if reason_value is not None:
|
|
173
|
+
raise ContractError(
|
|
174
|
+
f"{path}.reason_code",
|
|
175
|
+
"DISCOVERY_STATE",
|
|
176
|
+
"must be null when the lane was searched",
|
|
177
|
+
)
|
|
178
|
+
if not queries:
|
|
179
|
+
raise ContractError(
|
|
180
|
+
f"{path}.queries",
|
|
181
|
+
"DISCOVERY_STATE",
|
|
182
|
+
"a searched lane must record at least one query",
|
|
183
|
+
)
|
|
184
|
+
reason_code = None
|
|
185
|
+
else:
|
|
186
|
+
reason_code = _choice(
|
|
187
|
+
reason_value,
|
|
188
|
+
frozenset(DISCOVERY_UNAVAILABLE_REASONS[lane]),
|
|
189
|
+
f"{path}.reason_code",
|
|
190
|
+
)
|
|
191
|
+
if queries:
|
|
192
|
+
raise ContractError(
|
|
193
|
+
f"{path}.queries",
|
|
194
|
+
"DISCOVERY_STATE",
|
|
195
|
+
"an unavailable lane cannot claim queries",
|
|
196
|
+
)
|
|
197
|
+
return cls(
|
|
198
|
+
lane=lane,
|
|
199
|
+
status=status,
|
|
200
|
+
reason_code=reason_code,
|
|
201
|
+
capability_receipt_sha256=_sha256(
|
|
202
|
+
data["capability_receipt_sha256"], f"{path}.capability_receipt_sha256"
|
|
203
|
+
),
|
|
204
|
+
queries=queries,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def to_dict(self) -> dict[str, Any]:
|
|
208
|
+
return {
|
|
209
|
+
"lane": self.lane,
|
|
210
|
+
"status": self.status,
|
|
211
|
+
"reason_code": self.reason_code,
|
|
212
|
+
"capability_receipt_sha256": self.capability_receipt_sha256,
|
|
213
|
+
"queries": [item.to_dict() for item in self.queries],
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass(frozen=True)
|
|
218
|
+
class ComparisonDimension:
|
|
219
|
+
"""One lossless proposal-stage mapping to canonical source fitness evidence."""
|
|
220
|
+
|
|
221
|
+
dimension: str
|
|
222
|
+
outcome: str
|
|
223
|
+
summary: str
|
|
224
|
+
evidence_digests: tuple[str, ...]
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
def from_value(cls, value: Any, path: str) -> ComparisonDimension:
|
|
228
|
+
data = _object(value, path, {"dimension", "outcome", "summary", "evidence_digests"})
|
|
229
|
+
evidence_digests = tuple(
|
|
230
|
+
_sha256(item, f"{path}.evidence_digests[{index}]")
|
|
231
|
+
for index, item in enumerate(
|
|
232
|
+
_array(
|
|
233
|
+
data["evidence_digests"],
|
|
234
|
+
f"{path}.evidence_digests",
|
|
235
|
+
maximum=MAX_EVIDENCE,
|
|
236
|
+
nonempty=True,
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
_unique(evidence_digests, f"{path}.evidence_digests")
|
|
241
|
+
return cls(
|
|
242
|
+
dimension=_choice(data["dimension"], frozenset(FITNESS_CRITERIA), f"{path}.dimension"),
|
|
243
|
+
outcome=_choice(data["outcome"], _CRITERION_RESULTS, f"{path}.outcome"),
|
|
244
|
+
summary=_text(data["summary"], f"{path}.summary", maximum=1_000),
|
|
245
|
+
evidence_digests=evidence_digests,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def to_dict(self) -> dict[str, Any]:
|
|
249
|
+
return {
|
|
250
|
+
"dimension": self.dimension,
|
|
251
|
+
"outcome": self.outcome,
|
|
252
|
+
"summary": self.summary,
|
|
253
|
+
"evidence_digests": list(self.evidence_digests),
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass(frozen=True)
|
|
258
|
+
class DiscoveryReaderPin:
|
|
259
|
+
"""A certified Reader identity plus the digest of its complete sealed options."""
|
|
260
|
+
|
|
261
|
+
family_id: str
|
|
262
|
+
family_version: str
|
|
263
|
+
decode_options_digest: str
|
|
264
|
+
|
|
265
|
+
@classmethod
|
|
266
|
+
def from_value(cls, value: Any, path: str) -> DiscoveryReaderPin:
|
|
267
|
+
data = _object(value, path, {"family_id", "family_version", "decode_options_digest"})
|
|
268
|
+
try:
|
|
269
|
+
family_id = identifier(data["family_id"], f"{path}.family_id")
|
|
270
|
+
family_version = semver(data["family_version"], f"{path}.family_version")
|
|
271
|
+
except ReaderError as error:
|
|
272
|
+
raise ContractError(
|
|
273
|
+
path,
|
|
274
|
+
"DISCOVERY_READER_PIN",
|
|
275
|
+
"must contain a canonical certified Reader identity",
|
|
276
|
+
) from error
|
|
277
|
+
if (family_id, family_version) not in TOOLBOX.families():
|
|
278
|
+
raise ContractError(
|
|
279
|
+
path,
|
|
280
|
+
"DISCOVERY_READER_PIN",
|
|
281
|
+
"must resolve to an exact Reader coordinate in the certified Toolbox",
|
|
282
|
+
)
|
|
283
|
+
return cls(
|
|
284
|
+
family_id=family_id,
|
|
285
|
+
family_version=family_version,
|
|
286
|
+
decode_options_digest=_sha256(
|
|
287
|
+
data["decode_options_digest"], f"{path}.decode_options_digest"
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def to_dict(self) -> dict[str, Any]:
|
|
292
|
+
return {
|
|
293
|
+
"family_id": self.family_id,
|
|
294
|
+
"family_version": self.family_version,
|
|
295
|
+
"decode_options_digest": self.decode_options_digest,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@dataclass(frozen=True)
|
|
300
|
+
class SourceComparison:
|
|
301
|
+
"""A proposed source evaluated over every canonical fitness dimension."""
|
|
302
|
+
|
|
303
|
+
source_id: str
|
|
304
|
+
decision: str
|
|
305
|
+
rejection_kind: str | None
|
|
306
|
+
promoted_inspection_scope: RecoveryScope | None
|
|
307
|
+
promoted_inspection_receipt_sha256: str | None
|
|
308
|
+
dimensions: tuple[ComparisonDimension, ...]
|
|
309
|
+
rationale: str
|
|
310
|
+
recovery_attempt_ids: tuple[str, ...]
|
|
311
|
+
|
|
312
|
+
@classmethod
|
|
313
|
+
def from_value(cls, value: Any, path: str) -> SourceComparison:
|
|
314
|
+
data = _object(
|
|
315
|
+
value,
|
|
316
|
+
path,
|
|
317
|
+
{
|
|
318
|
+
"source_id",
|
|
319
|
+
"decision",
|
|
320
|
+
"rejection_kind",
|
|
321
|
+
"promoted_inspection_scope",
|
|
322
|
+
"promoted_inspection_receipt_sha256",
|
|
323
|
+
"dimensions",
|
|
324
|
+
"rationale",
|
|
325
|
+
"recovery_attempt_ids",
|
|
326
|
+
},
|
|
327
|
+
)
|
|
328
|
+
dimensions = tuple(
|
|
329
|
+
ComparisonDimension.from_value(item, f"{path}.dimensions[{index}]")
|
|
330
|
+
for index, item in enumerate(
|
|
331
|
+
_array(
|
|
332
|
+
data["dimensions"],
|
|
333
|
+
f"{path}.dimensions",
|
|
334
|
+
maximum=len(FITNESS_CRITERIA),
|
|
335
|
+
nonempty=True,
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
dimension_names = tuple(item.dimension for item in dimensions)
|
|
340
|
+
_unique(dimension_names, f"{path}.dimensions")
|
|
341
|
+
if set(dimension_names) != set(FITNESS_CRITERIA):
|
|
342
|
+
raise ContractError(
|
|
343
|
+
f"{path}.dimensions",
|
|
344
|
+
"FITNESS_DIMENSIONS",
|
|
345
|
+
"must contain every canonical source fitness dimension exactly once",
|
|
346
|
+
)
|
|
347
|
+
attempt_ids = tuple(
|
|
348
|
+
_identifier(item, f"{path}.recovery_attempt_ids[{index}]")
|
|
349
|
+
for index, item in enumerate(
|
|
350
|
+
_array(data["recovery_attempt_ids"], f"{path}.recovery_attempt_ids", maximum=64)
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
_unique(attempt_ids, f"{path}.recovery_attempt_ids")
|
|
354
|
+
decision = _choice(data["decision"], _FITNESS_DECISIONS, f"{path}.decision")
|
|
355
|
+
rejection_value = data["rejection_kind"]
|
|
356
|
+
if decision == "rejected":
|
|
357
|
+
rejection_kind = _choice(rejection_value, _REJECTION_KINDS, f"{path}.rejection_kind")
|
|
358
|
+
if data["promoted_inspection_scope"] is not None:
|
|
359
|
+
raise ContractError(
|
|
360
|
+
f"{path}.promoted_inspection_scope",
|
|
361
|
+
"DISCOVERY_STATE",
|
|
362
|
+
"must be null when the source is rejected",
|
|
363
|
+
)
|
|
364
|
+
promoted_inspection_scope = None
|
|
365
|
+
if data["promoted_inspection_receipt_sha256"] is not None:
|
|
366
|
+
raise ContractError(
|
|
367
|
+
f"{path}.promoted_inspection_receipt_sha256",
|
|
368
|
+
"DISCOVERY_STATE",
|
|
369
|
+
"must be null when the source is rejected",
|
|
370
|
+
)
|
|
371
|
+
promoted_inspection_receipt_sha256 = None
|
|
372
|
+
else:
|
|
373
|
+
if rejection_value is not None:
|
|
374
|
+
raise ContractError(
|
|
375
|
+
f"{path}.rejection_kind",
|
|
376
|
+
"DISCOVERY_STATE",
|
|
377
|
+
"must be null unless the source is rejected",
|
|
378
|
+
)
|
|
379
|
+
rejection_kind = None
|
|
380
|
+
if data["promoted_inspection_scope"] is None:
|
|
381
|
+
raise ContractError(
|
|
382
|
+
f"{path}.promoted_inspection_scope",
|
|
383
|
+
"DISCOVERY_PROMOTED_SCOPE_REQUIRED",
|
|
384
|
+
"a retained source requires its exact inspected execution coordinate",
|
|
385
|
+
)
|
|
386
|
+
promoted_inspection_scope = RecoveryScope.from_value(
|
|
387
|
+
data["promoted_inspection_scope"], f"{path}.promoted_inspection_scope"
|
|
388
|
+
)
|
|
389
|
+
promoted_inspection_receipt_sha256 = _sha256(
|
|
390
|
+
data["promoted_inspection_receipt_sha256"],
|
|
391
|
+
f"{path}.promoted_inspection_receipt_sha256",
|
|
392
|
+
)
|
|
393
|
+
if rejection_kind == "typed_refusal" and not attempt_ids:
|
|
394
|
+
raise ContractError(
|
|
395
|
+
f"{path}.recovery_attempt_ids",
|
|
396
|
+
"DISCOVERY_REFUSAL_RECOVERY_REQUIRED",
|
|
397
|
+
"a typed refusal requires a changed-scope recovery chain",
|
|
398
|
+
)
|
|
399
|
+
if rejection_kind == "fitness_failure" and attempt_ids:
|
|
400
|
+
raise ContractError(
|
|
401
|
+
f"{path}.recovery_attempt_ids",
|
|
402
|
+
"DISCOVERY_REFUSAL_CLASSIFICATION",
|
|
403
|
+
"a fitness failure cannot carry typed-refusal recovery attempts",
|
|
404
|
+
)
|
|
405
|
+
return cls(
|
|
406
|
+
source_id=_identifier(data["source_id"], f"{path}.source_id"),
|
|
407
|
+
decision=decision,
|
|
408
|
+
rejection_kind=rejection_kind,
|
|
409
|
+
promoted_inspection_scope=promoted_inspection_scope,
|
|
410
|
+
promoted_inspection_receipt_sha256=promoted_inspection_receipt_sha256,
|
|
411
|
+
dimensions=dimensions,
|
|
412
|
+
rationale=_text(data["rationale"], f"{path}.rationale"),
|
|
413
|
+
recovery_attempt_ids=attempt_ids,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def to_dict(self) -> dict[str, Any]:
|
|
417
|
+
return {
|
|
418
|
+
"source_id": self.source_id,
|
|
419
|
+
"decision": self.decision,
|
|
420
|
+
"rejection_kind": self.rejection_kind,
|
|
421
|
+
"promoted_inspection_scope": (
|
|
422
|
+
None
|
|
423
|
+
if self.promoted_inspection_scope is None
|
|
424
|
+
else self.promoted_inspection_scope.to_dict()
|
|
425
|
+
),
|
|
426
|
+
"promoted_inspection_receipt_sha256": self.promoted_inspection_receipt_sha256,
|
|
427
|
+
"dimensions": [item.to_dict() for item in self.dimensions],
|
|
428
|
+
"rationale": self.rationale,
|
|
429
|
+
"recovery_attempt_ids": list(self.recovery_attempt_ids),
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@dataclass(frozen=True)
|
|
434
|
+
class RecoveryScope:
|
|
435
|
+
"""The exact execution coordinates on one side of a refusal retry."""
|
|
436
|
+
|
|
437
|
+
locator_kind: str
|
|
438
|
+
locator: str
|
|
439
|
+
reader_pin: DiscoveryReaderPin | None
|
|
440
|
+
data_format: str
|
|
441
|
+
host_platform: str
|
|
442
|
+
|
|
443
|
+
@classmethod
|
|
444
|
+
def from_value(cls, value: Any, path: str) -> RecoveryScope:
|
|
445
|
+
data = _object(
|
|
446
|
+
value,
|
|
447
|
+
path,
|
|
448
|
+
{"locator_kind", "locator", "reader_pin", "data_format", "host_platform"},
|
|
449
|
+
)
|
|
450
|
+
reader_value = data["reader_pin"]
|
|
451
|
+
reader_pin = (
|
|
452
|
+
None
|
|
453
|
+
if reader_value is None
|
|
454
|
+
else DiscoveryReaderPin.from_value(reader_value, f"{path}.reader_pin")
|
|
455
|
+
)
|
|
456
|
+
locator_kind = _choice(
|
|
457
|
+
data["locator_kind"],
|
|
458
|
+
frozenset({"relative_path", "https_url", "artifact_reference"}),
|
|
459
|
+
f"{path}.locator_kind",
|
|
460
|
+
)
|
|
461
|
+
return cls(
|
|
462
|
+
locator_kind=locator_kind,
|
|
463
|
+
locator=validate_source_locator(data["locator"], locator_kind, f"{path}.locator"),
|
|
464
|
+
reader_pin=reader_pin,
|
|
465
|
+
data_format=_text(data["data_format"], f"{path}.data_format", maximum=64),
|
|
466
|
+
host_platform=_text(data["host_platform"], f"{path}.host_platform", maximum=256),
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
def to_dict(self) -> dict[str, Any]:
|
|
470
|
+
return {
|
|
471
|
+
"locator_kind": self.locator_kind,
|
|
472
|
+
"locator": self.locator,
|
|
473
|
+
"reader_pin": None if self.reader_pin is None else self.reader_pin.to_dict(),
|
|
474
|
+
"data_format": self.data_format,
|
|
475
|
+
"host_platform": self.host_platform,
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@dataclass(frozen=True)
|
|
480
|
+
class RecoveryAttempt:
|
|
481
|
+
"""One refusal receipt and one exact replacement inspection receipt."""
|
|
482
|
+
|
|
483
|
+
attempt_id: str
|
|
484
|
+
source_id: str
|
|
485
|
+
refusal_code: str
|
|
486
|
+
refusal_receipt_sha256: str
|
|
487
|
+
refusal_scope: RecoveryScope
|
|
488
|
+
replacement_scope: RecoveryScope
|
|
489
|
+
replacement_receipt_sha256: str
|
|
490
|
+
outcome: str
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def changed_fields(self) -> tuple[str, ...]:
|
|
494
|
+
return tuple(
|
|
495
|
+
field
|
|
496
|
+
for field in (
|
|
497
|
+
"locator_kind",
|
|
498
|
+
"locator",
|
|
499
|
+
"reader_pin",
|
|
500
|
+
"data_format",
|
|
501
|
+
"host_platform",
|
|
502
|
+
)
|
|
503
|
+
if getattr(self.refusal_scope, field) != getattr(self.replacement_scope, field)
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
@classmethod
|
|
507
|
+
def from_value(cls, value: Any, path: str) -> RecoveryAttempt:
|
|
508
|
+
data = _object(
|
|
509
|
+
value,
|
|
510
|
+
path,
|
|
511
|
+
{
|
|
512
|
+
"attempt_id",
|
|
513
|
+
"source_id",
|
|
514
|
+
"refusal_code",
|
|
515
|
+
"refusal_receipt_sha256",
|
|
516
|
+
"refusal_scope",
|
|
517
|
+
"replacement_scope",
|
|
518
|
+
"replacement_receipt_sha256",
|
|
519
|
+
"outcome",
|
|
520
|
+
},
|
|
521
|
+
)
|
|
522
|
+
refusal_code = _text(data["refusal_code"], f"{path}.refusal_code", maximum=128)
|
|
523
|
+
if _REFUSAL_CODE.fullmatch(refusal_code) is None:
|
|
524
|
+
raise ContractError(
|
|
525
|
+
f"{path}.refusal_code",
|
|
526
|
+
"DISCOVERY_REFUSAL_CODE",
|
|
527
|
+
"must be a canonical upper-case refusal code",
|
|
528
|
+
)
|
|
529
|
+
attempt = cls(
|
|
530
|
+
attempt_id=_identifier(data["attempt_id"], f"{path}.attempt_id"),
|
|
531
|
+
source_id=_identifier(data["source_id"], f"{path}.source_id"),
|
|
532
|
+
refusal_code=refusal_code,
|
|
533
|
+
refusal_receipt_sha256=_sha256(
|
|
534
|
+
data["refusal_receipt_sha256"], f"{path}.refusal_receipt_sha256"
|
|
535
|
+
),
|
|
536
|
+
refusal_scope=RecoveryScope.from_value(data["refusal_scope"], f"{path}.refusal_scope"),
|
|
537
|
+
replacement_scope=RecoveryScope.from_value(
|
|
538
|
+
data["replacement_scope"], f"{path}.replacement_scope"
|
|
539
|
+
),
|
|
540
|
+
replacement_receipt_sha256=_sha256(
|
|
541
|
+
data["replacement_receipt_sha256"], f"{path}.replacement_receipt_sha256"
|
|
542
|
+
),
|
|
543
|
+
outcome=_choice(data["outcome"], _RECOVERY_OUTCOMES, f"{path}.outcome"),
|
|
544
|
+
)
|
|
545
|
+
if not attempt.changed_fields:
|
|
546
|
+
raise ContractError(
|
|
547
|
+
f"{path}.replacement_scope",
|
|
548
|
+
"DISCOVERY_UNCHANGED_RECOVERY",
|
|
549
|
+
"must change at least one exact execution coordinate",
|
|
550
|
+
)
|
|
551
|
+
return attempt
|
|
552
|
+
|
|
553
|
+
def to_dict(self) -> dict[str, Any]:
|
|
554
|
+
return {
|
|
555
|
+
"attempt_id": self.attempt_id,
|
|
556
|
+
"source_id": self.source_id,
|
|
557
|
+
"refusal_code": self.refusal_code,
|
|
558
|
+
"refusal_receipt_sha256": self.refusal_receipt_sha256,
|
|
559
|
+
"refusal_scope": self.refusal_scope.to_dict(),
|
|
560
|
+
"replacement_scope": self.replacement_scope.to_dict(),
|
|
561
|
+
"replacement_receipt_sha256": self.replacement_receipt_sha256,
|
|
562
|
+
"outcome": self.outcome,
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@dataclass(frozen=True)
|
|
567
|
+
class SourceDiscoveryReport:
|
|
568
|
+
"""Canonical untrusted evidence that a host reports following the multi-lane policy."""
|
|
569
|
+
|
|
570
|
+
question_id: str
|
|
571
|
+
lanes: tuple[DiscoveryLane, ...]
|
|
572
|
+
comparisons: tuple[SourceComparison, ...]
|
|
573
|
+
recovery_attempts: tuple[RecoveryAttempt, ...]
|
|
574
|
+
schema_version: str = SOURCE_DISCOVERY_REPORT_SCHEMA_VERSION
|
|
575
|
+
|
|
576
|
+
@property
|
|
577
|
+
def canonical_bytes(self) -> bytes:
|
|
578
|
+
return canonical_json_bytes(self.to_dict())
|
|
579
|
+
|
|
580
|
+
@property
|
|
581
|
+
def digest(self) -> str:
|
|
582
|
+
return canonical_sha256(self.to_dict())
|
|
583
|
+
|
|
584
|
+
@classmethod
|
|
585
|
+
def from_value(cls, value: Any) -> SourceDiscoveryReport:
|
|
586
|
+
path = "source_discovery"
|
|
587
|
+
data = _object(
|
|
588
|
+
value,
|
|
589
|
+
path,
|
|
590
|
+
{"schema_version", "question_id", "lanes", "comparisons", "recovery_attempts"},
|
|
591
|
+
)
|
|
592
|
+
version = _text(data["schema_version"], f"{path}.schema_version", maximum=64)
|
|
593
|
+
if version != SOURCE_DISCOVERY_REPORT_SCHEMA_VERSION:
|
|
594
|
+
raise ContractError(
|
|
595
|
+
f"{path}.schema_version",
|
|
596
|
+
"VERSION",
|
|
597
|
+
f"must be {SOURCE_DISCOVERY_REPORT_SCHEMA_VERSION!r}",
|
|
598
|
+
)
|
|
599
|
+
lanes = tuple(
|
|
600
|
+
DiscoveryLane.from_value(item, f"{path}.lanes[{index}]")
|
|
601
|
+
for index, item in enumerate(
|
|
602
|
+
_array(data["lanes"], f"{path}.lanes", maximum=len(DISCOVERY_LANES), nonempty=True)
|
|
603
|
+
)
|
|
604
|
+
)
|
|
605
|
+
lane_names = tuple(item.lane for item in lanes)
|
|
606
|
+
_unique(lane_names, f"{path}.lanes")
|
|
607
|
+
if set(lane_names) != set(DISCOVERY_LANES):
|
|
608
|
+
raise ContractError(
|
|
609
|
+
f"{path}.lanes",
|
|
610
|
+
"DISCOVERY_LANES_INCOMPLETE",
|
|
611
|
+
f"must account for exactly {list(DISCOVERY_LANES)!r}",
|
|
612
|
+
)
|
|
613
|
+
comparisons = tuple(
|
|
614
|
+
SourceComparison.from_value(item, f"{path}.comparisons[{index}]")
|
|
615
|
+
for index, item in enumerate(
|
|
616
|
+
_array(data["comparisons"], f"{path}.comparisons", maximum=64)
|
|
617
|
+
)
|
|
618
|
+
)
|
|
619
|
+
_unique(tuple(item.source_id for item in comparisons), f"{path}.comparisons")
|
|
620
|
+
recovery_attempts = tuple(
|
|
621
|
+
RecoveryAttempt.from_value(item, f"{path}.recovery_attempts[{index}]")
|
|
622
|
+
for index, item in enumerate(
|
|
623
|
+
_array(data["recovery_attempts"], f"{path}.recovery_attempts", maximum=64)
|
|
624
|
+
)
|
|
625
|
+
)
|
|
626
|
+
_unique(tuple(item.attempt_id for item in recovery_attempts), f"{path}.recovery_attempts")
|
|
627
|
+
return cls(
|
|
628
|
+
schema_version=version,
|
|
629
|
+
question_id=_identifier(data["question_id"], f"{path}.question_id"),
|
|
630
|
+
lanes=lanes,
|
|
631
|
+
comparisons=comparisons,
|
|
632
|
+
recovery_attempts=recovery_attempts,
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
def validate_against(
|
|
636
|
+
self,
|
|
637
|
+
*,
|
|
638
|
+
question_id: str,
|
|
639
|
+
proposals: tuple[SourceProposal, ...],
|
|
640
|
+
feasibility: str,
|
|
641
|
+
required_start: str,
|
|
642
|
+
required_end: str,
|
|
643
|
+
) -> None:
|
|
644
|
+
if self.question_id != question_id:
|
|
645
|
+
raise ContractError(
|
|
646
|
+
"source_discovery.question_id",
|
|
647
|
+
"REFERENCE_MISMATCH",
|
|
648
|
+
"must reference question.question_id",
|
|
649
|
+
)
|
|
650
|
+
proposal_by_source = {item.source_id: item for item in proposals}
|
|
651
|
+
proposal_ids = set(proposal_by_source)
|
|
652
|
+
if {item.source_id for item in self.comparisons} != proposal_ids:
|
|
653
|
+
raise ContractError(
|
|
654
|
+
"source_discovery.comparisons",
|
|
655
|
+
"REFERENCE_MISMATCH",
|
|
656
|
+
"must compare every and only proposed source",
|
|
657
|
+
)
|
|
658
|
+
identities = tuple(
|
|
659
|
+
source_locator_identity(item.locator_kind, item.locator) for item in proposals
|
|
660
|
+
)
|
|
661
|
+
if len(set(identities)) != len(identities):
|
|
662
|
+
raise ContractError(
|
|
663
|
+
"source_proposals.locator",
|
|
664
|
+
"DISCOVERY_DUPLICATE_SOURCE",
|
|
665
|
+
"the same exact source locator cannot be proposed more than once",
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
discovered_ids: set[str] = set()
|
|
669
|
+
query_receipts_by_source: dict[str, set[str]] = {
|
|
670
|
+
source_id: set() for source_id in proposal_ids
|
|
671
|
+
}
|
|
672
|
+
for lane in self.lanes:
|
|
673
|
+
for query in lane.queries:
|
|
674
|
+
for source_id in query.result_source_ids:
|
|
675
|
+
if source_id not in proposal_ids:
|
|
676
|
+
raise ContractError(
|
|
677
|
+
f"source_discovery.lanes.{lane.lane}.queries.result_source_ids",
|
|
678
|
+
"REFERENCE_MISMATCH",
|
|
679
|
+
"must reference a proposed source",
|
|
680
|
+
)
|
|
681
|
+
discovered_ids.add(source_id)
|
|
682
|
+
query_receipts_by_source[source_id].add(query.search_receipt_sha256)
|
|
683
|
+
if discovered_ids != proposal_ids:
|
|
684
|
+
raise ContractError(
|
|
685
|
+
"source_discovery.lanes",
|
|
686
|
+
"DISCOVERY_UNDISCOVERED_PROPOSAL",
|
|
687
|
+
"every proposed source must appear in a searched lane query result",
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
attempt_by_id = {item.attempt_id: item for item in self.recovery_attempts}
|
|
691
|
+
referenced_attempts: set[str] = set()
|
|
692
|
+
for comparison in self.comparisons:
|
|
693
|
+
proposal = proposal_by_source[comparison.source_id]
|
|
694
|
+
if comparison.decision != proposal.fitness_decision:
|
|
695
|
+
raise ContractError(
|
|
696
|
+
f"source_discovery.comparisons.{comparison.source_id}.decision",
|
|
697
|
+
"DECISION_MISMATCH",
|
|
698
|
+
"must match source_proposal.fitness_decision",
|
|
699
|
+
)
|
|
700
|
+
outcomes = {item.dimension: item.outcome for item in comparison.dimensions}
|
|
701
|
+
if comparison.decision == "selected" and set(outcomes.values()) != {"pass"}:
|
|
702
|
+
raise ContractError(
|
|
703
|
+
f"source_discovery.comparisons.{comparison.source_id}.dimensions",
|
|
704
|
+
"UNFIT_SELECTED",
|
|
705
|
+
"a selected source requires every canonical fitness dimension to pass",
|
|
706
|
+
)
|
|
707
|
+
if comparison.rejection_kind == "fitness_failure" and "fail" not in outcomes.values():
|
|
708
|
+
raise ContractError(
|
|
709
|
+
f"source_discovery.comparisons.{comparison.source_id}.dimensions",
|
|
710
|
+
"DECISION_CRITERIA_MISMATCH",
|
|
711
|
+
"a fitness-failure rejection must identify a failed fitness dimension",
|
|
712
|
+
)
|
|
713
|
+
if comparison.rejection_kind == "typed_refusal" and "fail" in outcomes.values():
|
|
714
|
+
raise ContractError(
|
|
715
|
+
f"source_discovery.comparisons.{comparison.source_id}.dimensions",
|
|
716
|
+
"DISCOVERY_REFUSAL_CLASSIFICATION",
|
|
717
|
+
"a typed acquisition refusal cannot be represented as a fitness failure",
|
|
718
|
+
)
|
|
719
|
+
if comparison.decision == "eligible" and "fail" in outcomes.values():
|
|
720
|
+
raise ContractError(
|
|
721
|
+
f"source_discovery.comparisons.{comparison.source_id}.dimensions",
|
|
722
|
+
"DECISION_CRITERIA_MISMATCH",
|
|
723
|
+
"an eligible source cannot have a failed fitness dimension",
|
|
724
|
+
)
|
|
725
|
+
allowed_rights = {
|
|
726
|
+
"approved": {"pass"},
|
|
727
|
+
"conditional": {"pass", "unknown"},
|
|
728
|
+
"unclear": {"unknown", "fail"},
|
|
729
|
+
"prohibited": {"fail"},
|
|
730
|
+
}[proposal.rights_status]
|
|
731
|
+
if outcomes["rights"] not in allowed_rights:
|
|
732
|
+
raise ContractError(
|
|
733
|
+
f"source_discovery.comparisons.{comparison.source_id}.dimensions.rights",
|
|
734
|
+
"DISCOVERY_CRITERIA_EVIDENCE_MISMATCH",
|
|
735
|
+
"must agree with source_proposal.rights_status",
|
|
736
|
+
)
|
|
737
|
+
covers_required_history = (
|
|
738
|
+
proposal.historical_coverage.start_inclusive <= required_start
|
|
739
|
+
and proposal.historical_coverage.end_exclusive >= required_end
|
|
740
|
+
)
|
|
741
|
+
if outcomes["historical_coverage"] == "pass" and not covers_required_history:
|
|
742
|
+
raise ContractError(
|
|
743
|
+
f"source_discovery.comparisons.{comparison.source_id}.dimensions."
|
|
744
|
+
"historical_coverage",
|
|
745
|
+
"DISCOVERY_CRITERIA_EVIDENCE_MISMATCH",
|
|
746
|
+
"cannot pass when source coverage does not contain the required time range",
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
known_evidence = {
|
|
750
|
+
item.content_sha256 for item in proposal.evidence
|
|
751
|
+
} | query_receipts_by_source[comparison.source_id]
|
|
752
|
+
if comparison.promoted_inspection_receipt_sha256 is not None:
|
|
753
|
+
known_evidence.add(comparison.promoted_inspection_receipt_sha256)
|
|
754
|
+
attempts: list[RecoveryAttempt] = []
|
|
755
|
+
for attempt_id in comparison.recovery_attempt_ids:
|
|
756
|
+
attempt = attempt_by_id.get(attempt_id)
|
|
757
|
+
if attempt is None or attempt.source_id != comparison.source_id:
|
|
758
|
+
raise ContractError(
|
|
759
|
+
f"source_discovery.comparisons.{comparison.source_id}.recovery_attempt_ids",
|
|
760
|
+
"REFERENCE_MISMATCH",
|
|
761
|
+
"must reference recovery attempts for this source",
|
|
762
|
+
)
|
|
763
|
+
attempts.append(attempt)
|
|
764
|
+
referenced_attempts.add(attempt_id)
|
|
765
|
+
known_evidence.update(
|
|
766
|
+
{attempt.refusal_receipt_sha256, attempt.replacement_receipt_sha256}
|
|
767
|
+
)
|
|
768
|
+
for dimension in comparison.dimensions:
|
|
769
|
+
if not set(dimension.evidence_digests) <= known_evidence:
|
|
770
|
+
raise ContractError(
|
|
771
|
+
f"source_discovery.comparisons.{comparison.source_id}.dimensions."
|
|
772
|
+
f"{dimension.dimension}.evidence_digests",
|
|
773
|
+
"DISCOVERY_EVIDENCE_MISMATCH",
|
|
774
|
+
"must cite proposal, search, or refusal-recovery evidence for this source",
|
|
775
|
+
)
|
|
776
|
+
self._validate_recovery_chain(comparison, proposal, attempts)
|
|
777
|
+
|
|
778
|
+
if referenced_attempts != set(attempt_by_id):
|
|
779
|
+
raise ContractError(
|
|
780
|
+
"source_discovery.recovery_attempts",
|
|
781
|
+
"DISCOVERY_ORPHAN_RECOVERY",
|
|
782
|
+
"every recovery attempt must be ordered by exactly one source comparison",
|
|
783
|
+
)
|
|
784
|
+
selected = sum(item.fitness_decision == "selected" for item in proposals)
|
|
785
|
+
if feasibility in {"supportable", "supportable_with_limits"} and selected == 0:
|
|
786
|
+
raise ContractError(
|
|
787
|
+
"source_discovery.comparisons",
|
|
788
|
+
"DISCOVERY_FEASIBILITY_MISMATCH",
|
|
789
|
+
"a supportable decision requires at least one selected source",
|
|
790
|
+
)
|
|
791
|
+
if feasibility == "unsupported" and any(
|
|
792
|
+
item.fitness_decision != "rejected" for item in proposals
|
|
793
|
+
):
|
|
794
|
+
raise ContractError(
|
|
795
|
+
"source_discovery.comparisons",
|
|
796
|
+
"UNSUPPORTED_NOT_EXHAUSTED",
|
|
797
|
+
"unsupported requires every compared source to be rejected",
|
|
798
|
+
)
|
|
799
|
+
if feasibility == "unsupported":
|
|
800
|
+
for comparison in self.comparisons:
|
|
801
|
+
if comparison.rejection_kind != "typed_refusal":
|
|
802
|
+
continue
|
|
803
|
+
final_attempt = attempt_by_id[comparison.recovery_attempt_ids[-1]]
|
|
804
|
+
if final_attempt.outcome != "lawful_routes_exhausted":
|
|
805
|
+
raise ContractError(
|
|
806
|
+
f"source_discovery.recovery_attempts.{final_attempt.attempt_id}.outcome",
|
|
807
|
+
"UNSUPPORTED_NOT_EXHAUSTED",
|
|
808
|
+
"unsupported requires every typed-refusal chain to end "
|
|
809
|
+
"lawful_routes_exhausted",
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
@staticmethod
|
|
813
|
+
def _validate_recovery_chain(
|
|
814
|
+
comparison: SourceComparison,
|
|
815
|
+
proposal: SourceProposal,
|
|
816
|
+
attempts: list[RecoveryAttempt],
|
|
817
|
+
) -> None:
|
|
818
|
+
retained = comparison.promoted_inspection_scope
|
|
819
|
+
if retained is not None and (
|
|
820
|
+
retained.locator_kind != proposal.locator_kind
|
|
821
|
+
or retained.locator != proposal.locator
|
|
822
|
+
or retained.data_format != proposal.data_format
|
|
823
|
+
):
|
|
824
|
+
raise ContractError(
|
|
825
|
+
f"source_discovery.comparisons.{comparison.source_id}.promoted_inspection_scope",
|
|
826
|
+
"DISCOVERY_RECOVERY_BINDING",
|
|
827
|
+
"the promoted inspection coordinate must match the proposal locator and format",
|
|
828
|
+
)
|
|
829
|
+
for attempt in attempts[:-1]:
|
|
830
|
+
if attempt.outcome != "still_rejected":
|
|
831
|
+
raise ContractError(
|
|
832
|
+
f"source_discovery.recovery_attempts.{attempt.attempt_id}.outcome",
|
|
833
|
+
"DISCOVERY_RECOVERY_TERMINAL",
|
|
834
|
+
"every non-final refusal retry must end still_rejected",
|
|
835
|
+
)
|
|
836
|
+
for previous, current in pairwise(attempts):
|
|
837
|
+
if previous.replacement_scope != current.refusal_scope:
|
|
838
|
+
raise ContractError(
|
|
839
|
+
f"source_discovery.comparisons.{comparison.source_id}.recovery_attempt_ids",
|
|
840
|
+
"DISCOVERY_RECOVERY_CHAIN",
|
|
841
|
+
"each retry must begin at the exact replacement scope of the prior refusal",
|
|
842
|
+
)
|
|
843
|
+
if not attempts:
|
|
844
|
+
return
|
|
845
|
+
final = attempts[-1]
|
|
846
|
+
if comparison.decision in {"selected", "eligible"}:
|
|
847
|
+
if final.outcome != "recovered":
|
|
848
|
+
raise ContractError(
|
|
849
|
+
f"source_discovery.recovery_attempts.{final.attempt_id}.outcome",
|
|
850
|
+
"DISCOVERY_RECOVERY_OUTCOME",
|
|
851
|
+
"a retained source requires its final refusal retry to recover",
|
|
852
|
+
)
|
|
853
|
+
if final.replacement_scope != comparison.promoted_inspection_scope:
|
|
854
|
+
raise ContractError(
|
|
855
|
+
f"source_discovery.recovery_attempts.{final.attempt_id}.replacement_scope",
|
|
856
|
+
"DISCOVERY_RECOVERY_BINDING",
|
|
857
|
+
"a recovered replacement must match the exact promoted inspection coordinate",
|
|
858
|
+
)
|
|
859
|
+
if final.replacement_receipt_sha256 != comparison.promoted_inspection_receipt_sha256:
|
|
860
|
+
raise ContractError(
|
|
861
|
+
f"source_discovery.recovery_attempts.{final.attempt_id}."
|
|
862
|
+
"replacement_receipt_sha256",
|
|
863
|
+
"DISCOVERY_RECOVERY_BINDING",
|
|
864
|
+
"a recovered replacement receipt must be the promoted inspection receipt",
|
|
865
|
+
)
|
|
866
|
+
else:
|
|
867
|
+
first = attempts[0].refusal_scope
|
|
868
|
+
if (
|
|
869
|
+
first.locator_kind != proposal.locator_kind
|
|
870
|
+
or first.locator != proposal.locator
|
|
871
|
+
or first.data_format != proposal.data_format
|
|
872
|
+
):
|
|
873
|
+
raise ContractError(
|
|
874
|
+
f"source_discovery.recovery_attempts.{attempts[0].attempt_id}.refusal_scope",
|
|
875
|
+
"DISCOVERY_RECOVERY_BINDING",
|
|
876
|
+
"a rejected refusal chain must start at the proposed locator and format",
|
|
877
|
+
)
|
|
878
|
+
if final.outcome == "recovered":
|
|
879
|
+
raise ContractError(
|
|
880
|
+
f"source_discovery.recovery_attempts.{final.attempt_id}.outcome",
|
|
881
|
+
"DISCOVERY_RECOVERY_OUTCOME",
|
|
882
|
+
"a rejected source cannot end its refusal chain as recovered",
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
def to_dict(self) -> dict[str, Any]:
|
|
886
|
+
return {
|
|
887
|
+
"schema_version": self.schema_version,
|
|
888
|
+
"question_id": self.question_id,
|
|
889
|
+
"lanes": [item.to_dict() for item in self.lanes],
|
|
890
|
+
"comparisons": [item.to_dict() for item in self.comparisons],
|
|
891
|
+
"recovery_attempts": [item.to_dict() for item in self.recovery_attempts],
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def parse_source_discovery_report(value: Any) -> SourceDiscoveryReport:
|
|
896
|
+
"""Parse untrusted discovery evidence without performing search or acquisition."""
|
|
897
|
+
|
|
898
|
+
return SourceDiscoveryReport.from_value(value)
|