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,589 @@
|
|
|
1
|
+
"""``mr-data note``: the agent's decision record, written into the hosted run's own log.
|
|
2
|
+
|
|
3
|
+
WHAT WAS MISSING. The local notebook let the agent driving a build write the operator-facing
|
|
4
|
+
decision record -- outcome-led ``##`` headings, cells checkpointed against what had just happened,
|
|
5
|
+
a revision when a conclusion changed. The hosted notebook (Studio #88) renders from the run header
|
|
6
|
+
and the event log and nothing else, and the only client-writable path into that log was the fenced
|
|
7
|
+
worker surface, whose producer vocabulary is a closed, attempt-scoped six-value enum. So a hosted
|
|
8
|
+
run kept every number the local one had and lost every sentence: it could say what happened and
|
|
9
|
+
never why. This repository wrote that gap down as a fact of the hosted profile and told the agent
|
|
10
|
+
not to try -- which was the honest thing to say while it was true, and is no longer true.
|
|
11
|
+
|
|
12
|
+
Studio PR #98 added ``POST /v3/runs/{run_id}/narrative`` under a new ``run:narrate`` scope, narrow
|
|
13
|
+
the way ``approval:request`` is narrow: it grants exactly one power, causing a row that says
|
|
14
|
+
somebody wrote something. That scope is reachable by the organisation service principal the thin
|
|
15
|
+
CLI runs as, which is the entire point -- an autonomous agent can now write the record a person
|
|
16
|
+
reads, without being able to decide, release, or approve anything.
|
|
17
|
+
|
|
18
|
+
WHAT A CELL IS, AND IS NOT. It is a durable ``run_narrative_appended`` event carrying an explicitly
|
|
19
|
+
UNSEALED adjunct. The sealed record's only content is a ``payload_digest``, so the prose is
|
|
20
|
+
tamper-evident inside the log while claiming no authority: it is never attestation evidence, no
|
|
21
|
+
receipt quotes it, and no verifier reads it. ``sealed`` comes back ``false`` on every cell and this
|
|
22
|
+
lane asserts it rather than assuming it, because "not evidence" is the property the whole design
|
|
23
|
+
rests on and a client that stopped checking would be the first place it could quietly stop being
|
|
24
|
+
true.
|
|
25
|
+
|
|
26
|
+
⚠ THE BOUNDS ARE ENFORCED HERE TOO, IN STUDIO'S OWN WORDS. 65536 UTF-8 **bytes** of markdown, a
|
|
27
|
+
200-character single-line heading, and no control character but tab and newline -- carriage return
|
|
28
|
+
included, refused rather than normalised, because normalising would mean the digest binds bytes
|
|
29
|
+
the client never sent. Studio refuses all three itself; checking them here as well is not
|
|
30
|
+
belt-and-braces. An agent that has just composed four pages of prose should learn that it is one
|
|
31
|
+
kilobyte over the ceiling before the round trip and before the idempotency key is spent, and it
|
|
32
|
+
should read the same sentence either way -- so the messages below are Studio's, copied
|
|
33
|
+
deliberately, and ``tests/test_thin_narrative.py`` holds them equal to the contract's bounds.
|
|
34
|
+
|
|
35
|
+
⚠ WHAT THIS MODULE MAY NOT DO. Append, and list. There is no delete and no edit-in-place: a
|
|
36
|
+
correction is a new append that supersedes, the superseded cell stays in the log, and the record of
|
|
37
|
+
what was thought earlier survives being wrong. :data:`RUN_NARRATIVE_PATH` is the only route here
|
|
38
|
+
and both verbs are on it.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import argparse
|
|
44
|
+
import hashlib
|
|
45
|
+
import re
|
|
46
|
+
import sys
|
|
47
|
+
from collections.abc import Mapping, Sequence
|
|
48
|
+
from typing import Any
|
|
49
|
+
from uuid import UUID
|
|
50
|
+
|
|
51
|
+
from mostlyright.data_harness.thin import THIN_SCHEMA_PREFIX
|
|
52
|
+
from mostlyright.data_harness.thin.parity import _no_effect, _run_id
|
|
53
|
+
from mostlyright.data_harness.thin.runs import StudioApiClient, new_idempotency_key
|
|
54
|
+
from mostlyright.data_harness.thin.session import StudioSession, open_studio_session
|
|
55
|
+
from mostlyright.data_harness.thin.transport import ThinLaneError
|
|
56
|
+
from mostlyright.data_harness.ux.credentials import resolve_cloud_credentials
|
|
57
|
+
|
|
58
|
+
NOTE_SCHEMA = f"{THIN_SCHEMA_PREFIX}-narrative.v1"
|
|
59
|
+
|
|
60
|
+
#: The one route, and both verbs are on it. POST appends a cell, GET lists every cell this run
|
|
61
|
+
#: holds, superseded revisions included and flagged.
|
|
62
|
+
RUN_NARRATIVE_PATH = "/v3/runs/{run_id}/narrative"
|
|
63
|
+
|
|
64
|
+
#: The automation scope the append needs. Not checked here -- a client cannot check its own
|
|
65
|
+
#: authority, and pretending to would only teach a reader to trust the wrong assertion. It is
|
|
66
|
+
#: written down so the refusal a caller without it gets has a name in this file to match against.
|
|
67
|
+
NARRATE_SCOPE = "run:narrate"
|
|
68
|
+
|
|
69
|
+
#: ``run-narrative.schema.json#/$defs/markdown``, counted in BYTES. The character bound in the
|
|
70
|
+
#: schema is its loosest possible restatement; the API enforces this one.
|
|
71
|
+
MAX_MARKDOWN_BYTES = 65536
|
|
72
|
+
|
|
73
|
+
#: ``run-narrative.schema.json#/$defs/heading``.
|
|
74
|
+
MAX_HEADING_CHARS = 200
|
|
75
|
+
|
|
76
|
+
#: ``run-narrative.schema.json#/$defs/cell_id``.
|
|
77
|
+
MAX_CELL_ID_CHARS = 64
|
|
78
|
+
|
|
79
|
+
#: ``run-narrative.schema.json#/$defs/checkpoint_seq``. The contract's own ceiling, which is
|
|
80
|
+
#: JavaScript's exact-integer bound rather than a statement about how long a run can be.
|
|
81
|
+
MAX_CHECKPOINT_SEQ = 9007199254740991
|
|
82
|
+
_CELL_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
|
83
|
+
|
|
84
|
+
#: Every control character the body admits. Tab and newline, and nothing else -- carriage return
|
|
85
|
+
#: is a control character here and is refused rather than stripped.
|
|
86
|
+
_BODY_ALLOWED_CONTROLS = frozenset("\t\n")
|
|
87
|
+
|
|
88
|
+
#: A heading is one line by construction, so it admits none of them at all.
|
|
89
|
+
_HEADING_ALLOWED_CONTROLS: frozenset[str] = frozenset()
|
|
90
|
+
|
|
91
|
+
#: What a cursor from ``mr-data watch`` looks like: the same fixed-width, zero-padded decimal the
|
|
92
|
+
#: event stream carries as ``id``. Accepted beside a bare integer because the number an agent has
|
|
93
|
+
#: in its hand at a checkpoint is the one ``watch`` just printed, and making it retype that as an
|
|
94
|
+
#: unpadded integer is an invitation to retype it wrong.
|
|
95
|
+
_CURSOR = re.compile(r"^[0-9]{1,20}$")
|
|
96
|
+
|
|
97
|
+
#: Trimmed off a slugged heading so a derived identifier never opens or closes on punctuation.
|
|
98
|
+
_SLUG_TRIM = ".-_"
|
|
99
|
+
|
|
100
|
+
#: How much of the heading's digest a derived identifier carries when the slug does not fit.
|
|
101
|
+
#: Eight hex characters is 32 bits over at most 512 cells in one run, which is a collision this
|
|
102
|
+
#: repository is content to have never seen; the alternative, a bare truncation, collides on the
|
|
103
|
+
#: ordinary case of two headings that open the same way.
|
|
104
|
+
_DERIVED_DIGEST_CHARS = 8
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class StudioNarrativeClient(StudioApiClient):
|
|
108
|
+
"""The two narrative calls, and no third one.
|
|
109
|
+
|
|
110
|
+
``append_cell`` is the only mutation this lane can make, and what it can cause is one row
|
|
111
|
+
saying somebody wrote something. It cannot start, cancel, approve or release anything: those
|
|
112
|
+
are other routes under other scopes, and none of them is reachable from here.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def append_cell(
|
|
116
|
+
self,
|
|
117
|
+
run_id: str,
|
|
118
|
+
*,
|
|
119
|
+
body: Mapping[str, Any],
|
|
120
|
+
idempotency_key: str | None = None,
|
|
121
|
+
) -> dict[str, Any]:
|
|
122
|
+
"""Append one cell and return it as Studio stored it, with its log coordinates."""
|
|
123
|
+
|
|
124
|
+
return self._call(
|
|
125
|
+
"POST",
|
|
126
|
+
RUN_NARRATIVE_PATH.format(run_id=run_id),
|
|
127
|
+
body=body,
|
|
128
|
+
extra_headers={
|
|
129
|
+
"Idempotency-Key": idempotency_key or new_idempotency_key("mr-data-note")
|
|
130
|
+
},
|
|
131
|
+
expected=(201,),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def run_narrative(self, run_id: str) -> dict[str, Any]:
|
|
135
|
+
"""Every cell this run holds, in log order, superseded revisions included."""
|
|
136
|
+
|
|
137
|
+
return self._call("GET", RUN_NARRATIVE_PATH.format(run_id=run_id), expected=(200,))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def cell_id_from_heading(heading: str) -> str:
|
|
141
|
+
"""A stable identifier derived from the heading, for the caller who named none.
|
|
142
|
+
|
|
143
|
+
⚠ DERIVED, NOT RANDOM, AND THAT IS THE WHOLE POINT. ``cell_id`` is what makes a second append
|
|
144
|
+
a revision rather than a duplicate, so the default has to be a function of something the agent
|
|
145
|
+
will write the same way twice. A random id would make every correction a new cell and turn one
|
|
146
|
+
changing conclusion into three contradictory ones sitting side by side in the notebook. The
|
|
147
|
+
heading is the one thing an outcome-led cell keeps while its body is being rewritten.
|
|
148
|
+
|
|
149
|
+
A caller whose heading itself changes with the conclusion should pass ``--cell-id`` and choose
|
|
150
|
+
the stable name, which is why the flag exists and why this is a default rather than the rule.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
slug = re.sub(r"[^A-Za-z0-9]+", "-", heading).strip(_SLUG_TRIM).lower()
|
|
154
|
+
if not slug:
|
|
155
|
+
raise ThinLaneError(
|
|
156
|
+
"THIN_REQUEST_INVALID",
|
|
157
|
+
"this heading has no letters or digits to derive a cell identifier from; name one "
|
|
158
|
+
"with --cell-id",
|
|
159
|
+
)
|
|
160
|
+
if len(slug) > MAX_CELL_ID_CHARS:
|
|
161
|
+
# ⚠ TRUNCATION ALONE WOULD COLLIDE, AND A COLLISION HERE IS AN OVERWRITE. A heading may run
|
|
162
|
+
# to two hundred characters and an identifier to sixty-four, so two headings that differ
|
|
163
|
+
# only past the cut would slug to one id -- and writing an id again is how this command
|
|
164
|
+
# revises, so the second cell would silently replace the first. The tail is therefore a
|
|
165
|
+
# digest of the whole heading rather than the part that fitted: still a function of the
|
|
166
|
+
# heading, so a revision under the same heading still lands on the same cell, and no
|
|
167
|
+
# longer a function of a prefix of it.
|
|
168
|
+
stem = slug[: MAX_CELL_ID_CHARS - _DERIVED_DIGEST_CHARS - 1].rstrip(_SLUG_TRIM)
|
|
169
|
+
tail = hashlib.sha256(heading.encode("utf-8")).hexdigest()[:_DERIVED_DIGEST_CHARS]
|
|
170
|
+
slug = f"{stem}-{tail}"
|
|
171
|
+
if _CELL_ID.fullmatch(slug) is None: # pragma: no cover - the substitution above guarantees it
|
|
172
|
+
raise ThinLaneError(
|
|
173
|
+
"THIN_REQUEST_INVALID",
|
|
174
|
+
"this heading does not derive a usable cell identifier; name one with --cell-id",
|
|
175
|
+
)
|
|
176
|
+
return slug
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _cell_id(value: Any, *, flag: str) -> str:
|
|
180
|
+
"""Refuse an identifier the contract would refuse, before it is spent on a round trip."""
|
|
181
|
+
|
|
182
|
+
if not isinstance(value, str) or not value:
|
|
183
|
+
raise ThinLaneError("THIN_REQUEST_INVALID", f"{flag} names one cell identifier")
|
|
184
|
+
if len(value) > MAX_CELL_ID_CHARS or _CELL_ID.fullmatch(value) is None:
|
|
185
|
+
raise ThinLaneError(
|
|
186
|
+
"THIN_REQUEST_INVALID",
|
|
187
|
+
f"{flag} is at most {MAX_CELL_ID_CHARS} characters of letters, digits, dot, dash and "
|
|
188
|
+
"underscore, opening on a letter or a digit",
|
|
189
|
+
)
|
|
190
|
+
return value
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _forbidden_control_character(value: str, admitted: frozenset[str]) -> str | None:
|
|
194
|
+
"""The first control character this field may not carry, or ``None``."""
|
|
195
|
+
|
|
196
|
+
for character in value:
|
|
197
|
+
code = ord(character)
|
|
198
|
+
if (code < 0x20 or code == 0x7F) and character not in admitted:
|
|
199
|
+
return character
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _checkpoint(value: Any) -> int | None:
|
|
204
|
+
"""One run-event sequence, from a bare integer or from a cursor ``watch`` printed."""
|
|
205
|
+
|
|
206
|
+
if value is None:
|
|
207
|
+
return None
|
|
208
|
+
if type(value) is int:
|
|
209
|
+
sequence = value
|
|
210
|
+
elif isinstance(value, str) and _CURSOR.fullmatch(value.strip()):
|
|
211
|
+
sequence = int(value.strip(), 10)
|
|
212
|
+
else:
|
|
213
|
+
raise ThinLaneError(
|
|
214
|
+
"THIN_REQUEST_INVALID",
|
|
215
|
+
"--checkpoint names one run-event sequence, as the number or as the cursor "
|
|
216
|
+
"mr-data watch printed beside it",
|
|
217
|
+
)
|
|
218
|
+
if sequence < 1:
|
|
219
|
+
raise ThinLaneError(
|
|
220
|
+
"THIN_REQUEST_INVALID", "--checkpoint names a run-event sequence, counted from 1"
|
|
221
|
+
)
|
|
222
|
+
if sequence > MAX_CHECKPOINT_SEQ:
|
|
223
|
+
raise ThinLaneError(
|
|
224
|
+
"THIN_REQUEST_INVALID",
|
|
225
|
+
f"--checkpoint is at most {MAX_CHECKPOINT_SEQ}, which is the largest sequence this "
|
|
226
|
+
"contract admits; no run has that many events",
|
|
227
|
+
)
|
|
228
|
+
return sequence
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def check_cell_bounds(*, heading: str, markdown: str) -> None:
|
|
232
|
+
"""Refuse a cell the contract would refuse, in the words the contract would refuse it with.
|
|
233
|
+
|
|
234
|
+
Called by :func:`note` before a credential is resolved and again by :func:`append_cell_body`
|
|
235
|
+
on the way out, so the rule has one home whichever path reaches it. Not because the server is
|
|
236
|
+
untrusted: the caller is an agent that has just spent effort composing the thing being refused,
|
|
237
|
+
and it should learn the body is a kilobyte over the ceiling before the request rather than
|
|
238
|
+
after it -- reading the sentence it would have read either way.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
if not isinstance(heading, str) or not heading.strip():
|
|
242
|
+
raise ThinLaneError("THIN_REQUEST_INVALID", "--heading is the cell's one outcome-led line")
|
|
243
|
+
if not isinstance(markdown, str) or not markdown:
|
|
244
|
+
raise ThinLaneError(
|
|
245
|
+
"THIN_REQUEST_INVALID",
|
|
246
|
+
"a cell has a body; give it on standard input or name a file with --markdown-file",
|
|
247
|
+
)
|
|
248
|
+
if len(heading) > MAX_HEADING_CHARS:
|
|
249
|
+
raise ThinLaneError(
|
|
250
|
+
"THIN_NARRATIVE_CELL_TOO_LARGE", "Narrative heading exceeds its character ceiling."
|
|
251
|
+
)
|
|
252
|
+
markdown_bytes = len(markdown.encode("utf-8"))
|
|
253
|
+
if markdown_bytes > MAX_MARKDOWN_BYTES:
|
|
254
|
+
# Bytes, not characters. The ceiling protects the object store and the browser that opens
|
|
255
|
+
# the notebook, and both count bytes -- so a body of accented prose reaches it sooner than
|
|
256
|
+
# its character count suggests, which is exactly the surprise this sentence exists to
|
|
257
|
+
# remove.
|
|
258
|
+
raise ThinLaneError(
|
|
259
|
+
"THIN_NARRATIVE_CELL_TOO_LARGE",
|
|
260
|
+
f"Narrative markdown is {markdown_bytes} bytes; the ceiling is {MAX_MARKDOWN_BYTES}.",
|
|
261
|
+
)
|
|
262
|
+
for field, value, admitted in (
|
|
263
|
+
("heading", heading, _HEADING_ALLOWED_CONTROLS),
|
|
264
|
+
("markdown", markdown, _BODY_ALLOWED_CONTROLS),
|
|
265
|
+
):
|
|
266
|
+
offending = _forbidden_control_character(value, admitted)
|
|
267
|
+
if offending is not None:
|
|
268
|
+
raise ThinLaneError(
|
|
269
|
+
"THIN_NARRATIVE_CELL_CONTROL_CHARACTER",
|
|
270
|
+
f"Narrative {field} carries control character U+{ord(offending):04X}; "
|
|
271
|
+
"only tab and newline are admitted, and only in the body.",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def append_cell_body(
|
|
276
|
+
*,
|
|
277
|
+
workspace_id: UUID,
|
|
278
|
+
run_id: str,
|
|
279
|
+
cell_id: str,
|
|
280
|
+
heading: str,
|
|
281
|
+
markdown: str,
|
|
282
|
+
checkpoint_seq: int | None = None,
|
|
283
|
+
revision_of: str | None = None,
|
|
284
|
+
) -> dict[str, Any]:
|
|
285
|
+
"""The ``AppendNarrativeCellCommand`` this lane sends, with the contract applied here."""
|
|
286
|
+
|
|
287
|
+
check_cell_bounds(heading=heading, markdown=markdown)
|
|
288
|
+
body: dict[str, Any] = {
|
|
289
|
+
"schema_version": "3.0.0",
|
|
290
|
+
"workspace_id": str(workspace_id),
|
|
291
|
+
"run_id": run_id,
|
|
292
|
+
"cell_id": cell_id,
|
|
293
|
+
"heading": heading,
|
|
294
|
+
"markdown": markdown,
|
|
295
|
+
}
|
|
296
|
+
if checkpoint_seq is not None:
|
|
297
|
+
body["checkpoint_seq"] = checkpoint_seq
|
|
298
|
+
if revision_of is not None:
|
|
299
|
+
if revision_of == cell_id:
|
|
300
|
+
# Reusing the id IS the revision, so accepting both spellings would make one cell its
|
|
301
|
+
# own predecessor. Studio refuses it; this refuses it one round trip earlier, because
|
|
302
|
+
# `--revise X` with no `--cell-id` is the shape a caller reaches for first.
|
|
303
|
+
raise ThinLaneError(
|
|
304
|
+
"THIN_NARRATIVE_REVISION_INVALID",
|
|
305
|
+
"A cell revises itself by reusing its cell_id, not through revision_of.",
|
|
306
|
+
)
|
|
307
|
+
body["revision_of"] = revision_of
|
|
308
|
+
return body
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _session(args: argparse.Namespace) -> StudioSession:
|
|
312
|
+
return open_studio_session(resolve_cloud_credentials())
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _client(args: argparse.Namespace) -> StudioNarrativeClient:
|
|
316
|
+
return StudioNarrativeClient(_session(args))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _read_markdown(args: argparse.Namespace, *, stdin: Any = None) -> str:
|
|
320
|
+
"""The cell body: the named file, or standard input when no file was named.
|
|
321
|
+
|
|
322
|
+
Standard input is the default rather than a flag, because the caller is a program composing
|
|
323
|
+
prose and a heredoc is how a program hands over prose. A terminal with nobody typing into it is
|
|
324
|
+
not that, so it is refused by name rather than blocking forever on a read that will not come.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
named = getattr(args, "markdown_file", None)
|
|
328
|
+
stream = sys.stdin if stdin is None else stdin
|
|
329
|
+
if named is not None:
|
|
330
|
+
import os
|
|
331
|
+
from pathlib import Path
|
|
332
|
+
|
|
333
|
+
from mostlyright.data_harness.ux.plain_file import (
|
|
334
|
+
PlainFileRefusal,
|
|
335
|
+
open_plain_file,
|
|
336
|
+
read_bounded,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# ⚠ `open_plain_file` RATHER THAN `read_plain_file`, for the size. A file over the ceiling
|
|
340
|
+
# is not an unreadable file, and telling somebody it could not be read is answering a
|
|
341
|
+
# question they did not ask with a fact that is not true. The descriptor's own size is the
|
|
342
|
+
# byte count the ceiling counts, so the refusal can be the ceiling's own sentence, naming
|
|
343
|
+
# the real number -- which a bounded read cannot do, because it stops before it knows it.
|
|
344
|
+
target = Path(named).expanduser()
|
|
345
|
+
try:
|
|
346
|
+
descriptor, size = open_plain_file(target)
|
|
347
|
+
except PlainFileRefusal as refusal:
|
|
348
|
+
raise ThinLaneError(
|
|
349
|
+
"THIN_REQUEST_INVALID",
|
|
350
|
+
f"--markdown-file did not name one readable file: {refusal.path}",
|
|
351
|
+
) from refusal
|
|
352
|
+
except OSError as error:
|
|
353
|
+
# `open_plain_file` lets an OSError that is not one of its four named refusals travel,
|
|
354
|
+
# deliberately -- a permission denial is not a malformed path. It still has to arrive
|
|
355
|
+
# as a typed code rather than as a traceback out of the standard library.
|
|
356
|
+
raise ThinLaneError(
|
|
357
|
+
"THIN_REQUEST_INVALID", f"--markdown-file could not be opened: {target}"
|
|
358
|
+
) from error
|
|
359
|
+
try:
|
|
360
|
+
if size > MAX_MARKDOWN_BYTES:
|
|
361
|
+
raise ThinLaneError(
|
|
362
|
+
"THIN_NARRATIVE_CELL_TOO_LARGE",
|
|
363
|
+
f"Narrative markdown is {size} bytes; the ceiling is {MAX_MARKDOWN_BYTES}.",
|
|
364
|
+
)
|
|
365
|
+
# One byte past the ceiling, so a file that GREW between the descriptor's size and the
|
|
366
|
+
# read is caught as the ceiling it crossed rather than as a body that happens to end
|
|
367
|
+
# mid-character. `read_plain_file` makes the same second check for the same reason.
|
|
368
|
+
raw = read_bounded(descriptor, max_bytes=MAX_MARKDOWN_BYTES + 1)
|
|
369
|
+
finally:
|
|
370
|
+
os.close(descriptor)
|
|
371
|
+
if len(raw) > MAX_MARKDOWN_BYTES:
|
|
372
|
+
raise ThinLaneError(
|
|
373
|
+
"THIN_NARRATIVE_CELL_TOO_LARGE",
|
|
374
|
+
f"Narrative markdown grew past {MAX_MARKDOWN_BYTES} bytes while it was being read.",
|
|
375
|
+
)
|
|
376
|
+
if not raw:
|
|
377
|
+
raise ThinLaneError(
|
|
378
|
+
"THIN_REQUEST_INVALID", f"--markdown-file named an empty file: {target}"
|
|
379
|
+
)
|
|
380
|
+
try:
|
|
381
|
+
return raw.decode("utf-8")
|
|
382
|
+
except UnicodeDecodeError as error:
|
|
383
|
+
raise ThinLaneError(
|
|
384
|
+
"THIN_REQUEST_INVALID", "--markdown-file is not UTF-8 text"
|
|
385
|
+
) from error
|
|
386
|
+
if getattr(stream, "isatty", lambda: False)():
|
|
387
|
+
raise ThinLaneError(
|
|
388
|
+
"THIN_REQUEST_INVALID",
|
|
389
|
+
"a cell has a body; pipe it in on standard input, or name a file with --markdown-file",
|
|
390
|
+
)
|
|
391
|
+
# ⚠ THE SAME ENCODING GUARD THE FILE ARM HAS. `sys.stdin` decodes with `surrogateescape` under
|
|
392
|
+
# the default error handler, so bytes that are not UTF-8 arrive as lone surrogates rather than
|
|
393
|
+
# as a decode error -- and the first thing that touches them is `len(markdown.encode("utf-8"))`
|
|
394
|
+
# in the bounds check, which raises `UnicodeEncodeError` out of the middle of a validator. That
|
|
395
|
+
# is a traceback on the documented default input path. It is checked here, where the sentence
|
|
396
|
+
# can name standard input.
|
|
397
|
+
body = stream.read()
|
|
398
|
+
if not isinstance(body, str): # pragma: no cover - a text stream answers with text
|
|
399
|
+
raise ThinLaneError("THIN_REQUEST_INVALID", "standard input did not answer with text")
|
|
400
|
+
try:
|
|
401
|
+
body.encode("utf-8")
|
|
402
|
+
except UnicodeEncodeError as error:
|
|
403
|
+
raise ThinLaneError("THIN_REQUEST_INVALID", "standard input is not UTF-8 text") from error
|
|
404
|
+
return body
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _cell_receipt(cell: Mapping[str, Any]) -> dict[str, Any]:
|
|
408
|
+
"""One cell as Studio stored it, reduced to the coordinates that place it.
|
|
409
|
+
|
|
410
|
+
``sealed`` is asserted rather than reported. A narrative cell is an unsealed adjunct by
|
|
411
|
+
construction, and a client that quietly passed a ``true`` through would be the first surface
|
|
412
|
+
where "this is not evidence" stopped being checkable.
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
if cell.get("sealed") is not False:
|
|
416
|
+
raise ThinLaneError(
|
|
417
|
+
"THIN_RESPONSE_INVALID",
|
|
418
|
+
"Studio did not describe this narrative cell as unsealed; a cell is an unsealed "
|
|
419
|
+
"adjunct and is never attestation evidence",
|
|
420
|
+
)
|
|
421
|
+
return {
|
|
422
|
+
"cell_id": cell.get("cell_id"),
|
|
423
|
+
"heading": cell.get("heading"),
|
|
424
|
+
"checkpoint_seq": cell.get("checkpoint_seq"),
|
|
425
|
+
"revision_of": cell.get("revision_of"),
|
|
426
|
+
"sequence": cell.get("sequence"),
|
|
427
|
+
"cursor": cell.get("cursor"),
|
|
428
|
+
"appended_at": cell.get("appended_at"),
|
|
429
|
+
"payload_digest": cell.get("payload_digest"),
|
|
430
|
+
"superseded": cell.get("superseded"),
|
|
431
|
+
"markdown_bytes": len(str(cell.get("markdown", "")).encode("utf-8")),
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
#: `note` honours every argument it declares, so there is nothing to report as inert. The empty
|
|
436
|
+
#: table is here rather than absent so the payload key exists on this command exactly as it does on
|
|
437
|
+
#: every other hosted one, and a script does not have to branch on which command answered.
|
|
438
|
+
#:
|
|
439
|
+
#: ⚠ THIS IS EMPTY BECAUSE THE FLAGS ARE REFUSED, NOT BECAUSE THEY ARE HONOURED EVERYWHERE.
|
|
440
|
+
#: `--list` is a read, and the five flags that describe a cell mean nothing to it; the answer to
|
|
441
|
+
#: that is :data:`_WRITE_ONLY_FLAGS` below, which refuses the combination by name. `_no_effect` is
|
|
442
|
+
#: for a flag the hosted lane accepts and cannot honour, and none of these is that.
|
|
443
|
+
_NOTE_NO_EFFECT: Sequence[tuple[str, str, str]] = ()
|
|
444
|
+
|
|
445
|
+
#: The five arguments that describe a cell being written. `--list` writes nothing, so every one of
|
|
446
|
+
#: them is inapplicable there -- and inapplicable is not the same as inert. Silently dropping
|
|
447
|
+
#: `--heading` on a `--list` reads, to whoever typed it, like a filter that did not match.
|
|
448
|
+
_WRITE_ONLY_FLAGS: tuple[tuple[str, str], ...] = (
|
|
449
|
+
("heading", "--heading"),
|
|
450
|
+
("cell_id", "--cell-id"),
|
|
451
|
+
("markdown_file", "--markdown-file"),
|
|
452
|
+
("checkpoint", "--checkpoint"),
|
|
453
|
+
("revise", "--revise"),
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def note(
|
|
458
|
+
args: argparse.Namespace,
|
|
459
|
+
*,
|
|
460
|
+
client: StudioNarrativeClient | None = None,
|
|
461
|
+
stdin: Any = None,
|
|
462
|
+
) -> dict[str, Any]:
|
|
463
|
+
"""``mr-data note``: write one cell of the decision record, or list what is written.
|
|
464
|
+
|
|
465
|
+
⚠ ``--list`` IS A DIFFERENT ANSWER, NOT A FLAG ON THIS ONE. It reads under ``run:read`` and
|
|
466
|
+
writes nothing, so it is separated at the top rather than threaded through the append path:
|
|
467
|
+
a read that shares a code path with a write is a read that can accidentally become one.
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
run_id = _run_id(getattr(args, "run", None))
|
|
471
|
+
if getattr(args, "list", False):
|
|
472
|
+
given = [
|
|
473
|
+
flag for destination, flag in _WRITE_ONLY_FLAGS if getattr(args, destination, None)
|
|
474
|
+
]
|
|
475
|
+
if given:
|
|
476
|
+
raise ThinLaneError(
|
|
477
|
+
"THIN_REQUEST_INVALID",
|
|
478
|
+
f"--list reads what is already written and writes nothing, so {', '.join(given)} "
|
|
479
|
+
"cannot mean anything here; drop it, or drop --list to write the cell it describes",
|
|
480
|
+
)
|
|
481
|
+
return _list(client or _client(args), run_id)
|
|
482
|
+
heading = getattr(args, "heading", None)
|
|
483
|
+
if not isinstance(heading, str) or not heading.strip():
|
|
484
|
+
raise ThinLaneError(
|
|
485
|
+
"THIN_REQUEST_INVALID",
|
|
486
|
+
"--heading is the cell's one outcome-led line; it is what the notebook shows and "
|
|
487
|
+
"what a revision of this cell is recognised by",
|
|
488
|
+
)
|
|
489
|
+
heading = heading.strip()
|
|
490
|
+
named = getattr(args, "cell_id", None)
|
|
491
|
+
cell_id = (
|
|
492
|
+
_cell_id(named, flag="--cell-id") if named is not None else cell_id_from_heading(heading)
|
|
493
|
+
)
|
|
494
|
+
revise = getattr(args, "revise", None)
|
|
495
|
+
revision_of = _cell_id(revise, flag="--revise") if revise is not None else None
|
|
496
|
+
checkpoint_seq = _checkpoint(getattr(args, "checkpoint", None))
|
|
497
|
+
# ⚠ EVERY FLAG IS SETTLED BEFORE THE BODY IS READ. Standard input can be read once, so a typo
|
|
498
|
+
# in `--revise` discovered after the read would have consumed prose the caller then has to
|
|
499
|
+
# compose again. Reading it last costs nothing and makes the refusal recoverable.
|
|
500
|
+
markdown = _read_markdown(args, stdin=stdin)
|
|
501
|
+
check_cell_bounds(heading=heading, markdown=markdown)
|
|
502
|
+
# The credential is resolved only now, after everything a client can settle on its own has
|
|
503
|
+
# been settled. It is still before the bounds could be checked against the LOG -- the per-run
|
|
504
|
+
# cell budget and whether `--revise` names a real cell are Studio's to answer, and this lane
|
|
505
|
+
# does not read the log to guess at them.
|
|
506
|
+
selected = client or _client(args)
|
|
507
|
+
cell = selected.append_cell(
|
|
508
|
+
run_id,
|
|
509
|
+
body=append_cell_body(
|
|
510
|
+
workspace_id=selected.session.workspace_id,
|
|
511
|
+
run_id=run_id,
|
|
512
|
+
cell_id=cell_id,
|
|
513
|
+
heading=heading,
|
|
514
|
+
markdown=markdown,
|
|
515
|
+
checkpoint_seq=checkpoint_seq,
|
|
516
|
+
revision_of=revision_of,
|
|
517
|
+
),
|
|
518
|
+
)
|
|
519
|
+
return {
|
|
520
|
+
"schema_version": NOTE_SCHEMA,
|
|
521
|
+
"status": "narrative_cell_appended",
|
|
522
|
+
"lane": "hosted",
|
|
523
|
+
"run_id": run_id,
|
|
524
|
+
"workspace_id": str(selected.session.workspace_id),
|
|
525
|
+
"cell": _cell_receipt(cell),
|
|
526
|
+
"sealed": False,
|
|
527
|
+
"not_evidence": NOT_EVIDENCE,
|
|
528
|
+
"flags_without_effect": _no_effect(args, _NOTE_NO_EFFECT),
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _list(client: StudioNarrativeClient, run_id: str) -> dict[str, Any]:
|
|
533
|
+
"""``mr-data note --list``: every cell this run holds, superseded revisions flagged."""
|
|
534
|
+
|
|
535
|
+
narrative = client.run_narrative(run_id)
|
|
536
|
+
cells = narrative.get("cells")
|
|
537
|
+
if not isinstance(cells, list):
|
|
538
|
+
raise ThinLaneError(
|
|
539
|
+
"THIN_RESPONSE_INVALID", "Studio answered the narrative without a list of cells"
|
|
540
|
+
)
|
|
541
|
+
if any(not isinstance(cell, Mapping) for cell in cells):
|
|
542
|
+
# ⚠ REFUSED, NOT FILTERED. A list route elsewhere in this lane drops a malformed member,
|
|
543
|
+
# because a page of runs is still a useful page with one bad row in it. A narrative is a
|
|
544
|
+
# document: dropping a cell would report a smaller count than the run holds and read as
|
|
545
|
+
# the record being shorter than it is.
|
|
546
|
+
raise ThinLaneError(
|
|
547
|
+
"THIN_RESPONSE_INVALID", "Studio listed a narrative cell that is not an object"
|
|
548
|
+
)
|
|
549
|
+
listed = [_cell_receipt(cell) for cell in cells]
|
|
550
|
+
workspace_id = narrative.get("workspace_id")
|
|
551
|
+
return {
|
|
552
|
+
"schema_version": NOTE_SCHEMA,
|
|
553
|
+
"status": "narrative_listed",
|
|
554
|
+
"lane": "hosted",
|
|
555
|
+
"run_id": run_id,
|
|
556
|
+
"workspace_id": workspace_id if isinstance(workspace_id, str) else None,
|
|
557
|
+
"cell_count": len(listed),
|
|
558
|
+
"rendered_count": sum(1 for cell in listed if cell["superseded"] is not True),
|
|
559
|
+
"cells": listed,
|
|
560
|
+
"sealed": False,
|
|
561
|
+
"not_evidence": NOT_EVIDENCE,
|
|
562
|
+
# Present and empty, so the two answers this command gives have the same key set and a
|
|
563
|
+
# script does not have to know which one it got before it reads them.
|
|
564
|
+
"flags_without_effect": {},
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
#: Said in every answer, for the same reason ``approvals`` says ``never_decides`` in every one of
|
|
569
|
+
#: its own: the property has to survive being read by somebody who only sees one payload.
|
|
570
|
+
NOT_EVIDENCE = (
|
|
571
|
+
"A narrative cell is an unsealed adjunct: it records what was decided and why, carries no "
|
|
572
|
+
"authority, and is never attestation evidence."
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
__all__ = [
|
|
577
|
+
"MAX_CELL_ID_CHARS",
|
|
578
|
+
"MAX_HEADING_CHARS",
|
|
579
|
+
"MAX_MARKDOWN_BYTES",
|
|
580
|
+
"NARRATE_SCOPE",
|
|
581
|
+
"NOTE_SCHEMA",
|
|
582
|
+
"NOT_EVIDENCE",
|
|
583
|
+
"RUN_NARRATIVE_PATH",
|
|
584
|
+
"StudioNarrativeClient",
|
|
585
|
+
"append_cell_body",
|
|
586
|
+
"cell_id_from_heading",
|
|
587
|
+
"check_cell_bounds",
|
|
588
|
+
"note",
|
|
589
|
+
]
|