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,1099 @@
|
|
|
1
|
+
"""Trusted immutable repair execution and independent closure coordination."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import threading
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass, replace
|
|
9
|
+
from typing import Any, Protocol
|
|
10
|
+
|
|
11
|
+
from cryptography.exceptions import InvalidSignature
|
|
12
|
+
|
|
13
|
+
from mostlyright.data_harness.canonical import canonical_json_bytes, canonical_sha256
|
|
14
|
+
from mostlyright.data_harness.preparation.contracts import (
|
|
15
|
+
AsOfJoinSpec,
|
|
16
|
+
EqualityJoinSpec,
|
|
17
|
+
OperationSpec,
|
|
18
|
+
PreparationPlan,
|
|
19
|
+
)
|
|
20
|
+
from mostlyright.data_harness.preparation.engine import (
|
|
21
|
+
PreparationResult,
|
|
22
|
+
execute_preparation,
|
|
23
|
+
)
|
|
24
|
+
from mostlyright.data_harness.preparation.table import TableState
|
|
25
|
+
from mostlyright.data_harness.repair.adapters import (
|
|
26
|
+
append_join,
|
|
27
|
+
append_operation,
|
|
28
|
+
candidate_digest,
|
|
29
|
+
input_set_digest,
|
|
30
|
+
output_columns_digest,
|
|
31
|
+
plan_digest,
|
|
32
|
+
preparation_value,
|
|
33
|
+
replace_join,
|
|
34
|
+
replace_operation,
|
|
35
|
+
validation_policy_digest,
|
|
36
|
+
)
|
|
37
|
+
from mostlyright.data_harness.repair.contracts import (
|
|
38
|
+
ALL_QUALITY_CHECK_IDS,
|
|
39
|
+
BLOCKING_SEVERITIES,
|
|
40
|
+
CLOSURE_DECISION_VERSION,
|
|
41
|
+
CLOSURE_REPORT_VERSION,
|
|
42
|
+
CLOSURE_SCOPE_VERSION,
|
|
43
|
+
CYCLE_ANCHOR_VERSION,
|
|
44
|
+
MAX_FAILED_CANDIDATES,
|
|
45
|
+
REPAIR_ASSIGNMENT_VERSION,
|
|
46
|
+
REPAIR_SUCCESSOR_VERSION,
|
|
47
|
+
REQUIRED_CLOSURE_LENSES,
|
|
48
|
+
CandidateReference,
|
|
49
|
+
ClosureDecision,
|
|
50
|
+
ClosureReviewReport,
|
|
51
|
+
ClosureReviewScope,
|
|
52
|
+
CycleAnchor,
|
|
53
|
+
FindingInventory,
|
|
54
|
+
RepairAction,
|
|
55
|
+
RepairAssignment,
|
|
56
|
+
RepairFinding,
|
|
57
|
+
RepairSuccessor,
|
|
58
|
+
ResolutionEvidence,
|
|
59
|
+
ReviewerEnrollment,
|
|
60
|
+
load_public_key,
|
|
61
|
+
public_key_text,
|
|
62
|
+
)
|
|
63
|
+
from mostlyright.data_harness.repair.errors import RepairError
|
|
64
|
+
from mostlyright.data_harness.signing import TrustedSigner
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class RepairExecution:
|
|
69
|
+
"""The frozen successor plan, deterministic evidence, and lifecycle envelope."""
|
|
70
|
+
|
|
71
|
+
plan: PreparationPlan
|
|
72
|
+
result: PreparationResult
|
|
73
|
+
successor: RepairSuccessor
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TrustedCycleAnchorStore(Protocol):
|
|
77
|
+
"""External linearizable persistence for signed repair-cycle anchors."""
|
|
78
|
+
|
|
79
|
+
def load(self, run_id: str) -> CycleAnchor | None:
|
|
80
|
+
"""Return the authoritative current anchor for one run."""
|
|
81
|
+
|
|
82
|
+
def compare_and_swap(
|
|
83
|
+
self,
|
|
84
|
+
run_id: str,
|
|
85
|
+
expected_anchor_digest: str | None,
|
|
86
|
+
successor: CycleAnchor,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Persist only when the external current digest equals ``expected``."""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def bind_candidate(
|
|
92
|
+
plan: PreparationPlan,
|
|
93
|
+
result: PreparationResult,
|
|
94
|
+
*,
|
|
95
|
+
producer_principal: str,
|
|
96
|
+
producer_attempt: str,
|
|
97
|
+
status: str,
|
|
98
|
+
failed_candidate_count: int,
|
|
99
|
+
inputs: Mapping[str, TableState],
|
|
100
|
+
) -> CandidateReference:
|
|
101
|
+
"""Bind a preparation result to one exact immutable repair candidate."""
|
|
102
|
+
|
|
103
|
+
if not isinstance(result, PreparationResult):
|
|
104
|
+
raise RepairError("REPAIR_RESULT_TYPE", "result", "must be a PreparationResult")
|
|
105
|
+
if status in {"pending_independent_review", "reviewed_successor"} and not result.analysis_ready:
|
|
106
|
+
raise RepairError(
|
|
107
|
+
"REPAIR_STATUS",
|
|
108
|
+
"status",
|
|
109
|
+
"a deterministic failure cannot enter review or reviewed state",
|
|
110
|
+
)
|
|
111
|
+
if status == "human_direction_required" and failed_candidate_count != 3:
|
|
112
|
+
raise RepairError(
|
|
113
|
+
"REPAIR_STATUS",
|
|
114
|
+
"status",
|
|
115
|
+
"human direction requires exactly three failed candidates",
|
|
116
|
+
)
|
|
117
|
+
if status not in {
|
|
118
|
+
"fixes_required",
|
|
119
|
+
"human_direction_required",
|
|
120
|
+
"pending_independent_review",
|
|
121
|
+
"reviewed_successor",
|
|
122
|
+
}:
|
|
123
|
+
raise RepairError("REPAIR_STATUS", "status", "is unsupported")
|
|
124
|
+
return CandidateReference(
|
|
125
|
+
candidate_digest=candidate_digest(plan, result, inputs),
|
|
126
|
+
plan_digest=plan_digest(plan),
|
|
127
|
+
validation_policy_digest=validation_policy_digest(plan),
|
|
128
|
+
input_set_digest=input_set_digest(inputs),
|
|
129
|
+
evidence_digest=result.evidence_digest,
|
|
130
|
+
producer_principal=producer_principal,
|
|
131
|
+
producer_attempt=producer_attempt,
|
|
132
|
+
status=status,
|
|
133
|
+
failed_candidate_count=failed_candidate_count,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def make_closure_report(
|
|
138
|
+
scope: ClosureReviewScope,
|
|
139
|
+
enrollment: ReviewerEnrollment,
|
|
140
|
+
signer: TrustedSigner,
|
|
141
|
+
*,
|
|
142
|
+
decision: str,
|
|
143
|
+
verified_resolutions: tuple[tuple[str, str], ...],
|
|
144
|
+
findings: tuple[RepairFinding, ...] = (),
|
|
145
|
+
) -> ClosureReviewReport:
|
|
146
|
+
"""Sign one exact closure report without exposing reviewer private key bytes."""
|
|
147
|
+
|
|
148
|
+
if enrollment not in scope.reviewers:
|
|
149
|
+
raise RepairError(
|
|
150
|
+
"REPAIR_REVIEWER_SCOPE",
|
|
151
|
+
"enrollment",
|
|
152
|
+
"is not enrolled for the closure scope",
|
|
153
|
+
)
|
|
154
|
+
try:
|
|
155
|
+
signer_public = public_key_text(signer.public_key())
|
|
156
|
+
except Exception as exc:
|
|
157
|
+
raise RepairError(
|
|
158
|
+
"REPAIR_SIGNER",
|
|
159
|
+
"signer",
|
|
160
|
+
"did not expose a valid public verification key",
|
|
161
|
+
) from exc
|
|
162
|
+
if signer_public != enrollment.public_key:
|
|
163
|
+
raise RepairError(
|
|
164
|
+
"REPAIR_SIGNER",
|
|
165
|
+
"signer",
|
|
166
|
+
"does not match the enrolled reviewer key",
|
|
167
|
+
)
|
|
168
|
+
unsigned = {
|
|
169
|
+
"schema_version": CLOSURE_REPORT_VERSION,
|
|
170
|
+
"scope_digest": scope.digest,
|
|
171
|
+
"successor_candidate_digest": scope.successor_candidate_digest,
|
|
172
|
+
"successor_evidence_digest": scope.successor_evidence_digest,
|
|
173
|
+
"reviewer_principal": enrollment.principal_id,
|
|
174
|
+
"reviewer_attempt": enrollment.attempt_id,
|
|
175
|
+
"reviewer_key_id": enrollment.key_id,
|
|
176
|
+
"lens": enrollment.lens,
|
|
177
|
+
"decision": decision,
|
|
178
|
+
"verified_resolutions": [list(item) for item in verified_resolutions],
|
|
179
|
+
"findings": [item.to_dict() for item in findings],
|
|
180
|
+
}
|
|
181
|
+
try:
|
|
182
|
+
signature = base64.b64encode(signer.sign(canonical_json_bytes(unsigned))).decode("ascii")
|
|
183
|
+
except Exception as exc:
|
|
184
|
+
raise RepairError(
|
|
185
|
+
"REPAIR_SIGNER",
|
|
186
|
+
"signer",
|
|
187
|
+
"could not sign the exact closure report",
|
|
188
|
+
) from exc
|
|
189
|
+
return ClosureReviewReport(
|
|
190
|
+
schema_version=CLOSURE_REPORT_VERSION,
|
|
191
|
+
scope_digest=scope.digest,
|
|
192
|
+
successor_candidate_digest=scope.successor_candidate_digest,
|
|
193
|
+
successor_evidence_digest=scope.successor_evidence_digest,
|
|
194
|
+
reviewer_principal=enrollment.principal_id,
|
|
195
|
+
reviewer_attempt=enrollment.attempt_id,
|
|
196
|
+
reviewer_key_id=enrollment.key_id,
|
|
197
|
+
lens=enrollment.lens,
|
|
198
|
+
decision=decision,
|
|
199
|
+
verified_resolutions=verified_resolutions,
|
|
200
|
+
findings=findings,
|
|
201
|
+
signature=signature,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class ImmutableRepairCoordinator:
|
|
206
|
+
"""Linearizes one externally signed, append-only runtime repair chain."""
|
|
207
|
+
|
|
208
|
+
def __init__(
|
|
209
|
+
self,
|
|
210
|
+
*,
|
|
211
|
+
run_id: str,
|
|
212
|
+
predecessor: CandidateReference,
|
|
213
|
+
inventory: FindingInventory,
|
|
214
|
+
authority_key_id: str,
|
|
215
|
+
authority_signer: TrustedSigner,
|
|
216
|
+
anchor_store: TrustedCycleAnchorStore,
|
|
217
|
+
) -> None:
|
|
218
|
+
if predecessor.status not in {"fixes_required", "human_direction_required"}:
|
|
219
|
+
raise RepairError(
|
|
220
|
+
"REPAIR_LIFECYCLE",
|
|
221
|
+
"predecessor.status",
|
|
222
|
+
"must be a failed candidate",
|
|
223
|
+
)
|
|
224
|
+
if not callable(getattr(authority_signer, "sign", None)) or not callable(
|
|
225
|
+
getattr(authority_signer, "public_key", None)
|
|
226
|
+
):
|
|
227
|
+
raise RepairError(
|
|
228
|
+
"REPAIR_SIGNER",
|
|
229
|
+
"authority_signer",
|
|
230
|
+
"must be an opaque trusted signer capability",
|
|
231
|
+
)
|
|
232
|
+
self._lock = threading.RLock()
|
|
233
|
+
self._authority_signer = authority_signer
|
|
234
|
+
if not callable(getattr(anchor_store, "load", None)) or not callable(
|
|
235
|
+
getattr(anchor_store, "compare_and_swap", None)
|
|
236
|
+
):
|
|
237
|
+
raise RepairError(
|
|
238
|
+
"REPAIR_ANCHOR_STORE",
|
|
239
|
+
"anchor_store",
|
|
240
|
+
"must provide trusted load and compare-and-swap operations",
|
|
241
|
+
)
|
|
242
|
+
self._anchor_store = anchor_store
|
|
243
|
+
self._authority_key_id = authority_key_id
|
|
244
|
+
try:
|
|
245
|
+
self._authority_public_key = authority_signer.public_key()
|
|
246
|
+
self._authority_public_text = public_key_text(self._authority_public_key)
|
|
247
|
+
except Exception as exc:
|
|
248
|
+
raise RepairError(
|
|
249
|
+
"REPAIR_SIGNER",
|
|
250
|
+
"authority_signer",
|
|
251
|
+
"did not expose a valid public key",
|
|
252
|
+
) from exc
|
|
253
|
+
self._run_id = run_id
|
|
254
|
+
existing_anchor = self._load_external_anchor()
|
|
255
|
+
self._root_candidate_digest = (
|
|
256
|
+
predecessor.candidate_digest
|
|
257
|
+
if existing_anchor is None
|
|
258
|
+
else existing_anchor.root_candidate_digest
|
|
259
|
+
)
|
|
260
|
+
self._current = predecessor
|
|
261
|
+
self._inventory = inventory
|
|
262
|
+
self._issued_assignments: dict[str, RepairAssignment] = {}
|
|
263
|
+
self._consumed_assignments: set[str] = set()
|
|
264
|
+
self._pending_execution: RepairExecution | None = None
|
|
265
|
+
self._issued_scopes: dict[str, ClosureReviewScope] = {}
|
|
266
|
+
self._consumed_scopes: set[str] = set()
|
|
267
|
+
self._closed = False
|
|
268
|
+
if existing_anchor is None:
|
|
269
|
+
self._anchor = self._sign_anchor(
|
|
270
|
+
current=predecessor,
|
|
271
|
+
inventory_digest=inventory.digest,
|
|
272
|
+
sequence=0,
|
|
273
|
+
predecessor_anchor_digest=None,
|
|
274
|
+
)
|
|
275
|
+
self._commit_external_anchor(None, self._anchor)
|
|
276
|
+
else:
|
|
277
|
+
self._anchor = existing_anchor
|
|
278
|
+
if self._anchor.inventory_digest != inventory.digest:
|
|
279
|
+
raise RepairError(
|
|
280
|
+
"REPAIR_ANCHOR_STATE",
|
|
281
|
+
"inventory",
|
|
282
|
+
"does not match externally anchored current findings",
|
|
283
|
+
)
|
|
284
|
+
self._verify_current_anchor()
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def current_anchor(self) -> CycleAnchor:
|
|
288
|
+
return self._anchor
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def current_candidate(self) -> CandidateReference:
|
|
292
|
+
return self._current
|
|
293
|
+
|
|
294
|
+
def issue_repair_assignment(
|
|
295
|
+
self,
|
|
296
|
+
*,
|
|
297
|
+
assignment_id: str,
|
|
298
|
+
fixer_principal: str,
|
|
299
|
+
fixer_attempt: str,
|
|
300
|
+
actions: tuple[RepairAction, ...],
|
|
301
|
+
affected_check_ids: tuple[str, ...] = ALL_QUALITY_CHECK_IDS,
|
|
302
|
+
) -> RepairAssignment:
|
|
303
|
+
"""Bind one exact fixer and closed repair proposal to current trusted state."""
|
|
304
|
+
|
|
305
|
+
with self._lock:
|
|
306
|
+
self._verify_current_anchor()
|
|
307
|
+
if self._closed:
|
|
308
|
+
raise RepairError(
|
|
309
|
+
"REPAIR_LIFECYCLE",
|
|
310
|
+
"coordinator",
|
|
311
|
+
"the successor is already independently reviewed",
|
|
312
|
+
)
|
|
313
|
+
if self._pending_execution is not None:
|
|
314
|
+
raise RepairError(
|
|
315
|
+
"REPAIR_LIFECYCLE",
|
|
316
|
+
"coordinator",
|
|
317
|
+
"a successor awaits independent review",
|
|
318
|
+
)
|
|
319
|
+
if self._current.status == "human_direction_required":
|
|
320
|
+
raise RepairError(
|
|
321
|
+
"REPAIR_CYCLE_CAP",
|
|
322
|
+
"coordinator",
|
|
323
|
+
"three failed candidates require human direction",
|
|
324
|
+
)
|
|
325
|
+
if self._current.status != "fixes_required":
|
|
326
|
+
raise RepairError(
|
|
327
|
+
"REPAIR_LIFECYCLE",
|
|
328
|
+
"coordinator",
|
|
329
|
+
"current candidate does not accept automatic repair",
|
|
330
|
+
)
|
|
331
|
+
if fixer_principal in {
|
|
332
|
+
item.reviewer_principal
|
|
333
|
+
for item in (*self._inventory.blocking, *self._inventory.backlog)
|
|
334
|
+
}:
|
|
335
|
+
raise RepairError(
|
|
336
|
+
"REPAIR_IDENTITY",
|
|
337
|
+
"fixer_principal",
|
|
338
|
+
"fixer must differ from predecessor finding reviewers",
|
|
339
|
+
)
|
|
340
|
+
assignment = RepairAssignment(
|
|
341
|
+
schema_version=REPAIR_ASSIGNMENT_VERSION,
|
|
342
|
+
assignment_id=assignment_id,
|
|
343
|
+
run_id=self._run_id,
|
|
344
|
+
predecessor=self._current,
|
|
345
|
+
inventory=self._inventory,
|
|
346
|
+
anchor_digest=self._anchor.digest,
|
|
347
|
+
anchor_sequence=self._anchor.sequence,
|
|
348
|
+
fixer_principal=fixer_principal,
|
|
349
|
+
fixer_attempt=fixer_attempt,
|
|
350
|
+
actions=actions,
|
|
351
|
+
affected_check_ids=affected_check_ids,
|
|
352
|
+
)
|
|
353
|
+
if assignment.digest in self._issued_assignments:
|
|
354
|
+
raise RepairError(
|
|
355
|
+
"REPAIR_REPLAY",
|
|
356
|
+
"assignment",
|
|
357
|
+
"this exact assignment was already issued",
|
|
358
|
+
)
|
|
359
|
+
self._issued_assignments[assignment.digest] = assignment
|
|
360
|
+
return assignment
|
|
361
|
+
|
|
362
|
+
def execute_repair(
|
|
363
|
+
self,
|
|
364
|
+
assignment: RepairAssignment,
|
|
365
|
+
predecessor_plan: PreparationPlan,
|
|
366
|
+
inputs: Mapping[str, TableState],
|
|
367
|
+
) -> RepairExecution:
|
|
368
|
+
"""Create a distinct successor and rerun the complete deterministic gate set."""
|
|
369
|
+
|
|
370
|
+
with self._lock:
|
|
371
|
+
self._verify_current_anchor()
|
|
372
|
+
issued = self._issued_assignments.get(assignment.digest)
|
|
373
|
+
if issued is None or issued != assignment:
|
|
374
|
+
raise RepairError(
|
|
375
|
+
"REPAIR_ASSIGNMENT_TRUST",
|
|
376
|
+
"assignment",
|
|
377
|
+
"is not the exact coordinator-issued assignment",
|
|
378
|
+
)
|
|
379
|
+
if assignment.digest in self._consumed_assignments:
|
|
380
|
+
raise RepairError(
|
|
381
|
+
"REPAIR_REPLAY",
|
|
382
|
+
"assignment",
|
|
383
|
+
"has already been consumed",
|
|
384
|
+
)
|
|
385
|
+
if (
|
|
386
|
+
assignment.anchor_digest != self._anchor.digest
|
|
387
|
+
or assignment.anchor_sequence != self._anchor.sequence
|
|
388
|
+
or assignment.predecessor != self._current
|
|
389
|
+
or assignment.inventory != self._inventory
|
|
390
|
+
):
|
|
391
|
+
raise RepairError(
|
|
392
|
+
"REPAIR_STALE_ASSIGNMENT",
|
|
393
|
+
"assignment",
|
|
394
|
+
"does not bind the current monotonic coordinator state",
|
|
395
|
+
)
|
|
396
|
+
if plan_digest(predecessor_plan) != assignment.predecessor.plan_digest:
|
|
397
|
+
raise RepairError(
|
|
398
|
+
"REPAIR_PREDECESSOR",
|
|
399
|
+
"predecessor_plan",
|
|
400
|
+
"digest does not match the sealed predecessor",
|
|
401
|
+
)
|
|
402
|
+
if (
|
|
403
|
+
validation_policy_digest(predecessor_plan)
|
|
404
|
+
!= assignment.predecessor.validation_policy_digest
|
|
405
|
+
):
|
|
406
|
+
raise RepairError(
|
|
407
|
+
"REPAIR_POLICY",
|
|
408
|
+
"predecessor_plan",
|
|
409
|
+
"validation policy differs from the sealed predecessor",
|
|
410
|
+
)
|
|
411
|
+
if input_set_digest(inputs) != assignment.predecessor.input_set_digest:
|
|
412
|
+
raise RepairError(
|
|
413
|
+
"REPAIR_INPUT_SUBSTITUTION",
|
|
414
|
+
"inputs",
|
|
415
|
+
"exact input tables differ from the sealed predecessor",
|
|
416
|
+
)
|
|
417
|
+
successor_plan = predecessor_plan
|
|
418
|
+
for action in assignment.actions:
|
|
419
|
+
successor_plan = self._apply_action(successor_plan, action)
|
|
420
|
+
if plan_digest(successor_plan) == assignment.predecessor.plan_digest:
|
|
421
|
+
raise RepairError(
|
|
422
|
+
"REPAIR_NO_SUCCESSOR",
|
|
423
|
+
"assignment.actions",
|
|
424
|
+
"did not create a distinct successor plan",
|
|
425
|
+
)
|
|
426
|
+
if (
|
|
427
|
+
validation_policy_digest(successor_plan)
|
|
428
|
+
!= assignment.predecessor.validation_policy_digest
|
|
429
|
+
):
|
|
430
|
+
raise RepairError(
|
|
431
|
+
"REPAIR_POLICY",
|
|
432
|
+
"successor_plan",
|
|
433
|
+
"repair actions cannot change quality, temporal, or target policy",
|
|
434
|
+
)
|
|
435
|
+
result = execute_preparation(successor_plan, inputs)
|
|
436
|
+
checks = {item.check_id: item for item in result.quality_report.checks}
|
|
437
|
+
if tuple(sorted(checks)) != ALL_QUALITY_CHECK_IDS:
|
|
438
|
+
raise RepairError(
|
|
439
|
+
"REPAIR_CHECK_COVERAGE",
|
|
440
|
+
"result.quality_report",
|
|
441
|
+
"does not contain the complete expected check inventory",
|
|
442
|
+
)
|
|
443
|
+
check_evidence = tuple(
|
|
444
|
+
(
|
|
445
|
+
check_id,
|
|
446
|
+
checks[check_id].status,
|
|
447
|
+
canonical_sha256(checks[check_id].to_dict()),
|
|
448
|
+
)
|
|
449
|
+
for check_id in ALL_QUALITY_CHECK_IDS
|
|
450
|
+
)
|
|
451
|
+
resolutions = self._resolution_evidence(
|
|
452
|
+
assignment.inventory.blocking,
|
|
453
|
+
checks,
|
|
454
|
+
)
|
|
455
|
+
failed_count = assignment.predecessor.failed_candidate_count
|
|
456
|
+
if result.analysis_ready:
|
|
457
|
+
status = "pending_independent_review"
|
|
458
|
+
else:
|
|
459
|
+
failed_count += 1
|
|
460
|
+
status = (
|
|
461
|
+
"human_direction_required"
|
|
462
|
+
if failed_count >= MAX_FAILED_CANDIDATES
|
|
463
|
+
else "fixes_required"
|
|
464
|
+
)
|
|
465
|
+
candidate = bind_candidate(
|
|
466
|
+
successor_plan,
|
|
467
|
+
result,
|
|
468
|
+
producer_principal=assignment.fixer_principal,
|
|
469
|
+
producer_attempt=assignment.fixer_attempt,
|
|
470
|
+
status=status,
|
|
471
|
+
failed_candidate_count=failed_count,
|
|
472
|
+
inputs=inputs,
|
|
473
|
+
)
|
|
474
|
+
if candidate.input_set_digest != assignment.predecessor.input_set_digest:
|
|
475
|
+
raise RepairError(
|
|
476
|
+
"REPAIR_INPUT_SUBSTITUTION",
|
|
477
|
+
"successor",
|
|
478
|
+
"successor is not bound to the predecessor input set",
|
|
479
|
+
)
|
|
480
|
+
successor = RepairSuccessor(
|
|
481
|
+
schema_version=REPAIR_SUCCESSOR_VERSION,
|
|
482
|
+
assignment_digest=assignment.digest,
|
|
483
|
+
predecessor_candidate_digest=assignment.predecessor.candidate_digest,
|
|
484
|
+
candidate=candidate,
|
|
485
|
+
cycle_number=assignment.predecessor.failed_candidate_count + 1,
|
|
486
|
+
fixer_principal=assignment.fixer_principal,
|
|
487
|
+
fixer_attempt=assignment.fixer_attempt,
|
|
488
|
+
affected_check_ids=assignment.affected_check_ids,
|
|
489
|
+
check_evidence=check_evidence,
|
|
490
|
+
resolution_evidence=resolutions,
|
|
491
|
+
carried_backlog=assignment.inventory.backlog,
|
|
492
|
+
)
|
|
493
|
+
execution = RepairExecution(
|
|
494
|
+
plan=successor_plan,
|
|
495
|
+
result=result,
|
|
496
|
+
successor=successor,
|
|
497
|
+
)
|
|
498
|
+
self._consumed_assignments.add(assignment.digest)
|
|
499
|
+
self._current = candidate
|
|
500
|
+
if result.analysis_ready:
|
|
501
|
+
self._pending_execution = execution
|
|
502
|
+
next_inventory = assignment.inventory
|
|
503
|
+
else:
|
|
504
|
+
next_inventory = self._failed_inventory(
|
|
505
|
+
predecessor_inventory=assignment.inventory,
|
|
506
|
+
candidate=candidate,
|
|
507
|
+
checks=checks,
|
|
508
|
+
cycle_number=successor.cycle_number,
|
|
509
|
+
)
|
|
510
|
+
self._inventory = next_inventory
|
|
511
|
+
self._advance_anchor(candidate, next_inventory.digest)
|
|
512
|
+
return execution
|
|
513
|
+
|
|
514
|
+
def issue_closure_scope(
|
|
515
|
+
self,
|
|
516
|
+
execution: RepairExecution,
|
|
517
|
+
*,
|
|
518
|
+
scope_id: str,
|
|
519
|
+
reviewers: tuple[ReviewerEnrollment, ...],
|
|
520
|
+
) -> ClosureReviewScope:
|
|
521
|
+
"""Enroll two fresh independent reviewers over the exact successor."""
|
|
522
|
+
|
|
523
|
+
with self._lock:
|
|
524
|
+
self._verify_current_anchor()
|
|
525
|
+
if self._pending_execution != execution:
|
|
526
|
+
raise RepairError(
|
|
527
|
+
"REPAIR_REVIEW_SCOPE",
|
|
528
|
+
"execution",
|
|
529
|
+
"is not the exact pending successor",
|
|
530
|
+
)
|
|
531
|
+
successor = execution.successor
|
|
532
|
+
if successor.candidate.status != "pending_independent_review":
|
|
533
|
+
raise RepairError(
|
|
534
|
+
"REPAIR_REVIEW_SCOPE",
|
|
535
|
+
"successor",
|
|
536
|
+
"did not pass deterministic affected checks",
|
|
537
|
+
)
|
|
538
|
+
assignment = self._issued_assignments[successor.assignment_digest]
|
|
539
|
+
forbidden_principals = {
|
|
540
|
+
assignment.predecessor.producer_principal,
|
|
541
|
+
assignment.fixer_principal,
|
|
542
|
+
}
|
|
543
|
+
forbidden_attempts = {
|
|
544
|
+
assignment.predecessor.producer_attempt,
|
|
545
|
+
assignment.fixer_attempt,
|
|
546
|
+
}
|
|
547
|
+
if any(item.principal_id in forbidden_principals for item in reviewers):
|
|
548
|
+
raise RepairError(
|
|
549
|
+
"REPAIR_SELF_REVIEW",
|
|
550
|
+
"reviewers",
|
|
551
|
+
"producer or fixer cannot review the successor",
|
|
552
|
+
)
|
|
553
|
+
if any(item.attempt_id in forbidden_attempts for item in reviewers):
|
|
554
|
+
raise RepairError(
|
|
555
|
+
"REPAIR_SELF_REVIEW",
|
|
556
|
+
"reviewers",
|
|
557
|
+
"review attempts must be fresh",
|
|
558
|
+
)
|
|
559
|
+
blocking_ids = tuple(sorted(item.finding_id for item in assignment.inventory.blocking))
|
|
560
|
+
resolution_ids = tuple(item.finding_id for item in successor.resolution_evidence)
|
|
561
|
+
if resolution_ids != blocking_ids:
|
|
562
|
+
raise RepairError(
|
|
563
|
+
"REPAIR_RESOLUTION",
|
|
564
|
+
"successor.resolution_evidence",
|
|
565
|
+
"does not cover every carried blocking finding",
|
|
566
|
+
)
|
|
567
|
+
scope = ClosureReviewScope(
|
|
568
|
+
schema_version=CLOSURE_SCOPE_VERSION,
|
|
569
|
+
scope_id=scope_id,
|
|
570
|
+
run_id=self._run_id,
|
|
571
|
+
assignment_digest=successor.assignment_digest,
|
|
572
|
+
predecessor_candidate_digest=successor.predecessor_candidate_digest,
|
|
573
|
+
successor_candidate_digest=successor.candidate.candidate_digest,
|
|
574
|
+
successor_evidence_digest=successor.candidate.evidence_digest,
|
|
575
|
+
resolution_evidence_digest=successor.resolution_evidence_digest,
|
|
576
|
+
cycle_number=successor.cycle_number,
|
|
577
|
+
blocking_finding_ids=blocking_ids,
|
|
578
|
+
backlog_digest=canonical_sha256(
|
|
579
|
+
[item.to_dict() for item in successor.carried_backlog]
|
|
580
|
+
),
|
|
581
|
+
reviewers=reviewers,
|
|
582
|
+
)
|
|
583
|
+
if scope.digest in self._issued_scopes:
|
|
584
|
+
raise RepairError(
|
|
585
|
+
"REPAIR_REPLAY",
|
|
586
|
+
"scope",
|
|
587
|
+
"this exact closure scope was already issued",
|
|
588
|
+
)
|
|
589
|
+
self._issued_scopes[scope.digest] = scope
|
|
590
|
+
return scope
|
|
591
|
+
|
|
592
|
+
def finalize_closure(
|
|
593
|
+
self,
|
|
594
|
+
scope: ClosureReviewScope,
|
|
595
|
+
reports: tuple[ClosureReviewReport, ...],
|
|
596
|
+
) -> ClosureDecision:
|
|
597
|
+
"""Verify both signatures and derive closure or the next bounded failed state."""
|
|
598
|
+
|
|
599
|
+
with self._lock:
|
|
600
|
+
self._verify_current_anchor()
|
|
601
|
+
issued = self._issued_scopes.get(scope.digest)
|
|
602
|
+
if issued is None or issued != scope:
|
|
603
|
+
raise RepairError(
|
|
604
|
+
"REPAIR_SCOPE_TRUST",
|
|
605
|
+
"scope",
|
|
606
|
+
"is not the exact coordinator-issued closure scope",
|
|
607
|
+
)
|
|
608
|
+
if scope.digest in self._consumed_scopes:
|
|
609
|
+
raise RepairError(
|
|
610
|
+
"REPAIR_REPLAY",
|
|
611
|
+
"scope",
|
|
612
|
+
"has already been finalized",
|
|
613
|
+
)
|
|
614
|
+
if self._pending_execution is None:
|
|
615
|
+
raise RepairError(
|
|
616
|
+
"REPAIR_LIFECYCLE",
|
|
617
|
+
"scope",
|
|
618
|
+
"there is no pending successor",
|
|
619
|
+
)
|
|
620
|
+
if not isinstance(reports, tuple) or len(reports) != 2:
|
|
621
|
+
raise RepairError(
|
|
622
|
+
"REPAIR_REPORTS",
|
|
623
|
+
"reports",
|
|
624
|
+
"exactly two signed reports are required",
|
|
625
|
+
)
|
|
626
|
+
by_lens = {report.lens: report for report in reports}
|
|
627
|
+
if len(by_lens) != 2 or tuple(sorted(by_lens)) != REQUIRED_CLOSURE_LENSES:
|
|
628
|
+
raise RepairError(
|
|
629
|
+
"REPAIR_REPORTS",
|
|
630
|
+
"reports",
|
|
631
|
+
"must cover both required lenses exactly once",
|
|
632
|
+
)
|
|
633
|
+
execution = self._pending_execution
|
|
634
|
+
expected_resolutions = tuple(
|
|
635
|
+
(item.finding_id, item.digest) for item in execution.successor.resolution_evidence
|
|
636
|
+
)
|
|
637
|
+
all_findings: list[RepairFinding] = []
|
|
638
|
+
for enrollment in scope.reviewers:
|
|
639
|
+
report = by_lens[enrollment.lens]
|
|
640
|
+
self._verify_report(
|
|
641
|
+
report,
|
|
642
|
+
scope=scope,
|
|
643
|
+
enrollment=enrollment,
|
|
644
|
+
expected_resolutions=expected_resolutions,
|
|
645
|
+
)
|
|
646
|
+
all_findings.extend(report.findings)
|
|
647
|
+
finding_ids = tuple(item.finding_id for item in all_findings)
|
|
648
|
+
if len(finding_ids) != len(set(finding_ids)):
|
|
649
|
+
raise RepairError(
|
|
650
|
+
"REPAIR_FINDINGS",
|
|
651
|
+
"reports.findings",
|
|
652
|
+
"finding IDs must be unique across reviewers",
|
|
653
|
+
)
|
|
654
|
+
new_blocking = tuple(
|
|
655
|
+
sorted(
|
|
656
|
+
(
|
|
657
|
+
finding
|
|
658
|
+
for finding in all_findings
|
|
659
|
+
if finding.severity in BLOCKING_SEVERITIES
|
|
660
|
+
),
|
|
661
|
+
key=lambda item: item.finding_id,
|
|
662
|
+
)
|
|
663
|
+
)
|
|
664
|
+
rejected = any(report.decision == "reject" for report in reports)
|
|
665
|
+
if rejected and not new_blocking:
|
|
666
|
+
raise RepairError(
|
|
667
|
+
"REPAIR_REVIEW_DECISION",
|
|
668
|
+
"reports",
|
|
669
|
+
"a rejection requires at least one evidence-bound blocking finding",
|
|
670
|
+
)
|
|
671
|
+
if not rejected and new_blocking:
|
|
672
|
+
raise RepairError(
|
|
673
|
+
"REPAIR_REVIEW_DECISION",
|
|
674
|
+
"reports",
|
|
675
|
+
"an accepting report cannot contain a blocking finding",
|
|
676
|
+
)
|
|
677
|
+
new_backlog = tuple(
|
|
678
|
+
sorted(
|
|
679
|
+
(
|
|
680
|
+
*execution.successor.carried_backlog,
|
|
681
|
+
*(
|
|
682
|
+
finding
|
|
683
|
+
for finding in all_findings
|
|
684
|
+
if finding.severity not in BLOCKING_SEVERITIES
|
|
685
|
+
),
|
|
686
|
+
),
|
|
687
|
+
key=lambda item: item.finding_id,
|
|
688
|
+
)
|
|
689
|
+
)
|
|
690
|
+
backlog_ids = tuple(item.finding_id for item in new_backlog)
|
|
691
|
+
if len(backlog_ids) != len(set(backlog_ids)):
|
|
692
|
+
raise RepairError(
|
|
693
|
+
"REPAIR_BACKLOG",
|
|
694
|
+
"reports.findings",
|
|
695
|
+
"cannot replace or duplicate a carried backlog item",
|
|
696
|
+
)
|
|
697
|
+
report_digests = tuple(
|
|
698
|
+
canonical_sha256(preparation_value(by_lens[lens]))
|
|
699
|
+
for lens in REQUIRED_CLOSURE_LENSES
|
|
700
|
+
)
|
|
701
|
+
if not new_blocking:
|
|
702
|
+
candidate = replace(
|
|
703
|
+
execution.successor.candidate,
|
|
704
|
+
status="reviewed_successor",
|
|
705
|
+
)
|
|
706
|
+
status = "reviewed_successor"
|
|
707
|
+
self._closed = True
|
|
708
|
+
self._current = candidate
|
|
709
|
+
inventory_digest = canonical_sha256(
|
|
710
|
+
{
|
|
711
|
+
"blocking": [],
|
|
712
|
+
"backlog": [item.to_dict() for item in new_backlog],
|
|
713
|
+
}
|
|
714
|
+
)
|
|
715
|
+
else:
|
|
716
|
+
failed_count = execution.successor.candidate.failed_candidate_count + 1
|
|
717
|
+
status = (
|
|
718
|
+
"human_direction_required"
|
|
719
|
+
if failed_count >= MAX_FAILED_CANDIDATES
|
|
720
|
+
else "fixes_required"
|
|
721
|
+
)
|
|
722
|
+
candidate = replace(
|
|
723
|
+
execution.successor.candidate,
|
|
724
|
+
status=status,
|
|
725
|
+
failed_candidate_count=failed_count,
|
|
726
|
+
)
|
|
727
|
+
self._inventory = FindingInventory(
|
|
728
|
+
blocking=new_blocking,
|
|
729
|
+
backlog=new_backlog,
|
|
730
|
+
)
|
|
731
|
+
self._current = candidate
|
|
732
|
+
inventory_digest = self._inventory.digest
|
|
733
|
+
decision = ClosureDecision(
|
|
734
|
+
schema_version=CLOSURE_DECISION_VERSION,
|
|
735
|
+
scope_digest=scope.digest,
|
|
736
|
+
candidate=candidate,
|
|
737
|
+
status=status,
|
|
738
|
+
report_digests=report_digests,
|
|
739
|
+
preserved_backlog=new_backlog,
|
|
740
|
+
remaining_blocking=new_blocking,
|
|
741
|
+
)
|
|
742
|
+
self._pending_execution = None
|
|
743
|
+
self._consumed_scopes.add(scope.digest)
|
|
744
|
+
self._advance_anchor(candidate, inventory_digest)
|
|
745
|
+
return decision
|
|
746
|
+
|
|
747
|
+
def _apply_action(
|
|
748
|
+
self,
|
|
749
|
+
plan: PreparationPlan,
|
|
750
|
+
action: RepairAction,
|
|
751
|
+
) -> PreparationPlan:
|
|
752
|
+
if action.kind == "append_operation":
|
|
753
|
+
if not isinstance(action.replacement, OperationSpec):
|
|
754
|
+
raise RepairError(
|
|
755
|
+
"REPAIR_ACTION_SHAPE",
|
|
756
|
+
action.action_id,
|
|
757
|
+
"append action lost its validated operation replacement",
|
|
758
|
+
)
|
|
759
|
+
return append_operation(plan, action.replacement)
|
|
760
|
+
if action.kind in {"replace_operation", "remove_operation"}:
|
|
761
|
+
if action.expected_before_digest is None:
|
|
762
|
+
raise RepairError(
|
|
763
|
+
"REPAIR_ACTION_SHAPE",
|
|
764
|
+
action.action_id,
|
|
765
|
+
"operation edit lost its predecessor digest",
|
|
766
|
+
)
|
|
767
|
+
replacement = (
|
|
768
|
+
action.replacement
|
|
769
|
+
if action.kind == "replace_operation"
|
|
770
|
+
and isinstance(action.replacement, OperationSpec)
|
|
771
|
+
else None
|
|
772
|
+
)
|
|
773
|
+
if action.kind == "replace_operation" and replacement is None:
|
|
774
|
+
raise RepairError(
|
|
775
|
+
"REPAIR_ACTION_SHAPE",
|
|
776
|
+
action.action_id,
|
|
777
|
+
"replacement action lost its validated operation",
|
|
778
|
+
)
|
|
779
|
+
return replace_operation(
|
|
780
|
+
plan,
|
|
781
|
+
target_id=action.target_id,
|
|
782
|
+
expected_digest=action.expected_before_digest,
|
|
783
|
+
replacement=replacement,
|
|
784
|
+
)
|
|
785
|
+
if action.kind == "append_join":
|
|
786
|
+
if not isinstance(action.replacement, (EqualityJoinSpec, AsOfJoinSpec)):
|
|
787
|
+
raise RepairError(
|
|
788
|
+
"REPAIR_ACTION_SHAPE",
|
|
789
|
+
action.action_id,
|
|
790
|
+
"append action lost its validated join replacement",
|
|
791
|
+
)
|
|
792
|
+
return append_join(plan, action.replacement)
|
|
793
|
+
if action.kind in {"replace_join", "remove_join"}:
|
|
794
|
+
if action.expected_before_digest is None:
|
|
795
|
+
raise RepairError(
|
|
796
|
+
"REPAIR_ACTION_SHAPE",
|
|
797
|
+
action.action_id,
|
|
798
|
+
"join edit lost its predecessor digest",
|
|
799
|
+
)
|
|
800
|
+
replacement = (
|
|
801
|
+
action.replacement
|
|
802
|
+
if action.kind == "replace_join"
|
|
803
|
+
and isinstance(action.replacement, (EqualityJoinSpec, AsOfJoinSpec))
|
|
804
|
+
else None
|
|
805
|
+
)
|
|
806
|
+
if action.kind == "replace_join" and replacement is None:
|
|
807
|
+
raise RepairError(
|
|
808
|
+
"REPAIR_ACTION_SHAPE",
|
|
809
|
+
action.action_id,
|
|
810
|
+
"replacement action lost its validated join",
|
|
811
|
+
)
|
|
812
|
+
return replace_join(
|
|
813
|
+
plan,
|
|
814
|
+
target_id=action.target_id,
|
|
815
|
+
expected_digest=action.expected_before_digest,
|
|
816
|
+
replacement=replacement,
|
|
817
|
+
)
|
|
818
|
+
if action.kind == "replace_output_columns":
|
|
819
|
+
if action.expected_before_digest is None or not isinstance(
|
|
820
|
+
action.replacement,
|
|
821
|
+
tuple,
|
|
822
|
+
):
|
|
823
|
+
raise RepairError(
|
|
824
|
+
"REPAIR_ACTION_SHAPE",
|
|
825
|
+
action.action_id,
|
|
826
|
+
"output edit lost its validated predecessor or replacement",
|
|
827
|
+
)
|
|
828
|
+
if output_columns_digest(plan.output_columns) != action.expected_before_digest:
|
|
829
|
+
raise RepairError(
|
|
830
|
+
"REPAIR_STALE_ACTION",
|
|
831
|
+
action.target_id,
|
|
832
|
+
"output-column digest differs from the assigned predecessor",
|
|
833
|
+
)
|
|
834
|
+
return replace(plan, output_columns=action.replacement)
|
|
835
|
+
raise RepairError("REPAIR_ACTION_KIND", action.kind, "is not allowlisted")
|
|
836
|
+
|
|
837
|
+
def _resolution_evidence(
|
|
838
|
+
self,
|
|
839
|
+
findings: tuple[RepairFinding, ...],
|
|
840
|
+
checks: dict[str, Any],
|
|
841
|
+
) -> tuple[ResolutionEvidence, ...]:
|
|
842
|
+
resolutions: list[ResolutionEvidence] = []
|
|
843
|
+
for finding in findings:
|
|
844
|
+
selected = tuple(checks[check_id] for check_id in finding.affected_check_ids)
|
|
845
|
+
if any(check.status != "passed" for check in selected):
|
|
846
|
+
continue
|
|
847
|
+
resolutions.append(
|
|
848
|
+
ResolutionEvidence(
|
|
849
|
+
finding_id=finding.finding_id,
|
|
850
|
+
predecessor_evidence_digest=finding.evidence_digest,
|
|
851
|
+
acceptance_condition_digest=canonical_sha256(
|
|
852
|
+
{"acceptance_condition": finding.acceptance_condition}
|
|
853
|
+
),
|
|
854
|
+
check_evidence=tuple(
|
|
855
|
+
(
|
|
856
|
+
check.check_id,
|
|
857
|
+
canonical_sha256(check.to_dict()),
|
|
858
|
+
)
|
|
859
|
+
for check in selected
|
|
860
|
+
),
|
|
861
|
+
)
|
|
862
|
+
)
|
|
863
|
+
return tuple(sorted(resolutions, key=lambda item: item.finding_id))
|
|
864
|
+
|
|
865
|
+
def _failed_inventory(
|
|
866
|
+
self,
|
|
867
|
+
*,
|
|
868
|
+
predecessor_inventory: FindingInventory,
|
|
869
|
+
candidate: CandidateReference,
|
|
870
|
+
checks: dict[str, Any],
|
|
871
|
+
cycle_number: int,
|
|
872
|
+
) -> FindingInventory:
|
|
873
|
+
blockers = list(predecessor_inventory.blocking)
|
|
874
|
+
covered = {
|
|
875
|
+
check_id
|
|
876
|
+
for finding in predecessor_inventory.blocking
|
|
877
|
+
for check_id in finding.affected_check_ids
|
|
878
|
+
}
|
|
879
|
+
for check_id in ALL_QUALITY_CHECK_IDS:
|
|
880
|
+
check = checks[check_id]
|
|
881
|
+
if check.status == "passed" or check_id in covered:
|
|
882
|
+
continue
|
|
883
|
+
blockers.append(
|
|
884
|
+
RepairFinding(
|
|
885
|
+
finding_id=f"H4.C{cycle_number}.{check_id}",
|
|
886
|
+
severity=check.severity,
|
|
887
|
+
invariant=f"Quality check {check_id} must pass on the immutable successor.",
|
|
888
|
+
evidence_digest=canonical_sha256(check.to_dict()),
|
|
889
|
+
affected_artifact_digest=candidate.candidate_digest,
|
|
890
|
+
acceptance_condition=(
|
|
891
|
+
f"Rerun the policy-owned {check.threshold_id} check and obtain passed "
|
|
892
|
+
"evidence on a distinct successor."
|
|
893
|
+
),
|
|
894
|
+
affected_check_ids=(check_id,),
|
|
895
|
+
reviewer_principal="deterministic_validator",
|
|
896
|
+
)
|
|
897
|
+
)
|
|
898
|
+
return FindingInventory(
|
|
899
|
+
blocking=tuple(sorted(blockers, key=lambda item: item.finding_id)),
|
|
900
|
+
backlog=predecessor_inventory.backlog,
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
def _verify_report(
|
|
904
|
+
self,
|
|
905
|
+
report: ClosureReviewReport,
|
|
906
|
+
*,
|
|
907
|
+
scope: ClosureReviewScope,
|
|
908
|
+
enrollment: ReviewerEnrollment,
|
|
909
|
+
expected_resolutions: tuple[tuple[str, str], ...],
|
|
910
|
+
) -> None:
|
|
911
|
+
expected = {
|
|
912
|
+
"scope_digest": scope.digest,
|
|
913
|
+
"successor_candidate_digest": scope.successor_candidate_digest,
|
|
914
|
+
"successor_evidence_digest": scope.successor_evidence_digest,
|
|
915
|
+
"reviewer_principal": enrollment.principal_id,
|
|
916
|
+
"reviewer_attempt": enrollment.attempt_id,
|
|
917
|
+
"reviewer_key_id": enrollment.key_id,
|
|
918
|
+
"lens": enrollment.lens,
|
|
919
|
+
}
|
|
920
|
+
for name, value in expected.items():
|
|
921
|
+
if getattr(report, name) != value:
|
|
922
|
+
raise RepairError(
|
|
923
|
+
"REPAIR_REPORT_SCOPE",
|
|
924
|
+
f"report.{name}",
|
|
925
|
+
"does not match the enrolled closure scope",
|
|
926
|
+
)
|
|
927
|
+
if report.verified_resolutions != expected_resolutions:
|
|
928
|
+
raise RepairError(
|
|
929
|
+
"REPAIR_WAIVER",
|
|
930
|
+
"report.verified_resolutions",
|
|
931
|
+
"must verify every exact carried resolution; omission is not a waiver",
|
|
932
|
+
)
|
|
933
|
+
if any(item.reviewer_principal != enrollment.principal_id for item in report.findings):
|
|
934
|
+
raise RepairError(
|
|
935
|
+
"REPAIR_FINDING_AUTHOR",
|
|
936
|
+
"report.findings",
|
|
937
|
+
"finding reviewer must match the signed report reviewer",
|
|
938
|
+
)
|
|
939
|
+
if any(
|
|
940
|
+
item.affected_artifact_digest != scope.successor_candidate_digest
|
|
941
|
+
for item in report.findings
|
|
942
|
+
):
|
|
943
|
+
raise RepairError(
|
|
944
|
+
"REPAIR_FINDING_SCOPE",
|
|
945
|
+
"report.findings",
|
|
946
|
+
"every finding must bind the exact successor candidate",
|
|
947
|
+
)
|
|
948
|
+
report_has_blocking = any(item.severity in BLOCKING_SEVERITIES for item in report.findings)
|
|
949
|
+
if (report.decision == "reject") != report_has_blocking:
|
|
950
|
+
raise RepairError(
|
|
951
|
+
"REPAIR_REVIEW_DECISION",
|
|
952
|
+
"report.decision",
|
|
953
|
+
"must reconcile with this reviewer's blocking findings",
|
|
954
|
+
)
|
|
955
|
+
try:
|
|
956
|
+
signature = base64.b64decode(report.signature, validate=True)
|
|
957
|
+
public_key = next(item for item in scope.reviewers if item.lens == report.lens)
|
|
958
|
+
load_public_key(public_key.public_key).verify(
|
|
959
|
+
signature,
|
|
960
|
+
canonical_json_bytes(report.unsigned_dict()),
|
|
961
|
+
)
|
|
962
|
+
except (InvalidSignature, ValueError, TypeError, StopIteration) as exc:
|
|
963
|
+
raise RepairError(
|
|
964
|
+
"REPAIR_REPORT_SIGNATURE",
|
|
965
|
+
"report.signature",
|
|
966
|
+
"does not verify for the enrolled key and exact payload",
|
|
967
|
+
) from exc
|
|
968
|
+
|
|
969
|
+
def _advance_anchor(
|
|
970
|
+
self,
|
|
971
|
+
current: CandidateReference,
|
|
972
|
+
inventory_digest: str,
|
|
973
|
+
) -> None:
|
|
974
|
+
predecessor_digest = self._anchor.digest
|
|
975
|
+
successor = self._sign_anchor(
|
|
976
|
+
current=current,
|
|
977
|
+
inventory_digest=inventory_digest,
|
|
978
|
+
sequence=self._anchor.sequence + 1,
|
|
979
|
+
predecessor_anchor_digest=predecessor_digest,
|
|
980
|
+
)
|
|
981
|
+
self._commit_external_anchor(predecessor_digest, successor)
|
|
982
|
+
self._anchor = successor
|
|
983
|
+
|
|
984
|
+
def _sign_anchor(
|
|
985
|
+
self,
|
|
986
|
+
*,
|
|
987
|
+
current: CandidateReference,
|
|
988
|
+
inventory_digest: str,
|
|
989
|
+
sequence: int,
|
|
990
|
+
predecessor_anchor_digest: str | None,
|
|
991
|
+
) -> CycleAnchor:
|
|
992
|
+
unsigned = {
|
|
993
|
+
"schema_version": CYCLE_ANCHOR_VERSION,
|
|
994
|
+
"run_id": self._run_id,
|
|
995
|
+
"root_candidate_digest": self._root_candidate_digest,
|
|
996
|
+
"current_candidate_digest": current.candidate_digest,
|
|
997
|
+
"current_state_digest": canonical_sha256(current.to_dict()),
|
|
998
|
+
"inventory_digest": inventory_digest,
|
|
999
|
+
"failed_candidate_count": current.failed_candidate_count,
|
|
1000
|
+
"sequence": sequence,
|
|
1001
|
+
"predecessor_anchor_digest": predecessor_anchor_digest,
|
|
1002
|
+
"authority_key_id": self._authority_key_id,
|
|
1003
|
+
"authority_public_key": self._authority_public_text,
|
|
1004
|
+
}
|
|
1005
|
+
try:
|
|
1006
|
+
signature = base64.b64encode(
|
|
1007
|
+
self._authority_signer.sign(canonical_json_bytes(unsigned))
|
|
1008
|
+
).decode("ascii")
|
|
1009
|
+
except Exception as exc:
|
|
1010
|
+
raise RepairError(
|
|
1011
|
+
"REPAIR_SIGNER",
|
|
1012
|
+
"authority_signer",
|
|
1013
|
+
"could not sign the monotonic cycle anchor",
|
|
1014
|
+
) from exc
|
|
1015
|
+
return CycleAnchor(**unsigned, signature=signature)
|
|
1016
|
+
|
|
1017
|
+
def _verify_current_anchor(self) -> None:
|
|
1018
|
+
external = self._load_external_anchor()
|
|
1019
|
+
if external is None or external.digest != self._anchor.digest:
|
|
1020
|
+
raise RepairError(
|
|
1021
|
+
"REPAIR_ANCHOR_STORE",
|
|
1022
|
+
"anchor_store",
|
|
1023
|
+
"external current anchor differs; restart from authoritative state",
|
|
1024
|
+
)
|
|
1025
|
+
if self._anchor.authority_public_key != self._authority_public_text:
|
|
1026
|
+
raise RepairError(
|
|
1027
|
+
"REPAIR_ANCHOR_AUTHORITY",
|
|
1028
|
+
"anchor.authority_public_key",
|
|
1029
|
+
"does not match the coordinator authority",
|
|
1030
|
+
)
|
|
1031
|
+
try:
|
|
1032
|
+
self._authority_public_key.verify(
|
|
1033
|
+
base64.b64decode(self._anchor.signature, validate=True),
|
|
1034
|
+
canonical_json_bytes(self._anchor.unsigned_dict()),
|
|
1035
|
+
)
|
|
1036
|
+
except (InvalidSignature, ValueError, TypeError) as exc:
|
|
1037
|
+
raise RepairError(
|
|
1038
|
+
"REPAIR_ANCHOR_SIGNATURE",
|
|
1039
|
+
"anchor.signature",
|
|
1040
|
+
"does not verify for the trusted coordinator authority",
|
|
1041
|
+
) from exc
|
|
1042
|
+
if (
|
|
1043
|
+
self._anchor.current_candidate_digest != self._current.candidate_digest
|
|
1044
|
+
or self._anchor.current_state_digest != canonical_sha256(self._current.to_dict())
|
|
1045
|
+
or self._anchor.failed_candidate_count != self._current.failed_candidate_count
|
|
1046
|
+
):
|
|
1047
|
+
raise RepairError(
|
|
1048
|
+
"REPAIR_ANCHOR_STATE",
|
|
1049
|
+
"anchor",
|
|
1050
|
+
"does not bind the coordinator's current candidate state",
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
def _load_external_anchor(self) -> CycleAnchor | None:
|
|
1054
|
+
try:
|
|
1055
|
+
anchor = self._anchor_store.load(self._run_id)
|
|
1056
|
+
except RepairError:
|
|
1057
|
+
raise
|
|
1058
|
+
except Exception as exc:
|
|
1059
|
+
raise RepairError(
|
|
1060
|
+
"REPAIR_ANCHOR_STORE",
|
|
1061
|
+
"anchor_store",
|
|
1062
|
+
"could not load authoritative cycle state",
|
|
1063
|
+
) from exc
|
|
1064
|
+
if anchor is not None and not isinstance(anchor, CycleAnchor):
|
|
1065
|
+
raise RepairError(
|
|
1066
|
+
"REPAIR_ANCHOR_STORE",
|
|
1067
|
+
"anchor_store",
|
|
1068
|
+
"returned an invalid cycle anchor",
|
|
1069
|
+
)
|
|
1070
|
+
return anchor
|
|
1071
|
+
|
|
1072
|
+
def _commit_external_anchor(
|
|
1073
|
+
self,
|
|
1074
|
+
expected_anchor_digest: str | None,
|
|
1075
|
+
successor: CycleAnchor,
|
|
1076
|
+
) -> None:
|
|
1077
|
+
try:
|
|
1078
|
+
self._anchor_store.compare_and_swap(
|
|
1079
|
+
self._run_id,
|
|
1080
|
+
expected_anchor_digest,
|
|
1081
|
+
successor,
|
|
1082
|
+
)
|
|
1083
|
+
except RepairError:
|
|
1084
|
+
raise
|
|
1085
|
+
except Exception as exc:
|
|
1086
|
+
raise RepairError(
|
|
1087
|
+
"REPAIR_ANCHOR_STORE",
|
|
1088
|
+
"anchor_store",
|
|
1089
|
+
"rejected the monotonic compare-and-swap",
|
|
1090
|
+
) from exc
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
__all__ = [
|
|
1094
|
+
"ImmutableRepairCoordinator",
|
|
1095
|
+
"RepairExecution",
|
|
1096
|
+
"TrustedCycleAnchorStore",
|
|
1097
|
+
"bind_candidate",
|
|
1098
|
+
"make_closure_report",
|
|
1099
|
+
]
|