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,576 @@
|
|
|
1
|
+
"""Plain, bounded presentation of run-feed records.
|
|
2
|
+
|
|
3
|
+
The event feed is the authority for what happened. This module supplies the one presentation
|
|
4
|
+
vocabulary shared by Watch and the Notebook Viewer, plus a pure reducer for the Viewer's compact
|
|
5
|
+
Build block. It opens no files, reads no clocks, and never re-derives records from receipts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Mapping, Sequence
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from mostlyright.data_harness.events import NOTEBOOK_SIDECAR_EVENT_NAMES, validate_facts
|
|
14
|
+
|
|
15
|
+
# The rail is a plain-vocabulary rendering of the build and evidence stages declared by
|
|
16
|
+
# ``cli._agent_workflow_surface()``. Presentation-only observability is intentionally absent. A
|
|
17
|
+
# test owns the one-way subset contract because importing ``cli`` here would close a cycle.
|
|
18
|
+
STAGE_RAIL: tuple[Mapping[str, Any], ...] = (
|
|
19
|
+
{
|
|
20
|
+
"stage": "question_and_requirements",
|
|
21
|
+
"label": "Question and requirements",
|
|
22
|
+
"events": (),
|
|
23
|
+
"members": ("question.json", "requirements.json"),
|
|
24
|
+
"steps": (),
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"stage": "source_candidates_and_fitness",
|
|
28
|
+
"label": "Sources proposed",
|
|
29
|
+
"events": (),
|
|
30
|
+
"members": ("source-proposals.json",),
|
|
31
|
+
"steps": (),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"stage": "frozen_recipe",
|
|
35
|
+
"label": "Recipe written",
|
|
36
|
+
"events": (),
|
|
37
|
+
"members": ("recipe.json",),
|
|
38
|
+
"steps": (),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"stage": "recipe_approval",
|
|
42
|
+
"label": "Recipe approved",
|
|
43
|
+
"events": (),
|
|
44
|
+
"members": ("recipe-approval.json",),
|
|
45
|
+
"steps": (),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"stage": "acquire_clean_join_validate_package",
|
|
49
|
+
"label": "Build",
|
|
50
|
+
"events": ("run_started", "rights_checked", "build_failed", "run_interrupted"),
|
|
51
|
+
"steps": (
|
|
52
|
+
{"step": "sources_read", "label": "Sources read", "events": ("source_read",)},
|
|
53
|
+
{
|
|
54
|
+
"step": "sources_opened",
|
|
55
|
+
"label": "Sources opened",
|
|
56
|
+
"events": ("source_parsed",),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"step": "cleaning_applied",
|
|
60
|
+
"label": "Cleaning applied",
|
|
61
|
+
"events": ("cleaning_applied",),
|
|
62
|
+
},
|
|
63
|
+
{"step": "joined", "label": "Joined", "events": ("join_completed",)},
|
|
64
|
+
{"step": "checks", "label": "Checks", "events": ("checks_completed",)},
|
|
65
|
+
{
|
|
66
|
+
"step": "table_written",
|
|
67
|
+
"label": "Table written",
|
|
68
|
+
"events": ("table_written",),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"step": "receipt_sealed",
|
|
72
|
+
"label": "Receipt sealed",
|
|
73
|
+
"events": ("member_sealed", "build_sealed"),
|
|
74
|
+
},
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
{"stage": "independent_review", "label": "Review", "events": (), "steps": ()},
|
|
78
|
+
{
|
|
79
|
+
"stage": "independent_verification",
|
|
80
|
+
"label": "Independent check",
|
|
81
|
+
"events": (),
|
|
82
|
+
"steps": (),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"stage": "handoff",
|
|
86
|
+
"label": "Handoff",
|
|
87
|
+
"events": ("notebook_sidecar_ready",),
|
|
88
|
+
"steps": (),
|
|
89
|
+
},
|
|
90
|
+
{"stage": "hosted_export", "label": "Hosted export", "events": (), "steps": ()},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# One short sentence per event name. Internal event names never reach either page.
|
|
95
|
+
EVENT_SENTENCES: Mapping[str, str] = {
|
|
96
|
+
"run_started": "Build started",
|
|
97
|
+
"rights_checked": "Rights checked",
|
|
98
|
+
"sources_read_started": "Started reading {total} sources",
|
|
99
|
+
"source_read": "Read {source_id} ({bytes} bytes)",
|
|
100
|
+
"sources_parsed_started": "Started opening {total} sources",
|
|
101
|
+
"source_parsed": "Opened {source_id}: {rows} rows, {columns} columns",
|
|
102
|
+
"evidence_matched": "Sources matched their proposals: {matched} of {total}",
|
|
103
|
+
"cleaning_started": "Started {total} cleaning steps",
|
|
104
|
+
"cleaning_applied": "Cleaning step {step_index} of {step_total} on {source_id}: {operation}",
|
|
105
|
+
"sources_profiled": "Cleaned sources profiled: {sources} sources, {columns} columns",
|
|
106
|
+
"join_completed": "Joined: {matched_left_rows} of {left_rows} rows matched",
|
|
107
|
+
"rows_selected": "Rows selected: {rows} rows, {columns} columns",
|
|
108
|
+
"checks_started": "Started {total} output checks",
|
|
109
|
+
"check_completed": "Check {index} of {total}: {check_id}",
|
|
110
|
+
"checks_completed": "Checks: {passed} of {total} passed",
|
|
111
|
+
"table_written": "Table written: {rows} rows, {columns} columns",
|
|
112
|
+
"members_sealed_started": "Started sealing {total} receipt parts",
|
|
113
|
+
"member_sealed": "Receipt part sealed ({bytes} bytes)",
|
|
114
|
+
"candidate_installed": "Build installed: {members} parts, {bytes} bytes",
|
|
115
|
+
"snapshot_verify_started": "Started checking {total} receipt parts",
|
|
116
|
+
"member_verified": "Receipt part checked ({bytes} bytes)",
|
|
117
|
+
"snapshot_verified": "Build checked: {members} parts match",
|
|
118
|
+
"build_sealed": "Receipt sealed",
|
|
119
|
+
"build_failed": "Build stopped: {message}",
|
|
120
|
+
"run_interrupted": "Build interrupted",
|
|
121
|
+
"notebook_sidecar_started": "Generating table notebook",
|
|
122
|
+
"notebook_sidecar_ready": "Table notebook ready",
|
|
123
|
+
"notebook_sidecar_failed": "Table notebook could not be rendered ({code})",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
STOPPED_EVENTS = frozenset({"build_failed", "run_interrupted"})
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# A fixed span vocabulary makes the rendered block bounded independently of source, check, and
|
|
130
|
+
# receipt-member counts. Per-unit records update one existing line; they never create another line.
|
|
131
|
+
BUILD_SPANS: tuple[Mapping[str, Any], ...] = (
|
|
132
|
+
{
|
|
133
|
+
"key": "prepare",
|
|
134
|
+
"label": "Start the Build",
|
|
135
|
+
"complete": "run_started",
|
|
136
|
+
},
|
|
137
|
+
{"key": "rights", "label": "Check source rights", "complete": "rights_checked"},
|
|
138
|
+
{
|
|
139
|
+
"key": "read",
|
|
140
|
+
"label": "Read source bytes",
|
|
141
|
+
"start": "sources_read_started",
|
|
142
|
+
"unit": "source_read",
|
|
143
|
+
"unique": "source_id",
|
|
144
|
+
"noun": "sources read",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"key": "open",
|
|
148
|
+
"label": "Open source data",
|
|
149
|
+
"start": "sources_parsed_started",
|
|
150
|
+
"unit": "source_parsed",
|
|
151
|
+
"unique": "source_id",
|
|
152
|
+
"noun": "sources opened",
|
|
153
|
+
},
|
|
154
|
+
{"key": "match", "label": "Match source evidence", "complete": "evidence_matched"},
|
|
155
|
+
{
|
|
156
|
+
"key": "clean",
|
|
157
|
+
"label": "Apply declared cleaning",
|
|
158
|
+
"start": "cleaning_started",
|
|
159
|
+
"unit": "cleaning_applied",
|
|
160
|
+
"unique": "step_index",
|
|
161
|
+
"noun": "steps applied",
|
|
162
|
+
},
|
|
163
|
+
{"key": "profile", "label": "Profile cleaned sources", "complete": "sources_profiled"},
|
|
164
|
+
{"key": "join", "label": "Join sources", "complete": "join_completed"},
|
|
165
|
+
{"key": "select", "label": "Select dataset rows", "complete": "rows_selected"},
|
|
166
|
+
{
|
|
167
|
+
"key": "checks",
|
|
168
|
+
"label": "Run output checks",
|
|
169
|
+
"start": "checks_started",
|
|
170
|
+
"unit": "check_completed",
|
|
171
|
+
"unique": "index",
|
|
172
|
+
"complete": "checks_completed",
|
|
173
|
+
"noun": "checks run",
|
|
174
|
+
},
|
|
175
|
+
{"key": "write", "label": "Write the table", "complete": "table_written"},
|
|
176
|
+
{
|
|
177
|
+
"key": "seal",
|
|
178
|
+
"label": "Seal receipt parts",
|
|
179
|
+
"start": "members_sealed_started",
|
|
180
|
+
"unit": "member_sealed",
|
|
181
|
+
"unique": "evidence.member",
|
|
182
|
+
"noun": "parts sealed",
|
|
183
|
+
},
|
|
184
|
+
{"key": "install", "label": "Install the Build", "complete": "candidate_installed"},
|
|
185
|
+
{
|
|
186
|
+
"key": "verify",
|
|
187
|
+
"label": "Check the installed Build",
|
|
188
|
+
"start": "snapshot_verify_started",
|
|
189
|
+
"unit": "member_verified",
|
|
190
|
+
"unique": "evidence.member",
|
|
191
|
+
"complete": "snapshot_verified",
|
|
192
|
+
"noun": "parts checked",
|
|
193
|
+
},
|
|
194
|
+
{"key": "finish", "label": "Finish the Build", "complete": "build_sealed"},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def sentence(event: str, facts: Mapping[str, Any]) -> str:
|
|
199
|
+
"""Render one event in plain words, retaining a safe fallback for producer drift."""
|
|
200
|
+
|
|
201
|
+
template = EVENT_SENTENCES.get(event)
|
|
202
|
+
if template is None:
|
|
203
|
+
return event.replace("_", " ")
|
|
204
|
+
try:
|
|
205
|
+
return template.format(**facts)
|
|
206
|
+
except (KeyError, IndexError, ValueError, TypeError):
|
|
207
|
+
return event.replace("_", " ")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _facts(record: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
211
|
+
value = record.get("facts")
|
|
212
|
+
return value if isinstance(value, Mapping) else {}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _moment(record: Mapping[str, Any]) -> float | None:
|
|
216
|
+
value = record.get("at")
|
|
217
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
218
|
+
return float(value)
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _unique_value(record: Mapping[str, Any], key: str) -> object | None:
|
|
223
|
+
if key == "evidence.member":
|
|
224
|
+
evidence = record.get("evidence")
|
|
225
|
+
return evidence.get("member") if isinstance(evidence, Mapping) else None
|
|
226
|
+
return _facts(record).get(key)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _sequence(record: Mapping[str, Any]) -> int | None:
|
|
230
|
+
value = record.get("seq")
|
|
231
|
+
if isinstance(value, int) and not isinstance(value, bool) and value > 0:
|
|
232
|
+
return value
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _valid_sidecar_record(
|
|
237
|
+
record: Mapping[str, Any],
|
|
238
|
+
event: str,
|
|
239
|
+
*,
|
|
240
|
+
previous: Mapping[str, Any],
|
|
241
|
+
) -> bool:
|
|
242
|
+
"""Admit one schema-valid, evidence-free record at the next fixed sequence position."""
|
|
243
|
+
|
|
244
|
+
if (
|
|
245
|
+
record.get("event") != event
|
|
246
|
+
or "evidence" not in record
|
|
247
|
+
or record.get("evidence") is not None
|
|
248
|
+
):
|
|
249
|
+
return False
|
|
250
|
+
facts = record.get("facts")
|
|
251
|
+
try:
|
|
252
|
+
validate_facts(event, facts) # type: ignore[arg-type]
|
|
253
|
+
except ValueError:
|
|
254
|
+
return False
|
|
255
|
+
sequence = _sequence(record)
|
|
256
|
+
previous_sequence = _sequence(previous)
|
|
257
|
+
moment = _moment(record)
|
|
258
|
+
previous_moment = _moment(previous)
|
|
259
|
+
return (
|
|
260
|
+
sequence is not None
|
|
261
|
+
and previous_sequence is not None
|
|
262
|
+
and sequence == previous_sequence + 1
|
|
263
|
+
and moment is not None
|
|
264
|
+
and previous_moment is not None
|
|
265
|
+
and moment >= previous_moment
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _latest_attempt(records: Sequence[Mapping[str, Any]]) -> list[Mapping[str, Any]]:
|
|
270
|
+
"""Use the latest narrated run if malformed bytes contain two starts in one attempt."""
|
|
271
|
+
|
|
272
|
+
start = 0
|
|
273
|
+
for index, record in enumerate(records):
|
|
274
|
+
if record.get("event") == "run_started":
|
|
275
|
+
start = index
|
|
276
|
+
return list(records[start:])
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def coherent_build_seal_candidate(records: Sequence[Mapping[str, Any]]) -> str | None:
|
|
280
|
+
"""Return a selected attempt's schema-valid, evidence-free terminal candidate digest."""
|
|
281
|
+
|
|
282
|
+
selected = _latest_attempt(records)
|
|
283
|
+
terminal = next(
|
|
284
|
+
(record for record in selected if record.get("event") in {"build_sealed", *STOPPED_EVENTS}),
|
|
285
|
+
None,
|
|
286
|
+
)
|
|
287
|
+
if terminal is None or terminal.get("event") != "build_sealed":
|
|
288
|
+
return None
|
|
289
|
+
if "evidence" not in terminal or terminal.get("evidence") is not None:
|
|
290
|
+
return None
|
|
291
|
+
facts = terminal.get("facts")
|
|
292
|
+
try:
|
|
293
|
+
validate_facts("build_sealed", facts) # type: ignore[arg-type]
|
|
294
|
+
except ValueError:
|
|
295
|
+
return None
|
|
296
|
+
digest = facts.get("candidate_digest") if isinstance(facts, Mapping) else None
|
|
297
|
+
return digest if isinstance(digest, str) else None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def reduce_lifecycle(
|
|
301
|
+
records: Sequence[Mapping[str, Any]],
|
|
302
|
+
*,
|
|
303
|
+
table_ready: bool,
|
|
304
|
+
allow_legacy_handoff: bool = True,
|
|
305
|
+
) -> dict[str, Any]:
|
|
306
|
+
"""Reduce real feed records to one bounded, JSON-ready lifecycle snapshot.
|
|
307
|
+
|
|
308
|
+
``table_ready`` is the Viewer's independent verified-sidecar admission result. It authorizes
|
|
309
|
+
handoff; the untrusted feed narrates the Build but never authorizes access to dataset bytes.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
bounded = list(records)
|
|
313
|
+
post_seal: list[Mapping[str, Any]] = []
|
|
314
|
+
following: list[Mapping[str, Any]] = []
|
|
315
|
+
sidecar_narrated = False
|
|
316
|
+
first_terminal = next(
|
|
317
|
+
(
|
|
318
|
+
index
|
|
319
|
+
for index, record in enumerate(bounded)
|
|
320
|
+
if record.get("event") in {"build_sealed", *STOPPED_EVENTS}
|
|
321
|
+
),
|
|
322
|
+
None,
|
|
323
|
+
)
|
|
324
|
+
if first_terminal is not None:
|
|
325
|
+
terminal_prefix = bounded[: first_terminal + 1]
|
|
326
|
+
terminal_name = bounded[first_terminal].get("event")
|
|
327
|
+
if terminal_name == "build_sealed":
|
|
328
|
+
following = bounded[first_terminal + 1 :]
|
|
329
|
+
sidecar_narrated = any(
|
|
330
|
+
record.get("event") in NOTEBOOK_SIDECAR_EVENT_NAMES for record in following
|
|
331
|
+
)
|
|
332
|
+
# The sidecar tail is a fixed state machine, not an arbitrary post-seal extension. A
|
|
333
|
+
# forged or malformed ordering cannot claim readiness: facts must match the declared
|
|
334
|
+
# event schema, evidence must be absent, and sequence/time must grow exactly once from
|
|
335
|
+
# the seal through started -> ready|failed. The first valid outcome is terminal; no
|
|
336
|
+
# record after it is considered part of this attempt.
|
|
337
|
+
if following and _valid_sidecar_record(
|
|
338
|
+
following[0], "notebook_sidecar_started", previous=terminal_prefix[-1]
|
|
339
|
+
):
|
|
340
|
+
post_seal.append(following[0])
|
|
341
|
+
if len(following) > 1:
|
|
342
|
+
outcome = following[1]
|
|
343
|
+
outcome_name = outcome.get("event")
|
|
344
|
+
if outcome_name in NOTEBOOK_SIDECAR_EVENT_NAMES - {
|
|
345
|
+
"notebook_sidecar_started"
|
|
346
|
+
} and _valid_sidecar_record(
|
|
347
|
+
outcome,
|
|
348
|
+
str(outcome_name),
|
|
349
|
+
previous=post_seal[0],
|
|
350
|
+
):
|
|
351
|
+
post_seal.append(outcome)
|
|
352
|
+
bounded = [*terminal_prefix, *post_seal]
|
|
353
|
+
selected = _latest_attempt(bounded) if bounded else []
|
|
354
|
+
if not selected:
|
|
355
|
+
return {
|
|
356
|
+
"status": "completed" if table_ready else "idle",
|
|
357
|
+
"phase": "Table notebook ready" if table_ready else "Research notebook",
|
|
358
|
+
"progress": 100 if table_ready else 0,
|
|
359
|
+
"started_at": None,
|
|
360
|
+
"ended_at": None,
|
|
361
|
+
"sealed": table_ready,
|
|
362
|
+
"handoff": table_ready,
|
|
363
|
+
"lines": [],
|
|
364
|
+
"terminal": None,
|
|
365
|
+
"sidecar_narrated": False,
|
|
366
|
+
"sidecar_state": "absent",
|
|
367
|
+
"legacy_handoff_allowed": allow_legacy_handoff,
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
by_name: dict[str, list[Mapping[str, Any]]] = {}
|
|
371
|
+
for record in selected:
|
|
372
|
+
name = record.get("event")
|
|
373
|
+
if isinstance(name, str):
|
|
374
|
+
by_name.setdefault(name, []).append(record)
|
|
375
|
+
|
|
376
|
+
terminal_record = next(
|
|
377
|
+
(record for record in selected if record.get("event") in {"build_sealed", *STOPPED_EVENTS}),
|
|
378
|
+
None,
|
|
379
|
+
)
|
|
380
|
+
terminal_name = terminal_record.get("event") if terminal_record is not None else None
|
|
381
|
+
narrated_seal = coherent_build_seal_candidate(selected) is not None
|
|
382
|
+
sealed = narrated_seal
|
|
383
|
+
stopped = terminal_name in STOPPED_EVENTS
|
|
384
|
+
sidecar_started = bool(post_seal)
|
|
385
|
+
sidecar_outcome = post_seal[-1].get("event") if post_seal else None
|
|
386
|
+
sidecar_ready = sidecar_outcome == "notebook_sidecar_ready"
|
|
387
|
+
sidecar_failed = sidecar_outcome == "notebook_sidecar_failed"
|
|
388
|
+
if not sidecar_narrated:
|
|
389
|
+
sidecar_state = "absent"
|
|
390
|
+
elif not post_seal:
|
|
391
|
+
sidecar_state = "invalid"
|
|
392
|
+
elif len(post_seal) == 1 and len(following) > 1:
|
|
393
|
+
sidecar_state = "invalid"
|
|
394
|
+
elif sidecar_ready:
|
|
395
|
+
sidecar_state = "ready"
|
|
396
|
+
elif sidecar_failed:
|
|
397
|
+
sidecar_state = "failed"
|
|
398
|
+
else:
|
|
399
|
+
sidecar_state = "started"
|
|
400
|
+
# Existing sealed feeds predate sidecar narration and retain their independently admitted
|
|
401
|
+
# handoff. A current CLI attempt announces ``started`` before touching the path, so it cannot
|
|
402
|
+
# hand off on file timing alone: both its real ``ready`` record and independent admission are
|
|
403
|
+
# required.
|
|
404
|
+
# Only a feed with no sidecar names at all is legacy. A malformed, evidence-bearing, or
|
|
405
|
+
# out-of-sequence sidecar tail is current narration that failed validation, not permission to
|
|
406
|
+
# fall back to legacy handoff.
|
|
407
|
+
handoff = (
|
|
408
|
+
narrated_seal
|
|
409
|
+
and table_ready
|
|
410
|
+
and (sidecar_ready or (allow_legacy_handoff and not sidecar_narrated))
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
lines: list[dict[str, str]] = []
|
|
414
|
+
completed = 0
|
|
415
|
+
for span in BUILD_SPANS:
|
|
416
|
+
start_name = span.get("start")
|
|
417
|
+
complete_name = span.get("complete")
|
|
418
|
+
unit_name = span.get("unit")
|
|
419
|
+
starts = by_name.get(str(start_name), []) if start_name else []
|
|
420
|
+
completions = by_name.get(str(complete_name), []) if complete_name else []
|
|
421
|
+
units = by_name.get(str(unit_name), []) if unit_name else []
|
|
422
|
+
if not starts and not completions and not units:
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
is_done = bool(completions)
|
|
426
|
+
detail_record = completions[-1] if completions else units[-1] if units else starts[-1]
|
|
427
|
+
detail = sentence(str(detail_record.get("event", "")), _facts(detail_record))
|
|
428
|
+
if unit_name and starts:
|
|
429
|
+
total_value = _facts(starts[-1]).get("total")
|
|
430
|
+
total = (
|
|
431
|
+
total_value
|
|
432
|
+
if isinstance(total_value, int)
|
|
433
|
+
and not isinstance(total_value, bool)
|
|
434
|
+
and total_value >= 0
|
|
435
|
+
else None
|
|
436
|
+
)
|
|
437
|
+
unique_key = str(span.get("unique", ""))
|
|
438
|
+
values: set[object] = set()
|
|
439
|
+
for record in units:
|
|
440
|
+
value = _unique_value(record, unique_key)
|
|
441
|
+
if value is None:
|
|
442
|
+
continue
|
|
443
|
+
try:
|
|
444
|
+
hash(value)
|
|
445
|
+
except TypeError:
|
|
446
|
+
# Reader-side fact drift must not make the live viewer fail to start.
|
|
447
|
+
continue
|
|
448
|
+
values.add(value)
|
|
449
|
+
done = len(values)
|
|
450
|
+
if total is not None:
|
|
451
|
+
done = min(done, total)
|
|
452
|
+
is_done = is_done or done >= total
|
|
453
|
+
detail = f"{done} of {total} {span['noun']}"
|
|
454
|
+
if is_done:
|
|
455
|
+
completed += 1
|
|
456
|
+
lines.append(
|
|
457
|
+
{
|
|
458
|
+
"key": str(span["key"]),
|
|
459
|
+
"label": str(span["label"]),
|
|
460
|
+
"status": "done" if is_done else "running",
|
|
461
|
+
"detail": detail,
|
|
462
|
+
}
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
if stopped and terminal_record is not None:
|
|
466
|
+
lines.append(
|
|
467
|
+
{
|
|
468
|
+
"key": "stopped",
|
|
469
|
+
"label": "Build stopped",
|
|
470
|
+
"status": "failed",
|
|
471
|
+
"detail": sentence(str(terminal_name), _facts(terminal_record)),
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
if post_seal:
|
|
476
|
+
sidecar_record = post_seal[-1]
|
|
477
|
+
sidecar_name = str(sidecar_record.get("event", ""))
|
|
478
|
+
sidecar_status = "failed" if sidecar_failed else "done" if sidecar_ready else "running"
|
|
479
|
+
lines.append(
|
|
480
|
+
{
|
|
481
|
+
"key": "table-notebook",
|
|
482
|
+
"label": "Prepare table notebook",
|
|
483
|
+
"status": sidecar_status,
|
|
484
|
+
"detail": sentence(sidecar_name, _facts(sidecar_record)),
|
|
485
|
+
}
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
started_record = by_name.get("run_started", [selected[0]])[0]
|
|
489
|
+
started_at = _moment(started_record)
|
|
490
|
+
ended_at = _moment(terminal_record) if terminal_record is not None else None
|
|
491
|
+
latest = selected[-1]
|
|
492
|
+
latest_name = str(latest.get("event", ""))
|
|
493
|
+
if handoff:
|
|
494
|
+
status = "completed"
|
|
495
|
+
phase = "Build complete · table notebook ready"
|
|
496
|
+
elif sidecar_failed:
|
|
497
|
+
status = "completed"
|
|
498
|
+
phase = "Build complete · table notebook unavailable"
|
|
499
|
+
elif sidecar_ready:
|
|
500
|
+
status = "completed"
|
|
501
|
+
phase = "Build complete · checking table notebook"
|
|
502
|
+
elif sidecar_started:
|
|
503
|
+
status = "completed"
|
|
504
|
+
phase = "Build complete · generating table notebook"
|
|
505
|
+
elif sealed:
|
|
506
|
+
status = "completed"
|
|
507
|
+
phase = "Build complete · preparing table notebook"
|
|
508
|
+
elif stopped:
|
|
509
|
+
status = "failed"
|
|
510
|
+
phase = sentence(str(terminal_name), _facts(terminal_record or {}))
|
|
511
|
+
else:
|
|
512
|
+
status = "running"
|
|
513
|
+
phase = sentence(latest_name, _facts(latest))
|
|
514
|
+
progress = 100 if sealed else round(100 * completed / len(BUILD_SPANS))
|
|
515
|
+
terminal: dict[str, str | None] | None = None
|
|
516
|
+
if stopped and terminal_record is not None:
|
|
517
|
+
terminal_facts = _facts(terminal_record)
|
|
518
|
+
finding_id = terminal_facts.get("finding_id")
|
|
519
|
+
severity = terminal_facts.get("severity")
|
|
520
|
+
terminal = {
|
|
521
|
+
"event": str(terminal_name),
|
|
522
|
+
"title": "Build interrupted" if terminal_name == "run_interrupted" else "Build stopped",
|
|
523
|
+
"detail": sentence(str(terminal_name), terminal_facts),
|
|
524
|
+
"finding_id": finding_id if isinstance(finding_id, str) else None,
|
|
525
|
+
"severity": severity if isinstance(severity, str) else None,
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
"status": status,
|
|
529
|
+
"phase": phase,
|
|
530
|
+
"progress": progress,
|
|
531
|
+
"started_at": started_at,
|
|
532
|
+
"ended_at": ended_at,
|
|
533
|
+
"sealed": sealed,
|
|
534
|
+
"handoff": handoff,
|
|
535
|
+
"lines": lines,
|
|
536
|
+
"terminal": terminal,
|
|
537
|
+
"sidecar_narrated": sidecar_narrated,
|
|
538
|
+
"sidecar_state": sidecar_state,
|
|
539
|
+
"legacy_handoff_allowed": allow_legacy_handoff,
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def actual_run_started_record(
|
|
544
|
+
records: Sequence[Mapping[str, Any]],
|
|
545
|
+
) -> Mapping[str, Any] | None:
|
|
546
|
+
"""Return the selected attempt's real ``run_started`` record, never a fallback record."""
|
|
547
|
+
|
|
548
|
+
bounded: list[Mapping[str, Any]] = []
|
|
549
|
+
for record in records:
|
|
550
|
+
bounded.append(record)
|
|
551
|
+
if record.get("event") in {"build_sealed", *STOPPED_EVENTS}:
|
|
552
|
+
break
|
|
553
|
+
selected = _latest_attempt(bounded) if bounded else []
|
|
554
|
+
starts = [record for record in selected if record.get("event") == "run_started"]
|
|
555
|
+
return starts[0] if starts else None
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def elapsed_seconds(lifecycle: Mapping[str, Any], *, now: float) -> int | None:
|
|
559
|
+
"""Return a non-negative whole-second run clock from lifecycle timestamps."""
|
|
560
|
+
|
|
561
|
+
started = lifecycle.get("started_at")
|
|
562
|
+
if not isinstance(started, (int, float)) or isinstance(started, bool):
|
|
563
|
+
return None
|
|
564
|
+
ended = lifecycle.get("ended_at")
|
|
565
|
+
stop = float(ended) if isinstance(ended, (int, float)) and not isinstance(ended, bool) else now
|
|
566
|
+
return max(0, int(stop - float(started)))
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def format_elapsed(seconds: int | None) -> str:
|
|
570
|
+
"""Format the run clock without locale or wall-clock dependencies."""
|
|
571
|
+
|
|
572
|
+
if seconds is None:
|
|
573
|
+
return ""
|
|
574
|
+
hours, remainder = divmod(seconds, 3600)
|
|
575
|
+
minutes, secs = divmod(remainder, 60)
|
|
576
|
+
return f"{hours:d}:{minutes:02d}:{secs:02d}" if hours else f"{minutes:02d}:{secs:02d}"
|